Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] 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>

authored by samuel.fm

Eric Bailey and committed by
GitHub
22e1eb18 8eb3cebb

+225 -58
+43 -31
src/components/dms/MessageItem.tsx
··· 6 6 TextStyle, 7 7 View, 8 8 } from 'react-native' 9 - import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api' 9 + import { 10 + AppBskyEmbedRecord, 11 + ChatBskyConvoDefs, 12 + RichText as RichTextAPI, 13 + } from '@atproto/api' 10 14 import {msg} from '@lingui/macro' 11 15 import {useLingui} from '@lingui/react' 12 16 ··· 18 22 import {InlineLinkText} from '#/components/Link' 19 23 import {Text} from '#/components/Typography' 20 24 import {RichText} from '../RichText' 25 + import {MessageItemEmbed} from './MessageItemEmbed' 21 26 22 27 let MessageItem = ({ 23 28 item, ··· 77 82 return ( 78 83 <View style={[isFromSelf ? a.mr_md : a.ml_md]}> 79 84 <ActionsWrapper isFromSelf={isFromSelf} message={message}> 80 - <View 81 - style={[ 82 - a.py_sm, 83 - a.my_2xs, 84 - a.rounded_md, 85 - { 86 - paddingLeft: 14, 87 - paddingRight: 14, 88 - backgroundColor: isFromSelf 89 - ? isPending 90 - ? pendingColor 91 - : t.palette.primary_500 92 - : t.palette.contrast_50, 93 - borderRadius: 17, 94 - }, 95 - isFromSelf 96 - ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} 97 - : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, 98 - ]}> 99 - <RichText 100 - value={rt} 85 + {AppBskyEmbedRecord.isMain(message.embed) && ( 86 + <MessageItemEmbed embed={message.embed} /> 87 + )} 88 + {rt.text.length > 0 && ( 89 + <View 101 90 style={[ 102 - a.text_md, 103 - a.leading_snug, 104 - isFromSelf && {color: t.palette.white}, 105 - isPending && t.name !== 'light' && {color: t.palette.primary_300}, 106 - ]} 107 - interactiveStyle={a.underline} 108 - enableTags 109 - /> 110 - </View> 91 + a.py_sm, 92 + a.my_2xs, 93 + a.rounded_md, 94 + { 95 + paddingLeft: 14, 96 + paddingRight: 14, 97 + backgroundColor: isFromSelf 98 + ? isPending 99 + ? pendingColor 100 + : t.palette.primary_500 101 + : t.palette.contrast_50, 102 + borderRadius: 17, 103 + }, 104 + isFromSelf ? a.self_end : a.self_start, 105 + isFromSelf 106 + ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} 107 + : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, 108 + ]}> 109 + <RichText 110 + value={rt} 111 + style={[ 112 + a.text_md, 113 + a.leading_snug, 114 + isFromSelf && {color: t.palette.white}, 115 + isPending && 116 + t.name !== 'light' && {color: t.palette.primary_300}, 117 + ]} 118 + interactiveStyle={a.underline} 119 + enableTags 120 + /> 121 + </View> 122 + )} 111 123 </ActionsWrapper> 112 124 113 125 {isLastInGroup && (
+109
src/components/dms/MessageItemEmbed.tsx
··· 1 + import React, {useMemo} from 'react' 2 + import {View} from 'react-native' 3 + import { 4 + AppBskyEmbedRecord, 5 + AppBskyFeedPost, 6 + AtUri, 7 + RichText as RichTextAPI, 8 + } from '@atproto/api' 9 + 10 + import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 11 + import {makeProfileLink} from '#/lib/routes/links' 12 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 + import {usePostQuery} from '#/state/queries/post' 14 + import {PostEmbeds} from '#/view/com/util/post-embeds' 15 + import {PostMeta} from '#/view/com/util/PostMeta' 16 + import {atoms as a, useTheme} from '#/alf' 17 + import {Link} from '#/components/Link' 18 + import {ContentHider} from '#/components/moderation/ContentHider' 19 + import {PostAlerts} from '#/components/moderation/PostAlerts' 20 + import {RichText} from '#/components/RichText' 21 + 22 + let MessageItemEmbed = ({ 23 + embed, 24 + }: { 25 + embed: AppBskyEmbedRecord.Main 26 + }): React.ReactNode => { 27 + const t = useTheme() 28 + const {data: post} = usePostQuery(embed.record.uri) 29 + 30 + const moderationOpts = useModerationOpts() 31 + const moderation = useMemo( 32 + () => 33 + moderationOpts && post ? moderatePost(post, moderationOpts) : undefined, 34 + [moderationOpts, post], 35 + ) 36 + 37 + const {rt, record} = useMemo(() => { 38 + if ( 39 + post && 40 + AppBskyFeedPost.isRecord(post.record) && 41 + AppBskyFeedPost.validateRecord(post.record).success 42 + ) { 43 + return { 44 + rt: new RichTextAPI({ 45 + text: post.record.text, 46 + facets: post.record.facets, 47 + }), 48 + record: post.record, 49 + } 50 + } 51 + 52 + return {rt: undefined, record: undefined} 53 + }, [post]) 54 + 55 + if (!post || !moderation || !rt || !record) { 56 + return null 57 + } 58 + 59 + const itemUrip = new AtUri(post.uri) 60 + const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey) 61 + 62 + return ( 63 + <Link to={itemHref}> 64 + <View 65 + style={[ 66 + a.w_full, 67 + t.atoms.bg, 68 + t.atoms.border_contrast_low, 69 + a.rounded_md, 70 + a.border, 71 + a.p_md, 72 + a.my_xs, 73 + ]}> 74 + <PostMeta 75 + showAvatar 76 + author={post.author} 77 + moderation={moderation} 78 + authorHasWarning={!!post.author.labels?.length} 79 + timestamp={post.indexedAt} 80 + postHref={itemHref} 81 + /> 82 + <ContentHider modui={moderation.ui('contentView')}> 83 + <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} /> 84 + {rt.text && ( 85 + <View style={a.mt_xs}> 86 + <RichText 87 + enableTags 88 + testID="postText" 89 + value={rt} 90 + style={[a.text_sm, t.atoms.text_contrast_high]} 91 + authorHandle={post.author.handle} 92 + /> 93 + </View> 94 + )} 95 + {post.embed && ( 96 + <PostEmbeds 97 + embed={post.embed} 98 + moderation={moderation} 99 + style={a.mt_xs} 100 + quoteTextStyle={[a.text_sm, t.atoms.text_contrast_high]} 101 + /> 102 + )} 103 + </ContentHider> 104 + </View> 105 + </Link> 106 + ) 107 + } 108 + MessageItemEmbed = React.memo(MessageItemEmbed) 109 + export {MessageItemEmbed}
+36 -11
src/components/dms/MessageMenu.tsx
··· 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {richTextToString} from '#/lib/strings/rich-text-helpers' 9 + import {getTranslatorLink} from '#/locale/helpers' 10 + import {useLanguagePrefs} from '#/state/preferences' 11 + import {useOpenLink} from '#/state/preferences/in-app-browser' 9 12 import {isWeb} from 'platform/detection' 10 13 import {useConvoActive} from 'state/messages/convo' 11 14 import {useSession} from 'state/session' 12 15 import * as Toast from '#/view/com/util/Toast' 13 16 import {atoms as a, useTheme} from '#/alf' 14 17 import {ReportDialog} from '#/components/dms/ReportDialog' 18 + import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' 15 19 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 16 20 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 17 21 import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' ··· 35 39 const convo = useConvoActive() 36 40 const deleteControl = usePromptControl() 37 41 const reportControl = usePromptControl() 42 + const langPrefs = useLanguagePrefs() 43 + const openLink = useOpenLink() 38 44 39 45 const isFromSelf = message.sender?.did === currentAccount?.did 40 46 41 - const onCopyPostText = React.useCallback(() => { 47 + const onCopyMessage = React.useCallback(() => { 42 48 const str = richTextToString( 43 49 new RichText({ 44 50 text: message.text, ··· 51 57 Toast.show(_(msg`Copied to clipboard`)) 52 58 }, [_, message.text, message.facets]) 53 59 60 + const onPressTranslateMessage = React.useCallback(() => { 61 + const translatorUrl = getTranslatorLink( 62 + message.text, 63 + langPrefs.primaryLanguage, 64 + ) 65 + openLink(translatorUrl) 66 + }, [langPrefs.primaryLanguage, message.text, openLink]) 67 + 54 68 const onDelete = React.useCallback(() => { 55 69 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 56 70 convo ··· 81 95 )} 82 96 83 97 <Menu.Outer> 84 - <Menu.Group> 85 - <Menu.Item 86 - testID="messageDropdownCopyBtn" 87 - label={_(msg`Copy message text`)} 88 - onPress={onCopyPostText}> 89 - <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText> 90 - <Menu.ItemIcon icon={ClipboardIcon} position="right" /> 91 - </Menu.Item> 92 - </Menu.Group> 93 - <Menu.Divider /> 98 + {message.text.length > 0 && ( 99 + <> 100 + <Menu.Group> 101 + <Menu.Item 102 + testID="messageDropdownTranslateBtn" 103 + label={_(msg`Translate`)} 104 + onPress={onPressTranslateMessage}> 105 + <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> 106 + <Menu.ItemIcon icon={Translate} position="right" /> 107 + </Menu.Item> 108 + <Menu.Item 109 + testID="messageDropdownCopyBtn" 110 + label={_(msg`Copy message text`)} 111 + onPress={onCopyMessage}> 112 + <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText> 113 + <Menu.ItemIcon icon={ClipboardIcon} position="right" /> 114 + </Menu.Item> 115 + </Menu.Group> 116 + <Menu.Divider /> 117 + </> 118 + )} 94 119 <Menu.Group> 95 120 <Menu.Item 96 121 testID="messageDropdownDeleteBtn"
+10 -7
src/components/moderation/ContentHider.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 3 import {ModerationUI} from '@atproto/api' 4 - import {useLingui} from '@lingui/react' 5 4 import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 6 7 + import {isJustAMute} from '#/lib/moderation' 7 8 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 8 - import {isJustAMute} from '#/lib/moderation' 9 9 import {sanitizeDisplayName} from '#/lib/strings/display-names' 10 - 11 - import {atoms as a, useTheme, useBreakpoints, web} from '#/alf' 10 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 12 11 import {Button} from '#/components/Button' 13 - import {Text} from '#/components/Typography' 14 12 import { 15 13 ModerationDetailsDialog, 16 14 useModerationDetailsDialogControl, 17 15 } from '#/components/moderation/ModerationDetailsDialog' 16 + import {Text} from '#/components/Typography' 18 17 19 18 export function ContentHider({ 20 19 testID, ··· 52 51 <ModerationDetailsDialog control={control} modcause={blur} /> 53 52 54 53 <Button 55 - onPress={() => { 54 + onPress={e => { 55 + e.preventDefault() 56 + e.stopPropagation() 56 57 if (!modui.noOverride) { 57 58 setOverride(v => !v) 58 59 } else { ··· 121 122 122 123 {desc.source && blur.type === 'label' && !override && ( 123 124 <Button 124 - onPress={() => { 125 + onPress={e => { 126 + e.preventDefault() 127 + e.stopPropagation() 125 128 control.open() 126 129 }} 127 130 label={_(
+11 -7
src/view/com/util/post-embeds/QuoteEmbed.tsx
··· 2 2 import { 3 3 StyleProp, 4 4 StyleSheet, 5 + TextStyle, 5 6 TouchableOpacity, 6 7 View, 7 8 ViewStyle, ··· 31 32 import {makeProfileLink} from 'lib/routes/links' 32 33 import {precacheProfile} from 'state/queries/profile' 33 34 import {ComposerOptsQuote} from 'state/shell/composer' 34 - import {atoms as a} from '#/alf' 35 + import {atoms as a, flatten} from '#/alf' 35 36 import {RichText} from '#/components/RichText' 36 37 import {ContentHider} from '../../../../components/moderation/ContentHider' 37 38 import {PostAlerts} from '../../../../components/moderation/PostAlerts' ··· 45 46 embed, 46 47 onOpen, 47 48 style, 49 + textStyle, 48 50 }: { 49 51 embed: AppBskyEmbedRecord.View 50 52 onOpen?: () => void 51 53 style?: StyleProp<ViewStyle> 54 + textStyle?: StyleProp<TextStyle> 52 55 }) { 53 56 const pal = usePalette('default') 54 57 if ( ··· 62 65 postRecord={embed.record.value} 63 66 onOpen={onOpen} 64 67 style={style} 68 + textStyle={textStyle} 65 69 /> 66 70 ) 67 71 } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { ··· 91 95 postRecord, 92 96 onOpen, 93 97 style, 98 + textStyle, 94 99 }: { 95 100 viewRecord: AppBskyEmbedRecord.ViewRecord 96 101 postRecord: AppBskyFeedPost.Record 97 102 onOpen?: () => void 98 103 style?: StyleProp<ViewStyle> 104 + textStyle?: StyleProp<TextStyle> 99 105 }) { 100 106 const moderationOpts = useModerationOpts() 101 107 const moderation = React.useMemo(() => { ··· 120 126 moderation={moderation} 121 127 onOpen={onOpen} 122 128 style={style} 129 + textStyle={textStyle} 123 130 /> 124 131 ) 125 132 } ··· 129 136 moderation, 130 137 onOpen, 131 138 style, 139 + textStyle, 132 140 }: { 133 141 quote: ComposerOptsQuote 134 142 moderation?: ModerationDecision 135 143 onOpen?: () => void 136 144 style?: StyleProp<ViewStyle> 145 + textStyle?: StyleProp<TextStyle> 137 146 }) { 138 147 const queryClient = useQueryClient() 139 148 const pal = usePalette('default') ··· 192 201 {richText ? ( 193 202 <RichText 194 203 value={richText} 195 - style={[a.text_md]} 204 + style={[a.text_md, flatten(textStyle)]} 196 205 numberOfLines={20} 197 206 disableLinks 198 207 /> ··· 249 258 paddingVertical: 12, 250 259 paddingHorizontal: 12, 251 260 borderWidth: hairlineWidth, 252 - }, 253 - quotePost: { 254 - flex: 1, 255 - paddingLeft: 13, 256 - paddingRight: 8, 257 261 }, 258 262 errorContainer: { 259 263 flexDirection: 'row',
+16 -2
src/view/com/util/post-embeds/index.tsx
··· 4 4 StyleProp, 5 5 StyleSheet, 6 6 Text, 7 + TextStyle, 7 8 View, 8 9 ViewStyle, 9 10 } from 'react-native' ··· 41 42 moderation, 42 43 onOpen, 43 44 style, 45 + quoteTextStyle, 44 46 }: { 45 47 embed?: Embed 46 48 moderation?: ModerationDecision 47 49 onOpen?: () => void 48 50 style?: StyleProp<ViewStyle> 51 + quoteTextStyle?: StyleProp<TextStyle> 49 52 }) { 50 53 const pal = usePalette('default') 51 54 const {openLightbox} = useLightboxControls() ··· 60 63 moderation={moderation} 61 64 onOpen={onOpen} 62 65 /> 63 - <MaybeQuoteEmbed embed={embed.record} onOpen={onOpen} /> 66 + <MaybeQuoteEmbed 67 + embed={embed.record} 68 + onOpen={onOpen} 69 + textStyle={quoteTextStyle} 70 + /> 64 71 </View> 65 72 ) 66 73 } ··· 87 94 88 95 // quote post 89 96 // = 90 - return <MaybeQuoteEmbed embed={embed} style={style} onOpen={onOpen} /> 97 + return ( 98 + <MaybeQuoteEmbed 99 + embed={embed} 100 + style={style} 101 + textStyle={quoteTextStyle} 102 + onOpen={onOpen} 103 + /> 104 + ) 91 105 } 92 106 93 107 // image embed