diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/dms/MessageItem.tsx | 74 | ||||
-rw-r--r-- | src/components/dms/MessageItemEmbed.tsx | 109 | ||||
-rw-r--r-- | src/components/dms/MessageMenu.tsx | 47 | ||||
-rw-r--r-- | src/components/moderation/ContentHider.tsx | 17 |
4 files changed, 198 insertions, 49 deletions
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index c5ff81091..b498ddf1c 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -6,7 +6,11 @@ import { TextStyle, View, } from 'react-native' -import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api' +import { + AppBskyEmbedRecord, + ChatBskyConvoDefs, + RichText as RichTextAPI, +} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -18,6 +22,7 @@ import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {RichText} from '../RichText' +import {MessageItemEmbed} from './MessageItemEmbed' let MessageItem = ({ item, @@ -77,37 +82,44 @@ let MessageItem = ({ return ( <View style={[isFromSelf ? a.mr_md : a.ml_md]}> <ActionsWrapper isFromSelf={isFromSelf} message={message}> - <View - style={[ - a.py_sm, - a.my_2xs, - a.rounded_md, - { - paddingLeft: 14, - paddingRight: 14, - backgroundColor: isFromSelf - ? isPending - ? pendingColor - : t.palette.primary_500 - : t.palette.contrast_50, - borderRadius: 17, - }, - isFromSelf - ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} - : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, - ]}> - <RichText - value={rt} + {AppBskyEmbedRecord.isMain(message.embed) && ( + <MessageItemEmbed embed={message.embed} /> + )} + {rt.text.length > 0 && ( + <View style={[ - a.text_md, - a.leading_snug, - isFromSelf && {color: t.palette.white}, - isPending && t.name !== 'light' && {color: t.palette.primary_300}, - ]} - interactiveStyle={a.underline} - enableTags - /> - </View> + a.py_sm, + a.my_2xs, + a.rounded_md, + { + paddingLeft: 14, + paddingRight: 14, + backgroundColor: isFromSelf + ? isPending + ? pendingColor + : t.palette.primary_500 + : t.palette.contrast_50, + borderRadius: 17, + }, + isFromSelf ? a.self_end : a.self_start, + isFromSelf + ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} + : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, + ]}> + <RichText + value={rt} + style={[ + a.text_md, + a.leading_snug, + isFromSelf && {color: t.palette.white}, + isPending && + t.name !== 'light' && {color: t.palette.primary_300}, + ]} + interactiveStyle={a.underline} + enableTags + /> + </View> + )} </ActionsWrapper> {isLastInGroup && ( diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx new file mode 100644 index 000000000..d64563b91 --- /dev/null +++ b/src/components/dms/MessageItemEmbed.tsx @@ -0,0 +1,109 @@ +import React, {useMemo} from 'react' +import {View} from 'react-native' +import { + AppBskyEmbedRecord, + AppBskyFeedPost, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' + +import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {makeProfileLink} from '#/lib/routes/links' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {usePostQuery} from '#/state/queries/post' +import {PostEmbeds} from '#/view/com/util/post-embeds' +import {PostMeta} from '#/view/com/util/PostMeta' +import {atoms as a, useTheme} from '#/alf' +import {Link} from '#/components/Link' +import {ContentHider} from '#/components/moderation/ContentHider' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {RichText} from '#/components/RichText' + +let MessageItemEmbed = ({ + embed, +}: { + embed: AppBskyEmbedRecord.Main +}): React.ReactNode => { + const t = useTheme() + const {data: post} = usePostQuery(embed.record.uri) + + const moderationOpts = useModerationOpts() + const moderation = useMemo( + () => + moderationOpts && post ? moderatePost(post, moderationOpts) : undefined, + [moderationOpts, post], + ) + + const {rt, record} = useMemo(() => { + if ( + post && + AppBskyFeedPost.isRecord(post.record) && + AppBskyFeedPost.validateRecord(post.record).success + ) { + return { + rt: new RichTextAPI({ + text: post.record.text, + facets: post.record.facets, + }), + record: post.record, + } + } + + return {rt: undefined, record: undefined} + }, [post]) + + if (!post || !moderation || !rt || !record) { + return null + } + + const itemUrip = new AtUri(post.uri) + const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey) + + return ( + <Link to={itemHref}> + <View + style={[ + a.w_full, + t.atoms.bg, + t.atoms.border_contrast_low, + a.rounded_md, + a.border, + a.p_md, + a.my_xs, + ]}> + <PostMeta + showAvatar + author={post.author} + moderation={moderation} + authorHasWarning={!!post.author.labels?.length} + timestamp={post.indexedAt} + postHref={itemHref} + /> + <ContentHider modui={moderation.ui('contentView')}> + <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} /> + {rt.text && ( + <View style={a.mt_xs}> + <RichText + enableTags + testID="postText" + value={rt} + style={[a.text_sm, t.atoms.text_contrast_high]} + authorHandle={post.author.handle} + /> + </View> + )} + {post.embed && ( + <PostEmbeds + embed={post.embed} + moderation={moderation} + style={a.mt_xs} + quoteTextStyle={[a.text_sm, t.atoms.text_contrast_high]} + /> + )} + </ContentHider> + </View> + </Link> + ) +} +MessageItemEmbed = React.memo(MessageItemEmbed) +export {MessageItemEmbed} diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx index 21812d268..92913d1cb 100644 --- a/src/components/dms/MessageMenu.tsx +++ b/src/components/dms/MessageMenu.tsx @@ -6,12 +6,16 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {richTextToString} from '#/lib/strings/rich-text-helpers' +import {getTranslatorLink} from '#/locale/helpers' +import {useLanguagePrefs} from '#/state/preferences' +import {useOpenLink} from '#/state/preferences/in-app-browser' import {isWeb} from 'platform/detection' 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 {ReportDialog} from '#/components/dms/ReportDialog' +import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' @@ -35,10 +39,12 @@ export let MessageMenu = ({ const convo = useConvoActive() const deleteControl = usePromptControl() const reportControl = usePromptControl() + const langPrefs = useLanguagePrefs() + const openLink = useOpenLink() const isFromSelf = message.sender?.did === currentAccount?.did - const onCopyPostText = React.useCallback(() => { + const onCopyMessage = React.useCallback(() => { const str = richTextToString( new RichText({ text: message.text, @@ -51,6 +57,14 @@ export let MessageMenu = ({ Toast.show(_(msg`Copied to clipboard`)) }, [_, message.text, message.facets]) + const onPressTranslateMessage = React.useCallback(() => { + const translatorUrl = getTranslatorLink( + message.text, + langPrefs.primaryLanguage, + ) + openLink(translatorUrl) + }, [langPrefs.primaryLanguage, message.text, openLink]) + const onDelete = React.useCallback(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) convo @@ -81,16 +95,27 @@ export let MessageMenu = ({ )} <Menu.Outer> - <Menu.Group> - <Menu.Item - testID="messageDropdownCopyBtn" - label={_(msg`Copy message text`)} - onPress={onCopyPostText}> - <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText> - <Menu.ItemIcon icon={ClipboardIcon} position="right" /> - </Menu.Item> - </Menu.Group> - <Menu.Divider /> + {message.text.length > 0 && ( + <> + <Menu.Group> + <Menu.Item + testID="messageDropdownTranslateBtn" + label={_(msg`Translate`)} + onPress={onPressTranslateMessage}> + <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> + <Menu.ItemIcon icon={Translate} position="right" /> + </Menu.Item> + <Menu.Item + testID="messageDropdownCopyBtn" + label={_(msg`Copy message text`)} + onPress={onCopyMessage}> + <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText> + <Menu.ItemIcon icon={ClipboardIcon} position="right" /> + </Menu.Item> + </Menu.Group> + <Menu.Divider /> + </> + )} <Menu.Group> <Menu.Item testID="messageDropdownDeleteBtn" diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index 1e8f36d31..fd71ec838 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -1,20 +1,19 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {ModerationUI} from '@atproto/api' -import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {isJustAMute} from '#/lib/moderation' +import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {sanitizeDisplayName} from '#/lib/strings/display-names' - -import {atoms as a, useTheme, useBreakpoints, web} from '#/alf' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button} from '#/components/Button' -import {Text} from '#/components/Typography' import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog' +import {Text} from '#/components/Typography' export function ContentHider({ testID, @@ -52,7 +51,9 @@ export function ContentHider({ <ModerationDetailsDialog control={control} modcause={blur} /> <Button - onPress={() => { + onPress={e => { + e.preventDefault() + e.stopPropagation() if (!modui.noOverride) { setOverride(v => !v) } else { @@ -121,7 +122,9 @@ export function ContentHider({ {desc.source && blur.type === 'label' && !override && ( <Button - onPress={() => { + onPress={e => { + e.preventDefault() + e.stopPropagation() control.open() }} label={_( |