fix string comments more
2089 unmodified linesExpand all2090209120922093209420952096209720981781 unmodified linesExpand all2089 unmodified linesExpand all}
.untangled-thread-reply { margin-left: -1rem; width: calc(100% + 1rem) !important; display: flex; gap: 0.5rem; padding: 1rem 1rem 1rem 0;}1781 unmodified linesExpand all15 unmodified lines1617181920212223242524252627282930313233343335363537383940411584 unmodified linesExpand all16261627162816291630163116321633163410 unmodified lines164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169916481700170117021703170417051706170717081657170917101711171217131714171516641716171717181667171917201721167016711672167316741675167616771678167917221723172417251726172717281729173016801731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180417 unmodified lines18221823182418251710182618271828182918306 unmodified lines183718381839184017251841184218431844184524 unmodified lines15 unmodified lines CircleDot, GitPullRequest, LoaderCircle, MessageSquarePlus, Reply, X,} from 'lucide-solid';import { useAuth } from '../lib/auth';import { resolveActor } from '../lib/api/identity';import { marked } from 'marked';import { A, useNavigate } from '@solidjs/router';import { For, Show, createMemo, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js';import { A, useNavigate, useLocation } from '@solidjs/router';import { For, Show, createMemo, createSignal, createEffect, onCleanup, onMount, type Component, type JSX } from 'solid-js';import type { RepoContext, TreeEntry } from '../lib/api/repos';import type { Did } from '@atcute/lexicons/syntax';import { createQuery } from '@tanstack/solid-query';import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos';import { getRepoStarCount } from '../lib/api/stars';import { getRepoIssueCount } from '../lib/api/issues';import { getRepoPullCount } from '../lib/api/pulls';import { formatRelativeTime, languageColor, encodePath, formatLanguagePercent, blobHref, treeHref } from '../lib/repo-utils';import { formatRelativeTime, languageColor, encodePath, formatLanguagePercent, blobHref, treeHref, getErrorMessage } from '../lib/repo-utils';import { getTangledAppviewService } from '../lib/settings';import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles } from './common';import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles, textareaStyles } from './common';import { useRepoPreloader } from '../lib/preloading';
marked.setOptions({1584 unmodified linesExpand allexport { CodeView, DiffView, PullDiffView } from './code-view';
export interface GenericComment { uri: string; cid?: string; rkey: string; author: { did: string; handle: string }; value: { body: string;10 unmodified linesexport const CommentThreadsSection: Component<{ threads: GenericCommentThread[]; authorDid?: string; title?: string; onReplySubmit?: (body: string, parentComment: GenericComment) => Promise<void>;}> = (props) => { const auth = useAuth(); const location = useLocation(); const [replyingTo, setReplyingTo] = createSignal<GenericComment | null>(null); const [replyBody, setReplyBody] = createSignal(''); const [replyWorking, setReplyWorking] = createSignal(false); const [replyError, setReplyError] = createSignal<string | null>(null);
createEffect(() => { const hash = location.hash; const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash; if (cleanHash && cleanHash.startsWith('comment-') && props.threads.length > 0) { setTimeout(() => { const element = document.getElementById(cleanHash); if (element) { // Remove highlight from any other elements first document.querySelectorAll('.untangled-comment-highlight').forEach((el) => { el.classList.remove('untangled-comment-highlight'); }); element.scrollIntoView({ behavior: 'smooth', block: 'center' }); element.classList.add('untangled-comment-highlight'); } }, 150); } });
const viewerQuery = createQuery(() => ({ queryKey: ['viewer', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => resolveActor(auth.currentDid()!), }));
const handleReplySubmit = async (e: SubmitEvent, parent: GenericComment) => { e.preventDefault(); if (!props.onReplySubmit || !replyBody().trim() || replyWorking()) return;
setReplyWorking(true); setReplyError(null); try { await props.onReplySubmit(replyBody(), parent); setReplyBody(''); setReplyingTo(null); } catch (err) { setReplyError(getErrorMessage(err)); } finally { setReplyWorking(false); } };
return ( <div class="mt-6 space-y-4"> <div class={clsx(props.title && 'mt-6', 'space-y-4')}> <Show when={props.title}> <h3 class="text-lg font-bold border-b border-gray-200 dark:border-gray-700 pb-2 dark:text-white mb-4"> {props.title} </h3> </Show> <div class="space-y-4"> <For each={props.threads}> {(thread) => ( <div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50"> <div class="untangled-thread-group"> <ThreadCommentView id={`comment-${thread.item.rkey}`} author={thread.item.author} createdAt={thread.item.value.createdAt} markdown={thread.item.value.body} isAuthor={props.authorDid ? thread.item.author.did === props.authorDid : false} class="rounded px-6 py-4 bg-white dark:bg-gray-800" class="untangled-thread-comment" /> <Show when={thread.replies.length > 0}> <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> <div class="untangled-thread-replies-list"> <For each={thread.replies}> {(reply) => ( <div class="-ml-4"> <ThreadCommentView id={`comment-${reply.rkey}`} author={reply.author} createdAt={reply.value.createdAt} markdown={reply.value.body} isAuthor={props.authorDid ? reply.author.did === props.authorDid : false} isReply={true} class="py-4 pr-4" /> <ThreadCommentView id={`comment-${reply.rkey}`} author={reply.author} createdAt={reply.value.createdAt} markdown={reply.value.body} isAuthor={props.authorDid ? reply.author.did === props.authorDid : false} isReply={true} class="untangled-thread-reply" /> </div> )} </For> </div> </Show>
<Show when={props.onReplySubmit && auth.agent()}> <Show when={replyingTo()?.uri === thread.item.uri} fallback={ <button type="button" class="untangled-thread-reply-trigger" onClick={() => { setReplyingTo(thread.item); setReplyBody(''); setReplyError(null); }} > <Show when={auth.currentDid()}> {(did) => <Avatar did={did()} size="size-8 mr-1" />} </Show> <span class="w-full p-0">Leave a reply...</span> </button> } > <form onSubmit={(event) => void handleReplySubmit(event, thread.item)} class="untangled-thread-reply-form" > <Show when={viewerQuery.data}> {(viewer) => ( <div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"> <Avatar did={viewer().did} size="size-6" /> <span>{viewer().handle || viewer().did}</span> </div> )} </Show> <textarea value={replyBody()} onInput={(event) => setReplyBody(event.currentTarget.value)} class={textareaStyles()} rows="3" placeholder="Leave a reply..." /> <Show when={replyError()}> <div class="text-sm text-red-500 mt-1">{replyError()}</div> </Show> <div class="flex flex-wrap items-stretch justify-end gap-2 text-sm text-gray-500 dark:text-gray-400"> <button type="button" class={clsx(buttonStyles(), 'text-red-500 dark:text-red-400')} onClick={() => setReplyingTo(null)} > <X class="size-4" /> cancel </button> <button type="submit" class={buttonStyles('primary')} disabled={replyWorking() || !replyBody().trim()} > <Show when={replyWorking()} fallback={<Reply class="size-4" />}> <LoaderCircle class="size-4 animate-spin" /> </Show> reply </button> </div> </form> </Show> </Show> </div> )} </For> </div>17 unmodified lines queryFn: async () => resolveActor(auth.currentDid()!), }));
return ( <form onSubmit={props.onSubmit} class="group/form w-full mt-4 flex flex-col"> <form onSubmit={props.onSubmit} class="group/form w-full flex flex-col"> <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700"> <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> <Show when={viewerQuery.data} fallback={<div class="h-6" />}> {(viewer) => (6 unmodified lines </div> <textarea rows={5} placeholder={props.placeholder ?? 'Add to the discussion. Markdown is supported.'} class="w-full p-2 rounded border border-gray-200 dark:border-gray-700 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-blue-400 dark:text-white" class={textareaStyles()} required value={props.value} onInput={(event) => props.onValueChange(event.currentTarget.value)} />24 unmodified lines21 unmodified lines22232425262728293031323334195 unmodified linesExpand all2302312322332342352362372382392402412422436 unmodified lines2502512522532542552562572582592601 unmodified line26226326426525525625725825926026126226326426626726826927027127227327427527627727827928028128228328428528628728828929029129229329431 unmodified lines326327328329330331332333334335336349 unmodified linesExpand all21 unmodified linesimport { RepoStatsList } from '../components/repo';import { getIssueRecord, getIssue } from '../lib/api/issues';import { getPullRecord, getPull } from '../lib/api/pulls';import { getRepoByDid } from '../lib/api/repos';import { getStringRecord } from '../lib/api/strings';import { issueQueryKey, pullQueryKey } from './repo/shared';import { ISSUE_COLLECTION, STRING_COLLECTION,} from '../lib/api/constants';import { parseAtUri,} from '../lib/api/records';195 unmodified linesExpand all if (!uri) return undefined;
try { const parsed = parseAtUri(uri); if (parsed.collection === STRING_COLLECTION) { return { uri, kind: 'string' as const, }; } return { uri, kind: parsed.collection === ISSUE_COLLECTION ? 'issue' as const : 'pull' as const, };6 unmodified lines queryKey: ['search-parent', props.hit.nsid, parentInfo()?.uri], enabled: Boolean(parentInfo()?.uri), queryFn: async () => { const info = parentInfo()!; if (info.kind === 'string') { return { kind: 'string' as const, data: await getStringRecord(info.uri) }; } return info.kind === 'issue' ? { kind: 'issue' as const, record: await getIssueRecord(info.uri) } : { kind: 'pull' as const, record: await getPullRecord(info.uri) }; },1 unmodified line }));
return ( <Show when={parentQuery.data} fallback={<span>comment</span>}> {(parent) => { const repoDid = createMemo(() => parent().kind === 'issue' ? (parent().record.value as { repo: Did }).repo : (parent().record.value as { target: { repo: Did } }).target.repo); return ( <RepoThreadLink kind={parent().kind} repoDid={repoDid()} threadRef={parent().record.rkey} commentRef={props.hit.rkey}> comment on {parent().kind} </RepoThreadLink> ); }} {(parent) => ( <Switch> <Match when={parent().kind === 'string'}> <A href={`/strings/${(parent() as any).data.owner.handle || (parent() as any).data.owner.did}/${(parent() as any).data.record.rkey}#comment-${props.hit.rkey}`} class="truncate min-w-0 font-medium" > comment on string </A> </Match> <Match when={parent().kind !== 'string'}> <RepoThreadLink kind={(parent() as any).kind} repoDid={(parent() as any).kind === 'issue' ? ((parent() as any).record.value as { repo: Did }).repo : ((parent() as any).record.value as { target: { repo: Did } }).target.repo } threadRef={(parent() as any).record.rkey} commentRef={props.hit.rkey} > comment on {(parent() as any).kind} </RepoThreadLink> </Match> </Switch> )} </Show> );};
31 unmodified lines enabled: Boolean(parentUri()), queryFn: async () => { const uri = parentUri()!; const parsed = parseAtUri(uri); if (parsed.collection === STRING_COLLECTION) { return undefined; } if (parsed.collection === ISSUE_COLLECTION) { const issue = await getIssueRecord(uri); return (issue.value as { repo: Did }).repo; }349 unmodified linesExpand all290 unmodified linesExpand all291292293294295295296297298299300301302303304305306307308309310311312313314315336 unmodified linesExpand all290 unmodified linesExpand all </div> </section>
{/* Comments section */} <div class="flex flex-col gap-4 mt-4"> <div class="flex flex-col gap-4"> <Show when={commentsQuery.data}> <CommentThreadsSection threads={commentThreads()} authorDid={owner.did} onReplySubmit={async (body, parent) => { const agent = auth.agent(); if (!agent) return; await createStringComment( agent, record.uri, record.cid, body, { uri: parent.uri as ResourceUri, cid: parent.cid ?? '' } ); await queryClient.invalidateQueries({ queryKey: ['string-comments', record.uri] }); }} /> </Show>
<Show when={auth.agent()}>336 unmodified linesExpand all123 unmodified linesExpand all124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154204 unmodified linesExpand all123 unmodified linesExpand all }; return { owner, record };};
export const getStringRecord = async (uri: ResourceUri): Promise<{ owner: ResolvedActor; record: StringRecord }> => { const parsed = parseAtUri(uri); const owner = await resolveActor(parsed.did); const response = await ok( getRpc(owner.pds).get('com.atproto.repo.getRecord', { params: { repo: owner.did, collection: STRING_COLLECTION, rkey: parsed.rkey, }, }), );
const record: StringRecord = { uri: response.uri, cid: response.cid ?? '', rkey: parsed.rkey, value: response.value as ShTangledString.Main, }; return { owner, record };};
export const deleteString = async (agent: OAuthUserAgent, rkey: string): Promise<void> => { const rpc = createAuthRpc(agent); await ok( rpc.post('com.atproto.repo.deleteRecord', {204 unmodified linesExpand all