[DMs] Emoji reaction picker (#8023)
This commit is contained in:
@@ -23,28 +23,6 @@ export function ActionsWrapper({
|
||||
// will always be true, since this file is platform split
|
||||
trigger.isNative && (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
{/* {isNative && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.absolute,
|
||||
{bottom: '100%'},
|
||||
isFromSelf ? a.right_0 : a.left_0,
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.shadow_lg,
|
||||
a.py_xs,
|
||||
a.px_md,
|
||||
a.gap_md,
|
||||
a.mb_xs,
|
||||
]}>
|
||||
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
|
||||
<Text key={emoji} style={[a.text_center, {fontSize: 32}]}>
|
||||
{emoji}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)} */}
|
||||
<View
|
||||
style={[
|
||||
{maxWidth: '80%'},
|
||||
@@ -56,7 +34,7 @@ export function ActionsWrapper({
|
||||
accessibilityActions={[
|
||||
{name: 'activate', label: _(msg`Open message options`)},
|
||||
]}
|
||||
onAccessibilityAction={trigger.control.open}>
|
||||
onAccessibilityAction={() => trigger.control.open('full')}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -4,7 +4,9 @@ import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '../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 {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
|
||||
export function ActionsWrapper({
|
||||
message,
|
||||
@@ -47,10 +49,35 @@ export function ActionsWrapper({
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
isFromSelf
|
||||
? [a.mr_xl, {marginLeft: 'auto'}]
|
||||
: [a.ml_xl, {marginRight: 'auto'}],
|
||||
? [a.mr_md, {marginLeft: 'auto'}]
|
||||
: [a.ml_md, {marginRight: 'auto'}],
|
||||
]}>
|
||||
<EmojiReactionPicker message={message}>
|
||||
{({props, state, isNative, control}) => {
|
||||
// always false, file is platform split
|
||||
if (isNative) return null
|
||||
const showMenuTrigger = showActions || control.isOpen ? 1 : 0
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
style={[
|
||||
{opacity: showMenuTrigger},
|
||||
a.p_xs,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<EmojiSmileIcon
|
||||
size="md"
|
||||
style={t.atoms.text_contrast_medium}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</EmojiReactionPicker>
|
||||
<MessageContextMenu message={message}>
|
||||
{({props, state, isNative, control}) => {
|
||||
// always false, file is platform split
|
||||
@@ -61,11 +88,14 @@ export function ActionsWrapper({
|
||||
{...props}
|
||||
style={[
|
||||
{opacity: showMenuTrigger},
|
||||
a.p_sm,
|
||||
a.p_xs,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<DotsHorizontalIcon size="md" style={t.atoms.text} />
|
||||
<DotsHorizontalIcon
|
||||
size="md"
|
||||
style={t.atoms.text_contrast_medium}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import {useState} from 'react'
|
||||
import {Modal, Pressable, View} from 'react-native'
|
||||
// @ts-expect-error internal component, not supposed to be used directly
|
||||
// waiting on more customisability: https://github.com/okwasniewski/react-native-emoji-popup/issues/1#issuecomment-2737463753
|
||||
import EmojiPopupView from 'react-native-emoji-popup/src/EmojiPopupViewNativeComponent'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function EmojiPopup({
|
||||
children,
|
||||
onEmojiSelected,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onEmojiSelected: (emoji: string) => void
|
||||
}) {
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
accessibilityLabel={_('Open full emoji list')}
|
||||
accessibilityHint=""
|
||||
accessibilityRole="button"
|
||||
onPress={() => setModalVisible(true)}>
|
||||
{children}
|
||||
</Pressable>
|
||||
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => setModalVisible(false)}>
|
||||
<View style={[a.flex_1, {backgroundColor: t.palette.white}]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
a.pl_lg,
|
||||
a.pr_md,
|
||||
a.py_sm,
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
a.justify_between,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Text style={[a.font_bold, a.text_md]}>
|
||||
<Trans>Add Reaction</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_('Close')}
|
||||
onPress={() => setModalVisible(false)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round">
|
||||
<ButtonIcon icon={TimesLarge_Stroke2_Corner0_Rounded} />
|
||||
</Button>
|
||||
</View>
|
||||
<EmojiPopupView
|
||||
onEmojiSelected={({
|
||||
nativeEvent: {emoji},
|
||||
}: {
|
||||
nativeEvent: {emoji: string}
|
||||
}) => {
|
||||
setModalVisible(false)
|
||||
onEmojiSelected(emoji)
|
||||
}}
|
||||
style={[a.flex_1, a.w_full]}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {EmojiPopup} from 'react-native-emoji-popup'
|
||||
@@ -0,0 +1,118 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {Alert, useWindowDimensions, View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import * as ContextMenu from '#/components/ContextMenu'
|
||||
import {
|
||||
useContextMenuContext,
|
||||
useContextMenuMenuContext,
|
||||
} from '#/components/ContextMenu/context'
|
||||
import {
|
||||
EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyesIcon,
|
||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon,
|
||||
} from '#/components/icons/Emoji'
|
||||
import {type TriggerProps} from '#/components/Menu/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {EmojiPopup} from './EmojiPopup'
|
||||
|
||||
export function EmojiReactionPicker({
|
||||
message,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
children?: TriggerProps['children']
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const t = useTheme()
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
const {measurement, close} = useContextMenuContext()
|
||||
const {align} = useContextMenuMenuContext()
|
||||
const [layout, setLayout] = useState({width: 0, height: 0})
|
||||
const {width: screenWidth} = useWindowDimensions()
|
||||
|
||||
// 1 in 100 chance of showing heart eyes icon
|
||||
const EmojiIcon = useMemo(() => {
|
||||
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,
|
||||
y: (measurement?.y ?? 0) - tokens.space.xs - layout.height,
|
||||
height: layout.height,
|
||||
width: layout.width,
|
||||
}
|
||||
}, [measurement, align, screenWidth, layout])
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={evt => setLayout(evt.nativeEvent.layout)}
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.absolute,
|
||||
{bottom: '100%'},
|
||||
isFromSelf ? a.right_0 : a.left_0,
|
||||
t.scheme === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
||||
a.flex_row,
|
||||
a.p_xs,
|
||||
a.gap_xs,
|
||||
a.mb_xs,
|
||||
a.z_20,
|
||||
a.border,
|
||||
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>
|
||||
))}
|
||||
<EmojiPopup
|
||||
onEmojiSelected={emoji => {
|
||||
close()
|
||||
handleEmojiSelect(emoji)
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
t.scheme === 'light'
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg_contrast_50,
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<EmojiIcon size="xl" fill={t.palette.contrast_400} />
|
||||
</View>
|
||||
</EmojiPopup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {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 {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 {Text} from '#/components/Typography'
|
||||
|
||||
export function EmojiReactionPicker({
|
||||
children,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
children?: TriggerProps['children']
|
||||
}) {
|
||||
if (!children)
|
||||
throw new Error('EmojiReactionPicker requires the children prop on web')
|
||||
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Add emoji reaction`)}>{children}</Menu.Trigger>
|
||||
<Menu.Outer>
|
||||
<MenuInner />
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuInner() {
|
||||
const t = useTheme()
|
||||
const {control} = Menu.useMenuContext()
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const handleEmojiPickerResponse = (emoji: Emoji) => {
|
||||
handleEmojiSelect(emoji.native)
|
||||
}
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
control.close()
|
||||
window.alert(emoji)
|
||||
}
|
||||
|
||||
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>
|
||||
))}
|
||||
<PressableWithHover
|
||||
onPress={() => setExpanded(true)}
|
||||
hoverStyle={{backgroundColor: t.palette.primary_100}}
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<DotGridIcon size="lg" style={t.atoms.text_contrast_medium} />
|
||||
</PressableWithHover>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from 'react'
|
||||
import {LayoutAnimation} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {ChatBskyConvoDefs, RichText} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as ContextMenu from '#/components/ContextMenu'
|
||||
import {TriggerProps} from '#/components/ContextMenu/types'
|
||||
import {type TriggerProps} from '#/components/ContextMenu/types'
|
||||
import {ReportDialog} from '#/components/dms/ReportDialog'
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
@@ -21,6 +22,7 @@ import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {usePromptControl} from '#/components/Prompt'
|
||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
|
||||
export let MessageContextMenu = ({
|
||||
message,
|
||||
@@ -77,6 +79,12 @@ export let MessageContextMenu = ({
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
{isNative && (
|
||||
<ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
|
||||
<EmojiReactionPicker message={message} />
|
||||
</ContextMenu.AuxiliaryView>
|
||||
)}
|
||||
|
||||
<ContextMenu.Trigger
|
||||
label={_(msg`Message options`)}
|
||||
contentLabel={_(
|
||||
|
||||
Reference in New Issue
Block a user