mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 16:07:15 +02:00
Allow viewing chat without account
Previously, chat required an active account to even view messages, blocking users from browsing public group chats or live activity chats before signing in. Changes: - Remove account requirement from NIP-29 adapter resolveConversation() - Remove account requirement from NIP-53 adapter resolveConversation() - Replace "Sign in to send messages" text with interactive login button - Add LoginDialog to ChatViewer for one-click sign-in Users can now view all chat messages without logging in, and only need to sign in when they want to send messages, join groups, or perform other write operations.
This commit is contained in:
@@ -30,6 +30,7 @@ import { parseSlashCommand } from "@/lib/chat/slash-command-parser";
|
||||
import { UserName } from "./nostr/UserName";
|
||||
import { RichText } from "./nostr/RichText";
|
||||
import Timestamp from "./Timestamp";
|
||||
import LoginDialog from "./nostr/LoginDialog";
|
||||
import { ReplyPreview } from "./chat/ReplyPreview";
|
||||
import { MembersDropdown } from "./chat/MembersDropdown";
|
||||
import { RelaysDropdown } from "./chat/RelaysDropdown";
|
||||
@@ -339,6 +340,9 @@ export function ChatViewer({
|
||||
const activeAccount = use$(accountManager.active$);
|
||||
const hasActiveAccount = !!activeAccount;
|
||||
|
||||
// Login dialog state
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
|
||||
// Profile search for mentions
|
||||
const { searchProfiles } = useProfileSearch();
|
||||
|
||||
@@ -703,238 +707,251 @@ export function ChatViewer({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header with conversation info and controls */}
|
||||
<div className="pl-2 pr-0 border-b w-full py-0.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-1 min-w-0 items-center gap-2">
|
||||
{headerPrefix}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="text-sm font-semibold truncate cursor-help text-left">
|
||||
{customTitle || conversation.title}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="max-w-md p-3"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Icon + Name */}
|
||||
<div className="flex items-center gap-2">
|
||||
{conversation.metadata?.icon && (
|
||||
<img
|
||||
src={conversation.metadata.icon}
|
||||
alt=""
|
||||
className="size-6 rounded object-cover flex-shrink-0"
|
||||
onError={(e) => {
|
||||
// Hide image if it fails to load
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="font-semibold">
|
||||
{conversation.title}
|
||||
</span>
|
||||
</div>
|
||||
{/* Description */}
|
||||
{conversation.metadata?.description && (
|
||||
<p className="text-xs text-primary-foreground/90">
|
||||
{conversation.metadata.description}
|
||||
</p>
|
||||
)}
|
||||
{/* Protocol Type - Clickable */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleNipClick();
|
||||
}}
|
||||
className="rounded bg-primary-foreground/20 px-1.5 py-0.5 font-mono hover:bg-primary-foreground/30 transition-colors cursor-pointer text-primary-foreground"
|
||||
>
|
||||
{conversation.protocol.toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<span className="text-primary-foreground/60">•</span>
|
||||
)}
|
||||
<span className="capitalize text-primary-foreground/80">
|
||||
{conversation.type}
|
||||
</span>
|
||||
</div>
|
||||
{/* Live Activity Status */}
|
||||
{liveActivity?.status && (
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-primary-foreground/80">
|
||||
Status:
|
||||
<>
|
||||
<LoginDialog open={showLogin} onOpenChange={setShowLogin} />
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header with conversation info and controls */}
|
||||
<div className="pl-2 pr-0 border-b w-full py-0.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-1 min-w-0 items-center gap-2">
|
||||
{headerPrefix}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="text-sm font-semibold truncate cursor-help text-left">
|
||||
{customTitle || conversation.title}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="max-w-md p-3"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Icon + Name */}
|
||||
<div className="flex items-center gap-2">
|
||||
{conversation.metadata?.icon && (
|
||||
<img
|
||||
src={conversation.metadata.icon}
|
||||
alt=""
|
||||
className="size-6 rounded object-cover flex-shrink-0"
|
||||
onError={(e) => {
|
||||
// Hide image if it fails to load
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="font-semibold">
|
||||
{conversation.title}
|
||||
</span>
|
||||
<StatusBadge status={liveActivity.status} size="xs" />
|
||||
</div>
|
||||
)}
|
||||
{/* Host Info */}
|
||||
{liveActivity?.hostPubkey && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-primary-foreground/80">
|
||||
<span>Host:</span>
|
||||
<UserName
|
||||
pubkey={liveActivity.hostPubkey}
|
||||
className="text-xs text-primary-foreground"
|
||||
/>
|
||||
{/* Description */}
|
||||
{conversation.metadata?.description && (
|
||||
<p className="text-xs text-primary-foreground/90">
|
||||
{conversation.metadata.description}
|
||||
</p>
|
||||
)}
|
||||
{/* Protocol Type - Clickable */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleNipClick();
|
||||
}}
|
||||
className="rounded bg-primary-foreground/20 px-1.5 py-0.5 font-mono hover:bg-primary-foreground/30 transition-colors cursor-pointer text-primary-foreground"
|
||||
>
|
||||
{conversation.protocol.toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<span className="text-primary-foreground/60">•</span>
|
||||
)}
|
||||
<span className="capitalize text-primary-foreground/80">
|
||||
{conversation.type}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
|
||||
<MembersDropdown participants={derivedParticipants} />
|
||||
<RelaysDropdown conversation={conversation} />
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={handleNipClick}
|
||||
className="rounded bg-muted px-1.5 py-0.5 font-mono hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
>
|
||||
{conversation.protocol.toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
{/* Live Activity Status */}
|
||||
{liveActivity?.status && (
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-primary-foreground/80">
|
||||
Status:
|
||||
</span>
|
||||
<StatusBadge status={liveActivity.status} size="xs" />
|
||||
</div>
|
||||
)}
|
||||
{/* Host Info */}
|
||||
{liveActivity?.hostPubkey && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-primary-foreground/80">
|
||||
<span>Host:</span>
|
||||
<UserName
|
||||
pubkey={liveActivity.hostPubkey}
|
||||
className="text-xs text-primary-foreground"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
|
||||
<MembersDropdown participants={derivedParticipants} />
|
||||
<RelaysDropdown conversation={conversation} />
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
<button
|
||||
onClick={handleNipClick}
|
||||
className="rounded bg-muted px-1.5 py-0.5 font-mono hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
>
|
||||
{conversation.protocol.toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message timeline with virtualization */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{messagesWithMarkers && messagesWithMarkers.length > 0 ? (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messagesWithMarkers}
|
||||
initialTopMostItemIndex={messagesWithMarkers.length - 1}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Header: () =>
|
||||
hasMore && conversationResult.status === "success" ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
disabled={isLoadingOlder}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
{/* Message timeline with virtualization */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{messagesWithMarkers && messagesWithMarkers.length > 0 ? (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messagesWithMarkers}
|
||||
initialTopMostItemIndex={messagesWithMarkers.length - 1}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Header: () =>
|
||||
hasMore && conversationResult.status === "success" ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
disabled={isLoadingOlder}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{isLoadingOlder ? (
|
||||
<>
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
<span className="text-xs">Loading...</span>
|
||||
</>
|
||||
) : (
|
||||
"Load older messages"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : null,
|
||||
}}
|
||||
itemContent={(_index, item) => {
|
||||
if (item.type === "day-marker") {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center py-2"
|
||||
key={`marker-${item.timestamp}`}
|
||||
>
|
||||
{isLoadingOlder ? (
|
||||
<>
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
<span className="text-xs">Loading...</span>
|
||||
</>
|
||||
) : (
|
||||
"Load older messages"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : null,
|
||||
}}
|
||||
itemContent={(_index, item) => {
|
||||
if (item.type === "day-marker") {
|
||||
<Label className="text-[10px] text-muted-foreground">
|
||||
{item.data}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center py-2"
|
||||
key={`marker-${item.timestamp}`}
|
||||
>
|
||||
<Label className="text-[10px] text-muted-foreground">
|
||||
{item.data}
|
||||
</Label>
|
||||
</div>
|
||||
<MessageItem
|
||||
key={item.data.id}
|
||||
message={item.data}
|
||||
adapter={adapter}
|
||||
conversation={conversation}
|
||||
onReply={handleReply}
|
||||
canReply={hasActiveAccount}
|
||||
onScrollToMessage={handleScrollToMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MessageItem
|
||||
key={item.data.id}
|
||||
message={item.data}
|
||||
adapter={adapter}
|
||||
conversation={conversation}
|
||||
onReply={handleReply}
|
||||
canReply={hasActiveAccount}
|
||||
onScrollToMessage={handleScrollToMessage}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
/>
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
No messages yet. Start the conversation!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message composer - only show if user has active account */}
|
||||
{hasActiveAccount ? (
|
||||
<div className="border-t px-2 py-1 pb-0">
|
||||
{replyTo && (
|
||||
<ComposerReplyPreview
|
||||
replyToId={replyTo}
|
||||
onClear={() => setReplyTo(undefined)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="flex-shrink-0 size-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={openUpload}
|
||||
disabled={isSending}
|
||||
>
|
||||
<Paperclip className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Attach media</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<MentionEditor
|
||||
ref={editorRef}
|
||||
placeholder="Type a message..."
|
||||
searchProfiles={searchProfiles}
|
||||
searchEmojis={searchEmojis}
|
||||
searchCommands={searchCommands}
|
||||
onCommandExecute={handleCommandExecute}
|
||||
onSubmit={(content, emojiTags, blobAttachments) => {
|
||||
if (content.trim()) {
|
||||
handleSend(content, replyTo, emojiTags, blobAttachments);
|
||||
}
|
||||
}}
|
||||
className="flex-1 min-w-0"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-shrink-0 h-7 px-2 text-xs"
|
||||
disabled={isSending}
|
||||
onClick={() => {
|
||||
editorRef.current?.submit();
|
||||
}}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
"Send"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{uploadDialog}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
No messages yet. Start the conversation!
|
||||
<div className="border-t px-3 py-2 text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowLogin(true)}
|
||||
>
|
||||
Sign in to send messages
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message composer - only show if user has active account */}
|
||||
{hasActiveAccount ? (
|
||||
<div className="border-t px-2 py-1 pb-0">
|
||||
{replyTo && (
|
||||
<ComposerReplyPreview
|
||||
replyToId={replyTo}
|
||||
onClear={() => setReplyTo(undefined)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="flex-shrink-0 size-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={openUpload}
|
||||
disabled={isSending}
|
||||
>
|
||||
<Paperclip className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Attach media</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<MentionEditor
|
||||
ref={editorRef}
|
||||
placeholder="Type a message..."
|
||||
searchProfiles={searchProfiles}
|
||||
searchEmojis={searchEmojis}
|
||||
searchCommands={searchCommands}
|
||||
onCommandExecute={handleCommandExecute}
|
||||
onSubmit={(content, emojiTags, blobAttachments) => {
|
||||
if (content.trim()) {
|
||||
handleSend(content, replyTo, emojiTags, blobAttachments);
|
||||
}
|
||||
}}
|
||||
className="flex-1 min-w-0"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-shrink-0 h-7 px-2 text-xs"
|
||||
disabled={isSending}
|
||||
onClick={() => {
|
||||
editorRef.current?.submit();
|
||||
}}
|
||||
>
|
||||
{isSending ? <Loader2 className="size-3 animate-spin" /> : "Send"}
|
||||
</Button>
|
||||
</div>
|
||||
{uploadDialog}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t px-3 py-2 text-center text-sm text-muted-foreground">
|
||||
Sign in to send messages
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,11 +116,6 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
throw new Error("NIP-29 groups require a relay URL");
|
||||
}
|
||||
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (!activePubkey) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[NIP-29] Fetching group metadata for ${groupId} from ${relayUrl}`,
|
||||
);
|
||||
|
||||
@@ -93,11 +93,6 @@ export class Nip53Adapter extends ChatProtocolAdapter {
|
||||
const { pubkey, identifier: dTag } = identifier.value;
|
||||
const relayHints = identifier.relays || [];
|
||||
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (!activePubkey) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[NIP-53] Fetching live activity ${dTag} by ${pubkey.slice(0, 8)}...`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user