[🐴] Record message (#4230)

* send record via link in text

* re-trim text after removing link

* record message

* only show copy text if message + add translate

* reduce padding

* adjust padding

* Tweak spacing

* Stop clickthrough for hidden content

* Update bg to show labels

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Samuel Newman 2024-05-31 18:43:04 +03:00 committed by GitHub
parent 8eb3cebb36
commit 22e1eb18c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 225 additions and 58 deletions

View File

@ -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,6 +82,10 @@ let MessageItem = ({
return (
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
{AppBskyEmbedRecord.isMain(message.embed) && (
<MessageItemEmbed embed={message.embed} />
)}
{rt.text.length > 0 && (
<View
style={[
a.py_sm,
@ -92,6 +101,7 @@ let MessageItem = ({
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf ? a.self_end : a.self_start,
isFromSelf
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
@ -102,12 +112,14 @@ let MessageItem = ({
a.text_md,
a.leading_snug,
isFromSelf && {color: t.palette.white},
isPending && t.name !== 'light' && {color: t.palette.primary_300},
isPending &&
t.name !== 'light' && {color: t.palette.primary_300},
]}
interactiveStyle={a.underline}
enableTags
/>
</View>
)}
</ActionsWrapper>
{isLastInGroup && (

View File

@ -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}

View File

@ -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>
{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={onCopyPostText}>
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"

View File

@ -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={_(

View File

@ -2,6 +2,7 @@ import React from 'react'
import {
StyleProp,
StyleSheet,
TextStyle,
TouchableOpacity,
View,
ViewStyle,
@ -31,7 +32,7 @@ import {InfoCircleIcon} from 'lib/icons'
import {makeProfileLink} from 'lib/routes/links'
import {precacheProfile} from 'state/queries/profile'
import {ComposerOptsQuote} from 'state/shell/composer'
import {atoms as a} from '#/alf'
import {atoms as a, flatten} from '#/alf'
import {RichText} from '#/components/RichText'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {PostAlerts} from '../../../../components/moderation/PostAlerts'
@ -45,10 +46,12 @@ export function MaybeQuoteEmbed({
embed,
onOpen,
style,
textStyle,
}: {
embed: AppBskyEmbedRecord.View
onOpen?: () => void
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
}) {
const pal = usePalette('default')
if (
@ -62,6 +65,7 @@ export function MaybeQuoteEmbed({
postRecord={embed.record.value}
onOpen={onOpen}
style={style}
textStyle={textStyle}
/>
)
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
@ -91,11 +95,13 @@ function QuoteEmbedModerated({
postRecord,
onOpen,
style,
textStyle,
}: {
viewRecord: AppBskyEmbedRecord.ViewRecord
postRecord: AppBskyFeedPost.Record
onOpen?: () => void
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
}) {
const moderationOpts = useModerationOpts()
const moderation = React.useMemo(() => {
@ -120,6 +126,7 @@ function QuoteEmbedModerated({
moderation={moderation}
onOpen={onOpen}
style={style}
textStyle={textStyle}
/>
)
}
@ -129,11 +136,13 @@ export function QuoteEmbed({
moderation,
onOpen,
style,
textStyle,
}: {
quote: ComposerOptsQuote
moderation?: ModerationDecision
onOpen?: () => void
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
}) {
const queryClient = useQueryClient()
const pal = usePalette('default')
@ -192,7 +201,7 @@ export function QuoteEmbed({
{richText ? (
<RichText
value={richText}
style={[a.text_md]}
style={[a.text_md, flatten(textStyle)]}
numberOfLines={20}
disableLinks
/>
@ -250,11 +259,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
borderWidth: hairlineWidth,
},
quotePost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -4,6 +4,7 @@ import {
StyleProp,
StyleSheet,
Text,
TextStyle,
View,
ViewStyle,
} from 'react-native'
@ -41,11 +42,13 @@ export function PostEmbeds({
moderation,
onOpen,
style,
quoteTextStyle,
}: {
embed?: Embed
moderation?: ModerationDecision
onOpen?: () => void
style?: StyleProp<ViewStyle>
quoteTextStyle?: StyleProp<TextStyle>
}) {
const pal = usePalette('default')
const {openLightbox} = useLightboxControls()
@ -60,7 +63,11 @@ export function PostEmbeds({
moderation={moderation}
onOpen={onOpen}
/>
<MaybeQuoteEmbed embed={embed.record} onOpen={onOpen} />
<MaybeQuoteEmbed
embed={embed.record}
onOpen={onOpen}
textStyle={quoteTextStyle}
/>
</View>
)
}
@ -87,7 +94,14 @@ export function PostEmbeds({
// quote post
// =
return <MaybeQuoteEmbed embed={embed} style={style} onOpen={onOpen} />
return (
<MaybeQuoteEmbed
embed={embed}
style={style}
textStyle={quoteTextStyle}
onOpen={onOpen}
/>
)
}
// image embed