feat(desktop): set-nickname dialog + right-click trigger in friends list
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Avatar } from '../components/Avatar';
|
import { Avatar } from '../components/Avatar';
|
||||||
import { EmptyState } from '../components/EmptyState';
|
import { EmptyState } from '../components/EmptyState';
|
||||||
|
import { NicknameDialog } from '../components/NicknameDialog';
|
||||||
import {
|
import {
|
||||||
AddUserIcon,
|
AddUserIcon,
|
||||||
AlertIcon,
|
AlertIcon,
|
||||||
@@ -41,6 +42,7 @@ export function FriendsPage() {
|
|||||||
const [searchError, setSearchError] = useState<string | null>(null);
|
const [searchError, setSearchError] = useState<string | null>(null);
|
||||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||||
const [actionError, setActionError] = 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 searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const focusSearch = useCallback(() => {
|
const focusSearch = useCallback(() => {
|
||||||
@@ -184,6 +186,12 @@ export function FriendsPage() {
|
|||||||
<FriendList
|
<FriendList
|
||||||
items={accepted}
|
items={accepted}
|
||||||
emptyKey="app:friends.empty_friends"
|
emptyKey="app:friends.empty_friends"
|
||||||
|
onRowContextMenu={(f) =>
|
||||||
|
setNicknameDialog({
|
||||||
|
userId: f.peer.userId,
|
||||||
|
displayName: f.peer.displayName ?? f.peer.username ?? 'Freund',
|
||||||
|
})
|
||||||
|
}
|
||||||
renderActions={(f) => (
|
renderActions={(f) => (
|
||||||
<FriendActions
|
<FriendActions
|
||||||
busy={pendingId === f.peer.userId || pendingId === 'msg-' + f.peer.userId}
|
busy={pendingId === f.peer.userId || pendingId === 'msg-' + f.peer.userId}
|
||||||
@@ -233,6 +241,12 @@ export function FriendsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<NicknameDialog
|
||||||
|
open={nicknameDialog !== null}
|
||||||
|
userId={nicknameDialog?.userId ?? ''}
|
||||||
|
displayName={nicknameDialog?.displayName ?? ''}
|
||||||
|
onClose={() => setNicknameDialog(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -275,10 +289,12 @@ function FriendList({
|
|||||||
items,
|
items,
|
||||||
emptyKey,
|
emptyKey,
|
||||||
renderActions,
|
renderActions,
|
||||||
|
onRowContextMenu,
|
||||||
}: {
|
}: {
|
||||||
items: Friendship[];
|
items: Friendship[];
|
||||||
emptyKey: string;
|
emptyKey: string;
|
||||||
renderActions: (f: Friendship) => React.ReactNode;
|
renderActions: (f: Friendship) => React.ReactNode;
|
||||||
|
onRowContextMenu?: (f: Friendship) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation(['app']);
|
const { t } = useTranslation(['app']);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -296,16 +312,39 @@ function FriendList({
|
|||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{items.map((f) => (
|
{items.map((f) => (
|
||||||
<li key={f.peer.userId}>
|
<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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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 (
|
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
|
<Avatar
|
||||||
url={profile.avatarUrl}
|
url={profile.avatarUrl}
|
||||||
displayName={profile.displayName ?? profile.username}
|
displayName={profile.displayName ?? profile.username}
|
||||||
|
|||||||
Reference in New Issue
Block a user