From 152bc3c1ec74fadc687efe97361ae7b1b5bd73c3 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Fri, 28 Mar 2025 16:34:07 +0200
Subject: [DMs] Reactions - link up API (attempt 2) (#8074)

* update package

* wire up APIs

* get reactions to display

* allow removing emoji

* handle limits better

* listen to reactions in log

* update convo list with reactions

* tweaks to reaction display

* Handle empty message fallback case

* update package

* shift reacts up by 2px

---------

Co-authored-by: Eric Bailey <git@esb.lol>
---
 src/components/dms/MessageItem.tsx | 93 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 88 insertions(+), 5 deletions(-)

(limited to 'src/components/dms/MessageItem.tsx')

diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index 675b6a5ee..60b0b5ed6 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -1,23 +1,36 @@
 import React, {useCallback, useMemo} from 'react'
-import {GestureResponderEvent, StyleProp, TextStyle, View} from 'react-native'
+import {
+  type GestureResponderEvent,
+  type StyleProp,
+  type TextStyle,
+  View,
+} from 'react-native'
+import Animated, {
+  LayoutAnimationConfig,
+  LinearTransition,
+  ZoomIn,
+  ZoomOut,
+} from 'react-native-reanimated'
 import {
   AppBskyEmbedRecord,
   ChatBskyConvoDefs,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {I18n} from '@lingui/core'
+import {type I18n} from '@lingui/core'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {ConvoItem} from '#/state/messages/convo/types'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {useConvoActive} from '#/state/messages/convo'
+import {type ConvoItem} from '#/state/messages/convo/types'
 import {useSession} from '#/state/session'
 import {TimeElapsed} from '#/view/com/util/TimeElapsed'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, native, useTheme} from '#/alf'
 import {isOnlyEmoji} from '#/alf/typography'
 import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
 import {InlineLinkText} from '#/components/Link'
+import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
-import {RichText} from '../RichText'
 import {DateDivider} from './DateDivider'
 import {MessageItemEmbed} from './MessageItemEmbed'
 import {localDateString} from './util'
@@ -29,6 +42,8 @@ let MessageItem = ({
 }): React.ReactNode => {
   const t = useTheme()
   const {currentAccount} = useSession()
+  const {_} = useLingui()
+  const {convo} = useConvoActive()
 
   const {message, nextMessage, prevMessage} = item
   const isPending = item.type === 'pending-message'
@@ -134,6 +149,74 @@ let MessageItem = ({
           )}
         </ActionsWrapper>
 
+        <LayoutAnimationConfig skipEntering skipExiting>
+          {message.reactions && message.reactions.length > 0 && (
+            <View
+              style={[
+                isFromSelf ? a.align_end : a.align_start,
+                a.px_xs,
+                a.pb_2xs,
+              ]}>
+              <View
+                style={[
+                  a.flex_row,
+                  a.gap_2xs,
+                  a.py_xs,
+                  a.px_xs,
+                  a.justify_center,
+                  isFromSelf ? a.justify_end : a.justify_start,
+                  a.flex_wrap,
+                  a.pb_xs,
+                  t.atoms.bg,
+                  a.rounded_lg,
+                  a.border,
+                  t.atoms.border_contrast_low,
+                  {marginTop: -6},
+                ]}>
+                {message.reactions.map((reaction, _i, reactions) => {
+                  let label
+                  if (reaction.sender.did === currentAccount?.did) {
+                    label = _(msg`You reacted ${reaction.value}`)
+                  } else {
+                    const senderDid = reaction.sender.did
+                    const sender = convo.members.find(
+                      member => member.did === senderDid,
+                    )
+                    if (sender) {
+                      label = _(
+                        msg`${sanitizeDisplayName(
+                          sender.displayName || sender.handle,
+                        )} reacted ${reaction.value}`,
+                      )
+                    } else {
+                      label = _(msg`Someone reacted ${reaction.value}`)
+                    }
+                  }
+                  return (
+                    <Animated.View
+                      entering={native(ZoomIn.springify(200).delay(400))}
+                      exiting={
+                        reactions.length > 1 && native(ZoomOut.delay(200))
+                      }
+                      layout={native(LinearTransition.delay(300))}
+                      key={reaction.sender.did + reaction.value}
+                      style={[a.p_2xs]}
+                      accessible={true}
+                      accessibilityLabel={label}
+                      accessibilityHint={_(
+                        msg`Double tap or long press the message to add a reaction`,
+                      )}>
+                      <Text emoji style={[a.text_sm]}>
+                        {reaction.value}
+                      </Text>
+                    </Animated.View>
+                  )
+                })}
+              </View>
+            </View>
+          )}
+        </LayoutAnimationConfig>
+
         {isLastInGroup && (
           <MessageItemMetadata
             item={item}
-- 
cgit 1.4.1

