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:
parent
ff505ef885
commit
f4192e2f3d
6 changed files with 46 additions and 11 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue