Files
bsky-social-app/src/components/PostControls/ShareMenu/RecentChats.tsx
T
Samuel Newman c3f88e0a48 Share menu (#7840)
* move post ctrls to #/components

* restructure post controls, basic share menu

* add border radius to searchable people list for android

* Revert "add border radius to searchable people list for android"

This reverts commit 417449086e25b82f5683b12f6405d972f48ce50e.

* add copy link to native share menu

* reorg files again

* open native share menu on long press

* Translation comments

Thanks @surfdude29

* abs path

* update type imports, remove forwardRef

* rm react import

* equal spacing of buttons, extract disco debug

* add better icon

* add right offset to share button for visual alignment

* Add recent chats to share menu (#7853)

* add recent chats to share menu

* Update RecentChats.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update RecentChats.tsx

* add fading edge on andriod

* tweak scrollview

* Add metrics and A/B alt icon to share menu (#8401)

* add metrics

* add a/b tested alt icon

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* More descriptive share text/icon on web (#7854)

* more descriptive share text on web

* revert dev mode changes

* add missing import

* use modified share icon everywhere

* Add back conflicting changes

---------

Co-authored-by: Eric Bailey <git@esb.lol>

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2025-05-23 18:02:38 -05:00

201 lines
5.4 KiB
TypeScript

import {ScrollView, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogContext} from '#/components/Dialog'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import type * as bsky from '#/types/bsky'
export function RecentChats({postUri}: {postUri: string}) {
const control = useDialogContext()
const {_} = useLingui()
const {currentAccount} = useSession()
const {data} = useListConvosQuery({status: 'accepted'})
const convos = data?.pages[0]?.convos?.slice(0, 10)
const moderationOpts = useModerationOpts()
const navigation = useNavigation<NavigationProp>()
const onSelectChat = (convoId: string) => {
control.close(() => {
logger.metric('share:press:recentDm', {}, {statsig: true})
navigation.navigate('MessagesConversation', {
conversation: convoId,
embed: postUri,
})
})
}
if (!moderationOpts) return null
return (
<View
style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}>
<ScrollView
horizontal
style={[a.flex_1, a.pt_2xs, {minHeight: 98}]}
contentContainerStyle={[a.gap_sm, a.px_md]}
showsHorizontalScrollIndicator={false}
fadingEdgeLength={64}
nestedScrollEnabled>
{convos && convos.length > 0 ? (
convos.map(convo => {
const otherMember = convo.members.find(
member => member.did !== currentAccount?.did,
)
if (!otherMember || otherMember.handle === 'missing.invalid')
return null
return (
<RecentChatItem
key={convo.id}
profile={otherMember}
onPress={() => onSelectChat(convo.id)}
moderationOpts={moderationOpts}
/>
)
})
) : (
<>
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
</>
)}
</ScrollView>
{convos && convos.length === 0 && <NoConvos />}
</View>
)
}
const WIDTH = 80
function RecentChatItem({
profile,
onPress,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
onPress: () => void
moderationOpts: ModerationOpts
}) {
const {_} = useLingui()
const t = useTheme()
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const verification = useSimpleVerificationState({profile})
return (
<Button
onPress={onPress}
label={_(msg`Send post to ${name}`)}
style={[
a.flex_col,
{width: WIDTH},
a.gap_sm,
a.justify_start,
a.align_center,
]}>
<UserAvatar
avatar={profile.avatar}
size={WIDTH - 8}
type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')}
/>
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
<Text
emoji
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{name}
</Text>
{verification.showBadge && (
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View>
</Button>
)
}
function ConvoSkeleton() {
const t = useTheme()
return (
<View
style={[
a.flex_col,
{width: WIDTH, height: WIDTH + 15},
a.gap_xs,
a.justify_start,
a.align_center,
]}>
<View
style={[
t.atoms.bg_contrast_50,
{width: WIDTH - 8, height: WIDTH - 8},
a.rounded_full,
]}
/>
<View
style={[
t.atoms.bg_contrast_50,
{width: WIDTH - 8, height: 10},
a.rounded_xs,
]}
/>
</View>
)
}
function NoConvos() {
const t = useTheme()
return (
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.align_center,
a.px_2xl,
]}>
<View
style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]}
/>
<Text
style={[
a.text_sm,
t.atoms.text_contrast_high,
a.text_center,
a.font_bold,
]}>
<Trans>Start a conversation, and it will appear here.</Trans>
</Text>
</View>
)
}