feat: persist settings and enrich conversation context

This commit is contained in:
jjbrunton committed 2026-08-08 18:18:28 +01:00
1 parent a36723a325
commit 2c100d72b6
84 files changed
+2248 -644

No files matched your search

@@ -0,0 +1,72 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
export interface OriginPost {
title: string;
url: string;
imageUrl?: string | null;
subreddit?: string;
}
export function OriginPostCard({ post }: { post: OriginPost }) {
return (
<a aria-label={post.title} className="origin-post-card" href={post.url} rel="noreferrer" target="_blank">
{post.imageUrl ? <img alt={`Source post: ${post.title}`} loading="lazy" src={post.imageUrl} /> : null}
<span>
<small>Started from {post.subreddit ? `r/${post.subreddit}` : 'Reddit'}</small>
<strong>{post.title}</strong>
<span>Open original post</span>
</span>
</a>
);
}
export function OriginPostButton({ children, post }: { children: ReactNode; post: OriginPost }) {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!isOpen) return;
closeRef.current?.focus();
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false);
};
document.addEventListener('keydown', closeOnEscape);
return () => document.removeEventListener('keydown', closeOnEscape);
}, [isOpen]);
function close() {
setIsOpen(false);
queueMicrotask(() => triggerRef.current?.focus());
}
return (
<>
<button className="origin-post-trigger" onClick={() => setIsOpen(true)} ref={triggerRef} type="button">
{children}
</button>
{isOpen ? (
<div
aria-labelledby="origin-post-dialog-title"
aria-modal="true"
className="origin-post-backdrop"
onMouseDown={(event) => { if (event.target === event.currentTarget) close(); }}
role="dialog"
>
<article className="origin-post-dialog">
<div className="origin-post-dialog-heading">
<span>Conversation origin</span>
<button aria-label="Close source post" onClick={close} ref={closeRef} type="button">×</button>
</div>
{post.imageUrl ? <img alt={`Source post: ${post.title}`} loading="lazy" src={post.imageUrl} /> : null}
<div className="origin-post-dialog-copy">
{post.subreddit ? <small>r/{post.subreddit}</small> : null}
<h2 id="origin-post-dialog-title">{post.title}</h2>
<a className="button primary" href={post.url} rel="noreferrer" target="_blank">Open Reddit post</a>
</div>
</article>
</div>
) : null}
</>
);
}
@@ -0,0 +1,16 @@
import { Fragment } from 'react';
import { RedditUsernameLink } from './reddit-username-link.js';
const USER_MENTION = /(u\/[A-Za-z0-9_-]+|@?t2_[A-Za-z0-9]+(?::reddit\.com)?)/gi;
export function RedditUserText({ children }: { children: string }) {
return children.split(USER_MENTION).map((part, index) => {
if (/^u\/(?!t2_)[A-Za-z0-9_-]+$/i.test(part)) {
return <RedditUsernameLink key={`${part}-${index}`} username={part} />;
}
if (/^(?:@?t2_)/i.test(part) || /^u\/t2_/i.test(part)) {
return <Fragment key={`${part}-${index}`}>Reddit user</Fragment>;
}
return <Fragment key={`${part}-${index}`}>{part}</Fragment>;
});
}
@@ -0,0 +1,42 @@
export interface RedditUsernameLinkProps {
username: string;
showPrefix?: boolean;
}
export function normalizeRedditUsername(username: string): string {
return username.trim().replace(/^\/?u\//i, '');
}
export function isDisplayableRedditUsername(username: string): boolean {
const normalized = normalizeRedditUsername(username);
return normalized.length > 0
&& !/^(?:me|you|unknown|unknown reddit user)$/i.test(normalized)
&& !/^t2_/i.test(normalized)
&& !/^@t2_/i.test(normalized);
}
export function redditUsernameLabel(username: string, showPrefix = true): string {
if (!isDisplayableRedditUsername(username)) return 'Unknown Reddit user';
const normalized = normalizeRedditUsername(username);
return showPrefix ? `u/${normalized}` : normalized;
}
export function RedditUsernameLink({ username, showPrefix = true }: RedditUsernameLinkProps) {
const normalizedUsername = normalizeRedditUsername(username);
const label = redditUsernameLabel(username, showPrefix);
if (!isDisplayableRedditUsername(username)) {
return <span className="reddit-username-link is-unresolved">{label}</span>;
}
return (
<a
className="reddit-username-link"
href={`https://www.reddit.com/u/${encodeURIComponent(normalizedUsername)}`}
rel="noreferrer"
target="_blank"
>
{label}
</a>
);
}