[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>
This commit is contained in:
@@ -775,7 +775,10 @@ export function Item({
|
||||
]}>
|
||||
<ItemContext.Provider value={itemContext}>
|
||||
{typeof children === 'function'
|
||||
? children(focused || pressed || context.hoveredMenuItem === id)
|
||||
? children(
|
||||
(focused || pressed || context.hoveredMenuItem === id) &&
|
||||
!rest.disabled,
|
||||
)
|
||||
: children}
|
||||
</ItemContext.Provider>
|
||||
</Pressable>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {Alert, useWindowDimensions, View} from 'react-native'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -18,12 +18,15 @@ import {
|
||||
import {type TriggerProps} from '#/components/Menu/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {EmojiPopup} from './EmojiPopup'
|
||||
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
|
||||
|
||||
export function EmojiReactionPicker({
|
||||
message,
|
||||
onEmojiSelect,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
children?: TriggerProps['children']
|
||||
onEmojiSelect: (emoji: string) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
@@ -39,10 +42,6 @@ export function EmojiReactionPicker({
|
||||
return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon
|
||||
}, [])
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
Alert.alert(emoji)
|
||||
}
|
||||
|
||||
const position = useMemo(() => {
|
||||
return {
|
||||
x: align === 'left' ? 12 : screenWidth - layout.width - 12,
|
||||
@@ -52,6 +51,8 @@ export function EmojiReactionPicker({
|
||||
}
|
||||
}, [measurement, align, screenWidth, layout])
|
||||
|
||||
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={evt => setLayout(evt.nativeEvent.layout)}
|
||||
@@ -70,33 +71,49 @@ export function EmojiReactionPicker({
|
||||
t.atoms.border_contrast_low,
|
||||
a.shadow_md,
|
||||
]}>
|
||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
|
||||
<ContextMenu.Item
|
||||
position={position}
|
||||
label={_(msg`React with ${emoji}`)}
|
||||
key={emoji}
|
||||
onPress={() => handleEmojiSelect(emoji)}
|
||||
unstyled>
|
||||
{hovered => (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
hovered && {backgroundColor: t.palette.primary_500},
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
||||
{emoji}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ContextMenu.Item>
|
||||
))}
|
||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => {
|
||||
const alreadyReacted = hasAlreadyReacted(
|
||||
message,
|
||||
currentAccount?.did,
|
||||
emoji,
|
||||
)
|
||||
return (
|
||||
<ContextMenu.Item
|
||||
position={position}
|
||||
label={_(msg`React with ${emoji}`)}
|
||||
key={emoji}
|
||||
onPress={() => onEmojiSelect(emoji)}
|
||||
unstyled
|
||||
disabled={limitReacted ? !alreadyReacted : false}>
|
||||
{hovered => (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
hovered
|
||||
? {
|
||||
backgroundColor: alreadyReacted
|
||||
? t.palette.negative_100
|
||||
: t.palette.primary_500,
|
||||
}
|
||||
: alreadyReacted && {
|
||||
backgroundColor: t.palette.primary_200,
|
||||
},
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
||||
{emoji}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ContextMenu.Item>
|
||||
)
|
||||
})}
|
||||
<EmojiPopup
|
||||
onEmojiSelected={emoji => {
|
||||
close()
|
||||
handleEmojiSelect(emoji)
|
||||
onEmojiSelect(emoji)
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import EmojiPicker from '@emoji-mart/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {Emoji} from '#/view/com/composer/text-input/web/EmojiPicker.web'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type Emoji} from '#/view/com/composer/text-input/web/EmojiPicker.web'
|
||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useTheme} from '#/alf'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {TriggerProps} from '#/components/Menu/types'
|
||||
import {type TriggerProps} from '#/components/Menu/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
|
||||
|
||||
export function EmojiReactionPicker({
|
||||
message,
|
||||
children,
|
||||
onEmojiSelect,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
children?: TriggerProps['children']
|
||||
onEmojiSelect: (emoji: string) => void
|
||||
}) {
|
||||
if (!children)
|
||||
throw new Error('EmojiReactionPicker requires the children prop on web')
|
||||
@@ -29,15 +34,22 @@ export function EmojiReactionPicker({
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Add emoji reaction`)}>{children}</Menu.Trigger>
|
||||
<Menu.Outer>
|
||||
<MenuInner />
|
||||
<MenuInner message={message} onEmojiSelect={onEmojiSelect} />
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuInner() {
|
||||
function MenuInner({
|
||||
message,
|
||||
onEmojiSelect,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
onEmojiSelect: (emoji: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {control} = Menu.useMenuContext()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
@@ -47,29 +59,45 @@ function MenuInner() {
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
control.close()
|
||||
window.alert(emoji)
|
||||
onEmojiSelect(emoji)
|
||||
}
|
||||
|
||||
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||
|
||||
return expanded ? (
|
||||
<EmojiPicker onEmojiSelect={handleEmojiPickerResponse} autoFocus={true} />
|
||||
) : (
|
||||
<View style={[a.flex_row, a.gap_xs]}>
|
||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
|
||||
<PressableWithHover
|
||||
key={emoji}
|
||||
onPress={() => handleEmojiSelect(emoji)}
|
||||
hoverStyle={{backgroundColor: t.palette.primary_100}}
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
||||
{emoji}
|
||||
</Text>
|
||||
</PressableWithHover>
|
||||
))}
|
||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => {
|
||||
const alreadyReacted = hasAlreadyReacted(
|
||||
message,
|
||||
currentAccount?.did,
|
||||
emoji,
|
||||
)
|
||||
return (
|
||||
<PressableWithHover
|
||||
key={emoji}
|
||||
onPress={() => handleEmojiSelect(emoji)}
|
||||
hoverStyle={{
|
||||
backgroundColor: alreadyReacted
|
||||
? t.palette.negative_200
|
||||
: !limitReacted
|
||||
? t.palette.primary_300
|
||||
: undefined,
|
||||
}}
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
alreadyReacted && {backgroundColor: t.palette.primary_100},
|
||||
]}>
|
||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
||||
{emoji}
|
||||
</Text>
|
||||
</PressableWithHover>
|
||||
)
|
||||
})}
|
||||
<PressableWithHover
|
||||
onPress={() => setExpanded(true)}
|
||||
hoverStyle={{backgroundColor: t.palette.primary_100}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import {memo, useCallback} from 'react'
|
||||
import {LayoutAnimation} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
|
||||
@@ -23,6 +23,7 @@ import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/War
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {usePromptControl} from '#/components/Prompt'
|
||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
import {hasReachedReactionLimit} from './util'
|
||||
|
||||
export let MessageContextMenu = ({
|
||||
message,
|
||||
@@ -41,7 +42,7 @@ export let MessageContextMenu = ({
|
||||
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
|
||||
const onCopyMessage = React.useCallback(() => {
|
||||
const onCopyMessage = useCallback(() => {
|
||||
const str = richTextToString(
|
||||
new RichText({
|
||||
text: message.text,
|
||||
@@ -54,7 +55,7 @@ export let MessageContextMenu = ({
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}, [_, message.text, message.facets])
|
||||
|
||||
const onPressTranslateMessage = React.useCallback(() => {
|
||||
const onPressTranslateMessage = useCallback(() => {
|
||||
const translatorUrl = getTranslatorLink(
|
||||
message.text,
|
||||
langPrefs.primaryLanguage,
|
||||
@@ -62,7 +63,7 @@ export let MessageContextMenu = ({
|
||||
openLink(translatorUrl, true)
|
||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||
|
||||
const onDelete = React.useCallback(() => {
|
||||
const onDelete = useCallback(() => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
convo
|
||||
.deleteMessage(message.id)
|
||||
@@ -72,6 +73,30 @@ export let MessageContextMenu = ({
|
||||
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
||||
}, [_, convo, message.id])
|
||||
|
||||
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],
|
||||
)
|
||||
|
||||
const sender = convo.convo.members.find(
|
||||
member => member.did === message.sender.did,
|
||||
)
|
||||
@@ -81,7 +106,10 @@ export let MessageContextMenu = ({
|
||||
<ContextMenu.Root>
|
||||
{isNative && (
|
||||
<ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
|
||||
<EmojiReactionPicker message={message} />
|
||||
<EmojiReactionPicker
|
||||
message={message}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
</ContextMenu.AuxiliaryView>
|
||||
)}
|
||||
|
||||
@@ -156,4 +184,4 @@ export let MessageContextMenu = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
MessageContextMenu = React.memo(MessageContextMenu)
|
||||
MessageContextMenu = memo(MessageContextMenu)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
|
||||
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
||||
switch (profile.associated?.chat?.allowIncoming) {
|
||||
@@ -25,3 +28,29 @@ export function localDateString(date: Date) {
|
||||
// not padding with 0s because it's not necessary, it's just used for comparison
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
}
|
||||
|
||||
export function hasAlreadyReacted(
|
||||
message: ChatBskyConvoDefs.MessageView,
|
||||
myDid: string | undefined,
|
||||
emoji: string,
|
||||
): boolean {
|
||||
if (!message.reactions) {
|
||||
return false
|
||||
}
|
||||
return !!message.reactions.find(
|
||||
reaction => reaction.value === emoji && reaction.sender.did === myDid,
|
||||
)
|
||||
}
|
||||
|
||||
export function hasReachedReactionLimit(
|
||||
message: ChatBskyConvoDefs.MessageView,
|
||||
myDid: string | undefined,
|
||||
): boolean {
|
||||
if (!message.reactions) {
|
||||
return false
|
||||
}
|
||||
const myReactions = message.reactions.filter(
|
||||
reaction => reaction.sender.did === myDid,
|
||||
)
|
||||
return myReactions.length >= EMOJI_REACTION_LIMIT
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user