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/ActionsWrapper.web.tsx | 50 ++++++++++++++++++++++++++-----
 1 file changed, 42 insertions(+), 8 deletions(-)

(limited to 'src/components/dms/ActionsWrapper.web.tsx')

diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx
index 82113eba8..aaffc0cfb 100644
--- a/src/components/dms/ActionsWrapper.web.tsx
+++ b/src/components/dms/ActionsWrapper.web.tsx
@@ -1,12 +1,19 @@
-import React from 'react'
+import {useCallback, useRef, useState} from 'react'
 import {Pressable, View} from 'react-native'
-import {ChatBskyConvoDefs} from '@atproto/api'
+import {type ChatBskyConvoDefs} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import type React from 'react'
 
+import {useConvoActive} from '#/state/messages/convo'
+import {useSession} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
 import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
 import {EmojiReactionPicker} from './EmojiReactionPicker'
+import {hasReachedReactionLimit} from './util'
 
 export function ActionsWrapper({
   message,
@@ -17,26 +24,53 @@ export function ActionsWrapper({
   isFromSelf: boolean
   children: React.ReactNode
 }) {
-  const viewRef = React.useRef(null)
+  const viewRef = useRef(null)
   const t = useTheme()
+  const {_} = useLingui()
+  const convo = useConvoActive()
+  const {currentAccount} = useSession()
 
-  const [showActions, setShowActions] = React.useState(false)
+  const [showActions, setShowActions] = useState(false)
 
-  const onMouseEnter = React.useCallback(() => {
+  const onMouseEnter = useCallback(() => {
     setShowActions(true)
   }, [])
 
-  const onMouseLeave = React.useCallback(() => {
+  const onMouseLeave = useCallback(() => {
     setShowActions(false)
   }, [])
 
   // We need to handle the `onFocus` separately because we want to know if there is a related target (the element
   // that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed.
-  const onFocus = React.useCallback<React.FocusEventHandler>(e => {
+  const onFocus = useCallback<React.FocusEventHandler>(e => {
     if (e.nativeEvent.relatedTarget == null) return
     setShowActions(true)
   }, [])
 
+  const onEmojiSelect = useCallback(
+    (emoji: string) => {
+      if (
+        message.reactions?.find(
+          reaction =>
+            reaction.value === emoji &&
+            reaction.sender.did === currentAccount?.did,
+        )
+      ) {
+        convo
+          .removeReaction(message.id, emoji)
+          .catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
+      } else {
+        if (hasReachedReactionLimit(message, currentAccount?.did)) return
+        convo
+          .addReaction(message.id, emoji)
+          .catch(() =>
+            Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'),
+          )
+      }
+    },
+    [_, convo, message, currentAccount?.did],
+  )
+
   return (
     <View
       // @ts-expect-error web only
@@ -56,7 +90,7 @@ export function ActionsWrapper({
             ? [a.mr_md, {marginLeft: 'auto'}]
             : [a.ml_md, {marginRight: 'auto'}],
         ]}>
-        <EmojiReactionPicker message={message}>
+        <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
           {({props, state, isNative, control}) => {
             // always false, file is platform split
             if (isNative) return null
-- 
cgit 1.4.1

