diff --git a/bun.lock b/bun.lock index 3050f1a..57d87bd 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "dependencies": { "@auth/core": "^0.40.0", "@convex-dev/auth": "^0.0.87", - "convex": "^1.25.2", + "convex": "^1.27.1", "resend": "^4.7.0", }, "devDependencies": { @@ -1101,6 +1101,8 @@ "@mmailaender/convex-auth-svelte/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "@packages/convex/convex": ["convex@1.27.1", "", { "dependencies": { "esbuild": "0.25.4", "jwt-decode": "^4.0.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-kep7JFn5Bil9/OUZUsL1bgoo0G9DmEf7stkBW67+NqP2FrzBt2TX8yz4V6oKLygzepGy90Ura2FtqXawYKXYIg=="], + "@storybook/csf-plugin/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], diff --git a/packages/client/src/components/channels/Channel.svelte b/packages/client/src/components/channels/Channel.svelte index 9a13fc8..9bb37ad 100644 --- a/packages/client/src/components/channels/Channel.svelte +++ b/packages/client/src/components/channels/Channel.svelte @@ -30,5 +30,5 @@ {/if} - + diff --git a/packages/client/src/components/chat/MessageInput.svelte b/packages/client/src/components/chat/MessageInput.svelte index 46a0041..d3736b7 100644 --- a/packages/client/src/components/chat/MessageInput.svelte +++ b/packages/client/src/components/chat/MessageInput.svelte @@ -1,13 +1,14 @@
@@ -116,6 +164,10 @@ /> {/if} + {#if showVoteMaker} + + {/if} +
{showFileSelector ? "キャンセル" : "ファイル添付"} +
diff --git a/packages/client/src/components/chat/MessageList.svelte b/packages/client/src/components/chat/MessageList.svelte index 0f966a7..7811e9d 100644 --- a/packages/client/src/components/chat/MessageList.svelte +++ b/packages/client/src/components/chat/MessageList.svelte @@ -11,13 +11,15 @@ import MessageDropdown from "./MessageDropdown.svelte"; import ReactionButtons from "./ReactionButtons.svelte"; import ReactionList from "./ReactionList.svelte"; + import VoteViewer from "./VoteViewer.svelte"; interface Props { + organizationId: Id<"organizations">; channelId: Id<"channels">; replyingTo: Doc<"messages"> | null; } - let { channelId, replyingTo = $bindable() }: Props = $props(); + let { organizationId, channelId, replyingTo = $bindable() }: Props = $props(); const messages = useQuery(api.messages.list, () => ({ channelId, @@ -71,7 +73,7 @@ {#if messages.data} {#each messages.data as message (message._id)} {#snippet reactionListSnippet()} - + {/snippet} {#snippet dropdownContent()} @@ -168,6 +170,9 @@ {/each} {/if} + {#if message.vote} + + {/if}
diff --git a/packages/client/src/components/chat/ReactionList.svelte b/packages/client/src/components/chat/ReactionList.svelte index 01db719..71dd0e8 100644 --- a/packages/client/src/components/chat/ReactionList.svelte +++ b/packages/client/src/components/chat/ReactionList.svelte @@ -3,10 +3,11 @@ import { useQuery } from "convex-svelte"; interface Props { + organizationId: Id<"organizations">; messageId: Id<"messages">; } - let { messageId }: Props = $props(); + let { organizationId, messageId }: Props = $props(); const reactions = useQuery(api.messages.getReactions, () => ({ messageId })); @@ -30,8 +31,13 @@ reactions.data ? [...new Set(reactions.data.map((r) => r.userId))] : [], ); - const userNamesById = useQuery(api.users.getUserNames, () => ({ + // const userNamesById = useQuery(api.users.getUserNames, () => ({ + // userIds: allUserIdsInReactions, + // })); + + const userNamesById = useQuery(api.users.getUserNicknames, () => ({ userIds: allUserIdsInReactions, + organizationId: organizationId, })); function toggleUserList(emoji: string) { diff --git a/packages/client/src/components/chat/VoteMaker.svelte b/packages/client/src/components/chat/VoteMaker.svelte new file mode 100644 index 0000000..5dceba8 --- /dev/null +++ b/packages/client/src/components/chat/VoteMaker.svelte @@ -0,0 +1,71 @@ + + +
+

投票のタイトル:

+ +
+ +
+

一人が投票できる最大数:

+ { + vote.maxVotes = vote.maxVotes ?? 0; + }} + /> +
+
+ {#each vote.voteOptions as option, i} +
+

{i}:{option}

+ +
+ {/each} +
+ +
+

選択肢を追加:

+ + +
diff --git a/packages/client/src/components/chat/VoteViewer.svelte b/packages/client/src/components/chat/VoteViewer.svelte new file mode 100644 index 0000000..d5fe7b5 --- /dev/null +++ b/packages/client/src/components/chat/VoteViewer.svelte @@ -0,0 +1,107 @@ + + +
+

投票:

+

{vote.data?.title}

+

+ 一人の最大投票数:{vote.data?.maxVotes}票 +

+ {#each vote.data?.voteOptions as option, i} +
+

+ {option}{isResultVisible + ? ":" + numbersOfVotersPerOption[i] + "人" + : ""} +

+ +
+ {/each} + +
diff --git a/packages/convex/package.json b/packages/convex/package.json index 98c7b83..6b163d0 100644 --- a/packages/convex/package.json +++ b/packages/convex/package.json @@ -16,7 +16,7 @@ "dependencies": { "@auth/core": "^0.40.0", "@convex-dev/auth": "^0.0.87", - "convex": "^1.25.2", + "convex": "^1.27.1", "resend": "^4.7.0" } } diff --git a/packages/convex/src/convex/messages.ts b/packages/convex/src/convex/messages.ts index e8311f6..ac5365a 100644 --- a/packages/convex/src/convex/messages.ts +++ b/packages/convex/src/convex/messages.ts @@ -27,6 +27,7 @@ export const send = mutation({ author: v.string(), parentId: v.optional(v.id("messages")), attachments: v.optional(v.array(v.id("files"))), + vote: v.optional(v.id("votes")), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); @@ -54,6 +55,7 @@ export const send = mutation({ createdAt: Date.now(), parentId: args.parentId, attachments: args.attachments, + vote: args.vote, }); }, }); diff --git a/packages/convex/src/convex/schema.ts b/packages/convex/src/convex/schema.ts index 044203e..b0f147e 100644 --- a/packages/convex/src/convex/schema.ts +++ b/packages/convex/src/convex/schema.ts @@ -42,6 +42,8 @@ export default defineSchema({ parentId: v.optional(v.id("messages")), // 添付ファイル attachments: v.optional(v.array(v.id("files"))), + //投票 + vote: v.optional(v.id("votes")), }).index("by_channel", ["channelId"]), reactions: defineTable({ messageId: v.id("messages"), @@ -51,6 +53,18 @@ export default defineSchema({ }) .index("by_message", ["messageId"]) .index("by_user", ["userId"]), + votes: defineTable({ + title: v.string(), + maxVotes: v.number(), + //numberOfOptions: v.number(), + voteOptions: v.array(v.string()), + voters: v.array( + v.object({ + userId: v.id("users"), + votedOptions: v.array(v.number()), + }), + ), + }), personalization: defineTable({ userId: v.id("users"), organizationId: v.id("organizations"), diff --git a/packages/convex/src/convex/users.ts b/packages/convex/src/convex/users.ts index 2d795cb..757267f 100644 --- a/packages/convex/src/convex/users.ts +++ b/packages/convex/src/convex/users.ts @@ -31,3 +31,36 @@ export const getUserNames = query({ return userNames; }, }); + +export const getUserNicknames = query({ + args: { + userIds: v.array(v.id("users")), + organizationId: v.id("organizations"), + }, + handler: async (ctx, { userIds, organizationId }) => { + const users = await Promise.all( + userIds.map((userId) => ctx.db.get(userId)), + ); + const personalizations = await Promise.all( + userIds.map((userId) => + ctx.db + .query("personalization") + .filter((q) => q.eq(q.field("userId"), userId)) + .filter((q) => q.eq(q.field("organizationId"), organizationId)) + .unique(), + ), + ); + const userNicknames: Record, string> = Object.fromEntries( + users + .filter((user) => user !== null) + .map((user) => [ + user._id, + personalizations.find((p) => p?.userId === user._id)?.nickname ?? + user.name ?? + "", + ]), + ); + + return userNicknames; + }, +}); diff --git a/packages/convex/src/convex/vote.ts b/packages/convex/src/convex/vote.ts new file mode 100644 index 0000000..f337a7c --- /dev/null +++ b/packages/convex/src/convex/vote.ts @@ -0,0 +1,50 @@ +import { v } from "convex/values"; +import type { Id } from "./_generated/dataModel"; +import { mutation, query } from "./_generated/server"; + +export const addVote = mutation({ + args: { + title: v.string(), + maxVotes: v.number(), + voteOptions: v.array(v.string()), + }, + handler: async (ctx, args) => { + const id = await ctx.db.insert("votes", { + title: args.title, + maxVotes: args.maxVotes, + voteOptions: args.voteOptions, + voters: [], + }); + return id; + }, +}); + +export const getVote = query({ + args: { + id: v.id("votes"), + }, + handler: async (ctx, args) => { + return await ctx.db.get(args.id); + }, +}); + +export const vote = mutation({ + args: { + voteId: v.id("votes"), + userId: v.id("users"), + votedOptions: v.array(v.number()), + }, + handler: async (ctx, args) => { + const vote = await ctx.db.get(args.voteId); + const tempVoters = vote?.voters.filter( + (v: { userId: Id<"users">; votedOptions: Array }) => + v.userId !== args.userId, + ); + await ctx.db.patch(args.voteId, { + voters: [ + ...tempVoters, + { userId: args.userId, votedOptions: args.votedOptions }, + ], + }); + }, +});