@@ -17,9 +17,13 @@ import {
1717} from "@t3tools/contracts" ;
1818import { scopeThreadRef } from "@t3tools/client-runtime/environment" ;
1919import {
20+ guessThreadLinkRef ,
2021 hasThreadLinks ,
22+ parseThreadLinkHref ,
2123 relabelThreadLinks ,
22- resolveThreadLinkRef ,
24+ threadLinkCandidates ,
25+ threadLinkRefs ,
26+ type ThreadLink ,
2327} from "@t3tools/shared/threadLinks" ;
2428import * as Option from "effect/Option" ;
2529import * as Schema from "effect/Schema" ;
@@ -45,26 +49,35 @@ const EMPTY_SERVER_CONFIG_ATOM = Atom.make<ServerConfig | null>(null).pipe(
4549const EMPTY_THREAD_ATOM = Atom . make < EnvironmentThread | null > ( null ) . pipe (
4650 Atom . withLabel ( "mobile-thread:empty" ) ,
4751) ;
48- const EMPTY_THREAD_TITLES : ReadonlyMap < ThreadId , string > = new Map ( ) ;
49- const EMPTY_THREAD_TITLES_ATOM = Atom . make ( EMPTY_THREAD_TITLES ) . pipe (
50- Atom . withLabel ( "mobile-thread-titles:empty" ) ,
52+ const EMPTY_THREAD_LINK_TITLES_ATOM = Atom . make < ReadonlyMap < string , string > > ( new Map ( ) ) . pipe (
53+ Atom . withLabel ( "mobile-thread-link-titles:empty" ) ,
5154) ;
5255
53- const threadTitlesAtom = Atom . family ( ( environmentId : EnvironmentId ) => {
54- let previous = EMPTY_THREAD_TITLES ;
56+ const decodeThreadKey = Schema . decodeUnknownSync ( Schema . Tuple ( [ EnvironmentId , ThreadId ] ) ) ;
57+ const threadKey = ( ref : ScopedThreadRef ) => JSON . stringify ( [ ref . environmentId , ref . threadId ] ) ;
58+
59+ // Null when this client doesn't know the thread. Keyed, so a link never walks an environment's threads.
60+ const threadTitleAtom = Atom . family ( ( key : string ) => {
61+ const ref = scopeThreadRef ( ...decodeThreadKey ( JSON . parse ( key ) ) ) ;
62+ return Atom . make ( ( get ) => get ( environmentThreadShells . threadShellAtom ( ref ) ) ?. title ?? null ) . pipe (
63+ Atom . withLabel ( `mobile-thread-title:${ key } ` ) ,
64+ ) ;
65+ } ) ;
66+
67+ const decodeThreadKeys = Schema . decodeUnknownSync ( Schema . Array ( Schema . String ) ) ;
68+
69+ const threadLinkTitlesAtom = Atom . family ( ( key : string ) => {
70+ const refKeys = decodeThreadKeys ( JSON . parse ( key ) ) ;
5571 return Atom . make ( ( get ) => {
56- const index = get ( environmentThreadShells . environmentThreadIndexAtom ( environmentId ) ) ;
57- const unchanged =
58- index . size === previous . size &&
59- Array . from ( index ) . every ( ( [ threadId , shell ] ) => previous . get ( threadId ) === shell . title ) ;
60- if ( ! unchanged ) {
61- previous = new Map ( Array . from ( index , ( [ threadId , shell ] ) => [ threadId , shell . title ] ) ) ;
72+ const titles = new Map < string , string > ( ) ;
73+ for ( const refKey of refKeys ) {
74+ const title = get ( threadTitleAtom ( refKey ) ) ;
75+ if ( title !== null ) titles . set ( refKey , title ) ;
6276 }
63- return previous ;
64- } ) . pipe ( Atom . withLabel ( `mobile-thread-titles:${ environmentId } ` ) ) ;
77+ return titles ;
78+ } ) . pipe ( Atom . withLabel ( `mobile-thread-link- titles:${ key } ` ) ) ;
6579} ) ;
6680
67- const decodeThreadKey = Schema . decodeUnknownSync ( Schema . Tuple ( [ EnvironmentId , ThreadId ] ) ) ;
6881const decodeThreadChipTargets = Schema . decodeUnknownSync (
6982 Schema . Array (
7083 Schema . Struct ( {
@@ -118,13 +131,11 @@ const threadChipIconsAtom = Atom.family((key: string) => {
118131 return Atom . make ( ( get ) => {
119132 const icons = new Map < string , string > ( ) ;
120133 for ( const target of targets ) {
121- const source = scopeThreadRef ( target . environmentId , target . threadId ) ;
122- const ref = target . link
123- ? ( resolveThreadLinkRef ( source , get ( environmentThreadShells . threadRefsAtom ) ) ?? source )
124- : source ;
125- const icon = get (
126- threadProjectChipIconAtom ( JSON . stringify ( [ ref . environmentId , ref . threadId ] ) ) ,
127- ) ;
134+ const link = target . link ? parseThreadLinkHref ( target . href ) : null ;
135+ const ref =
136+ ( link ? resolveThreadLink ( get , link , target . environmentId ) : null ) ??
137+ scopeThreadRef ( target . environmentId , target . threadId ) ;
138+ const icon = get ( threadProjectChipIconAtom ( threadKey ( ref ) ) ) ;
128139 if ( icon ) icons . set ( target . href , icon ) ;
129140 }
130141 return icons ;
@@ -190,11 +201,26 @@ export function useChildThreadInputs(ref: ScopedThreadRef) {
190201
191202export function useLiveThreadLinkLabels ( markdown : string , environmentId : EnvironmentId ) : string {
192203 const titles = useAtomValue (
193- hasThreadLinks ( markdown ) ? threadTitlesAtom ( environmentId ) : EMPTY_THREAD_TITLES_ATOM ,
204+ hasThreadLinks ( markdown )
205+ ? threadLinkTitlesAtom ( JSON . stringify ( threadLinkRefs ( markdown , environmentId ) . map ( threadKey ) ) )
206+ : EMPTY_THREAD_LINK_TITLES_ATOM ,
207+ ) ;
208+ return relabelThreadLinks ( markdown , environmentId , ( ref ) => titles . get ( threadKey ( ref ) ) ) ;
209+ }
210+
211+ /** Keyed reads; every thread is read only to place a v1 link its message's environment lacks. */
212+ export function resolveThreadLink (
213+ read : < A > ( atom : Atom . Atom < A > ) => A ,
214+ link : ThreadLink ,
215+ messageEnvironmentId : EnvironmentId ,
216+ ) : ScopedThreadRef | null {
217+ const known = threadLinkCandidates ( link , messageEnvironmentId ) . find (
218+ ( ref ) => read ( threadTitleAtom ( threadKey ( ref ) ) ) !== null ,
194219 ) ;
195- return titles . size === 0
196- ? markdown
197- : relabelThreadLinks ( markdown , ( threadId ) => titles . get ( threadId ) ) ;
220+ if ( known ) return known ;
221+ return link . environmentId === null
222+ ? guessThreadLinkRef ( link , read ( environmentThreadShells . threadRefsAtom ) )
223+ : null ;
198224}
199225
200226export function useThreadChipIcons (
0 commit comments