@@ -137,6 +137,14 @@ function hash10(s: string): number {
137137 for ( const c of s ) h = ( h * 31 + c . charCodeAt ( 0 ) ) | 0 ;
138138 return Math . abs ( h ) & 0x3ff ;
139139}
140+ // Reaction rkey: synthesise time from the *message* ts so reactions live next to
141+ // their target in TID order; clockId distinguishes the emoji. With 10 bits of
142+ // clockId space and a small number of distinct emojis per message, collisions
143+ // are rare; collisions just merge two emoji into one reaction record, which
144+ // `slackRaw` can correct if we re-derive.
145+ function tidForReaction ( messageTs : string , emojiName : string ) {
146+ return tidFromSlackTs ( messageTs , hash10 ( `react:${ emojiName } ` ) ) ;
147+ }
140148function colibriChannelRkey ( slackChannelId : string ) : string {
141149 const ch = channelOf . get ( slackChannelId ) ;
142150 if ( ! ch ) throw new Error ( `unknown slack channel ${ slackChannelId } ` ) ;
@@ -380,6 +388,37 @@ function buildMessage(m: any, channelRkey: string, parentRkey?: string) {
380388 } ;
381389}
382390
391+ function emojiForReaction ( name : string ) : string {
392+ // Strip Slack ":name::skin-tone-X:" → look up base name, accept the loss of skin tone in v0.
393+ const baseName = name . split ( "::" ) [ 0 ] ;
394+ return EMOJI_MAP . get ( baseName ) ?? `:${ name } :` ;
395+ }
396+
397+ // Walk a message's `reactions` array → one reaction record per emoji (per
398+ // message). Multiple Slack users with the same emoji collapse into one record
399+ // (they'd all author from the bot anyway and the appview likely dedupes by
400+ // (author, emoji, target)). Multi-reactor count is preserved losslessly in
401+ // `slackRaw`.
402+ function reactionsFor ( m : any , targetMessageRkey : string ) {
403+ const out : { rkey : string ; record : any ; userCount : number ; name : string ; emoji : string } [ ] = [ ] ;
404+ for ( const r of m . reactions ?? [ ] ) {
405+ if ( ! r ?. name ) continue ;
406+ const emoji = emojiForReaction ( r . name ) ;
407+ out . push ( {
408+ rkey : tidForReaction ( m . ts , r . name ) ,
409+ name : r . name ,
410+ emoji,
411+ userCount : ( r . users ?? [ ] ) . length || r . count || 1 ,
412+ record : {
413+ $type : "social.colibri.reaction" ,
414+ emoji,
415+ targetMessage : targetMessageRkey ,
416+ } ,
417+ } ) ;
418+ }
419+ return out ;
420+ }
421+
383422function legacyTextFallback ( raw : string , b : FacetBuilder ) {
384423 const decoded = raw
385424 . replace ( / < ( [ ^ > | ] + ) \| ( [ ^ > ] + ) > / g, "$2" )
@@ -475,7 +514,9 @@ const fmtRow = (
475514) => {
476515 const tags = `${ built . hasBlocks ? "B" : "." } ${ built . facetCount . toString ( ) . padStart ( 2 , " " ) } ` ;
477516 const parentCol = parent ? `parent=${ parent } ` : " " ;
478- return ` ${ m . ts } ${ ( m . channel_name || "?" ) . padEnd ( 20 ) } ${ built . rkey } ${ parentCol } ${ tags } '${ built . record . text . slice ( 0 , 70 ) . replace ( / \n / g, " " ) } …'` ;
517+ const rxCount = ( m . reactions ?? [ ] ) . length ;
518+ const rxTag = rxCount ? `+${ rxCount } r` : " " ;
519+ return ` ${ m . ts } ${ ( m . channel_name || "?" ) . padEnd ( 20 ) } ${ built . rkey } ${ parentCol } ${ tags } ${ rxTag } '${ built . record . text . slice ( 0 , 70 ) . replace ( / \n / g, " " ) } …'` ;
479520} ;
480521
481522console . log ( "" ) ;
@@ -490,6 +531,26 @@ for (const m of replies) {
490531 console . log ( fmtRow ( m , buildMessage ( m , channelMap [ m . channel_id ] , parent ) , parent ) ) ;
491532}
492533
534+ const allWithReactions : { m : any ; targetRkey : string } [ ] = [ ] ;
535+ for ( const m of tops )
536+ if ( m . reactions ?. length )
537+ allWithReactions . push ( { m, targetRkey : tidFromSlackTs ( m . ts ) } ) ;
538+ for ( const m of replies )
539+ if ( m . reactions ?. length )
540+ allWithReactions . push ( { m, targetRkey : tidFromSlackTs ( m . ts ) } ) ;
541+
542+ if ( allWithReactions . length > 0 ) {
543+ console . log ( "" ) ;
544+ console . log ( "REACTIONS:" ) ;
545+ for ( const { m, targetRkey } of allWithReactions ) {
546+ for ( const r of reactionsFor ( m , targetRkey ) ) {
547+ console . log (
548+ ` ${ m . ts } target=${ targetRkey } rkey=${ r . rkey } ${ r . emoji } (:${ r . name } : ×${ r . userCount } )` ,
549+ ) ;
550+ }
551+ }
552+ }
553+
493554if ( dryRun ) {
494555 console . log ( "" ) ;
495556 console . log ( "(dry-run; pass --live to publish)" ) ;
@@ -614,4 +675,20 @@ for (const m of replies) {
614675}
615676
616677console . error ( "" ) ;
617- console . error ( `done: ${ okT } top-level, ${ okR } replies, ${ failT + failR } failed` ) ;
678+ console . error ( "reactions…" ) ;
679+ let okX = 0 , failX = 0 ;
680+ for ( const { m, targetRkey } of allWithReactions ) {
681+ for ( const r of reactionsFor ( m , targetRkey ) ) {
682+ try {
683+ await put ( "social.colibri.reaction" , r . rkey , r . record ) ;
684+ okX ++ ;
685+ } catch ( e ) {
686+ failX ++ ;
687+ console . error ( ` fail ${ m . ts } ${ r . name } : ${ e } ` ) ;
688+ }
689+ if ( delayMs > 0 ) await new Promise ( ( rs ) => setTimeout ( rs , delayMs ) ) ;
690+ }
691+ }
692+
693+ console . error ( "" ) ;
694+ console . error ( `done: ${ okT } top-level, ${ okR } replies, ${ okX } reactions, ${ failT + failR + failX } failed` ) ;
0 commit comments