diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/dms/MessageItem.tsx | 2 | ||||
-rw-r--r-- | src/components/dms/MessageItemEmbed.tsx | 99 | ||||
-rw-r--r-- | src/components/dms/dialogs/NewChatDialog.tsx | 67 | ||||
-rw-r--r-- | src/components/dms/dialogs/SearchablePeopleList.tsx (renamed from src/components/dms/NewChatDialog/index.tsx) | 454 | ||||
-rw-r--r-- | src/components/dms/dialogs/ShareViaChatDialog.tsx | 52 | ||||
-rw-r--r-- | src/components/dms/dialogs/TextInput.tsx (renamed from src/components/dms/NewChatDialog/TextInput.tsx) | 0 | ||||
-rw-r--r-- | src/components/dms/dialogs/TextInput.web.tsx (renamed from src/components/dms/NewChatDialog/TextInput.web.tsx) | 0 |
7 files changed, 330 insertions, 344 deletions
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index b498ddf1c..772fcb1b1 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -82,7 +82,7 @@ let MessageItem = ({ return ( <View style={[isFromSelf ? a.mr_md : a.ml_md]}> <ActionsWrapper isFromSelf={isFromSelf} message={message}> - {AppBskyEmbedRecord.isMain(message.embed) && ( + {AppBskyEmbedRecord.isView(message.embed) && ( <MessageItemEmbed embed={message.embed} /> )} {rt.text.length > 0 && ( diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index d64563b91..5d3656bac 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -1,108 +1,21 @@ -import React, {useMemo} from 'react' +import React from 'react' import {View} from 'react-native' -import { - AppBskyEmbedRecord, - AppBskyFeedPost, - AtUri, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyEmbedRecord} 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 + embed: AppBskyEmbedRecord.View }): 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> + <View style={[a.my_xs, t.atoms.bg, a.rounded_md, {flexBasis: 0}]}> + <PostEmbeds embed={embed} /> + </View> ) } MessageItemEmbed = React.memo(MessageItemEmbed) diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx new file mode 100644 index 000000000..2b90fb02b --- /dev/null +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -0,0 +1,67 @@ +import React, {useCallback} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' +import {logEvent} from 'lib/statsig/statsig' +import {FAB} from '#/view/com/util/fab/FAB' +import * as Toast from '#/view/com/util/Toast' +import {useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {SearchablePeopleList} from './SearchablePeopleList' + +export function NewChat({ + control, + onNewChat, +}: { + control: Dialog.DialogControlProps + onNewChat: (chatId: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + + const {mutate: createChat} = useGetConvoForMembers({ + onSuccess: data => { + onNewChat(data.convo.id) + + if (!data.convo.lastMessage) { + logEvent('chat:create', {logContext: 'NewChatDialog'}) + } + logEvent('chat:open', {logContext: 'NewChatDialog'}) + }, + onError: error => { + Toast.show(error.message) + }, + }) + + const onCreateChat = useCallback( + (did: string) => { + control.close(() => createChat([did])) + }, + [control, createChat], + ) + + return ( + <> + <FAB + testID="newChatFAB" + onPress={control.open} + icon={<Plus size="lg" fill={t.palette.white} />} + accessibilityRole="button" + accessibilityLabel={_(msg`New chat`)} + accessibilityHint="" + /> + + <Dialog.Outer + control={control} + testID="newChatDialog" + nativeOptions={{sheet: {snapPoints: ['100%']}}}> + <SearchablePeopleList + title={_(msg`Start a new chat`)} + onSelectChat={onCreateChat} + /> + </Dialog.Outer> + </> + ) +} diff --git a/src/components/dms/NewChatDialog/index.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx index a6c303043..2c212e56f 100644 --- a/src/components/dms/NewChatDialog/index.tsx +++ b/src/components/dms/dialogs/SearchablePeopleList.tsx @@ -16,23 +16,18 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useSession} from '#/state/session' -import {logEvent} from 'lib/statsig/statsig' import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' -import {FAB} from '#/view/com/util/fab/FAB' -import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' import {Button} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {TextInput} from '#/components/dms/NewChatDialog/TextInput' +import {TextInput} from '#/components/dms/dialogs/TextInput' import {canBeMessaged} from '#/components/dms/util' import {useInteractionState} from '#/components/hooks/useInteractionState' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Text} from '#/components/Typography' @@ -57,247 +52,12 @@ type Item = key: string } -export function NewChat({ - control, - onNewChat, +export function SearchablePeopleList({ + title, + onSelectChat, }: { - control: Dialog.DialogControlProps - onNewChat: (chatId: string) => void -}) { - const t = useTheme() - const {_} = useLingui() - - const {mutate: createChat} = useGetConvoForMembers({ - onSuccess: data => { - onNewChat(data.convo.id) - - if (!data.convo.lastMessage) { - logEvent('chat:create', {logContext: 'NewChatDialog'}) - } - logEvent('chat:open', {logContext: 'NewChatDialog'}) - }, - onError: error => { - Toast.show(error.message) - }, - }) - - const onCreateChat = useCallback( - (did: string) => { - control.close(() => createChat([did])) - }, - [control, createChat], - ) - - return ( - <> - <FAB - testID="newChatFAB" - onPress={control.open} - icon={<Plus size="lg" fill={t.palette.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New chat`)} - accessibilityHint="" - /> - - <Dialog.Outer - control={control} - testID="newChatDialog" - nativeOptions={{sheet: {snapPoints: ['100%']}}}> - <SearchablePeopleList onCreateChat={onCreateChat} /> - </Dialog.Outer> - </> - ) -} - -function ProfileCard({ - enabled, - profile, - moderationOpts, - onPress, -}: { - enabled: boolean - profile: AppBskyActorDefs.ProfileView - moderationOpts: ModerationOpts - onPress: (did: string) => void -}) { - const t = useTheme() - const {_} = useLingui() - const moderation = moderateProfile(profile, moderationOpts) - const handle = sanitizeHandle(profile.handle, '@') - const displayName = sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - ) - - const handleOnPress = useCallback(() => { - onPress(profile.did) - }, [onPress, profile.did]) - - return ( - <Button - disabled={!enabled} - label={_(msg`Start chat with ${displayName}`)} - onPress={handleOnPress}> - {({hovered, pressed, focused}) => ( - <View - style={[ - a.flex_1, - a.py_md, - a.px_lg, - a.gap_md, - a.align_center, - a.flex_row, - !enabled - ? {opacity: 0.5} - : pressed || focused - ? t.atoms.bg_contrast_25 - : hovered - ? t.atoms.bg_contrast_50 - : t.atoms.bg, - ]}> - <UserAvatar - size={42} - avatar={profile.avatar} - moderation={moderation.ui('avatar')} - type={profile.associated?.labeler ? 'labeler' : 'user'} - /> - <View style={[a.flex_1, a.gap_2xs]}> - <Text - style={[t.atoms.text, a.font_bold, a.leading_snug]} - numberOfLines={1}> - {displayName} - </Text> - <Text style={t.atoms.text_contrast_high} numberOfLines={2}> - {!enabled ? <Trans>{handle} can't be messaged</Trans> : handle} - </Text> - </View> - </View> - )} - </Button> - ) -} - -function ProfileCardSkeleton() { - const t = useTheme() - - return ( - <View - style={[ - a.flex_1, - a.py_md, - a.px_lg, - a.gap_md, - a.align_center, - a.flex_row, - ]}> - <View - style={[ - a.rounded_full, - {width: 42, height: 42}, - t.atoms.bg_contrast_25, - ]} - /> - - <View style={[a.flex_1, a.gap_sm]}> - <View - style={[ - a.rounded_xs, - {width: 80, height: 14}, - t.atoms.bg_contrast_25, - ]} - /> - <View - style={[ - a.rounded_xs, - {width: 120, height: 10}, - t.atoms.bg_contrast_25, - ]} - /> - </View> - </View> - ) -} - -function Empty({message}: {message: string}) { - const t = useTheme() - return ( - <View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}> - <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}> - {message} - </Text> - - <Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text> - </View> - ) -} - -function SearchInput({ - value, - onChangeText, - onEscape, - inputRef, -}: { - value: string - onChangeText: (text: string) => void - onEscape: () => void - inputRef: React.RefObject<TextInputType> -}) { - const t = useTheme() - const {_} = useLingui() - const { - state: hovered, - onIn: onMouseEnter, - onOut: onMouseLeave, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const interacted = hovered || focused - - return ( - <View - {...web({ - onMouseEnter, - onMouseLeave, - })} - style={[a.flex_row, a.align_center, a.gap_sm]}> - <Search - size="md" - fill={interacted ? t.palette.primary_500 : t.palette.contrast_300} - /> - - <TextInput - // @ts-ignore bottom sheet input types issue — esb - ref={inputRef} - placeholder={_(msg`Search`)} - value={value} - onChangeText={onChangeText} - onFocus={onFocus} - onBlur={onBlur} - style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]} - placeholderTextColor={t.palette.contrast_500} - keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} - returnKeyType="search" - clearButtonMode="while-editing" - maxLength={50} - onKeyPress={({nativeEvent}) => { - if (nativeEvent.key === 'Escape') { - onEscape() - } - }} - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - autoFocus - accessibilityLabel={_(msg`Search profiles`)} - accessibilityHint={_(msg`Search profiles`)} - /> - </View> - ) -} - -function SearchablePeopleList({ - onCreateChat, -}: { - onCreateChat: (did: string) => void + title: string + onSelectChat: (did: string) => void }) { const t = useTheme() const {_} = useLingui() @@ -388,7 +148,7 @@ function SearchablePeopleList({ enabled={item.enabled} profile={item.profile} moderationOpts={moderationOpts!} - onPress={onCreateChat} + onPress={onSelectChat} /> ) } @@ -402,7 +162,7 @@ function SearchablePeopleList({ return null } }, - [moderationOpts, onCreateChat], + [moderationOpts, onSelectChat], ) useLayoutEffect(() => { @@ -464,7 +224,7 @@ function SearchablePeopleList({ a.leading_tight, t.atoms.text_contrast_high, ]}> - <Trans>Start a new chat</Trans> + {title} </Text> </View> @@ -481,7 +241,16 @@ function SearchablePeopleList({ </View> </View> ) - }, [t, _, control, searchText]) + }, [ + t.atoms.border_contrast_low, + t.atoms.bg, + t.atoms.text_contrast_high, + t.palette.contrast_500, + _, + title, + searchText, + control, + ]) return ( <Dialog.InnerFlatList @@ -507,3 +276,188 @@ function SearchablePeopleList({ /> ) } + +function ProfileCard({ + enabled, + profile, + moderationOpts, + onPress, +}: { + enabled: boolean + profile: AppBskyActorDefs.ProfileView + moderationOpts: ModerationOpts + onPress: (did: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + const moderation = moderateProfile(profile, moderationOpts) + const handle = sanitizeHandle(profile.handle, '@') + const displayName = sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + ) + + const handleOnPress = useCallback(() => { + onPress(profile.did) + }, [onPress, profile.did]) + + return ( + <Button + disabled={!enabled} + label={_(msg`Start chat with ${displayName}`)} + onPress={handleOnPress}> + {({hovered, pressed, focused}) => ( + <View + style={[ + a.flex_1, + a.py_md, + a.px_lg, + a.gap_md, + a.align_center, + a.flex_row, + !enabled + ? {opacity: 0.5} + : pressed || focused + ? t.atoms.bg_contrast_25 + : hovered + ? t.atoms.bg_contrast_50 + : t.atoms.bg, + ]}> + <UserAvatar + size={42} + avatar={profile.avatar} + moderation={moderation.ui('avatar')} + type={profile.associated?.labeler ? 'labeler' : 'user'} + /> + <View style={[a.flex_1, a.gap_2xs]}> + <Text + style={[t.atoms.text, a.font_bold, a.leading_snug]} + numberOfLines={1}> + {displayName} + </Text> + <Text style={t.atoms.text_contrast_high} numberOfLines={2}> + {!enabled ? <Trans>{handle} can't be messaged</Trans> : handle} + </Text> + </View> + </View> + )} + </Button> + ) +} + +function ProfileCardSkeleton() { + const t = useTheme() + + return ( + <View + style={[ + a.flex_1, + a.py_md, + a.px_lg, + a.gap_md, + a.align_center, + a.flex_row, + ]}> + <View + style={[ + a.rounded_full, + {width: 42, height: 42}, + t.atoms.bg_contrast_25, + ]} + /> + + <View style={[a.flex_1, a.gap_sm]}> + <View + style={[ + a.rounded_xs, + {width: 80, height: 14}, + t.atoms.bg_contrast_25, + ]} + /> + <View + style={[ + a.rounded_xs, + {width: 120, height: 10}, + t.atoms.bg_contrast_25, + ]} + /> + </View> + </View> + ) +} + +function Empty({message}: {message: string}) { + const t = useTheme() + return ( + <View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}> + <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}> + {message} + </Text> + + <Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text> + </View> + ) +} + +function SearchInput({ + value, + onChangeText, + onEscape, + inputRef, +}: { + value: string + onChangeText: (text: string) => void + onEscape: () => void + inputRef: React.RefObject<TextInputType> +}) { + const t = useTheme() + const {_} = useLingui() + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const interacted = hovered || focused + + return ( + <View + {...web({ + onMouseEnter, + onMouseLeave, + })} + style={[a.flex_row, a.align_center, a.gap_sm]}> + <Search + size="md" + fill={interacted ? t.palette.primary_500 : t.palette.contrast_300} + /> + + <TextInput + // @ts-ignore bottom sheet input types issue — esb + ref={inputRef} + placeholder={_(msg`Search`)} + value={value} + onChangeText={onChangeText} + onFocus={onFocus} + onBlur={onBlur} + style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]} + placeholderTextColor={t.palette.contrast_500} + keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} + returnKeyType="search" + clearButtonMode="while-editing" + maxLength={50} + onKeyPress={({nativeEvent}) => { + if (nativeEvent.key === 'Escape') { + onEscape() + } + }} + autoCorrect={false} + autoComplete="off" + autoCapitalize="none" + autoFocus + accessibilityLabel={_(msg`Search profiles`)} + accessibilityHint={_(msg`Search profiles`)} + /> + </View> + ) +} diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx new file mode 100644 index 000000000..ac475f7c9 --- /dev/null +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -0,0 +1,52 @@ +import React, {useCallback} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' +import {logEvent} from 'lib/statsig/statsig' +import * as Toast from '#/view/com/util/Toast' +import * as Dialog from '#/components/Dialog' +import {SearchablePeopleList} from './SearchablePeopleList' + +export function SendViaChatDialog({ + control, + onSelectChat, +}: { + control: Dialog.DialogControlProps + onSelectChat: (chatId: string) => void +}) { + const {_} = useLingui() + + const {mutate: createChat} = useGetConvoForMembers({ + onSuccess: data => { + onSelectChat(data.convo.id) + + if (!data.convo.lastMessage) { + logEvent('chat:create', {logContext: 'SendViaChatDialog'}) + } + logEvent('chat:open', {logContext: 'SendViaChatDialog'}) + }, + onError: error => { + Toast.show(error.message) + }, + }) + + const onCreateChat = useCallback( + (did: string) => { + control.close(() => createChat([did])) + }, + [control, createChat], + ) + + return ( + <Dialog.Outer + control={control} + testID="sendViaChatChatDialog" + nativeOptions={{sheet: {snapPoints: ['100%']}}}> + <SearchablePeopleList + title={_(msg`Send post to...`)} + onSelectChat={onCreateChat} + /> + </Dialog.Outer> + ) +} diff --git a/src/components/dms/NewChatDialog/TextInput.tsx b/src/components/dms/dialogs/TextInput.tsx index b4e77e3e0..b4e77e3e0 100644 --- a/src/components/dms/NewChatDialog/TextInput.tsx +++ b/src/components/dms/dialogs/TextInput.tsx diff --git a/src/components/dms/NewChatDialog/TextInput.web.tsx b/src/components/dms/dialogs/TextInput.web.tsx index 5371a534f..5371a534f 100644 --- a/src/components/dms/NewChatDialog/TextInput.web.tsx +++ b/src/components/dms/dialogs/TextInput.web.tsx |