feat(desktop): set-nickname dialog + right-click trigger in friends list

This commit is contained in:
byGalax
2026-05-16 17:01:38 +02:00
parent a5eadef663
commit 898b7469bb
2 changed files with 123 additions and 3 deletions
@@ -0,0 +1,81 @@
import { useEffect, useRef, useState } from 'react';
import { getNickname, setNickname } from '../lib/friendNicknames';
interface Props {
open: boolean;
userId: string;
displayName: string;
onClose: () => void;
}
export function NicknameDialog({ open, userId, displayName, onClose }: Props) {
const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) return;
setValue(getNickname(userId) ?? '');
setTimeout(() => inputRef.current?.focus(), 0);
}, [open, userId]);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
const submit = (): void => {
setNickname(userId, value);
onClose();
};
return (
<div
role="dialog"
aria-modal="true"
aria-label="Spitzname setzen"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={onClose}
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-sm rounded-2xl border border-line bg-surface-2 p-5 shadow-xl"
>
<h3 className="font-display text-base font-semibold text-fg">Spitzname für {displayName}</h3>
<p className="mt-1 text-xs text-fg-muted">
Nur du siehst diesen Namen. Leer lassen = den richtigen Namen verwenden.
</p>
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
maxLength={32}
placeholder={displayName}
className="mt-4 w-full rounded-lg border border-line bg-surface-3 px-3 py-2 text-sm text-fg placeholder-fg-muted focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
/>
<div className="mt-4 flex justify-end gap-2">
<button
type="button"
onClick={onClose}
className="cursor-pointer rounded-md border border-line bg-surface-3 px-3 py-2 text-sm text-fg hover:bg-surface-2"
>
Abbrechen
</button>
<button
type="button"
onClick={submit}
className="cursor-pointer rounded-md bg-accent px-3 py-2 text-sm font-semibold text-accent-fg hover:brightness-110"
>
Speichern
</button>
</div>
</div>
</div>
);
}
+42 -3
View File
@@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom';
import { Avatar } from '../components/Avatar';
import { EmptyState } from '../components/EmptyState';
import { NicknameDialog } from '../components/NicknameDialog';
import {
AddUserIcon,
AlertIcon,
@@ -41,6 +42,7 @@ export function FriendsPage() {
const [searchError, setSearchError] = useState<string | null>(null);
const [pendingId, setPendingId] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const [nicknameDialog, setNicknameDialog] = useState<{ userId: string; displayName: string } | null>(null);
const searchInputRef = useRef<HTMLInputElement | null>(null);
const focusSearch = useCallback(() => {
@@ -184,6 +186,12 @@ export function FriendsPage() {
<FriendList
items={accepted}
emptyKey="app:friends.empty_friends"
onRowContextMenu={(f) =>
setNicknameDialog({
userId: f.peer.userId,
displayName: f.peer.displayName ?? f.peer.username ?? 'Freund',
})
}
renderActions={(f) => (
<FriendActions
busy={pendingId === f.peer.userId || pendingId === 'msg-' + f.peer.userId}
@@ -233,6 +241,12 @@ export function FriendsPage() {
/>
)}
</div>
<NicknameDialog
open={nicknameDialog !== null}
userId={nicknameDialog?.userId ?? ''}
displayName={nicknameDialog?.displayName ?? ''}
onClose={() => setNicknameDialog(null)}
/>
</div>
);
}
@@ -275,10 +289,12 @@ function FriendList({
items,
emptyKey,
renderActions,
onRowContextMenu,
}: {
items: Friendship[];
emptyKey: string;
renderActions: (f: Friendship) => React.ReactNode;
onRowContextMenu?: (f: Friendship) => void;
}) {
const { t } = useTranslation(['app']);
if (items.length === 0) {
@@ -296,16 +312,39 @@ function FriendList({
<ul className="space-y-1.5">
{items.map((f) => (
<li key={f.peer.userId}>
<FriendRow profile={f.peer}>{renderActions(f)}</FriendRow>
<FriendRow
profile={f.peer}
{...(onRowContextMenu
? {
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
onRowContextMenu(f);
},
}
: {})}
>
{renderActions(f)}
</FriendRow>
</li>
))}
</ul>
);
}
function FriendRow({ profile, children }: { profile: ProfileBrief; children: React.ReactNode }) {
function FriendRow({
profile,
children,
onContextMenu,
}: {
profile: ProfileBrief;
children: React.ReactNode;
onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void;
}) {
return (
<div className="flex items-center gap-3 rounded-xl border border-line bg-surface-2 px-4 py-3">
<div
onContextMenu={onContextMenu}
className="flex items-center gap-3 rounded-xl border border-line bg-surface-2 px-4 py-3"
>
<Avatar
url={profile.avatarUrl}
displayName={profile.displayName ?? profile.username}