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

DigitalTolk / ex / 24964032432

26 Apr 2026 06:35PM UTC coverage: 90.069% (-0.3%) from 90.372%
24964032432

Pull #13

github

web-flow
Merge 391ad3cab into c7a0902b0
Pull Request #13: Docker fixes

1506 of 1799 branches covered (83.71%)

Branch coverage included in aggregate %.

955 of 1086 new or added lines in 34 files covered. (87.94%)

7935 of 8683 relevant lines covered (91.39%)

18.33 hits per line

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

94.05
/frontend/src/components/chat/MentionAutocomplete.tsx
1
import { useEffect, useMemo, useRef, useState } from 'react';
2
import { useSearchUsers } from '@/hooks/useConversations';
3

4
// MentionSuggestion is the shape the editor inserts. user => @[id|name]
5
// pill; group => literal "@all"/"@here" text.
6
export type MentionSuggestion =
7
  | { kind: 'user'; id: string; displayName: string; email?: string }
8
  | { kind: 'group'; group: 'all' | 'here' };
9

10
interface Props {
11
  // Text the user typed after the @ (lowercased for matching).
12
  query: string;
13
  // Anchor for positioning — the mention popup appears just above the
14
  // caret so the user keeps reading downward as they type.
15
  anchorRect: DOMRect | null;
16
  // Pick a suggestion (Enter / Tab / click). The editor inserts the pill
17
  // and replaces the trigger range.
18
  onPick: (s: MentionSuggestion) => void;
19
  // Esc / lose focus — caller closes the popup.
20
  onDismiss: () => void;
21
}
22

23
const GROUPS: { kind: 'group'; group: 'all' | 'here'; description: string }[] = [
29✔
24
  { kind: 'group', group: 'all', description: 'Notify everyone in this channel' },
25
  { kind: 'group', group: 'here', description: 'Notify everyone currently online' },
26
];
27

28
export function MentionAutocomplete({ query, anchorRect, onPick, onDismiss }: Props) {
29
  const { data: users } = useSearchUsers(query);
24✔
30
  const [active, setActive] = useState(0);
24✔
31

32
  // Group entries first (Slack-style); then user matches. When the query
33
  // is non-empty, group entries also need to be filtered so typing
34
  // "alice" doesn't keep "@all" at the top of the list.
35
  const items: MentionSuggestion[] = useMemo(() => {
24✔
36
    const q = query.trim().toLowerCase();
18✔
37
    const groupItems: MentionSuggestion[] = GROUPS
18✔
38
      .filter((g) => g.group.startsWith(q) || q.length === 0)
36✔
39
      .map((g) => ({ kind: 'group', group: g.group }));
25✔
40
    const userItems: MentionSuggestion[] = (users ?? []).map((u) => ({
18✔
41
      kind: 'user',
42
      id: u.id,
43
      displayName: u.displayName,
44
      email: u.email,
45
    }));
46
    return [...groupItems, ...userItems];
18✔
47
  }, [query, users]);
48

49
  // Reset the highlighted index when the suggestion list changes — the
50
  // previous highlighted row may no longer exist after a query refines.
51
  // This is a deliberate sync from a derived input (items.length) into
52
  // local UI state, hence the lint suppression.
53
  useEffect(() => {
24✔
54
    // eslint-disable-next-line react-hooks/set-state-in-effect
55
    setActive(0);
18✔
56
  }, [items.length]);
57

58
  // Keyboard handling lives at this level — the editor surrenders Enter,
59
  // ArrowUp/Down, Escape to us as long as the popup is open.
60
  useEffect(() => {
24✔
61
    function onKey(e: KeyboardEvent) {
62
      if (e.key === 'ArrowDown') {
10✔
63
        e.preventDefault();
2✔
64
        setActive((i) => (items.length === 0 ? 0 : (i + 1) % items.length));
2!
65
        return;
2✔
66
      }
67
      if (e.key === 'ArrowUp') {
8✔
68
        e.preventDefault();
1✔
69
        setActive((i) => (items.length === 0 ? 0 : (i - 1 + items.length) % items.length));
1!
70
        return;
1✔
71
      }
72
      if (e.key === 'Enter' || e.key === 'Tab') {
7✔
73
        if (items.length === 0) return;
6!
74
        e.preventDefault();
6✔
75
        e.stopPropagation();
6✔
76
        onPick(items[active]);
6✔
77
        return;
6✔
78
      }
79
      if (e.key === 'Escape') {
1!
80
        e.preventDefault();
1✔
81
        onDismiss();
1✔
82
        return;
1✔
83
      }
84
    }
85
    window.addEventListener('keydown', onKey, { capture: true });
20✔
86
    return () => window.removeEventListener('keydown', onKey, { capture: true });
20✔
87
  }, [items, active, onPick, onDismiss]);
88

89
  const popupRef = useRef<HTMLDivElement>(null);
24✔
90
  // Position above the caret so the typed @ remains visible.
91
  const style: React.CSSProperties = anchorRect
24✔
92
    ? {
93
        position: 'fixed',
94
        left: Math.max(8, anchorRect.left),
95
        bottom: Math.max(8, window.innerHeight - anchorRect.top + 4),
96
        zIndex: 60,
97
      }
98
    : { display: 'none' };
99

100
  if (items.length === 0) {
24✔
101
    return null;
1✔
102
  }
103

104
  return (
23✔
105
    <div
106
      ref={popupRef}
107
      data-testid="mention-popup"
108
      role="listbox"
109
      aria-label="Mention suggestions"
110
      style={style}
111
      className="w-72 rounded-md border bg-popover p-1 shadow-lg"
112
    >
113
      {items.map((it, i) => {
114
        const isActive = i === active;
41✔
115
        const key = it.kind === 'user' ? `u-${it.id}` : `g-${it.group}`;
41✔
116
        const label = it.kind === 'user' ? `@${it.displayName}` : `@${it.group}`;
41✔
117
        const sub =
118
          it.kind === 'user'
41✔
119
            ? it.email
120
            : it.group === 'all'
35✔
121
              ? 'Notify everyone in this channel'
122
              : 'Notify everyone currently online';
123
        return (
41✔
124
          <button
125
            key={key}
126
            type="button"
127
            role="option"
128
            aria-selected={isActive}
129
            data-testid="mention-option"
130
            data-mention-active={isActive ? 'true' : 'false'}
41✔
131
            onMouseDown={(e) => {
132
              // Prevent the contentEditable from losing focus on click.
133
              e.preventDefault();
2✔
134
              onPick(it);
2✔
135
            }}
NEW
136
            onMouseEnter={() => setActive(i)}
×
137
            className={
138
              'flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm ' +
139
              (isActive ? 'bg-muted' : 'hover:bg-muted/50')
41✔
140
            }
141
          >
142
            <span className="font-medium">{label}</span>
143
            {sub && <span className="ml-auto truncate text-xs text-muted-foreground">{sub}</span>}
82✔
144
          </button>
145
        );
146
      })}
147
    </div>
148
  );
149
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc