• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DigitalTolk / ex / 25040414161

28 Apr 2026 07:43AM UTC coverage: 89.685% (-0.05%) from 89.738%
25040414161

Pull #27

github

web-flow
Merge dfaafbf6a into 542e2d60c
Pull Request #27: Fix thread emoji hover

1894 of 2283 branches covered (82.96%)

Branch coverage included in aggregate %.

11 of 15 new or added lines in 2 files covered. (73.33%)

36 existing lines in 9 files now uncovered.

8705 of 9535 relevant lines covered (91.3%)

25.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.59
/frontend/src/components/chat/ThreadPanel.tsx
1
import { useMemo, useRef } from 'react';
2
import { MessageItem } from './MessageItem';
3
import { MessageInput, type MessageInputHandle } from './MessageInput';
4
import { MessageDropZone } from './MessageDropZone';
5
import { Button } from '@/components/ui/button';
6
import { X } from 'lucide-react';
7
import { useSendMessage, type SendMessageInput } from '@/hooks/useMessages';
8
import { useThreadMessages } from '@/hooks/useThreads';
9
import { useUsersBatch } from '@/hooks/useUsersBatch';
10
import { collectMessageUserIDs } from '@/lib/message-users';
11
import type { UserMapEntry } from './MessageList';
12

13
interface ThreadPanelProps {
14
  channelId?: string;
15
  conversationId?: string;
16
  threadRootID: string;
17
  onClose: () => void;
18
  userMap: Record<string, UserMapEntry>;
19
  currentUserId?: string;
20
}
21

22
export function ThreadPanel({
23
  channelId,
24
  conversationId,
25
  threadRootID,
26
  onClose,
27
  userMap,
28
  currentUserId,
29
}: ThreadPanelProps) {
30
  const { data, isLoading } = useThreadMessages({ channelId, conversationId, threadRootID });
7✔
31

32
  // Authors + reactors of thread messages may not be in the parent
33
  // userMap (which was built from the channel page, not the thread).
34
  // Fetch any missing IDs so reaction tooltips don't show "Unknown".
35
  const missingUserIDs = useMemo(() => {
7✔
36
    const ids = collectMessageUserIDs(data ?? []);
7✔
37
    return ids.filter((id) => !userMap[id]);
7✔
38
  }, [data, userMap]);
39
  const { data: extras } = useUsersBatch(missingUserIDs);
7✔
40
  const mergedUserMap = useMemo(() => {
7✔
41
    if (!extras || extras.length === 0) return userMap;
4!
NEW
42
    const m: Record<string, UserMapEntry> = { ...userMap };
×
NEW
43
    for (const u of extras) {
×
NEW
44
      m[u.id] = { displayName: u.displayName || 'Unknown', avatarURL: u.avatarURL };
×
45
    }
NEW
46
    return m;
×
47
  }, [userMap, extras]);
48
  // Adapter for MessageItem — its userMap prop is the .get-style lookup
49
  // ThreadActionBar / reaction tooltip both consume.
50
  const userLookup = useMemo(
7✔
51
    () => ({ get: (id: string) => mergedUserMap[id] }),
4✔
52
    [mergedUserMap],
53
  );
54

55
  const send = useSendMessage({ channelId, conversationId });
7✔
56
  const inputRef = useRef<MessageInputHandle>(null);
7✔
57

58
  function handleReply(input: SendMessageInput) {
59
    send.mutate({ ...input, parentMessageID: threadRootID });
1✔
60
  }
61

62
  return (
7✔
63
    <aside className="w-[28rem] border-l flex flex-col" aria-label="Thread">
64
      <div className="flex items-center justify-between border-b px-4 py-3">
65
        <h2 className="text-sm font-semibold">Thread</h2>
66
        <Button
67
          variant="ghost"
68
          size="icon"
69
          className="h-7 w-7"
70
          onClick={onClose}
71
          aria-label="Close thread"
72
        >
73
          <X className="h-4 w-4" />
74
        </Button>
75
      </div>
UNCOV
76
      <MessageDropZone onFiles={(files) => void inputRef.current?.uploadFiles(files)}>
×
77
        <div className="flex-1 overflow-y-auto p-2 space-y-2">
78
          {isLoading && (
11✔
79
            <p className="text-xs text-muted-foreground p-2">Loading replies...</p>
80
          )}
81
          {data?.length === 0 && (
9✔
82
            <p className="text-xs text-muted-foreground p-2">No replies yet. Start the thread!</p>
83
          )}
84
          {data?.map((msg) => {
85
            const u = mergedUserMap[msg.authorID];
1✔
86
            return (
1✔
87
              <MessageItem
88
                key={msg.id}
89
                message={msg}
90
                authorName={u?.displayName ?? 'Unknown'}
1!
91
                authorAvatarURL={u?.avatarURL}
92
                authorOnline={u?.online}
93
                isOwn={msg.authorID === currentUserId}
94
                channelId={channelId}
95
                conversationId={conversationId}
96
                currentUserId={currentUserId}
97
                userMap={userLookup}
98
                inThread
99
              />
100
            );
101
          })}
102
        </div>
103
        <MessageInput
104
          ref={inputRef}
105
          onSend={handleReply}
106
          disabled={send.isPending}
107
          placeholder="Reply..."
108
          focusKey={threadRootID}
109
        />
110
      </MessageDropZone>
111
    </aside>
112
  );
113
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc