Skip to content

Commit cfac473

Browse files
feat(moments): reply via create-moment drawer with Kylie assist
Replace the in-page reply textarea with a FAB that opens the shared moment composer in reply mode (dynamic title/placeholder). Replies reuse the same agentic voice layer; doppelganger ingest now learns from posts, replies, and Nostr voice samples. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1910c1e commit cfac473

3 files changed

Lines changed: 212 additions & 109 deletions

File tree

app/(app)/connect/post/[id]/PostViewClient.tsx

Lines changed: 55 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { useParams, useRouter } from 'next/navigation';
55
import { useAuth } from '@/lib/auth';
66
import { useNostrIdentity } from '@/hooks/useNostrIdentity';
77
import {
8-
createMomentComment,
98
loadMomentEngagement,
109
parseMomentRouteId,
1110
toggleMomentLike,
@@ -18,7 +17,7 @@ import { UsersService } from '@/lib/services/users';
1817
import { fetchNostrEventById } from '@/lib/nostr/thread';
1918
import type { NostrEvent } from '@/lib/nostr/nostr';
2019
import {
21-
ArrowLeft, Globe, Heart, Lock, MessageCircle,
20+
ArrowLeft, Globe, Heart, Lock, MessageCircle, PenLine,
2221
Repeat2, Shield, Zap, X, Share2
2322
} from 'lucide-react';
2423
import { useUnifiedDrawer } from '@/context/UnifiedDrawerContext';
@@ -234,7 +233,6 @@ export function PostViewClient({
234233
const [reposts, setReposts] = useState(0);
235234
const [liked, setLiked] = useState(false);
236235
const [loading, setLoading] = useState(!preview?.content);
237-
const [replyContent, setReplyContent] = useState('');
238236
const [busy, setBusy] = useState(false);
239237

240238
// For Nostr event kind detection
@@ -372,42 +370,50 @@ export function PostViewClient({
372370
} finally { setBusy(false); }
373371
};
374372

375-
const sendReply = async () => {
376-
const text = replyContent.trim();
377-
if (!momentId || !text || busy) return;
373+
const openReplyComposer = () => {
374+
if (!momentId) return;
378375
if (source === 'nostr' && (isVaultLocked || !identity)) {
379-
toast.error('Unlock vault to comment on Nostr'); void unlockAndLoad(); return;
376+
toast.error('Unlock vault to reply on Nostr');
377+
void unlockAndLoad();
378+
return;
380379
}
381-
if (source === 'ecosystem' && !user) return;
382-
setBusy(true);
383-
try {
384-
const words = `${text} ${moment?.caption || moment?.content || ''}`.toLowerCase().match(/#?\w{3,}/g) || [];
385-
const topics = Array.from(new Set(words.slice(0, 10)));
386-
void import('@/lib/connect/feed-settings').then(({ recordFeedInteraction }) =>
387-
recordFeedInteraction({ topics, searchWeight: 3, isConsciousAction: true }),
388-
);
389-
390-
const created = await createMomentComment({
391-
source,
392-
id: momentId,
393-
content: text,
394-
userId: user?.$id,
395-
privateKeyBytes: identity?.privateKeyBytes,
396-
nsec: identity?.nsec,
397-
rootPubkey: moment?.pubkey || nostrEvent?.pubkey,
398-
nostrId: (moment as any)?.nostrId,
380+
if (source === 'ecosystem' && !user) {
381+
openUnifiedDrawer('login', {
382+
title: 'Reply to moment',
383+
subtitle: 'Sign in to reply with Kylie assist.',
384+
objectKind: 'moment',
399385
});
400-
setReplyContent('');
401-
if (created) {
402-
setReplies(prev => [...prev, created]);
386+
return;
387+
}
388+
const snippet = String(moment?.caption || moment?.content || preview?.content || '').trim();
389+
openUnifiedDrawer('moment-composer', {
390+
mode: 'reply',
391+
parentMomentId: momentId,
392+
source,
393+
parentSnippet: snippet.slice(0, 120),
394+
rootPubkey: moment?.pubkey || nostrEvent?.pubkey,
395+
nostrId: (moment as any)?.nostrId || (source === 'nostr' ? momentId : undefined),
396+
});
397+
};
398+
399+
useEffect(() => {
400+
const onReply = (ev: Event) => {
401+
const detail = (ev as CustomEvent).detail || {};
402+
if (String(detail.parentMomentId || '') !== String(momentId)) return;
403+
if (detail.comment) {
404+
setReplies((prev) => {
405+
if (prev.some((r) => r.id === detail.comment.id)) return prev;
406+
return [...prev, detail.comment];
407+
});
403408
} else {
404-
const refreshed = await loadMomentEngagement({ source, id: momentId, userId: user?.$id });
405-
setReplies(refreshed.comments);
409+
void loadMomentEngagement({ source, id: momentId, userId: user?.$id }).then((eng) => {
410+
setReplies(eng.comments);
411+
});
406412
}
407-
} catch (e) {
408-
console.error(e); toast.error('Could not post reply');
409-
} finally { setBusy(false); }
410-
};
413+
};
414+
window.addEventListener('kylrix:moment-reply-created', onReply);
415+
return () => window.removeEventListener('kylrix:moment-reply-created', onReply);
416+
}, [momentId, source, user?.$id]);
411417

412418
const handleShare = () => {
413419
if (!momentId) return;
@@ -500,14 +506,9 @@ export function PostViewClient({
500506
</div>
501507
</article>
502508

503-
{/* Reply composer skeleton */}
504-
<div className="flex gap-2 items-end min-w-0 max-w-full">
505-
<div className="min-w-0 flex-1 h-[42px] rounded-xl bg-[#000000] border-2 border-white/20 px-4 flex items-center text-sm text-white/50 font-satoshi">
506-
Write a reply…
507-
</div>
508-
<div className="shrink-0 h-[42px] px-4 rounded-xl bg-[#F59E0B]/30 text-black/70 font-extrabold text-sm flex items-center">
509-
Reply
510-
</div>
509+
{/* Reply FAB skeleton */}
510+
<div className="flex justify-end pt-2">
511+
<div className="h-14 w-14 rounded-2xl bg-[#F59E0B]/40 border-2 border-white/10" />
511512
</div>
512513
</div>
513514
</div>
@@ -710,59 +711,25 @@ export function PostViewClient({
710711
</article>
711712
)}
712713

713-
{/* Reply composer — shown for posts and replies, not reactions/reposts */}
714+
{/* Reply FAB → same create-moment drawer (Kylie assist) */}
714715
{!isReaction && !isRepost && (
715-
<div className="flex gap-2 items-end min-w-0 max-w-full">
716-
<textarea
717-
value={replyContent}
718-
onChange={e => {
719-
setReplyContent(e.target.value);
720-
const target = e.target;
721-
target.style.height = 'auto';
722-
target.style.height = `${Math.min(target.scrollHeight, 140)}px`;
723-
}}
724-
onKeyDown={e => {
725-
if (e.key === 'Enter' && !e.shiftKey) {
726-
e.preventDefault();
727-
void sendReply();
728-
}
729-
}}
730-
rows={1}
731-
autoComplete="off"
732-
autoCorrect="off"
733-
autoCapitalize="none"
734-
spellCheck={false}
735-
data-gramm="false"
736-
placeholder={source === 'nostr' && isVaultLocked ? 'Unlock vault to reply…' : 'Write a reply…'}
737-
className="min-w-0 flex-1 rounded-xl bg-[#000000] border-2 border-white/20 px-4 py-2.5 text-sm outline-none focus:border-white/40 resize-none max-h-[140px] leading-relaxed text-white font-satoshi placeholder:text-white/40"
738-
/>
739-
{source === 'nostr' && isVaultLocked ? (
740-
<button
741-
type="button"
742-
onClick={() => void unlockAndLoad()}
743-
className="shrink-0 h-[42px] rounded-xl bg-[#F59E0B]/15 text-[#F59E0B] font-bold text-sm px-3 inline-flex items-center gap-1.5 border-2 border-[#F59E0B]/40"
744-
>
745-
<Lock size={14} /> Unlock
746-
</button>
747-
) : (
748-
<button
749-
type="button"
750-
disabled={busy || !replyContent.trim() || (source === 'ecosystem' && !user)}
751-
onClick={() => void sendReply()}
752-
className="shrink-0 h-[42px] rounded-xl bg-[#F59E0B] text-black font-extrabold text-sm px-4 disabled:opacity-40 transition-opacity cursor-pointer"
753-
>
754-
Reply
755-
</button>
756-
)}
757-
</div>
716+
<button
717+
type="button"
718+
onClick={openReplyComposer}
719+
className="fixed bottom-6 right-5 z-[40] h-14 w-14 rounded-2xl bg-[#F59E0B] text-black shadow-[0_8px_24px_rgba(245,158,11,0.35)] border-2 border-black/20 inline-flex items-center justify-center hover:scale-105 active:scale-95 transition-transform cursor-pointer"
720+
title="Reply"
721+
aria-label="Reply with assist"
722+
>
723+
<PenLine size={22} strokeWidth={2.5} />
724+
</button>
758725
)}
759726

760727
{/* Replies list */}
761728
{!isReaction && !isRepost && (
762-
<ul className="space-y-2 min-w-0 max-w-full list-none p-0 m-0">
729+
<ul className="space-y-2 min-w-0 max-w-full list-none p-0 m-0 pb-24">
763730
{replies.length === 0 ? (
764731
<li className="rounded-[18px] border-2 border-white/20 bg-[#000000] px-4 py-8 text-center text-sm text-white/50">
765-
No comments yet
732+
No comments yet — tap the reply button to write with Kylie assist
766733
</li>
767734
) : (
768735
replies.map(r => (

0 commit comments

Comments
 (0)