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">
{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">
<MessageCircle className="h-4 w-4" />
</Button>

View file

@ -3,12 +3,14 @@ import { useParams, Link, useSearchParams } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
import { Layout } from '@/components/Layout';
import { LoginArea } from '@/components/auth/LoginArea';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import { useNostrLogin } from '@nostrify/react/login';
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
import { useConversation, useSendDirectMessage, type DirectMessage } from '@/hooks/useDirectMessages';
@ -70,6 +72,7 @@ export default function ConversationPage() {
const { npub } = useParams<{ npub: string }>();
const [searchParams] = useSearchParams();
const { user } = useCurrentUser();
const { toast } = useToast();
const { logins } = useNostrLogin();
const messagesEndRef = useRef<HTMLDivElement>(null);
@ -132,6 +135,15 @@ export default function ConversationPage() {
onSuccess: () => {
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">
<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>
<div className="mt-6 flex justify-center">
<LoginArea className="max-w-60" />
</div>
</CardContent>
</Card>
</div>

View file

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

View file

@ -1156,7 +1156,15 @@ export default function SettingsPage() {
const [searchParams] = useSearchParams();
// 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 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.
</p>
{hasNip05 ? (
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase w-full">
<Shield className="h-4 w-4" />
Claim This Shop
</Button>
<div>
<p className="text-xs text-gray-400 mb-3">
To claim this shop, add a NIP-05 identifier that matches the shop's website domain.
</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>
<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">
Add NIP-05 in Settings
</Button>
</a>
</Link>
</div>
)}
</div>

View file

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