feat: notes to self via self-DM

Backend:
- Removed self-DM restriction in conversation Create
- Added duplicate check for self-DMs (returns existing one)

Frontend:
- ConversationList: [📝] button creates/opens notes to self
- Self-DMs show as 'Notes' with 📝 icon in sidebar
- DMChat header shows 'Notes' for self-DMs
This commit is contained in:
2026-07-02 08:33:57 -04:00
parent 17c09edc4f
commit a5c3b25cc0
3 changed files with 67 additions and 22 deletions
+19 -5
View File
@@ -82,10 +82,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
memberSet[uid] = struct{}{} memberSet[uid] = struct{}{}
} }
} }
if len(memberSet) == 1 { // ponytail: allow self-DM (notes to self) — len(memberSet)==1 is fine
http.Error(w, `{"error":"cannot create conversation with yourself"}`, http.StatusBadRequest)
return
}
memberIDs := make([]string, 0, len(memberSet)) memberIDs := make([]string, 0, len(memberSet))
for uid := range memberSet { for uid := range memberSet {
@@ -94,7 +91,24 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
sort.Strings(memberIDs) sort.Strings(memberIDs)
// For a 1:1 DM, check if it already exists. // For a 1:1 DM, check if it already exists.
if len(memberIDs) == 2 { if len(memberIDs) == 1 {
var existingID string
err := h.db.QueryRowContext(r.Context(), `
SELECT c.id FROM conversations c
WHERE c.type = 'dm'
AND (SELECT COUNT(*) FROM conversation_members cm WHERE cm.conversation_id = c.id) = 1
AND EXISTS (SELECT 1 FROM conversation_members cm2 WHERE cm2.conversation_id = c.id AND cm2.user_id = $1)
LIMIT 1
`, memberIDs[0]).Scan(&existingID)
if err == nil {
h.getByID(w, r, existingID)
return
}
if !errors.Is(err, sql.ErrNoRows) {
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
return
}
} else if len(memberIDs) == 2 {
var existingID string var existingID string
err := h.db.QueryRowContext(r.Context(), ` err := h.db.QueryRowContext(r.Context(), `
SELECT c.id FROM conversations c SELECT c.id FROM conversations c
+38 -10
View File
@@ -8,11 +8,16 @@ function otherMemberName(conv: { members: { id: string; username: string }[] },
return other?.username || "unknown"; return other?.username || "unknown";
} }
function isSelfDM(conv: { members: { id: string }[] }, currentUserId: string) {
return conv.members.length === 1 && conv.members[0].id === currentUserId;
}
export function ConversationList() { export function ConversationList() {
const conversations = useConversationStore((s) => s.conversations); const conversations = useConversationStore((s) => s.conversations);
const activeId = useConversationStore((s) => s.activeConversationId); const activeId = useConversationStore((s) => s.activeConversationId);
const fetchConversations = useConversationStore((s) => s.fetchConversations); const fetchConversations = useConversationStore((s) => s.fetchConversations);
const setActive = useConversationStore((s) => s.setActiveConversation); const setActive = useConversationStore((s) => s.setActiveConversation);
const createConversation = useConversationStore((s) => s.createConversation);
const currentUser = useAuthStore((s) => s.user); const currentUser = useAuthStore((s) => s.user);
const [showNew, setShowNew] = useState(false); const [showNew, setShowNew] = useState(false);
@@ -20,25 +25,48 @@ export function ConversationList() {
fetchConversations(); fetchConversations();
}, [fetchConversations]); }, [fetchConversations]);
const openNotes = async () => {
// Find existing self-DM or create one
const existing = conversations.find((c) => isSelfDM(c, currentUser?.id || ""));
if (existing) {
setActive(existing.id);
return;
}
try {
await createConversation([]);
} catch { /* ignore */ }
};
return ( return (
<div className="h-full w-56 bg-gb-bg-s border-r border-gb-bg-t flex flex-col"> <div className="h-full w-56 bg-gb-bg-s border-r border-gb-bg-t flex flex-col">
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between"> <div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between">
<span>[DIRECT MESSAGES]</span> <span>[DIRECT MESSAGES]</span>
<button <div className="flex gap-1">
onClick={() => setShowNew(true)} <button
className="terminal-button text-xs" onClick={openNotes}
title="New conversation" className="terminal-button text-xs"
> title="Notes to self"
[+] >
</button> [📝]
</button>
<button
onClick={() => setShowNew(true)}
className="terminal-button text-xs"
title="New conversation"
>
[+]
</button>
</div>
</div> </div>
<div className="flex-1 overflow-y-auto p-2 font-mono text-sm"> <div className="flex-1 overflow-y-auto p-2 font-mono text-sm">
{conversations.length === 0 && ( {conversations.length === 0 && (
<p className="text-gb-fg-f">[no conversations]</p> <p className="text-gb-fg-f">[no conversations]</p>
)} )}
{conversations.map((conv) => { {conversations.map((conv) => {
const name = const self = isSelfDM(conv, currentUser?.id || "");
conv.type === "group_dm" const name = self
? "Notes"
: conv.type === "group_dm"
? conv.name || conv.members.map((m) => m.username).join(", ") ? conv.name || conv.members.map((m) => m.username).join(", ")
: otherMemberName(conv, currentUser?.id || ""); : otherMemberName(conv, currentUser?.id || "");
return ( return (
@@ -51,7 +79,7 @@ export function ConversationList() {
: "hover:bg-gb-bg-t text-gb-fg-s" : "hover:bg-gb-bg-t text-gb-fg-s"
}`} }`}
> >
<span className="text-gb-fg-f">@</span> <span className="text-gb-fg-f">{self ? "📝" : "@"}</span>
<span className="truncate">{name}</span> <span className="truncate">{name}</span>
</button> </button>
); );
+10 -7
View File
@@ -75,13 +75,16 @@ export function DMChat() {
} }
}; };
const title = conversation const selfDM = conversation && conversation.members.length === 1 && conversation.members[0].id === currentUser?.id;
? conversation.type === "group_dm" const title = selfDM
? conversation.name || conversation.members.map((m) => m.username).join(", ") ? "Notes"
: conversation.members.find((m) => m.id !== currentUser?.id)?.username || "DM" : conversation
: id ? conversation.type === "group_dm"
? "[LOADING...]" ? conversation.name || conversation.members.map((m) => m.username).join(", ")
: "[NO CONVERSATION]"; : conversation.members.find((m) => m.id !== currentUser?.id)?.username || "DM"
: id
? "[LOADING...]"
: "[NO CONVERSATION]";
return ( return (
<div className="flex flex-col h-full bg-gb-bg"> <div className="flex flex-col h-full bg-gb-bg">