[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:
+1
-1
@@ -58,7 +58,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.14.7",
|
"@atproto/api": "^0.14.14",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,9 @@
|
|||||||
import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
import {
|
||||||
|
Platform,
|
||||||
|
type StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import * as tokens from '#/alf/tokens'
|
import * as tokens from '#/alf/tokens'
|
||||||
import {ios, native, platform, web} from '#/alf/util/platform'
|
import {ios, native, platform, web} from '#/alf/util/platform'
|
||||||
@@ -126,6 +131,9 @@ export const atoms = {
|
|||||||
rounded_md: {
|
rounded_md: {
|
||||||
borderRadius: tokens.borderRadius.md,
|
borderRadius: tokens.borderRadius.md,
|
||||||
},
|
},
|
||||||
|
rounded_lg: {
|
||||||
|
borderRadius: tokens.borderRadius.lg,
|
||||||
|
},
|
||||||
rounded_full: {
|
rounded_full: {
|
||||||
borderRadius: tokens.borderRadius.full,
|
borderRadius: tokens.borderRadius.full,
|
||||||
},
|
},
|
||||||
@@ -358,6 +366,9 @@ export const atoms = {
|
|||||||
border_r: {
|
border_r: {
|
||||||
borderRightWidth: StyleSheet.hairlineWidth,
|
borderRightWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
},
|
||||||
|
border_transparent: {
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
curve_circular: ios({
|
curve_circular: ios({
|
||||||
borderCurve: 'circular',
|
borderCurve: 'circular',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const borderRadius = {
|
|||||||
xs: 4,
|
xs: 4,
|
||||||
sm: 8,
|
sm: 8,
|
||||||
md: 12,
|
md: 12,
|
||||||
|
lg: 16,
|
||||||
full: 999,
|
full: 999,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -775,7 +775,10 @@ export function Item({
|
|||||||
]}>
|
]}>
|
||||||
<ItemContext.Provider value={itemContext}>
|
<ItemContext.Provider value={itemContext}>
|
||||||
{typeof children === 'function'
|
{typeof children === 'function'
|
||||||
? children(focused || pressed || context.hoveredMenuItem === id)
|
? children(
|
||||||
|
(focused || pressed || context.hoveredMenuItem === id) &&
|
||||||
|
!rest.disabled,
|
||||||
|
)
|
||||||
: children}
|
: children}
|
||||||
</ItemContext.Provider>
|
</ItemContext.Provider>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import React from 'react'
|
import {useCallback, useRef, useState} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
||||||
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
||||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||||
|
import {hasReachedReactionLimit} from './util'
|
||||||
|
|
||||||
export function ActionsWrapper({
|
export function ActionsWrapper({
|
||||||
message,
|
message,
|
||||||
@@ -17,26 +24,53 @@ export function ActionsWrapper({
|
|||||||
isFromSelf: boolean
|
isFromSelf: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const viewRef = React.useRef(null)
|
const viewRef = useRef(null)
|
||||||
const t = useTheme()
|
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)
|
setShowActions(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onMouseLeave = React.useCallback(() => {
|
const onMouseLeave = useCallback(() => {
|
||||||
setShowActions(false)
|
setShowActions(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// We need to handle the `onFocus` separately because we want to know if there is a related target (the element
|
// 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.
|
// 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
|
if (e.nativeEvent.relatedTarget == null) return
|
||||||
setShowActions(true)
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
// @ts-expect-error web only
|
// @ts-expect-error web only
|
||||||
@@ -56,7 +90,7 @@ export function ActionsWrapper({
|
|||||||
? [a.mr_md, {marginLeft: 'auto'}]
|
? [a.mr_md, {marginLeft: 'auto'}]
|
||||||
: [a.ml_md, {marginRight: 'auto'}],
|
: [a.ml_md, {marginRight: 'auto'}],
|
||||||
]}>
|
]}>
|
||||||
<EmojiReactionPicker message={message}>
|
<EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
|
||||||
{({props, state, isNative, control}) => {
|
{({props, state, isNative, control}) => {
|
||||||
// always false, file is platform split
|
// always false, file is platform split
|
||||||
if (isNative) return null
|
if (isNative) return null
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useMemo, useState} from 'react'
|
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 {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -18,12 +18,15 @@ import {
|
|||||||
import {type TriggerProps} from '#/components/Menu/types'
|
import {type TriggerProps} from '#/components/Menu/types'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {EmojiPopup} from './EmojiPopup'
|
import {EmojiPopup} from './EmojiPopup'
|
||||||
|
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
|
||||||
|
|
||||||
export function EmojiReactionPicker({
|
export function EmojiReactionPicker({
|
||||||
message,
|
message,
|
||||||
|
onEmojiSelect,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
children?: TriggerProps['children']
|
children?: TriggerProps['children']
|
||||||
|
onEmojiSelect: (emoji: string) => void
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -39,10 +42,6 @@ export function EmojiReactionPicker({
|
|||||||
return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon
|
return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleEmojiSelect = (emoji: string) => {
|
|
||||||
Alert.alert(emoji)
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = useMemo(() => {
|
const position = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
x: align === 'left' ? 12 : screenWidth - layout.width - 12,
|
x: align === 'left' ? 12 : screenWidth - layout.width - 12,
|
||||||
@@ -52,6 +51,8 @@ export function EmojiReactionPicker({
|
|||||||
}
|
}
|
||||||
}, [measurement, align, screenWidth, layout])
|
}, [measurement, align, screenWidth, layout])
|
||||||
|
|
||||||
|
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
onLayout={evt => setLayout(evt.nativeEvent.layout)}
|
onLayout={evt => setLayout(evt.nativeEvent.layout)}
|
||||||
@@ -70,33 +71,49 @@ export function EmojiReactionPicker({
|
|||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
a.shadow_md,
|
a.shadow_md,
|
||||||
]}>
|
]}>
|
||||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
|
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => {
|
||||||
<ContextMenu.Item
|
const alreadyReacted = hasAlreadyReacted(
|
||||||
position={position}
|
message,
|
||||||
label={_(msg`React with ${emoji}`)}
|
currentAccount?.did,
|
||||||
key={emoji}
|
emoji,
|
||||||
onPress={() => handleEmojiSelect(emoji)}
|
)
|
||||||
unstyled>
|
return (
|
||||||
{hovered => (
|
<ContextMenu.Item
|
||||||
<View
|
position={position}
|
||||||
style={[
|
label={_(msg`React with ${emoji}`)}
|
||||||
a.rounded_full,
|
key={emoji}
|
||||||
hovered && {backgroundColor: t.palette.primary_500},
|
onPress={() => onEmojiSelect(emoji)}
|
||||||
{height: 40, width: 40},
|
unstyled
|
||||||
a.justify_center,
|
disabled={limitReacted ? !alreadyReacted : false}>
|
||||||
a.align_center,
|
{hovered => (
|
||||||
]}>
|
<View
|
||||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
style={[
|
||||||
{emoji}
|
a.rounded_full,
|
||||||
</Text>
|
hovered
|
||||||
</View>
|
? {
|
||||||
)}
|
backgroundColor: alreadyReacted
|
||||||
</ContextMenu.Item>
|
? 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
|
<EmojiPopup
|
||||||
onEmojiSelected={emoji => {
|
onEmojiSelected={emoji => {
|
||||||
close()
|
close()
|
||||||
handleEmojiSelect(emoji)
|
onEmojiSelect(emoji)
|
||||||
}}>
|
}}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import EmojiPicker from '@emoji-mart/react'
|
import EmojiPicker from '@emoji-mart/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
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 {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
|
||||||
import * as Menu from '#/components/Menu'
|
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 {Text} from '#/components/Typography'
|
||||||
|
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
|
||||||
|
|
||||||
export function EmojiReactionPicker({
|
export function EmojiReactionPicker({
|
||||||
|
message,
|
||||||
children,
|
children,
|
||||||
|
onEmojiSelect,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
children?: TriggerProps['children']
|
children?: TriggerProps['children']
|
||||||
|
onEmojiSelect: (emoji: string) => void
|
||||||
}) {
|
}) {
|
||||||
if (!children)
|
if (!children)
|
||||||
throw new Error('EmojiReactionPicker requires the children prop on web')
|
throw new Error('EmojiReactionPicker requires the children prop on web')
|
||||||
@@ -29,15 +34,22 @@ export function EmojiReactionPicker({
|
|||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Add emoji reaction`)}>{children}</Menu.Trigger>
|
<Menu.Trigger label={_(msg`Add emoji reaction`)}>{children}</Menu.Trigger>
|
||||||
<Menu.Outer>
|
<Menu.Outer>
|
||||||
<MenuInner />
|
<MenuInner message={message} onEmojiSelect={onEmojiSelect} />
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuInner() {
|
function MenuInner({
|
||||||
|
message,
|
||||||
|
onEmojiSelect,
|
||||||
|
}: {
|
||||||
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
onEmojiSelect: (emoji: string) => void
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {control} = Menu.useMenuContext()
|
const {control} = Menu.useMenuContext()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
@@ -47,29 +59,45 @@ function MenuInner() {
|
|||||||
|
|
||||||
const handleEmojiSelect = (emoji: string) => {
|
const handleEmojiSelect = (emoji: string) => {
|
||||||
control.close()
|
control.close()
|
||||||
window.alert(emoji)
|
onEmojiSelect(emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||||
|
|
||||||
return expanded ? (
|
return expanded ? (
|
||||||
<EmojiPicker onEmojiSelect={handleEmojiPickerResponse} autoFocus={true} />
|
<EmojiPicker onEmojiSelect={handleEmojiPickerResponse} autoFocus={true} />
|
||||||
) : (
|
) : (
|
||||||
<View style={[a.flex_row, a.gap_xs]}>
|
<View style={[a.flex_row, a.gap_xs]}>
|
||||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
|
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => {
|
||||||
<PressableWithHover
|
const alreadyReacted = hasAlreadyReacted(
|
||||||
key={emoji}
|
message,
|
||||||
onPress={() => handleEmojiSelect(emoji)}
|
currentAccount?.did,
|
||||||
hoverStyle={{backgroundColor: t.palette.primary_100}}
|
emoji,
|
||||||
style={[
|
)
|
||||||
a.rounded_xs,
|
return (
|
||||||
{height: 40, width: 40},
|
<PressableWithHover
|
||||||
a.justify_center,
|
key={emoji}
|
||||||
a.align_center,
|
onPress={() => handleEmojiSelect(emoji)}
|
||||||
]}>
|
hoverStyle={{
|
||||||
<Text style={[a.text_center, {fontSize: 30}]} emoji>
|
backgroundColor: alreadyReacted
|
||||||
{emoji}
|
? t.palette.negative_200
|
||||||
</Text>
|
: !limitReacted
|
||||||
</PressableWithHover>
|
? 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
|
<PressableWithHover
|
||||||
onPress={() => setExpanded(true)}
|
onPress={() => setExpanded(true)}
|
||||||
hoverStyle={{backgroundColor: t.palette.primary_100}}
|
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 {LayoutAnimation} from 'react-native'
|
||||||
import * as Clipboard from 'expo-clipboard'
|
import * as Clipboard from 'expo-clipboard'
|
||||||
import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
|
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 * as Prompt from '#/components/Prompt'
|
||||||
import {usePromptControl} from '#/components/Prompt'
|
import {usePromptControl} from '#/components/Prompt'
|
||||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||||
|
import {hasReachedReactionLimit} from './util'
|
||||||
|
|
||||||
export let MessageContextMenu = ({
|
export let MessageContextMenu = ({
|
||||||
message,
|
message,
|
||||||
@@ -41,7 +42,7 @@ export let MessageContextMenu = ({
|
|||||||
|
|
||||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||||
|
|
||||||
const onCopyMessage = React.useCallback(() => {
|
const onCopyMessage = useCallback(() => {
|
||||||
const str = richTextToString(
|
const str = richTextToString(
|
||||||
new RichText({
|
new RichText({
|
||||||
text: message.text,
|
text: message.text,
|
||||||
@@ -54,7 +55,7 @@ export let MessageContextMenu = ({
|
|||||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||||
}, [_, message.text, message.facets])
|
}, [_, message.text, message.facets])
|
||||||
|
|
||||||
const onPressTranslateMessage = React.useCallback(() => {
|
const onPressTranslateMessage = useCallback(() => {
|
||||||
const translatorUrl = getTranslatorLink(
|
const translatorUrl = getTranslatorLink(
|
||||||
message.text,
|
message.text,
|
||||||
langPrefs.primaryLanguage,
|
langPrefs.primaryLanguage,
|
||||||
@@ -62,7 +63,7 @@ export let MessageContextMenu = ({
|
|||||||
openLink(translatorUrl, true)
|
openLink(translatorUrl, true)
|
||||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||||
|
|
||||||
const onDelete = React.useCallback(() => {
|
const onDelete = useCallback(() => {
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
convo
|
convo
|
||||||
.deleteMessage(message.id)
|
.deleteMessage(message.id)
|
||||||
@@ -72,6 +73,30 @@ export let MessageContextMenu = ({
|
|||||||
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
||||||
}, [_, convo, message.id])
|
}, [_, 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(
|
const sender = convo.convo.members.find(
|
||||||
member => member.did === message.sender.did,
|
member => member.did === message.sender.did,
|
||||||
)
|
)
|
||||||
@@ -81,7 +106,10 @@ export let MessageContextMenu = ({
|
|||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
{isNative && (
|
{isNative && (
|
||||||
<ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
|
<ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
|
||||||
<EmojiReactionPicker message={message} />
|
<EmojiReactionPicker
|
||||||
|
message={message}
|
||||||
|
onEmojiSelect={onEmojiSelect}
|
||||||
|
/>
|
||||||
</ContextMenu.AuxiliaryView>
|
</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 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 {
|
import {
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {I18n} from '@lingui/core'
|
import {type I18n} from '@lingui/core'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
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 {useSession} from '#/state/session'
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
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 {isOnlyEmoji} from '#/alf/typography'
|
||||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {RichText} from '../RichText'
|
|
||||||
import {DateDivider} from './DateDivider'
|
import {DateDivider} from './DateDivider'
|
||||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||||
import {localDateString} from './util'
|
import {localDateString} from './util'
|
||||||
@@ -29,6 +42,8 @@ let MessageItem = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {convo} = useConvoActive()
|
||||||
|
|
||||||
const {message, nextMessage, prevMessage} = item
|
const {message, nextMessage, prevMessage} = item
|
||||||
const isPending = item.type === 'pending-message'
|
const isPending = item.type === 'pending-message'
|
||||||
@@ -134,6 +149,74 @@ let MessageItem = ({
|
|||||||
)}
|
)}
|
||||||
</ActionsWrapper>
|
</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 && (
|
{isLastInGroup && (
|
||||||
<MessageItemMetadata
|
<MessageItemMetadata
|
||||||
item={item}
|
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) {
|
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
||||||
switch (profile.associated?.chat?.allowIncoming) {
|
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
|
// not padding with 0s because it's not necessary, it's just used for comparison
|
||||||
return `${yyyy}-${mm}-${dd}`
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Insets, Platform} from 'react-native'
|
import {type Insets, Platform} from 'react-native'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
|
|
||||||
export const LOCAL_DEV_SERVICE =
|
export const LOCAL_DEV_SERVICE =
|
||||||
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
|
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
|
||||||
@@ -190,3 +190,5 @@ export const SUPPORTED_MIME_TYPES = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type SupportedMimeTypes = (typeof SUPPORTED_MIME_TYPES)[number]
|
export type SupportedMimeTypes = (typeof SUPPORTED_MIME_TYPES)[number]
|
||||||
|
|
||||||
|
export const EMOJI_REACTION_LIMIT = 5
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, {useCallback, useMemo, useState} from 'react'
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import {GestureResponderEvent, View} from 'react-native'
|
import {type GestureResponderEvent, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
ModerationOpts,
|
type ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -43,7 +43,7 @@ import {Link} from '#/components/Link'
|
|||||||
import {useMenuControl} from '#/components/Menu'
|
import {useMenuControl} from '#/components/Menu'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export let ChatListItem = ({
|
export let ChatListItem = ({
|
||||||
convo,
|
convo,
|
||||||
@@ -189,13 +189,62 @@ function ChatListItemReady({
|
|||||||
? _(msg`Conversation deleted`)
|
? _(msg`Conversation deleted`)
|
||||||
: _(msg`Message deleted`)
|
: _(msg`Message deleted`)
|
||||||
}
|
}
|
||||||
|
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastMessage)) {
|
||||||
|
const isFromMe =
|
||||||
|
convo.lastMessage.reaction.sender.did === currentAccount?.did
|
||||||
|
const lastMessageText = convo.lastMessage.message.text
|
||||||
|
const fallbackMessage = _(
|
||||||
|
msg({
|
||||||
|
message: 'a message',
|
||||||
|
comment: `If last message does not contain text, fall back to "{user} reacted to {a message}"`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isFromMe) {
|
||||||
|
lastMessage = _(
|
||||||
|
msg`You reacted ${convo.lastMessage.reaction.value} to ${
|
||||||
|
lastMessageText
|
||||||
|
? `"${convo.lastMessage.message.text}"`
|
||||||
|
: fallbackMessage
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const senderDid = convo.lastMessage.reaction.sender.did
|
||||||
|
const sender = convo.members.find(member => member.did === senderDid)
|
||||||
|
if (sender) {
|
||||||
|
lastMessage = _(
|
||||||
|
msg`${sanitizeDisplayName(
|
||||||
|
sender.displayName || sender.handle,
|
||||||
|
)} reacted ${convo.lastMessage.reaction.value} to ${
|
||||||
|
lastMessageText
|
||||||
|
? `"${convo.lastMessage.message.text}"`
|
||||||
|
: fallbackMessage
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lastMessage = _(
|
||||||
|
msg`Someone reacted ${convo.lastMessage.reaction.value} to ${
|
||||||
|
lastMessageText
|
||||||
|
? `"${convo.lastMessage.message.text}"`
|
||||||
|
: fallbackMessage
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastMessage,
|
lastMessage,
|
||||||
lastMessageSentAt,
|
lastMessageSentAt,
|
||||||
latestReportableMessage,
|
latestReportableMessage,
|
||||||
}
|
}
|
||||||
}, [_, convo.lastMessage, currentAccount?.did, isDeletedAccount])
|
}, [
|
||||||
|
_,
|
||||||
|
convo.lastMessage,
|
||||||
|
currentAccount?.did,
|
||||||
|
isDeletedAccount,
|
||||||
|
convo.members,
|
||||||
|
])
|
||||||
|
|
||||||
const [showActions, setShowActions] = useState(false)
|
const [showActions, setShowActions] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
BskyAgent,
|
type AtpAgent,
|
||||||
ChatBskyActorDefs,
|
type ChatBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
ChatBskyConvoGetLog,
|
type ChatBskyConvoGetLog,
|
||||||
ChatBskyConvoSendMessage,
|
type ChatBskyConvoSendMessage,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {XRPCError} from '@atproto/xrpc'
|
import {XRPCError} from '@atproto/xrpc'
|
||||||
import EventEmitter from 'eventemitter3'
|
import EventEmitter from 'eventemitter3'
|
||||||
@@ -19,19 +19,19 @@ import {
|
|||||||
NETWORK_FAILURE_STATUSES,
|
NETWORK_FAILURE_STATUSES,
|
||||||
} from '#/state/messages/convo/const'
|
} from '#/state/messages/convo/const'
|
||||||
import {
|
import {
|
||||||
ConvoDispatch,
|
type ConvoDispatch,
|
||||||
ConvoDispatchEvent,
|
ConvoDispatchEvent,
|
||||||
ConvoError,
|
type ConvoError,
|
||||||
ConvoErrorCode,
|
ConvoErrorCode,
|
||||||
ConvoEvent,
|
type ConvoEvent,
|
||||||
ConvoItem,
|
type ConvoItem,
|
||||||
ConvoItemError,
|
ConvoItemError,
|
||||||
ConvoParams,
|
type ConvoParams,
|
||||||
ConvoState,
|
type ConvoState,
|
||||||
ConvoStatus,
|
ConvoStatus,
|
||||||
} from '#/state/messages/convo/types'
|
} from '#/state/messages/convo/types'
|
||||||
import {MessagesEventBus} from '#/state/messages/events/agent'
|
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||||
import {MessagesEventBusError} from '#/state/messages/events/types'
|
import {type MessagesEventBusError} from '#/state/messages/events/types'
|
||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.ConversationAgent)
|
const logger = Logger.create(Logger.Context.ConversationAgent)
|
||||||
@@ -50,7 +50,7 @@ export function isConvoItemMessage(
|
|||||||
export class Convo {
|
export class Convo {
|
||||||
private id: string
|
private id: string
|
||||||
|
|
||||||
private agent: BskyAgent
|
private agent: AtpAgent
|
||||||
private events: MessagesEventBus
|
private events: MessagesEventBus
|
||||||
private senderUserDid: string
|
private senderUserDid: string
|
||||||
|
|
||||||
@@ -106,6 +106,8 @@ export class Convo {
|
|||||||
this.onFirehoseConnect = this.onFirehoseConnect.bind(this)
|
this.onFirehoseConnect = this.onFirehoseConnect.bind(this)
|
||||||
this.onFirehoseError = this.onFirehoseError.bind(this)
|
this.onFirehoseError = this.onFirehoseError.bind(this)
|
||||||
this.markConvoAccepted = this.markConvoAccepted.bind(this)
|
this.markConvoAccepted = this.markConvoAccepted.bind(this)
|
||||||
|
this.addReaction = this.addReaction.bind(this)
|
||||||
|
this.removeReaction = this.removeReaction.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private commit() {
|
private commit() {
|
||||||
@@ -147,6 +149,8 @@ export class Convo {
|
|||||||
sendMessage: undefined,
|
sendMessage: undefined,
|
||||||
fetchMessageHistory: undefined,
|
fetchMessageHistory: undefined,
|
||||||
markConvoAccepted: undefined,
|
markConvoAccepted: undefined,
|
||||||
|
addReaction: undefined,
|
||||||
|
removeReaction: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConvoStatus.Disabled:
|
case ConvoStatus.Disabled:
|
||||||
@@ -165,6 +169,8 @@ export class Convo {
|
|||||||
sendMessage: this.sendMessage,
|
sendMessage: this.sendMessage,
|
||||||
fetchMessageHistory: this.fetchMessageHistory,
|
fetchMessageHistory: this.fetchMessageHistory,
|
||||||
markConvoAccepted: this.markConvoAccepted,
|
markConvoAccepted: this.markConvoAccepted,
|
||||||
|
addReaction: this.addReaction,
|
||||||
|
removeReaction: this.removeReaction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConvoStatus.Error: {
|
case ConvoStatus.Error: {
|
||||||
@@ -180,6 +186,8 @@ export class Convo {
|
|||||||
sendMessage: undefined,
|
sendMessage: undefined,
|
||||||
fetchMessageHistory: undefined,
|
fetchMessageHistory: undefined,
|
||||||
markConvoAccepted: undefined,
|
markConvoAccepted: undefined,
|
||||||
|
addReaction: undefined,
|
||||||
|
removeReaction: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -195,6 +203,8 @@ export class Convo {
|
|||||||
sendMessage: undefined,
|
sendMessage: undefined,
|
||||||
fetchMessageHistory: undefined,
|
fetchMessageHistory: undefined,
|
||||||
markConvoAccepted: undefined,
|
markConvoAccepted: undefined,
|
||||||
|
addReaction: undefined,
|
||||||
|
removeReaction: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,6 +770,22 @@ export class Convo {
|
|||||||
this.deletedMessages.delete(ev.message.id)
|
this.deletedMessages.delete(ev.message.id)
|
||||||
needsCommit = true
|
needsCommit = true
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
(ChatBskyConvoDefs.isLogAddReaction(ev) ||
|
||||||
|
ChatBskyConvoDefs.isLogRemoveReaction(ev)) &&
|
||||||
|
ChatBskyConvoDefs.isMessageView(ev.message)
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
* Update if we have this in state - replace message wholesale. If we don't, don't worry about it.
|
||||||
|
*/
|
||||||
|
if (this.pastMessages.has(ev.message.id)) {
|
||||||
|
this.pastMessages.set(ev.message.id, ev.message)
|
||||||
|
needsCommit = true
|
||||||
|
}
|
||||||
|
if (this.newMessages.has(ev.message.id)) {
|
||||||
|
this.newMessages.set(ev.message.id, ev.message)
|
||||||
|
needsCommit = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1141,4 +1167,144 @@ export class Convo {
|
|||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an emoji reaction to a message
|
||||||
|
*
|
||||||
|
* @param messageId - the id of the message to add the reaction to
|
||||||
|
* @param emoji - must be one grapheme
|
||||||
|
*/
|
||||||
|
async addReaction(messageId: string, emoji: string) {
|
||||||
|
const optimisticReaction = {
|
||||||
|
value: emoji,
|
||||||
|
sender: {did: this.senderUserDid},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
let restore: null | (() => void) = null
|
||||||
|
if (this.pastMessages.has(messageId)) {
|
||||||
|
const prevMessage = this.pastMessages.get(messageId)
|
||||||
|
if (
|
||||||
|
ChatBskyConvoDefs.isMessageView(prevMessage) &&
|
||||||
|
// skip optimistic update if reaction already exists
|
||||||
|
!prevMessage.reactions?.find(
|
||||||
|
reaction =>
|
||||||
|
reaction.sender.did === this.senderUserDid &&
|
||||||
|
reaction.value === emoji,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (prevMessage.reactions) {
|
||||||
|
if (
|
||||||
|
prevMessage.reactions.filter(
|
||||||
|
reaction => reaction.sender.did === this.senderUserDid,
|
||||||
|
).length >= 5
|
||||||
|
) {
|
||||||
|
throw new Error('Maximum reactions reached')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pastMessages.set(messageId, {
|
||||||
|
...prevMessage,
|
||||||
|
reactions: [...(prevMessage.reactions ?? []), optimisticReaction],
|
||||||
|
})
|
||||||
|
this.commit()
|
||||||
|
restore = () => {
|
||||||
|
this.pastMessages.set(messageId, prevMessage)
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.newMessages.has(messageId)) {
|
||||||
|
const prevMessage = this.newMessages.get(messageId)
|
||||||
|
if (
|
||||||
|
ChatBskyConvoDefs.isMessageView(prevMessage) &&
|
||||||
|
!prevMessage.reactions?.find(reaction => reaction.value === emoji)
|
||||||
|
) {
|
||||||
|
if (prevMessage.reactions && prevMessage.reactions.length >= 5)
|
||||||
|
throw new Error('Maximum reactions reached')
|
||||||
|
this.newMessages.set(messageId, {
|
||||||
|
...prevMessage,
|
||||||
|
reactions: [...(prevMessage.reactions ?? []), optimisticReaction],
|
||||||
|
})
|
||||||
|
this.commit()
|
||||||
|
restore = () => {
|
||||||
|
this.newMessages.set(messageId, prevMessage)
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Adding reaction ${emoji} to message ${messageId}`)
|
||||||
|
const {data} = await this.agent.chat.bsky.convo.addReaction(
|
||||||
|
{messageId, value: emoji, convoId: this.convoId},
|
||||||
|
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
||||||
|
)
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(data.message)) {
|
||||||
|
if (this.pastMessages.has(messageId)) {
|
||||||
|
this.pastMessages.set(messageId, data.message)
|
||||||
|
this.commit()
|
||||||
|
} else if (this.newMessages.has(messageId)) {
|
||||||
|
this.newMessages.set(messageId, data.message)
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (restore) restore()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove a reaction from a message.
|
||||||
|
*
|
||||||
|
* @param messageId - The ID of the message to remove the reaction from.
|
||||||
|
* @param emoji - The emoji to remove.
|
||||||
|
*/
|
||||||
|
async removeReaction(messageId: string, emoji: string) {
|
||||||
|
let restore: null | (() => void) = null
|
||||||
|
if (this.pastMessages.has(messageId)) {
|
||||||
|
const prevMessage = this.pastMessages.get(messageId)
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(prevMessage)) {
|
||||||
|
this.pastMessages.set(messageId, {
|
||||||
|
...prevMessage,
|
||||||
|
reactions: prevMessage.reactions?.filter(
|
||||||
|
reaction =>
|
||||||
|
reaction.value !== emoji ||
|
||||||
|
reaction.sender.did !== this.senderUserDid,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
this.commit()
|
||||||
|
restore = () => {
|
||||||
|
this.pastMessages.set(messageId, prevMessage)
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.newMessages.has(messageId)) {
|
||||||
|
const prevMessage = this.newMessages.get(messageId)
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(prevMessage)) {
|
||||||
|
this.newMessages.set(messageId, {
|
||||||
|
...prevMessage,
|
||||||
|
reactions: prevMessage.reactions?.filter(
|
||||||
|
reaction =>
|
||||||
|
reaction.value !== emoji ||
|
||||||
|
reaction.sender.did !== this.senderUserDid,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
this.commit()
|
||||||
|
restore = () => {
|
||||||
|
this.newMessages.set(messageId, prevMessage)
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Removing reaction ${emoji} from message ${messageId}`)
|
||||||
|
await this.agent.chat.bsky.convo.removeReaction(
|
||||||
|
{messageId, value: emoji, convoId: this.convoId},
|
||||||
|
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (restore) restore()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, {useContext, useState, useSyncExternalStore} from 'react'
|
import React, {useContext, useState, useSyncExternalStore} from 'react'
|
||||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAppState} from '#/lib/hooks/useAppState'
|
import {useAppState} from '#/lib/hooks/useAppState'
|
||||||
import {Convo} from '#/state/messages/convo/agent'
|
import {Convo} from '#/state/messages/convo/agent'
|
||||||
import {
|
import {
|
||||||
ConvoParams,
|
type ConvoParams,
|
||||||
ConvoState,
|
type ConvoState,
|
||||||
ConvoStateBackgrounded,
|
type ConvoStateBackgrounded,
|
||||||
ConvoStateDisabled,
|
type ConvoStateDisabled,
|
||||||
ConvoStateReady,
|
type ConvoStateReady,
|
||||||
ConvoStateSuspended,
|
type ConvoStateSuspended,
|
||||||
} from '#/state/messages/convo/types'
|
} from '#/state/messages/convo/types'
|
||||||
import {isConvoActive} from '#/state/messages/convo/util'
|
import {isConvoActive} from '#/state/messages/convo/util'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
BskyAgent,
|
type BskyAgent,
|
||||||
ChatBskyActorDefs,
|
type ChatBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
type ChatBskyConvoDefs,
|
||||||
ChatBskyConvoSendMessage,
|
type ChatBskyConvoSendMessage,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {MessagesEventBus} from '#/state/messages/events/agent'
|
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||||
|
|
||||||
export type ConvoParams = {
|
export type ConvoParams = {
|
||||||
convoId: string
|
convoId: string
|
||||||
@@ -142,6 +142,8 @@ type SendMessage = (
|
|||||||
) => void
|
) => void
|
||||||
type FetchMessageHistory = () => Promise<void>
|
type FetchMessageHistory = () => Promise<void>
|
||||||
type MarkConvoAccepted = () => void
|
type MarkConvoAccepted = () => void
|
||||||
|
type AddReaction = (messageId: string, reaction: string) => Promise<void>
|
||||||
|
type RemoveReaction = (messageId: string, reaction: string) => Promise<void>
|
||||||
|
|
||||||
export type ConvoStateUninitialized = {
|
export type ConvoStateUninitialized = {
|
||||||
status: ConvoStatus.Uninitialized
|
status: ConvoStatus.Uninitialized
|
||||||
@@ -155,6 +157,8 @@ export type ConvoStateUninitialized = {
|
|||||||
sendMessage: undefined
|
sendMessage: undefined
|
||||||
fetchMessageHistory: undefined
|
fetchMessageHistory: undefined
|
||||||
markConvoAccepted: undefined
|
markConvoAccepted: undefined
|
||||||
|
addReaction: undefined
|
||||||
|
removeReaction: undefined
|
||||||
}
|
}
|
||||||
export type ConvoStateInitializing = {
|
export type ConvoStateInitializing = {
|
||||||
status: ConvoStatus.Initializing
|
status: ConvoStatus.Initializing
|
||||||
@@ -168,6 +172,8 @@ export type ConvoStateInitializing = {
|
|||||||
sendMessage: undefined
|
sendMessage: undefined
|
||||||
fetchMessageHistory: undefined
|
fetchMessageHistory: undefined
|
||||||
markConvoAccepted: undefined
|
markConvoAccepted: undefined
|
||||||
|
addReaction: undefined
|
||||||
|
removeReaction: undefined
|
||||||
}
|
}
|
||||||
export type ConvoStateReady = {
|
export type ConvoStateReady = {
|
||||||
status: ConvoStatus.Ready
|
status: ConvoStatus.Ready
|
||||||
@@ -181,6 +187,8 @@ export type ConvoStateReady = {
|
|||||||
sendMessage: SendMessage
|
sendMessage: SendMessage
|
||||||
fetchMessageHistory: FetchMessageHistory
|
fetchMessageHistory: FetchMessageHistory
|
||||||
markConvoAccepted: MarkConvoAccepted
|
markConvoAccepted: MarkConvoAccepted
|
||||||
|
addReaction: AddReaction
|
||||||
|
removeReaction: RemoveReaction
|
||||||
}
|
}
|
||||||
export type ConvoStateBackgrounded = {
|
export type ConvoStateBackgrounded = {
|
||||||
status: ConvoStatus.Backgrounded
|
status: ConvoStatus.Backgrounded
|
||||||
@@ -194,6 +202,8 @@ export type ConvoStateBackgrounded = {
|
|||||||
sendMessage: SendMessage
|
sendMessage: SendMessage
|
||||||
fetchMessageHistory: FetchMessageHistory
|
fetchMessageHistory: FetchMessageHistory
|
||||||
markConvoAccepted: MarkConvoAccepted
|
markConvoAccepted: MarkConvoAccepted
|
||||||
|
addReaction: AddReaction
|
||||||
|
removeReaction: RemoveReaction
|
||||||
}
|
}
|
||||||
export type ConvoStateSuspended = {
|
export type ConvoStateSuspended = {
|
||||||
status: ConvoStatus.Suspended
|
status: ConvoStatus.Suspended
|
||||||
@@ -207,6 +217,8 @@ export type ConvoStateSuspended = {
|
|||||||
sendMessage: SendMessage
|
sendMessage: SendMessage
|
||||||
fetchMessageHistory: FetchMessageHistory
|
fetchMessageHistory: FetchMessageHistory
|
||||||
markConvoAccepted: MarkConvoAccepted
|
markConvoAccepted: MarkConvoAccepted
|
||||||
|
addReaction: AddReaction
|
||||||
|
removeReaction: RemoveReaction
|
||||||
}
|
}
|
||||||
export type ConvoStateError = {
|
export type ConvoStateError = {
|
||||||
status: ConvoStatus.Error
|
status: ConvoStatus.Error
|
||||||
@@ -220,6 +232,8 @@ export type ConvoStateError = {
|
|||||||
sendMessage: undefined
|
sendMessage: undefined
|
||||||
fetchMessageHistory: undefined
|
fetchMessageHistory: undefined
|
||||||
markConvoAccepted: undefined
|
markConvoAccepted: undefined
|
||||||
|
addReaction: undefined
|
||||||
|
removeReaction: undefined
|
||||||
}
|
}
|
||||||
export type ConvoStateDisabled = {
|
export type ConvoStateDisabled = {
|
||||||
status: ConvoStatus.Disabled
|
status: ConvoStatus.Disabled
|
||||||
@@ -233,6 +247,8 @@ export type ConvoStateDisabled = {
|
|||||||
sendMessage: SendMessage
|
sendMessage: SendMessage
|
||||||
fetchMessageHistory: FetchMessageHistory
|
fetchMessageHistory: FetchMessageHistory
|
||||||
markConvoAccepted: MarkConvoAccepted
|
markConvoAccepted: MarkConvoAccepted
|
||||||
|
addReaction: AddReaction
|
||||||
|
removeReaction: RemoveReaction
|
||||||
}
|
}
|
||||||
export type ConvoState =
|
export type ConvoState =
|
||||||
| ConvoStateUninitialized
|
| ConvoStateUninitialized
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import React, {
|
import {createContext, useCallback, useContext, useEffect, useMemo} from 'react'
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
import {
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
ChatBskyConvoListConvos,
|
type ChatBskyConvoListConvos,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
ModerationOpts,
|
type ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
InfiniteData,
|
type InfiniteData,
|
||||||
QueryClient,
|
type QueryClient,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
@@ -316,6 +310,57 @@ export function ListConvosProviderInner({
|
|||||||
rev: logRef.rev,
|
rev: logRef.rev,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
} else if (ChatBskyConvoDefs.isLogAddReaction(log)) {
|
||||||
|
const logRef: ChatBskyConvoDefs.LogAddReaction = log
|
||||||
|
queryClient.setQueriesData(
|
||||||
|
{queryKey: [RQKEY_ROOT]},
|
||||||
|
(old?: ConvoListQueryData) =>
|
||||||
|
optimisticUpdate(logRef.convoId, old, convo => ({
|
||||||
|
...convo,
|
||||||
|
lastMessage: {
|
||||||
|
$type: 'chat.bsky.convo.defs#messageAndReactionView',
|
||||||
|
reaction: logRef.reaction,
|
||||||
|
message: logRef.message,
|
||||||
|
},
|
||||||
|
rev: logRef.rev,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
} else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) {
|
||||||
|
if (ChatBskyConvoDefs.isMessageView(log.message)) {
|
||||||
|
for (const [_queryKey, queryData] of queryClient.getQueriesData<
|
||||||
|
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||||
|
>({
|
||||||
|
queryKey: [RQKEY_ROOT],
|
||||||
|
})) {
|
||||||
|
if (!queryData?.pages) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const page of queryData.pages) {
|
||||||
|
for (const convo of page.convos) {
|
||||||
|
if (
|
||||||
|
// if the convo is the same
|
||||||
|
log.convoId === convo.id &&
|
||||||
|
ChatBskyConvoDefs.isMessageAndReactionView(
|
||||||
|
convo.lastMessage,
|
||||||
|
) &&
|
||||||
|
ChatBskyConvoDefs.isMessageView(
|
||||||
|
convo.lastMessage.message,
|
||||||
|
) &&
|
||||||
|
// ...and the message is the same
|
||||||
|
convo.lastMessage.message.id === log.message.id &&
|
||||||
|
// ...and the reaction is the same
|
||||||
|
convo.lastMessage.reaction.sender.did ===
|
||||||
|
log.reaction.sender.did &&
|
||||||
|
convo.lastMessage.reaction.value === log.reaction.value
|
||||||
|
) {
|
||||||
|
// refetch, because we don't know what the last message is now
|
||||||
|
debouncedRefetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,15 +80,15 @@
|
|||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/api@^0.14.7":
|
"@atproto/api@^0.14.14":
|
||||||
version "0.14.7"
|
version "0.14.14"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.7.tgz#3ffa02d6b3baf9e265dab170367ffade08023567"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.14.tgz#5d2d2e6156eab6ca0d463c114b4a3865275e9aac"
|
||||||
integrity sha512-YG2kvAtsgtajLlLrorYuHcxGgepG0c/RUB2/iJyBnwKjGqDLG8joOETf38JSNiGzs6NJbNKa9NHG6BQKourxBA==
|
integrity sha512-ryawcnmazVSWYfq11ujPHauY77GfkM3mF0rZOkqENN2Ptnl6BZXJvpA0zLA/sQ5YBLcHXSEWg5Xdq+8i1l+8gA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.4.0"
|
"@atproto/common-web" "^0.4.0"
|
||||||
"@atproto/lexicon" "^0.4.7"
|
"@atproto/lexicon" "^0.4.9"
|
||||||
"@atproto/syntax" "^0.3.3"
|
"@atproto/syntax" "^0.4.0"
|
||||||
"@atproto/xrpc" "^0.6.9"
|
"@atproto/xrpc" "^0.6.11"
|
||||||
await-lock "^2.2.2"
|
await-lock "^2.2.2"
|
||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
@@ -302,6 +302,17 @@
|
|||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto/lexicon@^0.4.9":
|
||||||
|
version "0.4.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.9.tgz#612951a85ecc1398366bd837cda6be89440f179d"
|
||||||
|
integrity sha512-/tmEuHQFr51V2V7EAVJzaA40sqJ7ylAZpR962VbOsPtmcdOHvezbjVHYEMXgfb927hS+xqbVyzBTbu5w9v8prA==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/common-web" "^0.4.0"
|
||||||
|
"@atproto/syntax" "^0.4.0"
|
||||||
|
iso-datestring-validator "^2.2.2"
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/oauth-provider@^0.2.17":
|
"@atproto/oauth-provider@^0.2.17":
|
||||||
version "0.2.17"
|
version "0.2.17"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.17.tgz#4644d391eedbbbbe5825ecc0e8cc03f1c6433b95"
|
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.17.tgz#4644d391eedbbbbe5825ecc0e8cc03f1c6433b95"
|
||||||
@@ -446,6 +457,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.3.tgz#6debe8983985378104822172a128e429931bf3f7"
|
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.3.tgz#6debe8983985378104822172a128e429931bf3f7"
|
||||||
integrity sha512-F1LZweesNYdBbZBXVa72N/cSvchG8Q1tG4/209ZXbIuM3FwQtkgn+zgmmV4P4ORmhOeXPBNXvMBpcqiwx/gEQQ==
|
integrity sha512-F1LZweesNYdBbZBXVa72N/cSvchG8Q1tG4/209ZXbIuM3FwQtkgn+zgmmV4P4ORmhOeXPBNXvMBpcqiwx/gEQQ==
|
||||||
|
|
||||||
|
"@atproto/syntax@^0.4.0":
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2"
|
||||||
|
integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==
|
||||||
|
|
||||||
"@atproto/xrpc-server@^0.7.11":
|
"@atproto/xrpc-server@^0.7.11":
|
||||||
version "0.7.11"
|
version "0.7.11"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.11.tgz#efadcfdaaaa0ff5576d1ee97e46dcbc6dafcb0b6"
|
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.11.tgz#efadcfdaaaa0ff5576d1ee97e46dcbc6dafcb0b6"
|
||||||
@@ -464,6 +480,14 @@
|
|||||||
ws "^8.12.0"
|
ws "^8.12.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto/xrpc@^0.6.11":
|
||||||
|
version "0.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.11.tgz#54c527e39a2f5ddd2655b11f7cb99b8f303d8364"
|
||||||
|
integrity sha512-J2cZP8FjoDN0UkyTYBlCvKvxwBbDm4dld47u6FQK30RJy9YpSiUkdxJJ10NYqpi7JVny3M0qWQgpWJDV94+PdA==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/lexicon" "^0.4.9"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/xrpc@^0.6.9":
|
"@atproto/xrpc@^0.6.9":
|
||||||
version "0.6.9"
|
version "0.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.9.tgz#6e1effc42cdab40741a73ead5c276183284887d2"
|
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.9.tgz#6e1effc42cdab40741a73ead5c276183284887d2"
|
||||||
|
|||||||
Reference in New Issue
Block a user