Skip to content

Commit a119fcb

Browse files
authored
Merge pull request #2 from ut-code/feat/reply
返信機能
2 parents 97b4e1b + 28ac50e commit a119fcb

File tree

5 files changed

+57
-5
lines changed

5 files changed

+57
-5
lines changed

packages/client/src/components/chat/Channel.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { api, type Id } from "@packages/convex";
3+
import type { Doc } from "@packages/convex/src/convex/_generated/dataModel";
34
import { useQuery } from "convex-svelte";
45
import MessageInput from "./MessageInput.svelte";
56
import MessageList from "./MessageList.svelte";
@@ -13,6 +14,8 @@
1314
const selectedChannel = useQuery(api.channels.get, () => ({
1415
id: selectedChannelId,
1516
}));
17+
18+
let replyingTo = $state<Doc<"messages"> | null>(null);
1619
</script>
1720

1821
<div class="border-base-300 bg-base-200 border-b p-4">
@@ -26,5 +29,5 @@
2629
{/if}
2730
</div>
2831

29-
<MessageList channelId={selectedChannelId} />
30-
<MessageInput channelId={selectedChannelId} />
32+
<MessageList channelId={selectedChannelId} bind:replyingTo />
33+
<MessageInput channelId={selectedChannelId} bind:replyingTo />

packages/client/src/components/chat/MessageInput.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
<script lang="ts">
22
import { api, type Id } from "@packages/convex";
3+
import type { Doc } from "@packages/convex/src/convex/_generated/dataModel";
34
import { useConvexClient, useQuery } from "convex-svelte";
45
56
interface Props {
67
channelId: Id<"channels">;
8+
replyingTo: Doc<"messages"> | null;
79
}
810
9-
const { channelId }: Props = $props();
11+
let { channelId, replyingTo = $bindable() }: Props = $props();
1012
1113
const convex = useConvexClient();
1214
const identity = useQuery(api.users.me, {});
@@ -27,9 +29,11 @@
2729
channelId,
2830
content: messageContent.trim(),
2931
author: authorName.trim() || "匿名",
32+
parentId: replyingTo?._id ?? undefined,
3033
});
3134
3235
messageContent = "";
36+
replyingTo = null;
3337
}
3438
3539
function handleKeyPress(event: KeyboardEvent) {
@@ -41,6 +45,14 @@
4145
</script>
4246

4347
<div class="border-base-300 bg-base-100 border-t p-4">
48+
{#if replyingTo}
49+
<div class="text-base-content/70 mb-2 text-sm">
50+
<span class="font-semibold">返信先:</span>
51+
<span class="text-primary font-semibold">{replyingTo.author}</span>
52+
<span>{replyingTo.content}</span>
53+
</div>
54+
{/if}
55+
4456
<div class="mb-2 flex gap-2">
4557
<input
4658
type="text"

packages/client/src/components/chat/MessageList.svelte

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
<script lang="ts">
22
import { api, type Id } from "@packages/convex";
3+
import type { Doc } from "@packages/convex/src/convex/_generated/dataModel";
34
import { useQuery } from "convex-svelte";
45
import { onMount } from "svelte";
56
67
interface Props {
78
channelId: Id<"channels">;
9+
replyingTo: Doc<"messages"> | null;
810
}
911
10-
const { channelId }: Props = $props();
12+
let { channelId, replyingTo = $bindable() }: Props = $props();
1113
1214
const messages = useQuery(api.messages.list, () => ({
1315
channelId,
1416
}));
17+
18+
const messagesById = $derived(
19+
new Map(messages.data?.map((message) => [message._id, message])),
20+
);
21+
1522
let messagesContainer: HTMLDivElement;
1623
1724
function formatTime(timestamp: number) {
@@ -41,7 +48,18 @@
4148
<div bind:this={messagesContainer} class="flex-1 space-y-2 overflow-y-auto p-4">
4249
{#if messages.data}
4350
{#each messages.data as message (message._id)}
44-
<div class="flex flex-col">
51+
{#if message.parentId && messages.data.find((m) => m._id === message.parentId)}
52+
<div class="flex items-center gap-2">
53+
<span class="text-base-content/60 text-xs">返信</span>
54+
<span class="text-primary font-semibold"
55+
>{messagesById.get(message.parentId)?.author}</span
56+
>
57+
<span class="text-base-content/60 text-xs">
58+
{messagesById.get(message.parentId)?.content}
59+
</span>
60+
</div>
61+
{/if}
62+
<div class="group relative flex flex-col">
4563
<div class="flex items-baseline gap-2">
4664
<span class="text-primary font-semibold">{message.author}</span>
4765
<span class="text-base-content/60 text-xs">
@@ -51,6 +69,22 @@
5169
<div class="text-base-content ml-0 whitespace-pre-wrap">
5270
{message.content}
5371
</div>
72+
<div
73+
class="bg-base-100 absolute top-0 right-4 -translate-y-1/2 rounded-md border opacity-0 group-hover:opacity-100"
74+
>
75+
<div class="dropdown dropdown-end">
76+
<button class="btn btn-ghost btn-sm p-2" tabindex="0"> ⋮ </button>
77+
<ul
78+
tabindex="0"
79+
role="menu"
80+
class="menu dropdown-content bg-base-100 z-[1] w-40 rounded-md border p-2 shadow"
81+
>
82+
<li>
83+
<button onclick={() => (replyingTo = message)}>返信</button>
84+
</li>
85+
</ul>
86+
</div>
87+
</div>
5488
</div>
5589
{:else}
5690
<div class="text-center text-base-content/60 py-8">

packages/convex/src/convex/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ export const send = mutation({
1717
channelId: v.id("channels"),
1818
content: v.string(),
1919
author: v.string(),
20+
parentId: v.optional(v.id("messages")),
2021
},
2122
handler: async (ctx, args) => {
2223
await ctx.db.insert("messages", {
2324
channelId: args.channelId,
2425
content: args.content,
2526
author: args.author,
2627
createdAt: Date.now(),
28+
parentId: args.parentId,
2729
});
2830
},
2931
});

packages/convex/src/convex/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default defineSchema({
1818
content: v.string(),
1919
author: v.string(),
2020
createdAt: v.number(),
21+
parentId: v.optional(v.id("messages")),
2122
}).index("by_channel", ["channelId"]),
2223
...authTables,
2324
});

0 commit comments

Comments
 (0)