fix(ux): Batch A part 1 — dead flows

- SpotDetailPage: 'Claim This Shop' was a no-op button; wire to NIP-05 settings
  (and stop the full-page <a href> reload)
- TourDetailPage: 'Contact Guide to Book' used /messages?to= which nothing read,
  so booking silently opened an empty inbox -> /messages/:npub
- ContactsPage: row 'Message' went to the generic inbox -> the contact's thread
- SettingsPage: ?section=follows|language|stash rendered a blank dead end;
  unknown sections now fall back to the menu
- DirectMessages/Conversation: logged-out states were dead ends -> add LoginArea
- Conversation: failed sends were silent -> error toast
This commit is contained in:
SkateSpots 2026-09-26 17:43:19 +07:00
parent ff505ef885
commit f4192e2f3d
6 changed files with 46 additions and 11 deletions

View file

@ -72,7 +72,7 @@ function ContactRow({
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
{zapTarget && <ZapButton target={zapTarget} className="text-xs" showCount={false} />} {zapTarget && <ZapButton target={zapTarget} className="text-xs" showCount={false} />}
<Link to="/messages" title="Message"> <Link to={`/messages/${npub}`} title="Message">
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-400 hover:text-teal-400"> <Button variant="ghost" size="icon" className="h-8 w-8 text-gray-400 hover:text-teal-400">
<MessageCircle className="h-4 w-4" /> <MessageCircle className="h-4 w-4" />
</Button> </Button>

View file

@ -3,12 +3,14 @@ import { useParams, Link, useSearchParams } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { LoginArea } from '@/components/auth/LoginArea';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import { useNostrLogin } from '@nostrify/react/login'; import { useNostrLogin } from '@nostrify/react/login';
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities'; import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
import { useConversation, useSendDirectMessage, type DirectMessage } from '@/hooks/useDirectMessages'; import { useConversation, useSendDirectMessage, type DirectMessage } from '@/hooks/useDirectMessages';
@ -70,6 +72,7 @@ export default function ConversationPage() {
const { npub } = useParams<{ npub: string }>(); const { npub } = useParams<{ npub: string }>();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { toast } = useToast();
const { logins } = useNostrLogin(); const { logins } = useNostrLogin();
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
@ -132,6 +135,15 @@ export default function ConversationPage() {
onSuccess: () => { onSuccess: () => {
setNewMessage(''); setNewMessage('');
}, },
onError: (error) => {
// Without this a failed send is silent (common with interactive
// signers / relay hiccups) and the draft just sits there.
toast({
title: 'Message failed to send',
description: error instanceof Error ? error.message : 'Please try again in a moment.',
variant: 'destructive',
});
},
} }
); );
}; };
@ -157,6 +169,9 @@ export default function ConversationPage() {
<CardContent className="py-16 text-center"> <CardContent className="py-16 text-center">
<Lock className="h-16 w-16 mx-auto mb-4 text-gray-600" /> <Lock className="h-16 w-16 mx-auto mb-4 text-gray-600" />
<h2 className="text-xl font-bold mb-2">Log in to view messages</h2> <h2 className="text-xl font-bold mb-2">Log in to view messages</h2>
<div className="mt-6 flex justify-center">
<LoginArea className="max-w-60" />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { LoginArea } from '@/components/auth/LoginArea';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
@ -179,6 +180,9 @@ export default function DirectMessagesPage() {
<p className="text-gray-400"> <p className="text-gray-400">
Private, encrypted messaging with skaters in the community Private, encrypted messaging with skaters in the community
</p> </p>
<div className="mt-6 flex justify-center">
<LoginArea className="max-w-60" />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -1156,7 +1156,15 @@ export default function SettingsPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
// Allow deep-linking into a specific section via ?section=profile etc. // Allow deep-linking into a specific section via ?section=profile etc.
const initialSection = (searchParams.get('section') as Section) ?? null; // Only sections that actually render are accepted; anything else (e.g. a
// stale bookmark to ?section=language) falls back to the menu rather than a
// blank screen with no way back.
const RENDERABLE_SECTIONS: Section[] = [
'profile', 'security', 'nip05', 'mydata', 'relays', 'crewnetwork', 'appearance', 'wallet', 'advanced',
];
const requestedSection = searchParams.get('section') as Section;
const initialSection =
requestedSection && RENDERABLE_SECTIONS.includes(requestedSection) ? requestedSection : null;
const [section, setSection] = useState<Section>(initialSection); const [section, setSection] = useState<Section>(initialSection);
const handleNavigate = (s: Section) => setSection(s); const handleNavigate = (s: Section) => setSection(s);

View file

@ -510,18 +510,25 @@ export default function SpotDetailPage() {
If you own or manage this shop, you can claim it to verify ownership and manage the listing. If you own or manage this shop, you can claim it to verify ownership and manage the listing.
</p> </p>
{hasNip05 ? ( {hasNip05 ? (
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full"> <div>
<Shield className="h-4 w-4" /> <p className="text-xs text-gray-400 mb-3">
Claim This Shop To claim this shop, add a NIP-05 identifier that matches the shop's website domain.
</Button> </p>
<Link to="/settings">
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full">
<Shield className="h-4 w-4" />
Manage NIP-05 to Claim
</Button>
</Link>
</div>
) : ( ) : (
<div> <div>
<p className="text-xs text-yellow-400 mb-2">⚠️ NIP-05 verification required to claim shops</p> <p className="text-xs text-yellow-400 mb-2">⚠️ NIP-05 verification required to claim shops</p>
<a href="/settings"> <Link to="/settings">
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold"> <Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
Add NIP-05 in Settings Add NIP-05 in Settings
</Button> </Button>
</a> </Link>
</div> </div>
)} )}
</div> </div>

View file

@ -54,9 +54,10 @@ export default function TourDetailPage() {
const handleBookTour = () => { const handleBookTour = () => {
if (!tour) return; if (!tour) return;
// Navigate to messages/DM with the guide // Open a DM with the guide. Must use the same /messages/:npub route every
// For now, navigate to messages page - in future can open DM composer // other entry point uses — the old `/messages?to=` param was ignored, so
navigate(`/messages?to=${tour.pubkey}`); // the CTA silently opened an empty inbox.
navigate(`/messages/${nip19.npubEncode(tour.pubkey)}`);
}; };
const handleShare = async () => { const handleShare = async () => {