Update handling of join link previews (#10798)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
+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 {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
import {CheckThick_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}>
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -127,9 +126,11 @@ export function JoinRequest({setScreenState}: Props) {
|
|||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
]}>
|
]}>
|
||||||
<Trans comment="The number of active group chat members out of the total number allowed.">
|
<Trans
|
||||||
{data.joinLinkPreviews[0].memberCount}/
|
context="group-chat-member-count"
|
||||||
{data.joinLinkPreviews[0].memberLimit}
|
comment="The number of active group chat members out of the total number allowed.">
|
||||||
|
{joinLinkPreview.memberCount}/
|
||||||
|
{joinLinkPreview.memberLimit}
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -142,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]}>
|
||||||
@@ -166,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}}
|
||||||
/>
|
/>
|
||||||
@@ -190,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
|
||||||
@@ -200,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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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 {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
@@ -12,6 +13,36 @@ import {STALE} from '#/state/queries/index'
|
|||||||
import {createQueryKey} from '#/state/queries/util'
|
import {createQueryKey} 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: {
|
||||||
@@ -104,7 +135,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 +143,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
|
||||||
|
|||||||
Reference in New Issue
Block a user