Update handling of join link previews (#10798)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
DS Boyce
2026-06-09 07:23:33 -07:00
committed by GitHub
parent 4c4ebd0510
commit c52231a449
24 changed files with 192 additions and 103 deletions
+24 -26
View File
@@ -1,6 +1,6 @@
import {View} from 'react-native'
import {ImageBackground} from 'expo-image'
import {moderateProfile} from '@atproto/api'
import {ChatBskyGroupDefs, moderateProfile} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
@@ -52,9 +52,7 @@ export function JoinRequest({setScreenState}: Props) {
? mobileDarkBg
: mobileLightBg
const requiresApproval = data?.joinLinkPreviews[0]?.requireApproval
const requiresFollow =
data?.joinLinkPreviews[0]?.joinRule === 'followedByOwner'
const joinLinkPreview = data?.joinLinkPreviews[0]
return (
<View style={[a.util_screen_outer, a.w_full, t.atoms.bg_contrast_25]}>
@@ -69,7 +67,9 @@ export function JoinRequest({setScreenState}: Props) {
a.justify_center,
a.align_center,
]}>
{error ? (
{error ||
(data &&
!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) ? (
<Wrapper>
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
<Text
@@ -80,20 +80,19 @@ export function JoinRequest({setScreenState}: Props) {
a.font_semi_bold,
t.atoms.text,
]}>
{l`This invite link has expired`}
<Trans>Chat invite link no longer available</Trans>
</Text>
<ActionButtons setScreenState={setScreenState} />
</Wrapper>
) : data && moderationOpts ? (
) : data &&
moderationOpts &&
ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview) ? (
<Wrapper>
<AvatarBubbles
profiles={[
data.joinLinkPreviews[0].owner,
joinLinkPreview.owner,
...Array(
Math.min(
3,
Math.max(0, data.joinLinkPreviews[0].memberCount - 1),
),
Math.min(3, Math.max(0, joinLinkPreview.memberCount - 1)),
).fill(undefined),
]}
size={135}
@@ -130,8 +129,8 @@ export function JoinRequest({setScreenState}: Props) {
<Trans
context="group-chat-member-count"
comment="The number of active group chat members out of the total number allowed.">
{data.joinLinkPreviews[0].memberCount}/
{data.joinLinkPreviews[0].memberLimit}
{joinLinkPreview.memberCount}/
{joinLinkPreview.memberLimit}
</Trans>
</Text>
</View>
@@ -144,7 +143,7 @@ export function JoinRequest({setScreenState}: Props) {
a.font_bold,
t.atoms.text,
]}>
{data.joinLinkPreviews[0].name}
{joinLinkPreview.name}
</Text>
</View>
<View style={[a.w_full]}>
@@ -168,17 +167,17 @@ export function JoinRequest({setScreenState}: Props) {
<Trans comment="The owner (creator) of a group chat.">
By{' '}
{createSanitizedDisplayName(
data.joinLinkPreviews[0].owner,
joinLinkPreview.owner,
true,
moderateProfile(
data.joinLinkPreviews[0].owner,
joinLinkPreview.owner,
moderationOpts,
).ui('displayName'),
)}
</Trans>
</Text>
<ProfileBadges
profile={data.joinLinkPreviews[0].owner}
profile={joinLinkPreview.owner}
size="sm"
style={{marginTop: -4}}
/>
@@ -192,7 +191,7 @@ export function JoinRequest({setScreenState}: Props) {
t.atoms.text_contrast_medium,
a.max_w_full,
]}>
{sanitizeHandle(data.joinLinkPreviews[0].owner.handle, '@')}
{sanitizeHandle(joinLinkPreview.owner.handle, '@')}
</Text>
</View>
<Text
@@ -202,17 +201,16 @@ export function JoinRequest({setScreenState}: Props) {
a.leading_snug,
t.atoms.text_contrast_high,
]}>
{requiresApproval
{joinLinkPreview.requireApproval
? l`Sign in to request access to this group chat.`
: l`Sign in to accept invite.`}{' '}
{requiresFollow &&
{joinLinkPreview.joinRule === 'followedByOwner' &&
l`Only people ${createSanitizedDisplayName(
data.joinLinkPreviews[0].owner,
joinLinkPreview.owner,
true,
moderateProfile(
data.joinLinkPreviews[0].owner,
moderationOpts,
).ui('displayName'),
moderateProfile(joinLinkPreview.owner, moderationOpts).ui(
'displayName',
),
)} follows can join.`}
</Text>
<ActionButtons setScreenState={setScreenState} />
@@ -4,6 +4,7 @@ import {
AppBskyFeedPost,
AppBskyRichtextFacet,
AtUri,
ChatBskyGroupDefs,
moderatePost,
RichText as RichTextAPI,
} from '@atproto/api'
@@ -30,6 +31,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import * as ChatInvite from '#/components/dms/ChatInvite'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader'
import * as MediaPreview from '#/components/MediaPreview'
import {ContentHider} from '#/components/moderation/ContentHider'
@@ -52,12 +54,12 @@ export function useMessageEmbed() {
const navigation = useNavigation<NavigationProp>()
const embedFromParams = route.params.embed
const [embed, setEmbedState] = useState<MessageEmbedState | undefined>(
const [embed, setEmbed] = useState<MessageEmbedState | undefined>(
embedFromParams ? {type: 'post', uri: embedFromParams} : undefined,
)
if (embedFromParams && embed?.type !== 'post') {
setEmbedState({type: 'post', uri: embedFromParams})
setEmbed({type: 'post', uri: embedFromParams})
}
return {
@@ -68,7 +70,7 @@ export function useMessageEmbed() {
// Only the post embed is reflected in the route param (used by the
// share-to-DM intent flow); invites are local-only.
navigation.setParams({embed: ''})
setEmbedState(undefined)
setEmbed(undefined)
return
}
@@ -77,7 +79,7 @@ export function useMessageEmbed() {
if (isBskyChatInviteUrl(embedUrl)) {
const code = getChatInviteCodeFromUrl(embedUrl)
if (code) {
setEmbedState({type: 'invite', code})
setEmbed({type: 'invite', code})
}
return
}
@@ -86,7 +88,7 @@ export function useMessageEmbed() {
const url = convertBskyAppUrlIfNeeded(embedUrl)
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
setEmbedState({type: 'post', uri})
setEmbed({type: 'post', uri})
}
},
[embedFromParams, navigation],
@@ -314,11 +316,19 @@ function MessageInputInviteEmbedBody() {
)
}
if (!preview) {
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
return (
<View style={[{minHeight: 64}, a.justify_center, a.align_center]}>
<Text style={[a.text_center, t.atoms.text_contrast_medium, a.italic]}>
<Trans>Could not load invite</Trans>
<View
style={[
{minHeight: 64},
a.flex_row,
a.gap_xs,
a.justify_center,
a.align_center,
]}>
<WarningIcon size="md" fill={t.atoms.text_contrast_medium.color} />
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
<Trans>Chat invite link no longer available</Trans>
</Text>
</View>
)
@@ -94,7 +94,8 @@ export function MessagesListGroupInfoPanel({
/>
{convo.details.name ? (
<Text
style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_xl, a.text_center]}>
style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_xl, a.text_center]}
emoji>
{convo.details.name}
</Text>
) : null}
@@ -107,7 +108,8 @@ export function MessagesListGroupInfoPanel({
a.text_sm,
t.atoms.text_contrast_high,
showButtons ? null : a.mb_4xl,
]}>
]}
emoji>
{names}
</Text>
) : null}
@@ -60,7 +60,8 @@ export function MessagesListInfoPanel({
]}>
<Text
style={[a.text_2xl, a.font_bold, a.text_center, a.flex_shrink]}
numberOfLines={1}>
numberOfLines={1}
emoji>
{displayName}
</Text>
<ProfileBadges profile={profile} size="lg" />
@@ -76,7 +76,14 @@ export function OutgoingRequestListItem({
moderationOpts={moderationOpts}
/>
<View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.pb_2xs]}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_xs,
a.pb_2xs,
]}>
<View style={[a.flex_shrink]}>
<Text
emoji
@@ -85,8 +92,8 @@ export function OutgoingRequestListItem({
{convoView.name}
</Text>
</View>
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={convoView.requestedAt}>
{convoView.viewer?.requestedAt ? (
<TimeElapsed timestamp={convoView.viewer.requestedAt}>
{({timeElapsed}) => (
<Text
style={[
@@ -98,7 +105,7 @@ export function OutgoingRequestListItem({
</Text>
)}
</TimeElapsed>
</View>
) : null}
</View>
<Text
numberOfLines={1}