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">
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue