Update handling of join link previews (#10798)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.6",
|
||||
"@atproto/api": "0.20.11",
|
||||
"@atproto/common": "^0.6.1",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"express": "^4.19.2",
|
||||
|
||||
Generated
+5
-5
@@ -208,8 +208,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.6
|
||||
version: 0.20.6
|
||||
specifier: 0.20.11
|
||||
version: 0.20.11
|
||||
'@atproto/common':
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1
|
||||
@@ -259,8 +259,8 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@atproto/api@0.20.6':
|
||||
resolution: {integrity: sha512-WnFPcUl+qZdXmt27+Tg93BDIvBt/WpXfLIiBzBTp3ms9aszM5hAsfc7G8KEsnsmnRvcm0xRfiKEjIt5FxTKdYg==}
|
||||
'@atproto/api@0.20.11':
|
||||
resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
@@ -1154,7 +1154,7 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@atproto/api@0.20.6':
|
||||
'@atproto/api@0.20.11':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/lexicon': 0.7.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {type Express} from 'express'
|
||||
import satori from 'satori'
|
||||
@@ -32,7 +32,7 @@ export default function (ctx: AppContext, app: Express) {
|
||||
codes: [code],
|
||||
})
|
||||
const found = result.data.joinLinkPreviews[0]
|
||||
if (!found) {
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(found)) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
preview = found
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@
|
||||
"prettier": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.9",
|
||||
"@atproto/api": "0.20.11",
|
||||
"@atproto/syntax": "0.6.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
|
||||
Generated
+5
-5
@@ -242,8 +242,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.9
|
||||
version: 0.20.9
|
||||
specifier: 0.20.11
|
||||
version: 0.20.11
|
||||
'@atproto/syntax':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
@@ -877,8 +877,8 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@atproto/api@0.20.9':
|
||||
resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==}
|
||||
'@atproto/api@0.20.11':
|
||||
resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
@@ -9493,7 +9493,7 @@ snapshots:
|
||||
|
||||
'@0no-co/graphql.web@1.2.0': {}
|
||||
|
||||
'@atproto/api@0.20.9':
|
||||
'@atproto/api@0.20.11':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/lexicon': 0.7.1
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {
|
||||
type ChatInvitePreview,
|
||||
isKnownJoinLinkPreview,
|
||||
} from '#/state/queries/join-links'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
@@ -23,11 +27,12 @@ export function JoinRequestEmbed({
|
||||
onOpen,
|
||||
}: {
|
||||
code?: string
|
||||
preview?: ChatBskyGroupDefs.JoinLinkPreviewView
|
||||
preview?: ChatInvitePreview
|
||||
style?: StyleProp<ViewStyle>
|
||||
onOpen?: () => void
|
||||
}) {
|
||||
const resolvedCode = code ?? preview?.code
|
||||
const resolvedCode =
|
||||
code ?? (isKnownJoinLinkPreview(preview) ? preview.code : undefined)
|
||||
if (!resolvedCode) return null
|
||||
|
||||
return (
|
||||
@@ -73,7 +78,7 @@ export function JoinRequestEmbedBody({
|
||||
)
|
||||
}
|
||||
|
||||
if (!preview) {
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
@@ -20,7 +21,7 @@ export function Card({size}: {size: 'large' | 'small'}) {
|
||||
const t = useTheme()
|
||||
const {preview, hasFixedHeight} = useChatInvite()
|
||||
|
||||
if (!preview) return null
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) return null
|
||||
|
||||
const ownerDisplayName = createSanitizedDisplayName(preview.owner)
|
||||
const ownerHandle = sanitizeHandle(preview.owner.handle, '@')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {createContext, useContext} from 'react'
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
|
||||
import {type ChatInvitePreview} from '#/state/queries/join-links'
|
||||
import {type ButtonColor} from '#/components/Button'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
|
||||
@@ -26,7 +26,7 @@ export type ChatInviteContextValue = {
|
||||
code: string
|
||||
loading: boolean
|
||||
error: boolean
|
||||
preview: ChatBskyGroupDefs.JoinLinkPreviewView | undefined
|
||||
preview: ChatInvitePreview | undefined
|
||||
/**
|
||||
* The derived action descriptor. Undefined while loading or when there's no
|
||||
* preview to act on.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
|
||||
import {
|
||||
type ChatInvitePreview,
|
||||
useJoinLinkPreviewsQuery,
|
||||
} from '#/state/queries/join-links'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type ButtonColor} from '#/components/Button'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||
@@ -13,7 +16,6 @@ import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {type ChatInviteAction, ChatInviteProvider} from './Context'
|
||||
@@ -34,7 +36,7 @@ export function Root({
|
||||
children,
|
||||
}: {
|
||||
code: string
|
||||
initialPreview?: ChatBskyGroupDefs.JoinLinkPreviewView
|
||||
initialPreview?: ChatInvitePreview
|
||||
/**
|
||||
* The convo this invite is being viewed within, if any. When the invite
|
||||
* links to the same chat, the action becomes "Copy link" instead of
|
||||
@@ -62,7 +64,7 @@ export function Root({
|
||||
const loading = isPending && !preview
|
||||
|
||||
let action: ChatInviteAction | undefined
|
||||
if (preview) {
|
||||
if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||
const convoId = preview.convo?.id
|
||||
const isFollowing = preview.owner.viewer?.following ?? false
|
||||
const hasRequested = !convoId && preview.viewer?.requestedAt != null
|
||||
@@ -99,12 +101,7 @@ export function Root({
|
||||
let icon: React.ComponentType<SVGIconProps> = JoinIcon
|
||||
let label = preview.requireApproval ? l`Request to join` : l`Join`
|
||||
let color: ButtonColor = 'primary'
|
||||
if (preview.enabledStatus !== 'enabled') {
|
||||
canJoin = false
|
||||
icon = WarningIcon
|
||||
label = l`Chat invite link no longer available`
|
||||
color = 'secondary'
|
||||
} else if (preview.memberCount >= preview.memberLimit) {
|
||||
if (preview.memberCount >= preview.memberLimit) {
|
||||
canJoin = false
|
||||
icon = HandIcon
|
||||
label = l`This chat is full`
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useWindowDimensions, View} from 'react-native'
|
||||
import {type $Typed, type ChatBskyEmbedJoinLink} from '@atproto/api'
|
||||
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {isKnownJoinLinkPreview} from '#/state/queries/join-links'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||
import {MessageContextProvider} from './MessageContext'
|
||||
@@ -27,6 +28,11 @@ let MessageItemInviteEmbed = ({
|
||||
const screen = useWindowDimensions()
|
||||
const convo = useConvoActive()
|
||||
|
||||
const code = isKnownJoinLinkPreview(embed.joinLinkPreview)
|
||||
? embed.joinLinkPreview.code
|
||||
: undefined
|
||||
if (!code) return null
|
||||
|
||||
return (
|
||||
<MessageContextProvider>
|
||||
<View
|
||||
@@ -72,7 +78,7 @@ let MessageItemInviteEmbed = ({
|
||||
},
|
||||
]}>
|
||||
<ChatInvite.Root
|
||||
code={embed.joinLinkPreview.code}
|
||||
code={code}
|
||||
initialPreview={embed.joinLinkPreview}
|
||||
currentConvoId={convo.convo.view.id}
|
||||
hasFixedHeight={false}>
|
||||
|
||||
@@ -121,7 +121,8 @@ function ProfileHeaderReady({
|
||||
<View style={[a.flex_row, a.align_center, a.flex_1, web(a.mb_2xs)]}>
|
||||
<Text
|
||||
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
||||
numberOfLines={1}>
|
||||
numberOfLines={1}
|
||||
emoji>
|
||||
{displayName}
|
||||
</Text>
|
||||
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
|
||||
@@ -185,7 +186,8 @@ function GroupHeaderReady({
|
||||
<View style={[a.flex_row, a.flex_1, a.align_center]}>
|
||||
<Text
|
||||
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
||||
numberOfLines={1}>
|
||||
numberOfLines={1}
|
||||
emoji>
|
||||
{convo.details.name}
|
||||
</Text>
|
||||
<MuteStatus muted={convo.view.muted} />
|
||||
|
||||
@@ -45,7 +45,8 @@ export function SystemMessageItem({
|
||||
a.text_center,
|
||||
t.atoms.text_contrast_medium,
|
||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||
]}>
|
||||
]}
|
||||
emoji>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
ChatBskyGroupDefs,
|
||||
ChatBskyGroupRequestJoin,
|
||||
ChatBskyGroupWithdrawJoinRequest,
|
||||
moderateProfile,
|
||||
@@ -211,7 +212,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
||||
|
||||
const joinLinkPreview = data.joinLinkPreviews[0]
|
||||
|
||||
if (!joinLinkPreview) {
|
||||
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) {
|
||||
return (
|
||||
<>
|
||||
<View style={[a.py_lg, a.align_center]}>
|
||||
@@ -253,12 +254,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
||||
? l`Request to join`
|
||||
: l`Join`
|
||||
let buttonColor: ButtonColor = 'primary'
|
||||
if (joinLinkPreview.enabledStatus !== 'enabled') {
|
||||
canJoin = false
|
||||
ButtonIconImage = WarningIcon
|
||||
buttonText = l`Chat invite link no longer available`
|
||||
buttonColor = 'secondary'
|
||||
} else if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) {
|
||||
if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) {
|
||||
canJoin = false
|
||||
ButtonIconImage = HandIcon
|
||||
buttonText = l`This chat is full`
|
||||
@@ -311,10 +307,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.ml_md]}>
|
||||
<PersonGroupIcon
|
||||
size="xs"
|
||||
style={[a.mr_xs, t.atoms.text, {marginTop: -2}]}
|
||||
/>
|
||||
<PersonGroupIcon size="xs" style={[a.mr_xs, t.atoms.text]} />
|
||||
</View>
|
||||
<Text
|
||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||
@@ -371,7 +364,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
<ProfileBadges
|
||||
profile={data.joinLinkPreviews[0].owner}
|
||||
profile={joinLinkPreview.owner}
|
||||
size="sm"
|
||||
style={{marginTop: -3}}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AppBskyFeedPost,
|
||||
BlobRef,
|
||||
type BskyAgent,
|
||||
ChatBskyGroupDefs,
|
||||
type ComAtprotoLabelDefs,
|
||||
type ComAtprotoRepoApplyWrites,
|
||||
type ComAtprotoRepoStrongRef,
|
||||
@@ -463,7 +464,10 @@ async function resolveMedia(
|
||||
},
|
||||
}
|
||||
}
|
||||
if (resolvedLink.type === 'chat-invite' && resolvedLink.view) {
|
||||
if (
|
||||
resolvedLink.type === 'chat-invite' &&
|
||||
ChatBskyGroupDefs.isJoinLinkPreviewView(resolvedLink.view)
|
||||
) {
|
||||
return {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyGraphDefs,
|
||||
type BskyAgent,
|
||||
type ChatBskyGroupDefs,
|
||||
type AtpAgent,
|
||||
type ComAtprotoRepoStrongRef,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {type ComposerImage} from '#/state/gallery'
|
||||
import {createComposerImage} from '#/state/gallery'
|
||||
import {type ChatInvitePreview} from '#/state/queries/join-links'
|
||||
import {type Gif} from '#/features/gifPicker/types'
|
||||
import {createGIFDescription} from '../gif-alt-text'
|
||||
|
||||
@@ -77,7 +77,7 @@ type ResolvedChatInvite = {
|
||||
type: 'chat-invite'
|
||||
uri: string
|
||||
code: string
|
||||
view?: ChatBskyGroupDefs.JoinLinkPreviewView
|
||||
view?: ChatInvitePreview
|
||||
}
|
||||
|
||||
export type ResolvedLink =
|
||||
@@ -95,7 +95,7 @@ export class EmbeddingDisabledError extends Error {
|
||||
}
|
||||
|
||||
export async function resolveLink(
|
||||
agent: BskyAgent,
|
||||
agent: AtpAgent,
|
||||
uri: string,
|
||||
): Promise<ResolvedLink> {
|
||||
if (isShortLink(uri)) {
|
||||
@@ -217,7 +217,7 @@ export async function resolveLink(
|
||||
}
|
||||
|
||||
export async function resolveGif(
|
||||
agent: BskyAgent,
|
||||
agent: AtpAgent,
|
||||
gif: Gif,
|
||||
): Promise<ResolvedExternalLink> {
|
||||
const gifUrl = gif.media_formats.gif.url
|
||||
@@ -259,7 +259,7 @@ function getFileSlug(url: string | undefined): string | undefined {
|
||||
}
|
||||
|
||||
async function resolveExternal(
|
||||
agent: BskyAgent,
|
||||
agent: AtpAgent,
|
||||
uri: string,
|
||||
): Promise<ResolvedExternalLink> {
|
||||
const result = await getLinkMeta(agent, uri)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
import {useCallback} from 'react'
|
||||
import {
|
||||
type $Typed,
|
||||
AtpAgent,
|
||||
type ChatBskyGroupDefs,
|
||||
ChatBskyGroupDefs,
|
||||
type ChatBskyGroupGetJoinLinkPreviews,
|
||||
} from '@atproto/api'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries/index'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {createQueryKey, type StructuredQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
/**
|
||||
* The three preview shapes we currently support. Excludes the `{$type: string}`
|
||||
* open-union fallback for unrecognized future variants - use
|
||||
* `ChatInvitePreview` for that.
|
||||
*/
|
||||
export type KnownChatInvitePreview =
|
||||
| $Typed<ChatBskyGroupDefs.JoinLinkPreviewView>
|
||||
| $Typed<ChatBskyGroupDefs.DisabledJoinLinkPreviewView>
|
||||
| $Typed<ChatBskyGroupDefs.InvalidJoinLinkPreviewView>
|
||||
|
||||
/**
|
||||
* The full open-union shape, including the `{$type: string}` fallback for
|
||||
* future variants.
|
||||
*/
|
||||
export type ChatInvitePreview = KnownChatInvitePreview | {$type: string}
|
||||
|
||||
/**
|
||||
* Narrows a preview to one of the three known variants, filtering out the
|
||||
* `{$type: string}` open-union fallback for unrecognized future shapes.
|
||||
*/
|
||||
export function isKnownJoinLinkPreview(
|
||||
preview: unknown,
|
||||
): preview is KnownChatInvitePreview {
|
||||
return (
|
||||
ChatBskyGroupDefs.isJoinLinkPreviewView(preview) ||
|
||||
ChatBskyGroupDefs.isDisabledJoinLinkPreviewView(preview) ||
|
||||
ChatBskyGroupDefs.isInvalidJoinLinkPreviewView(preview)
|
||||
)
|
||||
}
|
||||
|
||||
const joinLinkPreviewQueryKeyRoot = 'join-link-preview'
|
||||
|
||||
export const createJoinLinkPreviewQueryKey = (args: {
|
||||
@@ -22,6 +53,29 @@ export const createJoinLinkPreviewQueryKey = (args: {
|
||||
persistedVersion: 1,
|
||||
})
|
||||
|
||||
/**
|
||||
* Invalidate any join link preview queries whose `codes` include the given
|
||||
* code. Use this when a link's state changes (e.g. it's disabled) so cached
|
||||
* previews refetch and reflect the new state.
|
||||
*/
|
||||
export function invalidateJoinLinkPreviewsForCode(
|
||||
queryClient: QueryClient,
|
||||
code: string,
|
||||
) {
|
||||
return queryClient.invalidateQueries({
|
||||
predicate: query => {
|
||||
const [root, args] = query.queryKey as Partial<
|
||||
StructuredQueryKey<{codes?: string[]}>
|
||||
>
|
||||
return (
|
||||
root === joinLinkPreviewQueryKeyRoot &&
|
||||
Array.isArray(args?.codes) &&
|
||||
args.codes.includes(code)
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchJoinLinkPreviews({
|
||||
agent,
|
||||
codes,
|
||||
@@ -104,7 +158,7 @@ export function useGetJoinLinkPreview() {
|
||||
}: {
|
||||
code: string
|
||||
hasSession: boolean
|
||||
}): Promise<ChatBskyGroupDefs.JoinLinkPreviewView | undefined> => {
|
||||
}): Promise<KnownChatInvitePreview | undefined> => {
|
||||
try {
|
||||
const data = await queryClient.fetchQuery({
|
||||
queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}),
|
||||
@@ -112,7 +166,8 @@ export function useGetJoinLinkPreview() {
|
||||
fetchJoinLinkPreviews({agent, codes: [code], hasSession}),
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
})
|
||||
return data.joinLinkPreviews[0]
|
||||
const found = data.joinLinkPreviews[0]
|
||||
return isKnownJoinLinkPreview(found) ? found : undefined
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch join link preview', {safeMessage: error})
|
||||
return undefined
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
@@ -59,6 +60,7 @@ export function useDisableJoinLink(
|
||||
}
|
||||
})
|
||||
}
|
||||
void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code)
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
@@ -56,6 +57,7 @@ export function useEnableJoinLink(
|
||||
}
|
||||
})
|
||||
}
|
||||
void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code)
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
type AppBskyUnspeccedGetPostThreadV2,
|
||||
AtUri,
|
||||
type BskyAgent,
|
||||
ChatBskyGroupDefs,
|
||||
type RichText,
|
||||
} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
@@ -886,7 +887,9 @@ export const ComposePost = ({
|
||||
})),
|
||||
})
|
||||
const hasUnavailableChatInvite = linkQueries.some(
|
||||
q => q.data?.type === 'chat-invite' && !q.data.view,
|
||||
q =>
|
||||
q.data?.type === 'chat-invite' &&
|
||||
!ChatBskyGroupDefs.isJoinLinkPreviewView(q.data.view),
|
||||
)
|
||||
|
||||
const canPost =
|
||||
|
||||
Reference in New Issue
Block a user