feat: per-thread DM mute, event pass, a11y header, undo mute
This commit is contained in:
parent
be00aa3fed
commit
673bb983b8
41 changed files with 382 additions and 13 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
|
|
@ -78,6 +79,13 @@ export function FirstRunChecklist({ onFindNearby }: { onFindNearby?: () => void
|
|||
>
|
||||
{t('mapUi.gotIt')}
|
||||
</Button>
|
||||
<Link
|
||||
to="/crews"
|
||||
onClick={() => setDismissed(true)}
|
||||
className="self-center text-xs font-medium text-gray-500 underline underline-offset-2 hover:text-gray-300"
|
||||
>
|
||||
{t('mapUi.firstRunJoinCrew')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -843,7 +843,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
alt="SkateSpots"
|
||||
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
|
||||
/>
|
||||
<span className="font-display text-xl neon-red tracking-wide" style={{
|
||||
<span className="min-w-0 truncate font-display text-xl neon-red tracking-wide" style={{
|
||||
textShadow: '0 0 8px #ff0040'
|
||||
}}>SKATESPOTS</span>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { VolumeX } from 'lucide-react';
|
||||
import { Volume2, VolumeX } from 'lucide-react';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useMuteList } from '@/hooks/useMuteList';
|
||||
import { useMuteUser } from '@/hooks/useMuteUser';
|
||||
import { useUnmuteUser } from '@/hooks/useUnmuteUser';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
/** Inline mute action for content cards. Renders nothing when logged out. */
|
||||
|
|
@ -17,21 +18,23 @@ export function MuteMenuItem({
|
|||
const { user } = useCurrentUser();
|
||||
const { data: muted } = useMuteList();
|
||||
const mute = useMuteUser();
|
||||
const unmute = useUnmuteUser();
|
||||
if (!user || user.pubkey === pubkey) return null;
|
||||
|
||||
const isMuted = !!muted?.has(pubkey);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
disabled={isMuted || mute.isPending}
|
||||
disabled={mute.isPending || unmute.isPending}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!isMuted) mute.mutate(pubkey);
|
||||
if (isMuted) unmute.mutate(pubkey);
|
||||
else mute.mutate(pubkey);
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<VolumeX className="mr-2 h-4 w-4" />
|
||||
{isMuted ? t('walletUi.trust.muted') : t('walletUi.trust.mute')}
|
||||
{isMuted ? <Volume2 className="mr-2 h-4 w-4" /> : <VolumeX className="mr-2 h-4 w-4" />}
|
||||
{isMuted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
41
src/hooks/useDmMute.ts
Normal file
41
src/hooks/useDmMute.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const KEY = 'skatespots:dm-muted-threads';
|
||||
const EVENT = 'skatespots:dm-muted-threads-change';
|
||||
|
||||
function read(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(KEY);
|
||||
const parsed = raw ? JSON.parse(raw) as unknown : [];
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Device-local per-thread mute for 1:1 DMs, group DMs and crew chat keys. */
|
||||
export function useDmMute() {
|
||||
const [keys, setKeys] = useState<string[]>(read);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<string[]>).detail;
|
||||
if (Array.isArray(detail)) setKeys(detail);
|
||||
};
|
||||
window.addEventListener(EVENT, onChange);
|
||||
return () => window.removeEventListener(EVENT, onChange);
|
||||
}, []);
|
||||
|
||||
const write = useCallback((next: string[]) => {
|
||||
setKeys(next);
|
||||
try { localStorage.setItem(KEY, JSON.stringify(next)); } catch { /* ignore */ }
|
||||
window.dispatchEvent(new CustomEvent(EVENT, { detail: next }));
|
||||
}, []);
|
||||
|
||||
const isMuted = useCallback((key: string | undefined) => !!key && keys.includes(key), [keys]);
|
||||
const toggle = useCallback((key: string) => {
|
||||
write(keys.includes(key) ? keys.filter((k) => k !== key) : [...keys, key].slice(-200));
|
||||
}, [keys, write]);
|
||||
|
||||
return { isMuted, toggle };
|
||||
}
|
||||
51
src/hooks/useUnmuteUser.ts
Normal file
51
src/hooks/useUnmuteUser.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { onlyVerified } from '@/lib/verifyEvents';
|
||||
import { removeLocalMute } from '@/lib/localMutes';
|
||||
import { MUTE_LIST_KIND } from '@/hooks/useMuteList';
|
||||
|
||||
/** Remove a pubkey from the viewer's NIP-51 kind-10000 mute list. */
|
||||
export function useUnmuteUser() {
|
||||
const { nostr } = useNostr();
|
||||
const { mutateAsync: publish } = useNostrPublish();
|
||||
const { user } = useCurrentUser();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (target: string) => {
|
||||
if (!user) throw new Error(t('walletUi.trust.loginToMute'));
|
||||
const events = onlyVerified(await nostr.query(
|
||||
[{ kinds: [MUTE_LIST_KIND], authors: [user.pubkey], limit: 1 }],
|
||||
{ signal: AbortSignal.timeout(8000) },
|
||||
));
|
||||
const existing = (events[0]?.tags ?? [])
|
||||
.filter(([name]) => name === 'p')
|
||||
.map(([, value]) => value)
|
||||
.filter((value) => value && value !== target);
|
||||
await publish({
|
||||
kind: MUTE_LIST_KIND,
|
||||
content: '',
|
||||
tags: existing.map((pk) => ['p', pk]),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, target) => {
|
||||
removeLocalMute(target);
|
||||
queryClient.invalidateQueries({ queryKey: ['mute-list', user?.pubkey] });
|
||||
queryClient.invalidateQueries();
|
||||
toast({ title: t('walletUi.trust.unmuted'), description: t('walletUi.trust.unmutedDesc') });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: t('walletUi.trust.unmuteFailed'),
|
||||
description: error instanceof Error ? error.message : undefined,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1123,6 +1123,7 @@ const batch4 = {
|
|||
"eventDetail": {
|
||||
"aCrew": "una crew",
|
||||
"addToCalendar": "Afegeix al calendari",
|
||||
"eventPass": "Passi de l'esdeveniment",
|
||||
"backToEvents": "Torna als esdeveniments",
|
||||
"cancelled": "Aquest esdeveniment s'ha cancel·lat",
|
||||
"checkUpdate": ": consulta l'actualització",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ const batch6 = {
|
|||
mute: "Silencia",
|
||||
muted: "Silenciat",
|
||||
mutedDesc: "No veuràs el seu contingut.",
|
||||
unmute: "Deixa de silenciar",
|
||||
unmuted: "Silenciament desactivat",
|
||||
unmutedDesc: "El seu contingut tornarà a aparèixer.",
|
||||
unmuteFailed: "No s'ha pogut deixar de silenciar. Torna-ho a provar.",
|
||||
muteFailed: "No s'ha pogut silenciar. Torna-ho a provar.",
|
||||
loginToMute: "Inicia sessió per silenciar.",
|
||||
attributedNetwork: "De gent de la teva xarxa",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ const drift = {
|
|||
"mute": "Ztlumit",
|
||||
"muted": "Ztlumeno",
|
||||
"mutedDesc": "Nebudeš vídat jejich obsah.",
|
||||
"unmute": "Zrušit ztlumení",
|
||||
"unmuted": "Ztlumení zrušeno",
|
||||
"unmutedDesc": "Jejich obsah se znovu objeví.",
|
||||
"unmuteFailed": "Ztlumení se nepodařilo zrušit. Zkus to znovu.",
|
||||
"muteFailed": "Ztlumení se nezdařilo. Zkus to znovu.",
|
||||
"loginToMute": "Přihlas se, abys mohl ztlumit.",
|
||||
"attributedNetwork": "Od lidí ve tvé síti",
|
||||
|
|
@ -467,6 +471,11 @@ const drift = {
|
|||
offerPickup: "Tento týden si to můžu vyzvednout.",
|
||||
offerShipping: "Můžeš spojit dopravu?",
|
||||
},
|
||||
"placesUi": {
|
||||
"eventDetail": {
|
||||
"eventPass": "Vstupenka na akci"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ const drift = {
|
|||
mute: "Stummschalten",
|
||||
muted: "Stummgeschaltet",
|
||||
mutedDesc: "Du siehst deren Inhalte nicht.",
|
||||
unmute: "Stummschaltung aufheben",
|
||||
unmuted: "Stummschaltung aufgehoben",
|
||||
unmutedDesc: "Ihre Inhalte werden wieder angezeigt.",
|
||||
unmuteFailed: "Stummschaltung konnte nicht aufgehoben werden. Versuch es erneut.",
|
||||
muteFailed: "Stummschalten fehlgeschlagen. Versuche es erneut.",
|
||||
loginToMute: "Melde dich an, um stummzuschalten.",
|
||||
attributedNetwork: "Von Personen in deinem Netzwerk",
|
||||
|
|
@ -469,6 +473,11 @@ const drift = {
|
|||
offerPickup: "Ich kann es diese Woche abholen.",
|
||||
offerShipping: "Kannst du den Versand bündeln?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Event-Pass"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ const drift = {
|
|||
mute: "Σίγαση",
|
||||
muted: "Σε σίγαση",
|
||||
mutedDesc: "Δεν θα βλέπεις το περιεχόμενό τους.",
|
||||
unmute: "Κατάργηση σίγασης",
|
||||
unmuted: "Η σίγαση καταργήθηκε",
|
||||
unmutedDesc: "Το περιεχόμενό τους θα εμφανιστεί ξανά.",
|
||||
unmuteFailed: "Δεν ήταν δυνατή η κατάργηση της σίγασης. Δοκίμασε ξανά.",
|
||||
muteFailed: "Δεν ήταν δυνατή η σίγαση. Δοκίμασε ξανά.",
|
||||
loginToMute: "Συνδέσου για σίγαση.",
|
||||
attributedNetwork: "Από άτομα στο δίκτυό σου",
|
||||
|
|
@ -471,6 +475,11 @@ const drift = {
|
|||
offerPickup: "Μπορώ να το παραλάβω αυτή την εβδομάδα.",
|
||||
offerShipping: "Μπορείς να συνδυάσεις τα μεταφορικά;",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Πάσο εκδήλωσης"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -3427,6 +3427,7 @@ placesUi: {
|
|||
eventDetail: {
|
||||
aCrew: 'a crew',
|
||||
addToCalendar: 'Add to calendar',
|
||||
eventPass: 'Event pass',
|
||||
backToEvents: 'Back to Events',
|
||||
cancelled: 'This event was cancelled',
|
||||
checkUpdate: '— check the update',
|
||||
|
|
@ -6207,6 +6208,10 @@ walletUi: {
|
|||
mute: 'Mute',
|
||||
muted: 'Muted',
|
||||
mutedDesc: 'You will not see their content.',
|
||||
unmute: 'Unmute',
|
||||
unmuted: 'Unmuted',
|
||||
unmutedDesc: 'Their content will appear again.',
|
||||
unmuteFailed: 'Could not unmute. Try again.',
|
||||
muteFailed: 'Could not mute. Try again.',
|
||||
loginToMute: 'Log in to mute.',
|
||||
attributedNetwork: 'From people in your network',
|
||||
|
|
|
|||
|
|
@ -3427,6 +3427,7 @@ placesUi: {
|
|||
eventDetail: {
|
||||
aCrew: 'una crew',
|
||||
addToCalendar: 'Añadir al calendario',
|
||||
eventPass: 'Pase del evento',
|
||||
backToEvents: 'Volver a eventos',
|
||||
cancelled: 'Este evento se canceló',
|
||||
checkUpdate: '— consulta la actualización',
|
||||
|
|
@ -6207,6 +6208,10 @@ walletUi: {
|
|||
mute: 'Silenciar',
|
||||
muted: 'Silenciado',
|
||||
mutedDesc: 'No veras su contenido.',
|
||||
unmute: 'Dejar de silenciar',
|
||||
unmuted: 'Sin silencio',
|
||||
unmutedDesc: 'Su contenido volvera a aparecer.',
|
||||
unmuteFailed: 'No se pudo dejar de silenciar. Intentalo de nuevo.',
|
||||
muteFailed: 'No se pudo silenciar. Intentalo de nuevo.',
|
||||
loginToMute: 'Inicia sesion para silenciar.',
|
||||
attributedNetwork: 'De personas de tu red',
|
||||
|
|
|
|||
|
|
@ -1123,6 +1123,7 @@ const batch4 = {
|
|||
eventDetail: {
|
||||
aCrew: "krew bat",
|
||||
addToCalendar: "Gehitu egutegira",
|
||||
eventPass: "Ekitaldiko pasea",
|
||||
backToEvents: "Itzuli ekitaldietara",
|
||||
cancelled: "Ekitaldi hau bertan behera utzi da",
|
||||
checkUpdate: "— begiratu eguneratzea",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ const batch6 = {
|
|||
mute: "Mututu",
|
||||
muted: "Mutututa",
|
||||
mutedDesc: "Ez duzu haien edukia ikusiko.",
|
||||
unmute: "Mutua kendu",
|
||||
unmuted: "Mutua kenduta",
|
||||
unmutedDesc: "Haien edukia berriro agertuko da.",
|
||||
unmuteFailed: "Ezin izan da mutua kendu. Saiatu berriro.",
|
||||
muteFailed: "Ezin izan da mututu. Saiatu berriro.",
|
||||
loginToMute: "Hasi saioa mututzeko.",
|
||||
attributedNetwork: "Zure sareko pertsonen eskutik",
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ const drift = {
|
|||
mute: "Masquer",
|
||||
muted: "Masqué",
|
||||
mutedDesc: "Vous ne verrez pas son contenu.",
|
||||
unmute: "Ne plus masquer",
|
||||
unmuted: "Plus masqué",
|
||||
unmutedDesc: "Son contenu réapparaîtra.",
|
||||
unmuteFailed: "Impossible de ne plus masquer. Réessayez.",
|
||||
muteFailed: "Impossible de masquer. Réessayez.",
|
||||
loginToMute: "Connectez-vous pour masquer.",
|
||||
attributedNetwork: "De personnes de votre réseau",
|
||||
|
|
@ -469,6 +473,11 @@ const drift = {
|
|||
offerPickup: "Je peux venir le chercher cette semaine.",
|
||||
offerShipping: "Peux-tu regrouper l'expédition ?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Pass événement"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -1123,6 +1123,7 @@ const batch4 = {
|
|||
eventDetail: {
|
||||
aCrew: "un crew",
|
||||
addToCalendar: "Engadir ao calendario",
|
||||
eventPass: "Pase do evento",
|
||||
backToEvents: "Volver aos eventos",
|
||||
cancelled: "Este evento cancelouse",
|
||||
checkUpdate: ": consulta a actualización",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ const batch6 = {
|
|||
"mute": "Silenciar",
|
||||
"muted": "Silenciado",
|
||||
"mutedDesc": "Non verás o seu contido.",
|
||||
"unmute": "Deixar de silenciar",
|
||||
"unmuted": "Silenciamento desfeito",
|
||||
"unmutedDesc": "O seu contido volverá aparecer.",
|
||||
"unmuteFailed": "Non se puido deixar de silenciar. Téntao de novo.",
|
||||
"muteFailed": "Non se puido silenciar. Téntao de novo.",
|
||||
"loginToMute": "Inicia sesión para silenciar.",
|
||||
"attributedNetwork": "De persoas da túa rede",
|
||||
|
|
|
|||
|
|
@ -1123,6 +1123,7 @@ const batch4 = {
|
|||
"eventDetail": {
|
||||
"aCrew": "एक क्रू",
|
||||
"addToCalendar": "कैलेंडर में जोड़ें",
|
||||
"eventPass": "इवेंट पास",
|
||||
"backToEvents": "इवेंट्स पर वापस जाएँ",
|
||||
"cancelled": "यह इवेंट रद्द हो गया था",
|
||||
"checkUpdate": "— अपडेट देखें",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ const batch6 = {
|
|||
"mute": "म्यूट करें",
|
||||
"muted": "म्यूट किया",
|
||||
"mutedDesc": "आपको इनका कंटेंट नहीं दिखेगा।",
|
||||
"unmute": "अनम्यूट करें",
|
||||
"unmuted": "अनम्यूट किया",
|
||||
"unmutedDesc": "उनका कंटेंट फिर से दिखाई देगा।",
|
||||
"unmuteFailed": "अनम्यूट नहीं हो सका। फिर कोशिश करें।",
|
||||
"muteFailed": "म्यूट नहीं हो सका। फिर कोशिश करें।",
|
||||
"loginToMute": "म्यूट करने के लिए लॉग इन करें।",
|
||||
"attributedNetwork": "आपके नेटवर्क के लोगों से",
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ const driftStrings = {
|
|||
mute: 'Bisukan',
|
||||
muted: 'Dibisukan',
|
||||
mutedDesc: 'Kamu tidak akan melihat konten mereka.',
|
||||
unmute: 'Batalkan bisukan',
|
||||
unmuted: 'Bisukan dibatalkan',
|
||||
unmutedDesc: 'Konten mereka akan muncul lagi.',
|
||||
unmuteFailed: 'Gagal membatalkan bisukan. Coba lagi.',
|
||||
muteFailed: 'Gagal membisukan. Coba lagi.',
|
||||
loginToMute: 'Masuk untuk membisukan.',
|
||||
attributedNetwork: 'Dari orang-orang di jaringanmu',
|
||||
|
|
@ -463,6 +467,11 @@ const driftStrings = {
|
|||
offerPickup: "Saya bisa ambil sendiri minggu ini.",
|
||||
offerShipping: "Bisa gabung ongkos kirimnya?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Tiket acara"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default driftStrings;
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ const drift = {
|
|||
mute: "Silenzia",
|
||||
muted: "Silenziato",
|
||||
mutedDesc: "Non vedrai i suoi contenuti.",
|
||||
unmute: "Annulla silenziamento",
|
||||
unmuted: "Silenziamento annullato",
|
||||
unmutedDesc: "I suoi contenuti torneranno visibili.",
|
||||
unmuteFailed: "Impossibile annullare il silenziamento. Riprova.",
|
||||
muteFailed: "Impossibile silenziare. Riprova.",
|
||||
loginToMute: "Accedi per silenziare.",
|
||||
attributedNetwork: "Da persone nella tua rete",
|
||||
|
|
@ -469,6 +473,11 @@ const drift = {
|
|||
offerPickup: "Posso ritirarlo questa settimana.",
|
||||
offerShipping: "Puoi raggruppare la spedizione?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Pass evento"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ const drift: Dict = {
|
|||
mute: 'ミュート',
|
||||
muted: 'ミュート済み',
|
||||
mutedDesc: '相手のコンテンツは表示されません。',
|
||||
unmute: 'ミュート解除',
|
||||
unmuted: 'ミュート解除済み',
|
||||
unmutedDesc: '相手のコンテンツが再び表示されます。',
|
||||
unmuteFailed: 'ミュートを解除できませんでした。もう一度お試しください。',
|
||||
muteFailed: 'ミュートできませんでした。もう一度お試しください。',
|
||||
loginToMute: 'ミュートするにはログインしてください。',
|
||||
attributedNetwork: 'あなたのネットワークの人から',
|
||||
|
|
@ -479,6 +483,11 @@ const drift: Dict = {
|
|||
offerPickup: "今週中に受け取れます。",
|
||||
offerShipping: "送料をまとめられますか?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "イベントパス"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ const drift: Dict = {
|
|||
mute: '뮤트',
|
||||
muted: '뮤트됨',
|
||||
mutedDesc: '상대방의 콘텐츠가 표시되지 않습니다.',
|
||||
unmute: '뮤트 해제',
|
||||
unmuted: '뮤트 해제됨',
|
||||
unmutedDesc: '상대방의 콘텐츠가 다시 표시됩니다.',
|
||||
unmuteFailed: '뮤트를 해제하지 못했습니다. 다시 시도하세요.',
|
||||
muteFailed: '뮤트하지 못했습니다. 다시 시도하세요.',
|
||||
loginToMute: '뮤트하려면 로그인하세요.',
|
||||
attributedNetwork: '당신의 네트워크에 있는 사람들로부터',
|
||||
|
|
@ -479,6 +483,11 @@ const drift: Dict = {
|
|||
offerPickup: "이번 주에 직접 수령할 수 있어요.",
|
||||
offerShipping: "배송비를 묶을 수 있나요?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "이벤트 패스"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ const drift = {
|
|||
"mute": "Dempen",
|
||||
"muted": "Gedempt",
|
||||
"mutedDesc": "Je ziet hun content niet meer.",
|
||||
"unmute": "Dempen opheffen",
|
||||
"unmuted": "Dempen opgeheven",
|
||||
"unmutedDesc": "Je ziet hun content weer.",
|
||||
"unmuteFailed": "Kon dempen niet opheffen. Probeer het opnieuw.",
|
||||
"muteFailed": "Kon niet dempen. Probeer het opnieuw.",
|
||||
"loginToMute": "Log in om te dempen.",
|
||||
"attributedNetwork": "Van mensen in je netwerk",
|
||||
|
|
@ -467,6 +471,11 @@ const drift = {
|
|||
offerPickup: "Ik kan het deze week ophalen.",
|
||||
offerShipping: "Kun je de verzendkosten bundelen?",
|
||||
},
|
||||
"placesUi": {
|
||||
"eventDetail": {
|
||||
"eventPass": "Evenementenpas"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ const drift = {
|
|||
"mute": "Wycisz",
|
||||
"muted": "Wyciszony",
|
||||
"mutedDesc": "Nie zobaczysz ich treści.",
|
||||
"unmute": "Wyłącz wyciszenie",
|
||||
"unmuted": "Wyciszenie wyłączone",
|
||||
"unmutedDesc": "Ich treści znów będą widoczne.",
|
||||
"unmuteFailed": "Nie udało się wyłączyć wyciszenia. Spróbuj ponownie.",
|
||||
"muteFailed": "Nie udało się wyciszyć. Spróbuj ponownie.",
|
||||
"loginToMute": "Zaloguj się, aby wyciszyć.",
|
||||
"attributedNetwork": "Od osób w twojej sieci",
|
||||
|
|
@ -467,6 +471,11 @@ const drift = {
|
|||
offerPickup: "Mogę odebrać w tym tygodniu.",
|
||||
offerShipping: "Czy możesz połączyć wysyłkę?",
|
||||
},
|
||||
"placesUi": {
|
||||
"eventDetail": {
|
||||
"eventPass": "Karnet na wydarzenie"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ const drift = {
|
|||
mute: "Silenciar",
|
||||
muted: "Silenciado",
|
||||
mutedDesc: "Você não verá o conteúdo dela.",
|
||||
unmute: "Deixar de silenciar",
|
||||
unmuted: "Silenciamento desfeito",
|
||||
unmutedDesc: "O conteúdo dela voltará a aparecer.",
|
||||
unmuteFailed: "Não foi possível deixar de silenciar. Tente novamente.",
|
||||
muteFailed: "Não foi possível silenciar. Tente novamente.",
|
||||
loginToMute: "Faça login para silenciar.",
|
||||
attributedNetwork: "De pessoas da sua rede",
|
||||
|
|
@ -469,6 +473,11 @@ const drift = {
|
|||
offerPickup: "Consigo retirar esta semana.",
|
||||
offerShipping: "Você consegue agrupar o frete?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Passe do evento"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ const drift = {
|
|||
"mute": "Oprește sunetul",
|
||||
"muted": "Fără sunet",
|
||||
"mutedDesc": "Nu le vei vedea conținutul.",
|
||||
"unmute": "Reia sunetul",
|
||||
"unmuted": "Sunet reactivat",
|
||||
"unmutedDesc": "Conținutul lor va apărea din nou.",
|
||||
"unmuteFailed": "Nu s-a putut reactiva sunetul. Încearcă din nou.",
|
||||
"muteFailed": "Nu s-a putut opri sunetul. Încearcă din nou.",
|
||||
"loginToMute": "Autentifică-te pentru a opri sunetul.",
|
||||
"attributedNetwork": "De la persoane din rețeaua ta",
|
||||
|
|
@ -467,6 +471,11 @@ const drift = {
|
|||
offerPickup: "Pot ridica personal săptămâna asta.",
|
||||
offerShipping: "Poți combina transportul?",
|
||||
},
|
||||
"placesUi": {
|
||||
"eventDetail": {
|
||||
"eventPass": "Pass de eveniment"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ const drift = {
|
|||
mute: "Заглушить",
|
||||
muted: "Заглушено",
|
||||
mutedDesc: "Вы не будете видеть их контент.",
|
||||
unmute: "Снять заглушение",
|
||||
unmuted: "Заглушение снято",
|
||||
unmutedDesc: "Их контент снова будет виден.",
|
||||
unmuteFailed: "Не удалось снять заглушение. Попробуйте снова.",
|
||||
muteFailed: "Не удалось заглушить. Попробуйте снова.",
|
||||
loginToMute: "Войдите, чтобы заглушить.",
|
||||
attributedNetwork: "От людей из вашей сети",
|
||||
|
|
@ -471,6 +475,11 @@ const drift = {
|
|||
offerPickup: "Могу забрать на этой неделе.",
|
||||
offerShipping: "Можете объединить доставку?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Пропуск на мероприятие"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ const drift = {
|
|||
mute: "Tysta",
|
||||
muted: "Tystad",
|
||||
mutedDesc: "Du kommer inte se deras innehåll.",
|
||||
unmute: "Sluta tysta",
|
||||
unmuted: "Inte längre tystad",
|
||||
unmutedDesc: "Deras innehåll visas igen.",
|
||||
unmuteFailed: "Kunde inte sluta tysta. Försök igen.",
|
||||
muteFailed: "Kunde inte tysta. Försök igen.",
|
||||
loginToMute: "Logga in för att tysta.",
|
||||
attributedNetwork: "Från personer i ditt nätverk",
|
||||
|
|
@ -471,6 +475,11 @@ const drift = {
|
|||
offerPickup: "Jag kan hämta den här veckan.",
|
||||
offerShipping: "Kan du samla frakten?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Evenemangspass"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ const driftStrings = {
|
|||
mute: 'ปิดเสียง',
|
||||
muted: 'ปิดเสียงแล้ว',
|
||||
mutedDesc: 'คุณจะไม่เห็นเนื้อหาของพวกเขา',
|
||||
unmute: 'เปิดเสียง',
|
||||
unmuted: 'เปิดเสียงแล้ว',
|
||||
unmutedDesc: 'เนื้อหาของพวกเขาจะปรากฏอีกครั้ง',
|
||||
unmuteFailed: 'เปิดเสียงไม่สำเร็จ ลองอีกครั้ง',
|
||||
muteFailed: 'ปิดเสียงไม่สำเร็จ ลองอีกครั้ง',
|
||||
loginToMute: 'เข้าสู่ระบบเพื่อปิดเสียง',
|
||||
attributedNetwork: 'จากคนในเครือข่ายของคุณ',
|
||||
|
|
@ -463,6 +467,11 @@ const driftStrings = {
|
|||
offerPickup: "ฉันรับเองได้ภายในสัปดาห์นี้",
|
||||
offerShipping: "รวมค่าส่งได้ไหม?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "บัตรอีเวนต์"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default driftStrings;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ const driftStrings = {
|
|||
mute: 'Sessize al',
|
||||
muted: 'Sessize alındı',
|
||||
mutedDesc: 'İçeriklerini görmezsin.',
|
||||
unmute: 'Sesi aç',
|
||||
unmuted: 'Sesi açıldı',
|
||||
unmutedDesc: 'İçerikleri yeniden görünür.',
|
||||
unmuteFailed: 'Sesi açılamadı. Tekrar dene.',
|
||||
muteFailed: 'Sessize alınamadı. Tekrar dene.',
|
||||
loginToMute: 'Sessize almak için giriş yap.',
|
||||
attributedNetwork: 'Ağındaki kişilerden',
|
||||
|
|
@ -463,6 +467,11 @@ const driftStrings = {
|
|||
offerPickup: "Bu hafta kendim alabilirim.",
|
||||
offerShipping: "Kargo ücretini birleştirebilir misin?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Etkinlik pası"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default driftStrings;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ const drift = {
|
|||
mute: "Заглушити",
|
||||
muted: "Заглушено",
|
||||
mutedDesc: "Ви не бачитимете їхній вміст.",
|
||||
unmute: "Зняти заглушення",
|
||||
unmuted: "Заглушення знято",
|
||||
unmutedDesc: "Їхній вміст знову буде видно.",
|
||||
unmuteFailed: "Не вдалося зняти заглушення. Спробуйте знову.",
|
||||
muteFailed: "Не вдалося заглушити. Спробуйте знову.",
|
||||
loginToMute: "Увійдіть, щоб заглушити.",
|
||||
attributedNetwork: "Від людей з вашої мережі",
|
||||
|
|
@ -471,6 +475,11 @@ const drift = {
|
|||
offerPickup: "Можу забрати цього тижня.",
|
||||
offerShipping: "Можете об'єднати доставку?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Перепустка на подію"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ const driftStrings = {
|
|||
mute: 'Ẩn',
|
||||
muted: 'Đã ẩn',
|
||||
mutedDesc: 'Bạn sẽ không thấy nội dung của họ.',
|
||||
unmute: 'Bỏ ẩn',
|
||||
unmuted: 'Đã bỏ ẩn',
|
||||
unmutedDesc: 'Nội dung của họ sẽ hiện lại.',
|
||||
unmuteFailed: 'Không thể bỏ ẩn. Thử lại.',
|
||||
muteFailed: 'Không thể ẩn. Thử lại.',
|
||||
loginToMute: 'Đăng nhập để ẩn.',
|
||||
attributedNetwork: 'Từ những người trong mạng lưới của bạn',
|
||||
|
|
@ -463,6 +467,11 @@ const driftStrings = {
|
|||
offerPickup: "Tôi có thể tự lấy trong tuần này.",
|
||||
offerShipping: "Bạn gộp phí ship được không?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "Thẻ sự kiện"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default driftStrings;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ const drift: Dict = {
|
|||
mute: '屏蔽',
|
||||
muted: '已屏蔽',
|
||||
mutedDesc: '你将不会看到他们的内容。',
|
||||
unmute: '取消屏蔽',
|
||||
unmuted: '已取消屏蔽',
|
||||
unmutedDesc: '他们的内容将重新显示。',
|
||||
unmuteFailed: '无法取消屏蔽。请重试。',
|
||||
muteFailed: '无法屏蔽。请重试。',
|
||||
loginToMute: '登录以屏蔽。',
|
||||
attributedNetwork: '来自你网络中的人',
|
||||
|
|
@ -479,6 +483,11 @@ const drift: Dict = {
|
|||
offerPickup: "我这周可以自取。",
|
||||
offerShipping: "可以合并运费吗?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "活动通行证"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ const drift: Dict = {
|
|||
mute: '靜音',
|
||||
muted: '已靜音',
|
||||
mutedDesc: '你不會看到他們的內容。',
|
||||
unmute: '取消靜音',
|
||||
unmuted: '已取消靜音',
|
||||
unmutedDesc: '他們的內容將重新顯示。',
|
||||
unmuteFailed: '無法取消靜音。請重試。',
|
||||
muteFailed: '無法靜音。請重試。',
|
||||
loginToMute: '登入以靜音。',
|
||||
attributedNetwork: '來自你網路中的人',
|
||||
|
|
@ -479,6 +483,11 @@ const drift: Dict = {
|
|||
offerPickup: "我這週可以自取。",
|
||||
offerShipping: "可以合併運費嗎?",
|
||||
},
|
||||
placesUi: {
|
||||
eventDetail: {
|
||||
eventPass: "活動通行證"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
|
|
|
|||
|
|
@ -39,3 +39,7 @@ export function addLocalMute(pubkey: string): void {
|
|||
list.push(pubkey);
|
||||
write(list);
|
||||
}
|
||||
|
||||
export function removeLocalMute(pubkey: string): void {
|
||||
write(read().filter((pk) => pk !== pubkey));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow';
|
|||
import { EscrowPanel } from '@/components/EscrowPanel';
|
||||
import { TranslateMessage } from '@/components/chat/TranslateMessage';
|
||||
import { useDMRead } from '@/hooks/useDMRead';
|
||||
import { useDmMute } from '@/hooks/useDmMute';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
|
||||
|
|
@ -29,6 +30,8 @@ import {
|
|||
Loader2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
VolumeX,
|
||||
Volume2,
|
||||
CheckCircle2,
|
||||
Handshake,
|
||||
} from 'lucide-react';
|
||||
|
|
@ -115,6 +118,8 @@ export default function ConversationPage() {
|
|||
const escrow = useCashuEscrow();
|
||||
const [isRedeeming, setIsRedeeming] = useState(false);
|
||||
const { markRead } = useDMRead();
|
||||
const { isMuted, toggle: toggleMute } = useDmMute();
|
||||
const muted = isMuted(otherPubkey ?? undefined);
|
||||
const author = useAuthor(otherPubkey || undefined);
|
||||
const metadata = author.data?.metadata;
|
||||
const displayName = metadata?.name ?? (otherPubkey ? genUserName(otherPubkey) : t('socialUi.conversation.unknown'));
|
||||
|
|
@ -316,6 +321,18 @@ export default function ConversationPage() {
|
|||
{t('socialUi.conversation.encrypted')}
|
||||
</p>
|
||||
</div>
|
||||
{otherPubkey && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => toggleMute(otherPubkey)}
|
||||
aria-label={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||
title={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||
className={cn('text-gray-400 hover:text-white', muted && 'text-amber-400')}
|
||||
>
|
||||
{muted ? <Volume2 className="h-4 w-4" /> : <VolumeX className="h-4 w-4" />}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useCordnGroupGids } from '@/hooks/useCordnNative';
|
|||
import { SKATE_CREW_KIND } from '@/lib/types';
|
||||
import { useDMRead } from '@/hooks/useDMRead';
|
||||
import { useDmUnreadSummary } from '@/hooks/useDmUnreadSummary';
|
||||
import { useDmMute } from '@/hooks/useDmMute';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
|
|
@ -82,9 +83,10 @@ function GroupPreview({ conversation }: { conversation: Conversation }) {
|
|||
const participants = conversation.participants ?? [];
|
||||
const { user } = useCurrentUser();
|
||||
const { getLastRead } = useDMRead();
|
||||
const { isMuted } = useDmMute();
|
||||
const last = conversation.lastMessage;
|
||||
const isFromMe = last?.pubkey === user?.pubkey;
|
||||
const unread = !!last && !isFromMe && last.createdAt > getLastRead(conversation.pubkey);
|
||||
const unread = !!last && !isFromMe && !isMuted(conversation.pubkey) && last.createdAt > getLastRead(conversation.pubkey);
|
||||
const timeAgo = last
|
||||
? formatDistanceToNow(new Date(last.createdAt * 1000), { addSuffix: true })
|
||||
: '';
|
||||
|
|
@ -229,6 +231,7 @@ export default function DirectMessagesPage() {
|
|||
const { data: groups, refetch: refetchGroups } = useGroupConversations();
|
||||
const { getLastRead } = useDMRead();
|
||||
const { markSeen: markDmEnvelopesSeen } = useDmUnreadSummary(!!user);
|
||||
const { isMuted } = useDmMute();
|
||||
// The viewer's crews — surfaced here as entry points to their private (MLS)
|
||||
// chat. `cordnGids` is a local IndexedDB read (no signer prompt) used to mark
|
||||
// which crews already have a chat on this device.
|
||||
|
|
@ -244,7 +247,7 @@ export default function DirectMessagesPage() {
|
|||
// and newer than the last time we opened that conversation.
|
||||
const isUnread = (conv: { pubkey: string; lastMessage?: { createdAt: number; pubkey: string } }) => {
|
||||
const lm = conv.lastMessage;
|
||||
if (!lm || lm.pubkey === user?.pubkey) return false;
|
||||
if (!lm || lm.pubkey === user?.pubkey || isMuted(conv.pubkey)) return false;
|
||||
return lm.createdAt > getLastRead(conv.pubkey);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { cn } from '@/lib/utils';
|
|||
import { EVENT_TYPE_LABELS, SKATE_CREW_KIND, SKATE_MUSICIAN_KIND } from '@/lib/types';
|
||||
import { Users } from 'lucide-react';
|
||||
import NotFound from './NotFound';
|
||||
import { Calendar, Clock, MapPin, ArrowLeft, Share2, Navigation, CalendarPlus, Pencil, Music, Megaphone, XCircle, CalendarClock } from 'lucide-react';
|
||||
import { Calendar, Clock, MapPin, ArrowLeft, Share2, Navigation, CalendarPlus, Pencil, Music, Megaphone, XCircle, CalendarClock, Ticket } from 'lucide-react';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { ControllerBadge } from '@/components/ControllerBadge';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
|
@ -146,6 +146,7 @@ export default function EventDetailPage() {
|
|||
|
||||
// NIP-52 RSVPs for this event (counts + attendee avatars).
|
||||
const rsvpSummary = useEventRsvps(event ? eventCoord(event) : undefined);
|
||||
const hasEventPass = !!user && !!event && (user.pubkey === event.pubkey || rsvpSummary.myStatus === 'accepted' || rsvpSummary.myStatus === 'tentative');
|
||||
|
||||
// Optional crowdfunding goal (sats) set by the host as a `goal` tag.
|
||||
const goalSats = Number(event?.event.tags.find(([t]) => t === 'goal')?.[1] ?? 0) || 0;
|
||||
|
|
@ -377,6 +378,18 @@ export default function EventDetailPage() {
|
|||
<CalendarPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasEventPass && (
|
||||
<Link to={`/card?type=event&event=${event.id}`}>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-9 w-9 bg-black/50 text-white backdrop-blur hover:bg-black/70"
|
||||
aria-label={t('placesUi.eventDetail.eventPass')}
|
||||
title={t('placesUi.eventDetail.eventPass')}
|
||||
>
|
||||
<Ticket className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleShare}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import { useNostrLogin } from '@nostrify/react/login';
|
|||
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
|
||||
import { useGroupConversation, useSendDirectMessage, getGroupId } from '@/hooks/useDirectMessages';
|
||||
import { useDMRead } from '@/hooks/useDMRead';
|
||||
import { useDmMute } from '@/hooks/useDmMute';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
|
||||
import { ArrowLeft, Lock, Loader2, RefreshCw, Send, Users } from 'lucide-react';
|
||||
import { ArrowLeft, Lock, Loader2, RefreshCw, Send, Users, Volume2, VolumeX } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function ParticipantName({ pubkey }: { pubkey: string }) {
|
||||
|
|
@ -56,7 +57,9 @@ export default function GroupConversationPage() {
|
|||
|
||||
const { data: messages, isLoading, refetch, isRefetching } = useGroupConversation(allParticipants);
|
||||
const { markRead } = useDMRead();
|
||||
const { isMuted, toggle: toggleMute } = useDmMute();
|
||||
const groupId = useMemo(() => getGroupId(allParticipants), [allParticipants]);
|
||||
const muted = isMuted(groupId);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
|
|
@ -156,6 +159,16 @@ export default function GroupConversationPage() {
|
|||
{t('socialUi.groupConversation.encryptedGroup')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||
title={muted ? t('walletUi.trust.unmute') : t('walletUi.trust.mute')}
|
||||
onClick={() => toggleMute(groupId)}
|
||||
className={cn('text-gray-400 hover:text-white', muted && 'text-amber-400')}
|
||||
>
|
||||
{muted ? <Volume2 className="h-4 w-4" /> : <VolumeX className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -224,8 +224,15 @@ export default function RiderCardPage() {
|
|||
// ── Page tab: 'card' | 'badges' | 'award' | 'passes' ─────────────────────
|
||||
const [pageTab, setPageTab] = useState<'card' | 'badges' | 'award' | 'passes'>('card');
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// ── Card state ────────────────────────────────────────────────────────────
|
||||
const [cardType, setCardType] = useState<'rider' | 'park' | 'crew' | 'event'>('rider');
|
||||
const requestedType = searchParams.get('type');
|
||||
const [cardType, setCardType] = useState<'rider' | 'park' | 'crew' | 'event'>(
|
||||
requestedType === 'park' || requestedType === 'crew' || requestedType === 'event'
|
||||
? requestedType
|
||||
: 'rider',
|
||||
);
|
||||
const [theme, setTheme] = useState<ThemeId>('thug');
|
||||
const [holographic, setHolographic] = useState(true);
|
||||
|
||||
|
|
@ -260,7 +267,7 @@ export default function RiderCardPage() {
|
|||
const activeCrew = memberships.find(m => m.crew.id === crewId)?.crew ?? memberships[0]?.crew;
|
||||
|
||||
// Event pass / wristband: events you organize, or that you RSVP'd going to.
|
||||
const [eventId, setEventId] = useState('');
|
||||
const [eventId, setEventId] = useState(() => searchParams.get('event') ?? '');
|
||||
const { data: allEvents } = useSkateEvents(200);
|
||||
const { data: rsvpCoords = [] } = useMyRsvpCoords(user?.pubkey);
|
||||
const myEvents = useMemo(() => {
|
||||
|
|
@ -291,7 +298,6 @@ export default function RiderCardPage() {
|
|||
|
||||
// Deep link from a venue page: /card?venue=<kind>:<pubkey>:<d> opens the park
|
||||
// pass editor with that venue preselected.
|
||||
const [searchParams] = useSearchParams();
|
||||
const venueParam = searchParams.get('venue');
|
||||
useEffect(() => {
|
||||
if (!venueParam) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue