feat(desktop): apply friend nicknames in header / bubble / mentions / call tile
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import type { ConversationSummary } from '@chat-app/shared/chat';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useNickname } from '../lib/friendNicknames';
|
||||
import { Avatar } from './Avatar';
|
||||
|
||||
type Member = ConversationSummary['members'][number];
|
||||
|
||||
interface Props {
|
||||
members: ConversationSummary['members'];
|
||||
query: string;
|
||||
@@ -63,37 +66,58 @@ export function MentionAutocomplete({ members, query, excludeUserId, onSelect, o
|
||||
aria-label="Mitglieder"
|
||||
className="absolute bottom-full left-0 right-0 z-20 mb-2 max-h-64 overflow-y-auto rounded-xl border border-line bg-surface-2 p-1 shadow-xl dark:bg-[#2b2d31]"
|
||||
>
|
||||
{matches.map((m, idx) => {
|
||||
const name = m.profile?.displayName ?? m.profile?.username ?? '?';
|
||||
const handle = m.profile?.username ?? '';
|
||||
const isActive = idx === active;
|
||||
return (
|
||||
<button
|
||||
key={m.userId}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onMouseEnter={() => setActive(idx)}
|
||||
onClick={() => {
|
||||
if (m.profile?.username) onSelect(m.profile.username);
|
||||
}}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition ' +
|
||||
(isActive
|
||||
? 'bg-accent/20 text-fg'
|
||||
: 'text-fg-muted hover:bg-surface-3 dark:hover:bg-[#383a40]')
|
||||
}
|
||||
>
|
||||
<Avatar
|
||||
displayName={name}
|
||||
url={m.profile?.avatarUrl ?? null}
|
||||
className="h-6 w-6 text-[10px]"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-fg">{name}</span>
|
||||
<span className="shrink-0 text-[10px] text-fg-muted">@{handle}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{matches.map((m, idx) => (
|
||||
<MemberRow
|
||||
key={m.userId}
|
||||
member={m}
|
||||
isActive={idx === active}
|
||||
onActivate={() => setActive(idx)}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Extracted per-row component so we can call `useNickname` once per member
|
||||
// at the top of THIS component (hooks can't run inside .map callbacks).
|
||||
function MemberRow({
|
||||
member,
|
||||
isActive,
|
||||
onActivate,
|
||||
onSelect,
|
||||
}: {
|
||||
member: Member;
|
||||
isActive: boolean;
|
||||
onActivate: () => void;
|
||||
onSelect: (username: string) => void;
|
||||
}) {
|
||||
const fallback = member.profile?.displayName ?? member.profile?.username ?? '?';
|
||||
const name = useNickname(member.userId, fallback);
|
||||
const handle = member.profile?.username ?? '';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onMouseEnter={onActivate}
|
||||
onClick={() => {
|
||||
if (member.profile?.username) onSelect(member.profile.username);
|
||||
}}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition ' +
|
||||
(isActive
|
||||
? 'bg-accent/20 text-fg'
|
||||
: 'text-fg-muted hover:bg-surface-3 dark:hover:bg-[#383a40]')
|
||||
}
|
||||
>
|
||||
<Avatar
|
||||
displayName={name}
|
||||
url={member.profile?.avatarUrl ?? null}
|
||||
className="h-6 w-6 text-[10px]"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-fg">{name}</span>
|
||||
<span className="shrink-0 text-[10px] text-fg-muted">@{handle}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user