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"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.6",
|
"@atproto/api": "0.20.11",
|
||||||
"@atproto/common": "^0.6.1",
|
"@atproto/common": "^0.6.1",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
|||||||
Generated
+5
-5
@@ -208,8 +208,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atproto/api':
|
||||||
specifier: 0.20.6
|
specifier: 0.20.11
|
||||||
version: 0.20.6
|
version: 0.20.11
|
||||||
'@atproto/common':
|
'@atproto/common':
|
||||||
specifier: ^0.6.1
|
specifier: ^0.6.1
|
||||||
version: 0.6.1
|
version: 0.6.1
|
||||||
@@ -259,8 +259,8 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@atproto/api@0.20.6':
|
'@atproto/api@0.20.11':
|
||||||
resolution: {integrity: sha512-WnFPcUl+qZdXmt27+Tg93BDIvBt/WpXfLIiBzBTp3ms9aszM5hAsfc7G8KEsnsmnRvcm0xRfiKEjIt5FxTKdYg==}
|
resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.0':
|
'@atproto/common-web@0.5.0':
|
||||||
@@ -1154,7 +1154,7 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@atproto/api@0.20.6':
|
'@atproto/api@0.20.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.0
|
'@atproto/common-web': 0.5.0
|
||||||
'@atproto/lexicon': 0.7.1
|
'@atproto/lexicon': 0.7.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert'
|
import assert from 'node:assert'
|
||||||
|
|
||||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||||
import resvg from '@resvg/resvg-js'
|
import resvg from '@resvg/resvg-js'
|
||||||
import {type Express} from 'express'
|
import {type Express} from 'express'
|
||||||
import satori from 'satori'
|
import satori from 'satori'
|
||||||
@@ -32,7 +32,7 @@ export default function (ctx: AppContext, app: Express) {
|
|||||||
codes: [code],
|
codes: [code],
|
||||||
})
|
})
|
||||||
const found = result.data.joinLinkPreviews[0]
|
const found = result.data.joinLinkPreviews[0]
|
||||||
if (!found) {
|
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(found)) {
|
||||||
return res.status(404).end('not found')
|
return res.status(404).end('not found')
|
||||||
}
|
}
|
||||||
preview = found
|
preview = found
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@
|
|||||||
"prettier": "prettier --check ."
|
"prettier": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.9",
|
"@atproto/api": "0.20.11",
|
||||||
"@atproto/syntax": "0.6.1",
|
"@atproto/syntax": "0.6.1",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
|
|||||||
Generated
+5
-5
@@ -242,8 +242,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atproto/api':
|
||||||
specifier: 0.20.9
|
specifier: 0.20.11
|
||||||
version: 0.20.9
|
version: 0.20.11
|
||||||
'@atproto/syntax':
|
'@atproto/syntax':
|
||||||
specifier: 0.6.1
|
specifier: 0.6.1
|
||||||
version: 0.6.1
|
version: 0.6.1
|
||||||
@@ -877,8 +877,8 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@atproto/api@0.20.9':
|
'@atproto/api@0.20.11':
|
||||||
resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==}
|
resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.0':
|
'@atproto/common-web@0.5.0':
|
||||||
@@ -9493,7 +9493,7 @@ snapshots:
|
|||||||
|
|
||||||
'@0no-co/graphql.web@1.2.0': {}
|
'@0no-co/graphql.web@1.2.0': {}
|
||||||
|
|
||||||
'@atproto/api@0.20.9':
|
'@atproto/api@0.20.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.0
|
'@atproto/common-web': 0.5.0
|
||||||
'@atproto/lexicon': 0.7.1
|
'@atproto/lexicon': 0.7.1
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
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 {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChatInvitePreview,
|
||||||
|
isKnownJoinLinkPreview,
|
||||||
|
} from '#/state/queries/join-links'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
@@ -23,11 +27,12 @@ export function JoinRequestEmbed({
|
|||||||
onOpen,
|
onOpen,
|
||||||
}: {
|
}: {
|
||||||
code?: string
|
code?: string
|
||||||
preview?: ChatBskyGroupDefs.JoinLinkPreviewView
|
preview?: ChatInvitePreview
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
}) {
|
}) {
|
||||||
const resolvedCode = code ?? preview?.code
|
const resolvedCode =
|
||||||
|
code ?? (isKnownJoinLinkPreview(preview) ? preview.code : undefined)
|
||||||
if (!resolvedCode) return null
|
if (!resolvedCode) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +78,7 @@ export function JoinRequestEmbedBody({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preview) {
|
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||||
import {Plural, Trans} from '@lingui/react/macro'
|
import {Plural, Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
@@ -20,7 +21,7 @@ export function Card({size}: {size: 'large' | 'small'}) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {preview, hasFixedHeight} = useChatInvite()
|
const {preview, hasFixedHeight} = useChatInvite()
|
||||||
|
|
||||||
if (!preview) return null
|
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) return null
|
||||||
|
|
||||||
const ownerDisplayName = createSanitizedDisplayName(preview.owner)
|
const ownerDisplayName = createSanitizedDisplayName(preview.owner)
|
||||||
const ownerHandle = sanitizeHandle(preview.owner.handle, '@')
|
const ownerHandle = sanitizeHandle(preview.owner.handle, '@')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {createContext, useContext} from 'react'
|
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 ButtonColor} from '#/components/Button'
|
||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export type ChatInviteContextValue = {
|
|||||||
code: string
|
code: string
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: boolean
|
error: boolean
|
||||||
preview: ChatBskyGroupDefs.JoinLinkPreviewView | undefined
|
preview: ChatInvitePreview | undefined
|
||||||
/**
|
/**
|
||||||
* The derived action descriptor. Undefined while loading or when there's no
|
* The derived action descriptor. Undefined while loading or when there's no
|
||||||
* preview to act on.
|
* preview to act on.
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import {setStringAsync} from 'expo-clipboard'
|
import {setStringAsync} from 'expo-clipboard'
|
||||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
import {ChatBskyGroupDefs} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
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 {useSession} from '#/state/session'
|
||||||
import {type ButtonColor} from '#/components/Button'
|
import {type ButtonColor} from '#/components/Button'
|
||||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
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 {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
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 {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {type ChatInviteAction, ChatInviteProvider} from './Context'
|
import {type ChatInviteAction, ChatInviteProvider} from './Context'
|
||||||
@@ -34,7 +36,7 @@ export function Root({
|
|||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
code: string
|
code: string
|
||||||
initialPreview?: ChatBskyGroupDefs.JoinLinkPreviewView
|
initialPreview?: ChatInvitePreview
|
||||||
/**
|
/**
|
||||||
* The convo this invite is being viewed within, if any. When the invite
|
* 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
|
* links to the same chat, the action becomes "Copy link" instead of
|
||||||
@@ -62,7 +64,7 @@ export function Root({
|
|||||||
const loading = isPending && !preview
|
const loading = isPending && !preview
|
||||||
|
|
||||||
let action: ChatInviteAction | undefined
|
let action: ChatInviteAction | undefined
|
||||||
if (preview) {
|
if (ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||||
const convoId = preview.convo?.id
|
const convoId = preview.convo?.id
|
||||||
const isFollowing = preview.owner.viewer?.following ?? false
|
const isFollowing = preview.owner.viewer?.following ?? false
|
||||||
const hasRequested = !convoId && preview.viewer?.requestedAt != null
|
const hasRequested = !convoId && preview.viewer?.requestedAt != null
|
||||||
@@ -99,12 +101,7 @@ export function Root({
|
|||||||
let icon: React.ComponentType<SVGIconProps> = JoinIcon
|
let icon: React.ComponentType<SVGIconProps> = JoinIcon
|
||||||
let label = preview.requireApproval ? l`Request to join` : l`Join`
|
let label = preview.requireApproval ? l`Request to join` : l`Join`
|
||||||
let color: ButtonColor = 'primary'
|
let color: ButtonColor = 'primary'
|
||||||
if (preview.enabledStatus !== 'enabled') {
|
if (preview.memberCount >= preview.memberLimit) {
|
||||||
canJoin = false
|
|
||||||
icon = WarningIcon
|
|
||||||
label = l`Chat invite link no longer available`
|
|
||||||
color = 'secondary'
|
|
||||||
} else if (preview.memberCount >= preview.memberLimit) {
|
|
||||||
canJoin = false
|
canJoin = false
|
||||||
icon = HandIcon
|
icon = HandIcon
|
||||||
label = l`This chat is full`
|
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 {type $Typed, type ChatBskyEmbedJoinLink} from '@atproto/api'
|
||||||
|
|
||||||
import {useConvoActive} from '#/state/messages/convo'
|
import {useConvoActive} from '#/state/messages/convo'
|
||||||
|
import {isKnownJoinLinkPreview} from '#/state/queries/join-links'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||||
import {MessageContextProvider} from './MessageContext'
|
import {MessageContextProvider} from './MessageContext'
|
||||||
@@ -27,6 +28,11 @@ let MessageItemInviteEmbed = ({
|
|||||||
const screen = useWindowDimensions()
|
const screen = useWindowDimensions()
|
||||||
const convo = useConvoActive()
|
const convo = useConvoActive()
|
||||||
|
|
||||||
|
const code = isKnownJoinLinkPreview(embed.joinLinkPreview)
|
||||||
|
? embed.joinLinkPreview.code
|
||||||
|
: undefined
|
||||||
|
if (!code) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContextProvider>
|
<MessageContextProvider>
|
||||||
<View
|
<View
|
||||||
@@ -72,7 +78,7 @@ let MessageItemInviteEmbed = ({
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<ChatInvite.Root
|
<ChatInvite.Root
|
||||||
code={embed.joinLinkPreview.code}
|
code={code}
|
||||||
initialPreview={embed.joinLinkPreview}
|
initialPreview={embed.joinLinkPreview}
|
||||||
currentConvoId={convo.convo.view.id}
|
currentConvoId={convo.convo.view.id}
|
||||||
hasFixedHeight={false}>
|
hasFixedHeight={false}>
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ function ProfileHeaderReady({
|
|||||||
<View style={[a.flex_row, a.align_center, a.flex_1, web(a.mb_2xs)]}>
|
<View style={[a.flex_row, a.align_center, a.flex_1, web(a.mb_2xs)]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
|
<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]}>
|
<View style={[a.flex_row, a.flex_1, a.align_center]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
style={[a.text_lg, a.font_semi_bold, a.flex_shrink]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
{convo.details.name}
|
{convo.details.name}
|
||||||
</Text>
|
</Text>
|
||||||
<MuteStatus muted={convo.view.muted} />
|
<MuteStatus muted={convo.view.muted} />
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export function SystemMessageItem({
|
|||||||
a.text_center,
|
a.text_center,
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||||
]}>
|
]}
|
||||||
|
emoji>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
|
ChatBskyGroupDefs,
|
||||||
ChatBskyGroupRequestJoin,
|
ChatBskyGroupRequestJoin,
|
||||||
ChatBskyGroupWithdrawJoinRequest,
|
ChatBskyGroupWithdrawJoinRequest,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
@@ -211,7 +212,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
|||||||
|
|
||||||
const joinLinkPreview = data.joinLinkPreviews[0]
|
const joinLinkPreview = data.joinLinkPreviews[0]
|
||||||
|
|
||||||
if (!joinLinkPreview) {
|
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={[a.py_lg, a.align_center]}>
|
<View style={[a.py_lg, a.align_center]}>
|
||||||
@@ -253,12 +254,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
|||||||
? l`Request to join`
|
? l`Request to join`
|
||||||
: l`Join`
|
: l`Join`
|
||||||
let buttonColor: ButtonColor = 'primary'
|
let buttonColor: ButtonColor = 'primary'
|
||||||
if (joinLinkPreview.enabledStatus !== 'enabled') {
|
if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) {
|
||||||
canJoin = false
|
|
||||||
ButtonIconImage = WarningIcon
|
|
||||||
buttonText = l`Chat invite link no longer available`
|
|
||||||
buttonColor = 'secondary'
|
|
||||||
} else if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) {
|
|
||||||
canJoin = false
|
canJoin = false
|
||||||
ButtonIconImage = HandIcon
|
ButtonIconImage = HandIcon
|
||||||
buttonText = l`This chat is full`
|
buttonText = l`This chat is full`
|
||||||
@@ -311,10 +307,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
|||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[a.flex_row, a.ml_md]}>
|
<View style={[a.flex_row, a.ml_md]}>
|
||||||
<PersonGroupIcon
|
<PersonGroupIcon size="xs" style={[a.mr_xs, t.atoms.text]} />
|
||||||
size="xs"
|
|
||||||
style={[a.mr_xs, t.atoms.text, {marginTop: -2}]}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||||
@@ -371,7 +364,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
|
|||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileBadges
|
<ProfileBadges
|
||||||
profile={data.joinLinkPreviews[0].owner}
|
profile={joinLinkPreview.owner}
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{marginTop: -3}}
|
style={{marginTop: -3}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
BlobRef,
|
BlobRef,
|
||||||
type BskyAgent,
|
type BskyAgent,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
type ComAtprotoLabelDefs,
|
type ComAtprotoLabelDefs,
|
||||||
type ComAtprotoRepoApplyWrites,
|
type ComAtprotoRepoApplyWrites,
|
||||||
type ComAtprotoRepoStrongRef,
|
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 {
|
return {
|
||||||
$type: 'app.bsky.embed.external',
|
$type: 'app.bsky.embed.external',
|
||||||
external: {
|
external: {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
type AppBskyGraphDefs,
|
type AppBskyGraphDefs,
|
||||||
type BskyAgent,
|
type AtpAgent,
|
||||||
type ChatBskyGroupDefs,
|
|
||||||
type ComAtprotoRepoStrongRef,
|
type ComAtprotoRepoStrongRef,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {type ComposerImage} from '#/state/gallery'
|
import {type ComposerImage} from '#/state/gallery'
|
||||||
import {createComposerImage} from '#/state/gallery'
|
import {createComposerImage} from '#/state/gallery'
|
||||||
|
import {type ChatInvitePreview} from '#/state/queries/join-links'
|
||||||
import {type Gif} from '#/features/gifPicker/types'
|
import {type Gif} from '#/features/gifPicker/types'
|
||||||
import {createGIFDescription} from '../gif-alt-text'
|
import {createGIFDescription} from '../gif-alt-text'
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ type ResolvedChatInvite = {
|
|||||||
type: 'chat-invite'
|
type: 'chat-invite'
|
||||||
uri: string
|
uri: string
|
||||||
code: string
|
code: string
|
||||||
view?: ChatBskyGroupDefs.JoinLinkPreviewView
|
view?: ChatInvitePreview
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedLink =
|
export type ResolvedLink =
|
||||||
@@ -95,7 +95,7 @@ export class EmbeddingDisabledError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveLink(
|
export async function resolveLink(
|
||||||
agent: BskyAgent,
|
agent: AtpAgent,
|
||||||
uri: string,
|
uri: string,
|
||||||
): Promise<ResolvedLink> {
|
): Promise<ResolvedLink> {
|
||||||
if (isShortLink(uri)) {
|
if (isShortLink(uri)) {
|
||||||
@@ -217,7 +217,7 @@ export async function resolveLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveGif(
|
export async function resolveGif(
|
||||||
agent: BskyAgent,
|
agent: AtpAgent,
|
||||||
gif: Gif,
|
gif: Gif,
|
||||||
): Promise<ResolvedExternalLink> {
|
): Promise<ResolvedExternalLink> {
|
||||||
const gifUrl = gif.media_formats.gif.url
|
const gifUrl = gif.media_formats.gif.url
|
||||||
@@ -259,7 +259,7 @@ function getFileSlug(url: string | undefined): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveExternal(
|
async function resolveExternal(
|
||||||
agent: BskyAgent,
|
agent: AtpAgent,
|
||||||
uri: string,
|
uri: string,
|
||||||
): Promise<ResolvedExternalLink> {
|
): Promise<ResolvedExternalLink> {
|
||||||
const result = await getLinkMeta(agent, uri)
|
const result = await getLinkMeta(agent, uri)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ImageBackground} from 'expo-image'
|
import {ImageBackground} from 'expo-image'
|
||||||
import {moderateProfile} from '@atproto/api'
|
import {ChatBskyGroupDefs, moderateProfile} from '@atproto/api'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
@@ -52,9 +52,7 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
? mobileDarkBg
|
? mobileDarkBg
|
||||||
: mobileLightBg
|
: mobileLightBg
|
||||||
|
|
||||||
const requiresApproval = data?.joinLinkPreviews[0]?.requireApproval
|
const joinLinkPreview = data?.joinLinkPreviews[0]
|
||||||
const requiresFollow =
|
|
||||||
data?.joinLinkPreviews[0]?.joinRule === 'followedByOwner'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.util_screen_outer, a.w_full, t.atoms.bg_contrast_25]}>
|
<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.justify_center,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
]}>
|
]}>
|
||||||
{error ? (
|
{error ||
|
||||||
|
(data &&
|
||||||
|
!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) ? (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
|
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
|
||||||
<Text
|
<Text
|
||||||
@@ -80,20 +80,19 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
a.font_semi_bold,
|
a.font_semi_bold,
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
]}>
|
]}>
|
||||||
{l`This invite link has expired`}
|
<Trans>Chat invite link no longer available</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ActionButtons setScreenState={setScreenState} />
|
<ActionButtons setScreenState={setScreenState} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
) : data && moderationOpts ? (
|
) : data &&
|
||||||
|
moderationOpts &&
|
||||||
|
ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview) ? (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<AvatarBubbles
|
<AvatarBubbles
|
||||||
profiles={[
|
profiles={[
|
||||||
data.joinLinkPreviews[0].owner,
|
joinLinkPreview.owner,
|
||||||
...Array(
|
...Array(
|
||||||
Math.min(
|
Math.min(3, Math.max(0, joinLinkPreview.memberCount - 1)),
|
||||||
3,
|
|
||||||
Math.max(0, data.joinLinkPreviews[0].memberCount - 1),
|
|
||||||
),
|
|
||||||
).fill(undefined),
|
).fill(undefined),
|
||||||
]}
|
]}
|
||||||
size={135}
|
size={135}
|
||||||
@@ -130,8 +129,8 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
<Trans
|
<Trans
|
||||||
context="group-chat-member-count"
|
context="group-chat-member-count"
|
||||||
comment="The number of active group chat members out of the total number allowed.">
|
comment="The number of active group chat members out of the total number allowed.">
|
||||||
{data.joinLinkPreviews[0].memberCount}/
|
{joinLinkPreview.memberCount}/
|
||||||
{data.joinLinkPreviews[0].memberLimit}
|
{joinLinkPreview.memberLimit}
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -144,7 +143,7 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
a.font_bold,
|
a.font_bold,
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
]}>
|
]}>
|
||||||
{data.joinLinkPreviews[0].name}
|
{joinLinkPreview.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.w_full]}>
|
<View style={[a.w_full]}>
|
||||||
@@ -168,17 +167,17 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
<Trans comment="The owner (creator) of a group chat.">
|
<Trans comment="The owner (creator) of a group chat.">
|
||||||
By{' '}
|
By{' '}
|
||||||
{createSanitizedDisplayName(
|
{createSanitizedDisplayName(
|
||||||
data.joinLinkPreviews[0].owner,
|
joinLinkPreview.owner,
|
||||||
true,
|
true,
|
||||||
moderateProfile(
|
moderateProfile(
|
||||||
data.joinLinkPreviews[0].owner,
|
joinLinkPreview.owner,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
).ui('displayName'),
|
).ui('displayName'),
|
||||||
)}
|
)}
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileBadges
|
<ProfileBadges
|
||||||
profile={data.joinLinkPreviews[0].owner}
|
profile={joinLinkPreview.owner}
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{marginTop: -4}}
|
style={{marginTop: -4}}
|
||||||
/>
|
/>
|
||||||
@@ -192,7 +191,7 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
a.max_w_full,
|
a.max_w_full,
|
||||||
]}>
|
]}>
|
||||||
{sanitizeHandle(data.joinLinkPreviews[0].owner.handle, '@')}
|
{sanitizeHandle(joinLinkPreview.owner.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
@@ -202,17 +201,16 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
]}>
|
]}>
|
||||||
{requiresApproval
|
{joinLinkPreview.requireApproval
|
||||||
? l`Sign in to request access to this group chat.`
|
? l`Sign in to request access to this group chat.`
|
||||||
: l`Sign in to accept invite.`}{' '}
|
: l`Sign in to accept invite.`}{' '}
|
||||||
{requiresFollow &&
|
{joinLinkPreview.joinRule === 'followedByOwner' &&
|
||||||
l`Only people ${createSanitizedDisplayName(
|
l`Only people ${createSanitizedDisplayName(
|
||||||
data.joinLinkPreviews[0].owner,
|
joinLinkPreview.owner,
|
||||||
true,
|
true,
|
||||||
moderateProfile(
|
moderateProfile(joinLinkPreview.owner, moderationOpts).ui(
|
||||||
data.joinLinkPreviews[0].owner,
|
'displayName',
|
||||||
moderationOpts,
|
),
|
||||||
).ui('displayName'),
|
|
||||||
)} follows can join.`}
|
)} follows can join.`}
|
||||||
</Text>
|
</Text>
|
||||||
<ActionButtons setScreenState={setScreenState} />
|
<ActionButtons setScreenState={setScreenState} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyRichtextFacet,
|
AppBskyRichtextFacet,
|
||||||
AtUri,
|
AtUri,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
moderatePost,
|
moderatePost,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
@@ -30,6 +31,7 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
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 {Loader} from '#/components/Loader'
|
||||||
import * as MediaPreview from '#/components/MediaPreview'
|
import * as MediaPreview from '#/components/MediaPreview'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
@@ -52,12 +54,12 @@ export function useMessageEmbed() {
|
|||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const embedFromParams = route.params.embed
|
const embedFromParams = route.params.embed
|
||||||
|
|
||||||
const [embed, setEmbedState] = useState<MessageEmbedState | undefined>(
|
const [embed, setEmbed] = useState<MessageEmbedState | undefined>(
|
||||||
embedFromParams ? {type: 'post', uri: embedFromParams} : undefined,
|
embedFromParams ? {type: 'post', uri: embedFromParams} : undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (embedFromParams && embed?.type !== 'post') {
|
if (embedFromParams && embed?.type !== 'post') {
|
||||||
setEmbedState({type: 'post', uri: embedFromParams})
|
setEmbed({type: 'post', uri: embedFromParams})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -68,7 +70,7 @@ export function useMessageEmbed() {
|
|||||||
// Only the post embed is reflected in the route param (used by the
|
// Only the post embed is reflected in the route param (used by the
|
||||||
// share-to-DM intent flow); invites are local-only.
|
// share-to-DM intent flow); invites are local-only.
|
||||||
navigation.setParams({embed: ''})
|
navigation.setParams({embed: ''})
|
||||||
setEmbedState(undefined)
|
setEmbed(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ export function useMessageEmbed() {
|
|||||||
if (isBskyChatInviteUrl(embedUrl)) {
|
if (isBskyChatInviteUrl(embedUrl)) {
|
||||||
const code = getChatInviteCodeFromUrl(embedUrl)
|
const code = getChatInviteCodeFromUrl(embedUrl)
|
||||||
if (code) {
|
if (code) {
|
||||||
setEmbedState({type: 'invite', code})
|
setEmbed({type: 'invite', code})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,7 +88,7 @@ export function useMessageEmbed() {
|
|||||||
const url = convertBskyAppUrlIfNeeded(embedUrl)
|
const url = convertBskyAppUrlIfNeeded(embedUrl)
|
||||||
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
|
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
|
||||||
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
|
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
|
||||||
setEmbedState({type: 'post', uri})
|
setEmbed({type: 'post', uri})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[embedFromParams, navigation],
|
[embedFromParams, navigation],
|
||||||
@@ -314,11 +316,19 @@ function MessageInputInviteEmbedBody() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preview) {
|
if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
|
||||||
return (
|
return (
|
||||||
<View style={[{minHeight: 64}, a.justify_center, a.align_center]}>
|
<View
|
||||||
<Text style={[a.text_center, t.atoms.text_contrast_medium, a.italic]}>
|
style={[
|
||||||
<Trans>Could not load invite</Trans>
|
{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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ export function MessagesListGroupInfoPanel({
|
|||||||
/>
|
/>
|
||||||
{convo.details.name ? (
|
{convo.details.name ? (
|
||||||
<Text
|
<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}
|
{convo.details.name}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -107,7 +108,8 @@ export function MessagesListGroupInfoPanel({
|
|||||||
a.text_sm,
|
a.text_sm,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
showButtons ? null : a.mb_4xl,
|
showButtons ? null : a.mb_4xl,
|
||||||
]}>
|
]}
|
||||||
|
emoji>
|
||||||
{names}
|
{names}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export function MessagesListInfoPanel({
|
|||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_2xl, a.font_bold, a.text_center, a.flex_shrink]}
|
style={[a.text_2xl, a.font_bold, a.text_center, a.flex_shrink]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileBadges profile={profile} size="lg" />
|
<ProfileBadges profile={profile} size="lg" />
|
||||||
|
|||||||
@@ -76,7 +76,14 @@ export function OutgoingRequestListItem({
|
|||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
<View style={[a.flex_1]}>
|
<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]}>
|
<View style={[a.flex_shrink]}>
|
||||||
<Text
|
<Text
|
||||||
emoji
|
emoji
|
||||||
@@ -85,8 +92,8 @@ export function OutgoingRequestListItem({
|
|||||||
{convoView.name}
|
{convoView.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.pl_xs]}>
|
{convoView.viewer?.requestedAt ? (
|
||||||
<TimeElapsed timestamp={convoView.requestedAt}>
|
<TimeElapsed timestamp={convoView.viewer.requestedAt}>
|
||||||
{({timeElapsed}) => (
|
{({timeElapsed}) => (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -98,7 +105,7 @@ export function OutgoingRequestListItem({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TimeElapsed>
|
</TimeElapsed>
|
||||||
</View>
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {
|
import {
|
||||||
|
type $Typed,
|
||||||
AtpAgent,
|
AtpAgent,
|
||||||
type ChatBskyGroupDefs,
|
ChatBskyGroupDefs,
|
||||||
type ChatBskyGroupGetJoinLinkPreviews,
|
type ChatBskyGroupGetJoinLinkPreviews,
|
||||||
} from '@atproto/api'
|
} 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 {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries/index'
|
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'
|
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'
|
const joinLinkPreviewQueryKeyRoot = 'join-link-preview'
|
||||||
|
|
||||||
export const createJoinLinkPreviewQueryKey = (args: {
|
export const createJoinLinkPreviewQueryKey = (args: {
|
||||||
@@ -22,6 +53,29 @@ export const createJoinLinkPreviewQueryKey = (args: {
|
|||||||
persistedVersion: 1,
|
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({
|
async function fetchJoinLinkPreviews({
|
||||||
agent,
|
agent,
|
||||||
codes,
|
codes,
|
||||||
@@ -104,7 +158,7 @@ export function useGetJoinLinkPreview() {
|
|||||||
}: {
|
}: {
|
||||||
code: string
|
code: string
|
||||||
hasSession: boolean
|
hasSession: boolean
|
||||||
}): Promise<ChatBskyGroupDefs.JoinLinkPreviewView | undefined> => {
|
}): Promise<KnownChatInvitePreview | undefined> => {
|
||||||
try {
|
try {
|
||||||
const data = await queryClient.fetchQuery({
|
const data = await queryClient.fetchQuery({
|
||||||
queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}),
|
queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}),
|
||||||
@@ -112,7 +166,8 @@ export function useGetJoinLinkPreview() {
|
|||||||
fetchJoinLinkPreviews({agent, codes: [code], hasSession}),
|
fetchJoinLinkPreviews({agent, codes: [code], hasSession}),
|
||||||
staleTime: STALE.SECONDS.FIFTEEN,
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
})
|
})
|
||||||
return data.joinLinkPreviews[0]
|
const found = data.joinLinkPreviews[0]
|
||||||
|
return isKnownJoinLinkPreview(found) ? found : undefined
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch join link preview', {safeMessage: error})
|
logger.error('Failed to fetch join link preview', {safeMessage: error})
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {
|
import {
|
||||||
rollbackConvoOptimistic,
|
rollbackConvoOptimistic,
|
||||||
@@ -59,6 +60,7 @@ export function useDisableJoinLink(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code)
|
||||||
onSuccess?.(data)
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
onError: (e, _variables, context) => {
|
onError: (e, _variables, context) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {
|
import {
|
||||||
rollbackConvoOptimistic,
|
rollbackConvoOptimistic,
|
||||||
@@ -56,6 +57,7 @@ export function useEnableJoinLink(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code)
|
||||||
onSuccess?.(data)
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
onError: (e, _variables, context) => {
|
onError: (e, _variables, context) => {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
type AppBskyUnspeccedGetPostThreadV2,
|
type AppBskyUnspeccedGetPostThreadV2,
|
||||||
AtUri,
|
AtUri,
|
||||||
type BskyAgent,
|
type BskyAgent,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
type RichText,
|
type RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {plural} from '@lingui/core/macro'
|
import {plural} from '@lingui/core/macro'
|
||||||
@@ -886,7 +887,9 @@ export const ComposePost = ({
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
const hasUnavailableChatInvite = linkQueries.some(
|
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 =
|
const canPost =
|
||||||
|
|||||||
Reference in New Issue
Block a user