@atproto/api@next integration (#7344)

* Bump SDK

* Use consistent type in profile query

* Omit  from constraint for profile shadow

* Replace isRecord with isValidRecord in QuoteEmbed

* Omit type from constraint for old ProfileCard

* Omit type from constraint in profile queries where appropriate

* Use correct type for update profile mutation

* Conslidate and fix check for isValidRecord in Post.tsx

* Replace isRecord with isValidRecord in PostThreadItem

* Remove redundant cast in PostThreadFollowBtn

* Ignore errors in DebugMod screen

* Use matching type in ProfileFollows screen

* Use matching type in ProfileFollowers screen

* Migrate to isValidRecord in PostFeedItem

* Use matching type if PostRepostedBy

* Omit type from constraint in avatar props

* Use matching types in NotificationFeedItem

* Todo

* Use isValidRecord in NotfyFeedItem

* Improve MediaPreview types

* Migrate another isValidRecord in NotificationFeedItem

* Migrate to isValidView in queries/util

* Migrate to isValidRecord in threadgate/util

* Fix types in threadgates

* Fix up types in starter-packs queries

* Todo

* Specify exact types in search-posts

* Use internal type util to align types

* Ditto last

* Migrate postgate/index

* Specify exact types in post-thread

* Use correct type in post-quotes

* FIX potential bug in post-thread

* Use correct type in post-feed

* Add correct type guards to notifications/feed

* Migrate a guard in notifications/util

* Migrate guard in Wizard/State

* [@next] Profile handling, migrate `ProfileCard` (#7347)

* Introduce new utils for profiles, migrate old ProfileCard

* Rename, reorg

* Add parseEmbed utils

* Expand AnyProfileView to include chat profile view, update post shadow to reflect this

* Cast for perf reasons

* Tighten up types now that we have AnyProfileView

* Add fastIsType util

* Use `assertDid`

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use util types

* Comment

* Use fastIsType where no validation was happening before

* suggestions (#7382)

* suggestions

* Revert unneeded changes

---------

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

* Use new util

* Rename to dangerousIsType

* Convert object shape

* Use dangerous util

* Use dangerous util

* Use dangerous util, we can trust post records

* Use dangerous util

* Use AnyProfileVIew

* Convert object shape

* Clean up handling

* Patch moderateProfile to accept known profile views, to discuss

* Add AnyStarterPackView and related, implement in first usage

* Remove validation, fix type, fix ref

* Migrate over list-conversations

* Clarify intent behind precacheProfile and its unstable query cache

* Clean up unstable profile cache

* Fix types during label creation in PwiOptOut (#7346)

* Tighten types in queries/list

* Chat: use correct profile views

* Chat: fix log type check

* Chat: construct lexically correct shape, even though it's only internal usage

* Chat: use correct profile types

* Chat: fix type check in logs

* Starter: use correct profile types

* Starter: use correct profile types

* Starter: tighten types to match lex

* Any profile type will work in blocked-and-muted

* Use dangerous util

* Use dangerousIsType

* Update new ProfileCard to use AnyProfileView

* Use dangerousIsType

* Remove outdated todo

* Use correct profile type

* Use correct profile types

* Tighten up types

* Use dangerousIsType

* Chat: more type fixes

* Remove unused file

* Add a few  utils

* Remove unused file

* Ignore feedPost.__source

* Clean up types, leave validation in critical path

* Use dangerousIstype

* Use ANyProfileView

* Use isValidRecord

* Use dangerousIsType

* Fix types in ListCard

* Fix FeedInterstitials types

* Fix types in FeedCard

* Fix types in dms ReportDialog

* Fix types in SearchablePeopleList

* Fix bad type in composer opts

* Starter: ok these need to be loose too

* Clarify docs

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Less code

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

* Bump sdk, update $Typed imports

* Format

* Format

* Fix weird TS error

* Remove patch

* Beter name

* It's memo, can validate

* Tighten up parseEmbed, dogfood

* Bump sdk

* Use asPredicate

* Loosen types a bit

* use asPredicate

* Fix types

* Use asPredicate

* Use asPredicate

* Use asPredicate

* Clean up upsertProfile types

* Use asPredicate

* Use Un util

* Fix types

* Use new AnyProfileView

* Use dangerousIsType

* Use asPredicate

* Use asPredicate

* Add fallback content-type to pass typecheck

* Clean up upsertProfile types

* Align types

* Use dangerousIsType

* Use dangerousIsType

* Use asPredicate

* Align types

* Convert findLast

* Align types

* Just ignore type errors and use findLast

* Rename atproto -> bsky

* Add validate util

* Fix type error

* Loosen types

* Export post

* rename atp bsky

* Remove unused code

* minor changes

* Bump deps

* Fix types

* Tighten back up loose check

* Tighten back up loose check

* Fix small bug

* Update comment

* Revert change

---------

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>
Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
This commit is contained in:
Eric Bailey
2025-02-18 08:54:25 -06:00
committed by GitHub
parent e2c0f78719
commit a51fc8e434
93 changed files with 1027 additions and 728 deletions
+2 -2
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AtUri,
@@ -32,6 +31,7 @@ import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {RichText, RichTextProps} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
type Props = {
view: AppBskyFeedDefs.GeneratorView
@@ -115,7 +115,7 @@ export function TitleAndByline({
creator,
}: {
title: string
creator?: AppBskyActorDefs.ProfileViewBasic
creator?: bsky.profile.AnyProfileView
}) {
const t = useTheme()
+5 -3
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {ScrollView, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -26,6 +27,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
import {InlineLinkText} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {ProgressGuideList} from './ProgressGuide/List'
const MOBILE_CARD_WIDTH = 300
@@ -227,7 +229,7 @@ export function ProfileGrid({
viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: AppBskyActorDefs.ProfileViewDetailed[]
profiles: bsky.profile.AnyProfileView[]
recId?: number
error: Error | null
viewContext: 'profile' | 'feed'
+3 -2
View File
@@ -10,6 +10,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
const AVI_SIZE = 30
const AVI_SIZE_SMALL = 20
@@ -33,7 +34,7 @@ export function KnownFollowers({
onLinkPress,
minimal,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress']
minimal?: boolean
@@ -77,7 +78,7 @@ function KnownFollowersInner({
onLinkPress,
minimal,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress']
+2 -2
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyGraphDefs,
AtUri,
moderateUserList,
@@ -26,6 +25,7 @@ import {
import {Link as InternalLink, LinkProps} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
/*
* This component is based on `FeedCard` and is tightly coupled with that
@@ -107,7 +107,7 @@ export function TitleAndByline({
modUi,
}: {
title: string
creator?: AppBskyActorDefs.ProfileViewBasic
creator?: bsky.profile.AnyProfileView
purpose?: AppBskyGraphDefs.ListView['purpose']
modUi?: ModerationUI
}) {
+22 -36
View File
@@ -1,19 +1,15 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
} from '@atproto/api'
import {AppBskyFeedDefs} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {isTenorGifUri} from '#/lib/strings/embed-player'
import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import * as bsky from '#/types/bsky'
/**
* Streamlined MediaPreview component which just handles images, gifs, and videos
@@ -22,20 +18,17 @@ export function Embed({
embed,
style,
}: {
embed?:
| AppBskyEmbedImages.View
| AppBskyEmbedRecordWithMedia.View
| AppBskyEmbedExternal.View
| AppBskyEmbedVideo.View
| {[k: string]: unknown}
embed: AppBskyFeedDefs.PostView['embed']
style?: StyleProp<ViewStyle>
}) {
let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
const e = bsky.post.parseEmbed(embed)
if (AppBskyEmbedImages.isView(media)) {
if (!e) return null
if (e.type === 'images') {
return (
<Outer style={style}>
{media.images.map(image => (
{e.view.images.map(image => (
<ImageItem
key={image.thumb}
thumbnail={image.thumb}
@@ -44,28 +37,21 @@ export function Embed({
))}
</Outer>
)
} else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
let url: URL | undefined
try {
url = new URL(media.external.uri)
} catch {}
if (url) {
const {success} = parseTenorGif(url)
if (success) {
return (
<Outer style={style}>
<GifItem
thumbnail={media.external.thumb}
alt={media.external.title}
/>
</Outer>
)
}
}
} else if (AppBskyEmbedVideo.isView(media)) {
} else if (e.type === 'link') {
if (!e.view.external.thumb) return null
if (!isTenorGifUri(e.view.external.uri)) return null
return (
<Outer style={style}>
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
<GifItem
thumbnail={e.view.external.thumb}
alt={e.view.external.title}
/>
</Outer>
)
} else if (e.type === 'video') {
return (
<Outer style={style}>
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
</Outer>
)
}
+11 -12
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
moderateProfile,
ModerationOpts,
RichText as RichTextApi,
@@ -25,13 +24,14 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function Default({
profile,
moderationOpts,
logContext = 'ProfileCard',
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
@@ -51,7 +51,7 @@ export function Card({
moderationOpts,
logContext = 'ProfileCard',
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
@@ -101,7 +101,7 @@ export function Link({
style,
...rest
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
} & Omit<LinkProps, 'to' | 'label'>) {
const {_} = useLingui()
return (
@@ -126,7 +126,7 @@ export function Avatar({
profile,
moderationOpts,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const moderation = moderateProfile(profile, moderationOpts)
@@ -161,7 +161,7 @@ export function NameAndHandle({
profile,
moderationOpts,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const t = useTheme()
@@ -224,17 +224,16 @@ export function Description({
profile: profileUnshadowed,
numberOfLines = 3,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: bsky.profile.AnyProfileView
numberOfLines?: number
}) {
const profile = useProfileShadow(profileUnshadowed)
const {description} = profile
const rt = React.useMemo(() => {
if (!description) return
const rt = new RichTextApi({text: description || ''})
if (!('description' in profile)) return
const rt = new RichTextApi({text: profile.description || ''})
rt.detectFacetsWithoutResolution()
return rt
}, [description])
}, [profile])
if (!rt) return null
if (
profile.viewer &&
@@ -281,7 +280,7 @@ export function DescriptionPlaceholder({
}
export type FollowButtonProps = {
profile: AppBskyActorDefs.ProfileViewBasic
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
+7 -1
View File
@@ -13,6 +13,7 @@ import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
const LazyViewShot = React.lazy(
// @ts-expect-error dynamic import
@@ -30,7 +31,12 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
) {
const {record} = starterPack
if (!AppBskyGraphStarterpack.isRecord(record)) {
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
+7 -1
View File
@@ -18,6 +18,7 @@ import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {QrCode} from '#/components/StarterPack/QrCode'
import * as bsky from '#/types/bsky'
export function QrCodeDialog({
starterPack,
@@ -77,7 +78,12 @@ export function QrCodeDialog({
} else {
setIsProcessing(true)
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
if (
!bsky.validate(
starterPack.record,
AppBskyGraphStarterpack.validateRecord,
)
) {
return
}
+13 -7
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -15,11 +15,12 @@ import {atoms as a, useTheme} from '#/alf'
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
import {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function Default({
starterPack,
}: {
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
starterPack?: bsky.starterPack.AnyStarterPackView
}) {
if (!starterPack) return null
return (
@@ -32,7 +33,7 @@ export function Default({
export function Notification({
starterPack,
}: {
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
starterPack?: bsky.starterPack.AnyStarterPackView
}) {
if (!starterPack) return null
return (
@@ -47,7 +48,7 @@ export function Card({
noIcon,
noDescription,
}: {
starterPack: AppBskyGraphDefs.StarterPackViewBasic
starterPack: bsky.starterPack.AnyStarterPackView
noIcon?: boolean
noDescription?: boolean
}) {
@@ -57,7 +58,12 @@ export function Card({
const t = useTheme()
const {currentAccount} = useSession()
if (!AppBskyGraphStarterpack.isRecord(record)) {
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
@@ -100,7 +106,7 @@ export function Link({
starterPack,
children,
}: {
starterPack: AppBskyGraphDefs.StarterPackViewBasic
starterPack: bsky.starterPack.AnyStarterPackView
onPress?: () => void
children: BaseLinkProps['children']
}) {
@@ -139,7 +145,7 @@ export function Link({
export function Embed({
starterPack,
}: {
starterPack: AppBskyGraphDefs.StarterPackViewBasic
starterPack: bsky.starterPack.AnyStarterPackView
}) {
const t = useTheme()
const imageUri = getStarterPackOgCard(starterPack)
@@ -38,7 +38,7 @@ export function WizardEditListDialog({
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const t = useTheme()
@@ -22,6 +22,7 @@ import {Button, ButtonText} from '#/components/Button'
import * as Toggle from '#/components/forms/Toggle'
import {Checkbox} from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
function WizardListCard({
type,
@@ -123,7 +124,7 @@ export function WizardProfileCard({
btnType: 'checkbox' | 'remove'
state: WizardState
dispatch: (action: WizardAction) => void
profile: AppBskyActorDefs.ProfileViewBasic
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const {currentAccount} = useSession()
+7 -1
View File
@@ -27,6 +27,7 @@ import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
function getBlackColor(t: ReturnType<typeof useTheme>) {
return select(t.name, {
@@ -78,7 +79,12 @@ export function VideoPostCard({
if (!AppBskyEmbedVideo.isView(embed)) return null
const author = post.author
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
const text = bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? post.record?.text
: ''
const likeCount = post?.likeCount ?? 0
const repostCount = post?.repostCount ?? 0
const {thumbnail} = embed
+5 -1
View File
@@ -29,6 +29,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
interface WhoCanReplyProps {
@@ -48,7 +49,10 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
* unexpectedly, we should check to make sure it's for sure the root URI.
*/
const rootUri =
AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
) && post.record.reply?.root
? post.record.reply.root.uri
: post.uri
const settings = React.useMemo(() => {
+4 -7
View File
@@ -1,10 +1,6 @@
import React, {useCallback} from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {
AppBskyActorDefs,
ChatBskyConvoDefs,
ModerationCause,
} from '@atproto/api'
import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -34,6 +30,7 @@ import {
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import * as bsky from '#/types/bsky'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
import {ReportDialog} from './ReportDialog'
@@ -49,7 +46,7 @@ let ConvoMenu = ({
style,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile: Shadow<bsky.profile.AnyProfileView>
control?: Menu.MenuControlProps
currentScreen: 'list' | 'conversation'
showMarkAsRead?: boolean
@@ -148,7 +145,7 @@ function MenuContent({
blockedByListControl,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile: Shadow<bsky.profile.AnyProfileView>
showMarkAsRead?: boolean
blockInfo: {
listBlocks: ModerationCause[]
+1 -1
View File
@@ -19,7 +19,7 @@ import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
export function MessageProfileButton({
profile,
}: {
profile: AppBskyActorDefs.ProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const t = useTheme()
@@ -1,6 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {ModerationDecision} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -14,6 +14,7 @@ import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function MessagesListBlockedFooter({
recipient: initialRecipient,
@@ -21,7 +22,7 @@ export function MessagesListBlockedFooter({
hasMessages,
moderation,
}: {
recipient: AppBskyActorDefs.ProfileViewBasic
recipient: bsky.profile.AnyProfileView
convoId: string
hasMessages: boolean
moderation: ModerationDecision
+6 -3
View File
@@ -17,6 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {isWeb} from '#/platform/detection'
import {Shadow} from '#/state/cache/profile-shadow'
import {isConvoActive, useConvo} from '#/state/messages/convo'
import {ConvoItem} from '#/state/messages/convo/types'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
@@ -31,7 +32,7 @@ export let MessagesListHeader = ({
profile,
moderation,
}: {
profile?: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation?: ModerationDecision
}): React.ReactNode => {
const t = useTheme()
@@ -138,7 +139,7 @@ function HeaderReady({
moderation,
blockInfo,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation: ModerationDecision
blockInfo: {
listBlocks: ModerationCause[]
@@ -157,8 +158,10 @@ function HeaderReady({
moderation.ui('displayName'),
)
// @ts-ignore findLast is polyfilled - esb
const latestMessageFromOther = convoState.items.findLast(
item => item.type === 'message' && item.message.sender.did === profile.did,
(item: ConvoItem) =>
item.type === 'message' && item.message.sender.did === profile.did,
)
const latestReportableMessage =
+9 -7
View File
@@ -1,6 +1,7 @@
import React, {memo, useMemo, useState} from 'react'
import {View} from 'react-native'
import {
$Typed,
AppBskyActorDefs,
ChatBskyConvoDefs,
ComAtprotoModerationCreateReport,
@@ -154,15 +155,16 @@ function SubmitStep({
mutationFn: async () => {
if (params.type === 'convoMessage') {
const {convoId, message} = params
const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
$type: 'chat.bsky.convo.defs#messageRef',
messageId: message.id,
convoId,
did: message.sender.did,
}
const report = {
reasonType: reportOption.reason,
subject: {
$type: 'chat.bsky.convo.defs#messageRef',
messageId: message.id,
convoId,
did: message.sender.did,
} satisfies ChatBskyConvoDefs.MessageRef,
subject,
reason: details,
} satisfies ComAtprotoModerationCreateReport.InputSchema
@@ -285,7 +287,7 @@ function DoneStep({
}: {
convoId: string
currentScreen: 'list' | 'conversation'
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
@@ -6,7 +6,7 @@ import React, {
useState,
} from 'react'
import {TextInput, View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -28,13 +28,14 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
type Item =
| {
type: 'profile'
key: string
enabled: boolean
profile: AppBskyActorDefs.ProfileView
profile: bsky.profile.AnyProfileView
}
| {
type: 'empty'
@@ -330,7 +331,7 @@ function ProfileCard({
onPress,
}: {
enabled: boolean
profile: AppBskyActorDefs.ProfileView
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
onPress: (did: string) => void
}) {
+2 -2
View File
@@ -1,6 +1,6 @@
import {AppBskyActorDefs} from '@atproto/api'
import * as bsky from '#/types/bsky'
export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) {
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
switch (profile.associated?.chat?.allowIncoming) {
case 'none':
return false
+2 -2
View File
@@ -1,5 +1,4 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -9,12 +8,13 @@ import {Shadow} from '#/state/cache/types'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useRequireAuth} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import * as bsky from '#/types/bsky'
export function useFollowMethods({
profile,
logContext,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile: Shadow<bsky.profile.AnyProfileView>
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
}) {
+7 -4
View File
@@ -6,6 +6,7 @@ import {
AppBskyFeedPost,
} from '@atproto/api'
import * as bsky from '#/types/bsky'
import {isPostInLanguage} from '../../locale/helpers'
import {FALLBACK_MARKER_POST} from './feed/home'
import {ReasonFeedSource} from './feed/types'
@@ -57,7 +58,9 @@ export class FeedViewPostsSlice {
}
this._feedPost = feedPost
this._reactKey = `slice-${post.uri}-${
feedPost.reason?.indexedAt || post.indexedAt
feedPost.reason && 'indexedAt' in feedPost.reason
? feedPost.reason.indexedAt
: post.indexedAt
}`
if (feedPost.post.uri === FALLBACK_MARKER_POST.post.uri) {
this.isFallbackMarker = true
@@ -65,7 +68,7 @@ export class FeedViewPostsSlice {
}
if (
!AppBskyFeedPost.isRecord(post.record) ||
!AppBskyFeedPost.validateRecord(post.record).success
!bsky.validate(post.record, AppBskyFeedPost.validateRecord)
) {
return
}
@@ -97,7 +100,7 @@ export class FeedViewPostsSlice {
if (
!AppBskyFeedDefs.isPostView(parent) ||
!AppBskyFeedPost.isRecord(parent.record) ||
!AppBskyFeedPost.validateRecord(parent.record).success
!bsky.validate(parent.record, AppBskyFeedPost.validateRecord)
) {
this.isOrphan = true
return
@@ -139,7 +142,7 @@ export class FeedViewPostsSlice {
if (
!AppBskyFeedDefs.isPostView(root) ||
!AppBskyFeedPost.isRecord(root.record) ||
!AppBskyFeedPost.validateRecord(root.record).success
!bsky.validate(root.record, AppBskyFeedPost.validateRecord)
) {
this.isOrphan = true
return
+1
View File
@@ -311,6 +311,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
)
// attach source info
for (const post of res.data.feed) {
// @ts-ignore
post.__source = this.sourceInfo
}
return res
-24
View File
@@ -1,24 +0,0 @@
import {
AppBskyFeedDefs,
AppBskyFeedPost,
ComAtprotoRepoStrongRef,
} from '@atproto/api'
/**
* HACK
* The server doesnt seem to be correctly giving the notFound view yet
* so I'm adding it manually for now
* -prf
*/
export function hackAddDeletedEmbed(post: AppBskyFeedDefs.PostView) {
const record = post.record as AppBskyFeedPost.Record
if (record.embed?.$type === 'app.bsky.embed.record' && !post.embed) {
post.embed = {
$type: 'app.bsky.embed.record#view',
record: {
$type: 'app.bsky.embed.record#viewNotFound',
uri: (record.embed.record as ComAtprotoRepoStrongRef.Main).uri,
},
}
}
}
+11 -10
View File
@@ -1,4 +1,5 @@
import {
$Typed,
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
@@ -74,7 +75,7 @@ export async function post(
}
const did = agent.assertDid
const writes: ComAtprotoRepoApplyWrites.Create[] = []
const writes: $Typed<ComAtprotoRepoApplyWrites.Create>[] = []
const uris: string[] = []
let now = new Date()
@@ -91,7 +92,7 @@ export async function post(
draft,
opts.onStateChange,
)
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
let labels: $Typed<ComAtprotoLabelDefs.SelfLabels> | undefined
if (draft.labels.length) {
labels = {
$type: 'com.atproto.label.defs#selfLabels',
@@ -230,11 +231,11 @@ async function resolveEmbed(
draft: PostDraft,
onStateChange: ((state: string) => void) | undefined,
): Promise<
| AppBskyEmbedImages.Main
| AppBskyEmbedVideo.Main
| AppBskyEmbedExternal.Main
| AppBskyEmbedRecord.Main
| AppBskyEmbedRecordWithMedia.Main
| $Typed<AppBskyEmbedImages.Main>
| $Typed<AppBskyEmbedVideo.Main>
| $Typed<AppBskyEmbedExternal.Main>
| $Typed<AppBskyEmbedRecord.Main>
| $Typed<AppBskyEmbedRecordWithMedia.Main>
| undefined
> {
if (draft.embed.quote) {
@@ -288,9 +289,9 @@ async function resolveMedia(
embedDraft: EmbedDraft,
onStateChange: ((state: string) => void) | undefined,
): Promise<
| AppBskyEmbedExternal.Main
| AppBskyEmbedImages.Main
| AppBskyEmbedVideo.Main
| $Typed<AppBskyEmbedExternal.Main>
| $Typed<AppBskyEmbedImages.Main>
| $Typed<AppBskyEmbedVideo.Main>
| undefined
> {
if (embedDraft.media?.type === 'images') {
-24
View File
@@ -1,24 +0,0 @@
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
} from '@atproto/api'
export function isEmbedByEmbedder(
embed: AppBskyFeedDefs.PostView['embed'],
did: string,
): boolean {
if (!embed) {
return false
}
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
return embed.record.author.did === did
}
if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record)
) {
return embed.record.record.author.did === did
}
return true
}
+13 -4
View File
@@ -1,7 +1,9 @@
import {
$Typed,
AppBskyActorDefs,
AppBskyGraphGetStarterPack,
BskyAgent,
ComAtprotoRepoApplyWrites,
Facet,
} from '@atproto/api'
import {msg} from '@lingui/macro'
@@ -13,6 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
export const createStarterPackList = async ({
name,
@@ -24,7 +27,7 @@ export const createStarterPackList = async ({
name: string
description?: string
descriptionFacets?: Facet[]
profiles: AppBskyActorDefs.ProfileViewBasic[]
profiles: bsky.profile.AnyProfileView[]
agent: BskyAgent
}): Promise<{uri: string; cid: string}> => {
if (profiles.length === 0) throw new Error('No profiles given')
@@ -68,8 +71,8 @@ export function useGenerateStarterPackMutation({
return useMutation<{uri: string; cid: string}, Error, void>({
mutationFn: async () => {
let profile: AppBskyActorDefs.ProfileViewBasic | undefined
let profiles: AppBskyActorDefs.ProfileViewBasic[] | undefined
let profile: AppBskyActorDefs.ProfileViewDetailed | undefined
let profiles: AppBskyActorDefs.ProfileView[] | undefined
await Promise.all([
(async () => {
@@ -136,7 +139,13 @@ export function useGenerateStarterPackMutation({
})
}
function createListItem({did, listUri}: {did: string; listUri: string}) {
function createListItem({
did,
listUri,
}: {
did: string
listUri: string
}): $Typed<ComAtprotoRepoApplyWrites.Create> {
return {
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.graph.listitem',
+3 -11
View File
@@ -1,17 +1,9 @@
import {AppBskyActorDefs} from '@atproto/api'
import * as bsky from '#/types/bsky'
export function isBlockedOrBlocking(
profile:
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
export function isBlockedOrBlocking(profile: bsky.profile.AnyProfileView) {
return profile.viewer?.blockedBy || profile.viewer?.blocking
}
export function isMuted(
profile:
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
export function isMuted(profile: bsky.profile.AnyProfileView) {
return profile.viewer?.muted || profile.viewer?.mutedByList
}
+9
View File
@@ -568,3 +568,12 @@ export function parseTenorGif(urlp: URL):
dimensions,
}
}
export function isTenorGifUri(url: URL | string) {
try {
return parseTenorGif(typeof url === 'string' ? new URL(url) : url).success
} catch {
// Invalid URL
return false
}
}
+4 -2
View File
@@ -1,4 +1,6 @@
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
import {AtUri} from '@atproto/api'
import * as bsky from '#/types/bsky'
export function createStarterPackLinkFromAndroidReferrer(
referrerQueryString: string,
@@ -79,7 +81,7 @@ export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
}
export function getStarterPackOgCard(
didOrStarterPack: AppBskyGraphDefs.StarterPackView | string,
didOrStarterPack: bsky.starterPack.AnyStarterPackView | string,
rkey?: string,
) {
if (typeof didOrStarterPack === 'string') {
+1 -1
View File
@@ -165,7 +165,7 @@ function InnerReady({
setHasScrolled,
}: {
moderation: ModerationDecision
recipient: Shadow<AppBskyActorDefs.ProfileViewBasic>
recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed>
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
}) {
@@ -1,7 +1,6 @@
import React, {useCallback, useMemo, useState} from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
ChatBskyConvoDefs,
moderateProfile,
@@ -44,6 +43,7 @@ import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export let ChatListItem = ({
convo,
@@ -78,7 +78,7 @@ function ChatListItemReady({
moderationOpts,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: AppBskyActorDefs.ProfileViewBasic
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const t = useTheme()
@@ -30,6 +30,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function useMessageEmbed() {
const route =
@@ -113,8 +114,10 @@ export function MessageInputEmbed({
const {rt, record} = useMemo(() => {
if (
post &&
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
) {
return {
rt: new RichTextAPI({
@@ -10,7 +10,12 @@ import Animated, {
} from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
import {
$Typed,
AppBskyEmbedRecord,
AppBskyRichtextFacet,
RichText,
} from '@atproto/api'
import {clamp} from '#/lib/numbers'
import {ScrollProvider} from '#/lib/ScrollContext'
@@ -297,7 +302,7 @@ export function MessagesList({
// we want to remove the post link from the text, re-trim, then detect facets
rt.detectFacetsWithoutResolution()
let embed: AppBskyEmbedRecord.Main | undefined
let embed: $Typed<AppBskyEmbedRecord.Main> | undefined
if (embedUri) {
try {
+21 -10
View File
@@ -1,6 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {
AppBskyActorProfile,
AppBskyGraphDefs,
AppBskyGraphStarterpack,
Un$Typed,
} from '@atproto/api'
import {SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
import {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro'
@@ -44,6 +49,7 @@ import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function StepFinished() {
const {_} = useLingui()
@@ -141,29 +147,29 @@ export function StepFinished() {
: undefined
await agent.upsertProfile(async existing => {
existing = existing ?? {}
let next: Un$Typed<AppBskyActorProfile.Record> = existing ?? {}
if (blobPromise) {
const res = await blobPromise
if (res.data.blob) {
existing.avatar = res.data.blob
next.avatar = res.data.blob
}
}
if (starterPack) {
existing.joinedViaStarterPack = {
next.joinedViaStarterPack = {
uri: starterPack.uri,
cid: starterPack.cid,
}
}
existing.displayName = ''
next.displayName = ''
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
existing.createdAt = new Date().toISOString()
return existing
next.createdAt = new Date().toISOString()
return next
})
logEvent('onboarding:finished:avatarResult', {
@@ -205,9 +211,14 @@ export function StepFinished() {
onboardDispatch({type: 'finish'})
logEvent('onboarding:finished:nextPressed', {
usedStarterPack: Boolean(starterPack),
starterPackName: AppBskyGraphStarterpack.isRecord(starterPack?.record)
? starterPack.record.name
: undefined,
starterPackName:
starterPack &&
bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
starterPack.record,
AppBskyGraphStarterpack.isRecord,
)
? starterPack.record.name
: undefined,
starterPackCreator: starterPack?.creator.did,
starterPackUri: starterPack?.uri,
profilesFollowed: listItems?.length ?? 0,
+10 -7
View File
@@ -1,7 +1,9 @@
import {
$Typed,
AppBskyGraphFollow,
AppBskyGraphGetFollows,
BskyAgent,
ComAtprotoRepoApplyWrites,
} from '@atproto/api'
import {TID} from '@atproto/common-web'
import chunk from 'lodash.chunk'
@@ -15,7 +17,7 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
throw new Error(`bulkWriteFollows failed: no session`)
}
const followRecords: AppBskyGraphFollow.Record[] = dids.map(did => {
const followRecords: $Typed<AppBskyGraphFollow.Record>[] = dids.map(did => {
return {
$type: 'app.bsky.graph.follow',
subject: did,
@@ -23,12 +25,13 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
}
})
const followWrites = followRecords.map(r => ({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.graph.follow',
rkey: TID.nextStr(),
value: r,
}))
const followWrites: $Typed<ComAtprotoRepoApplyWrites.Create>[] =
followRecords.map(r => ({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.graph.follow',
rkey: TID.nextStr(),
value: r,
}))
const chunks = chunk(followWrites, 50)
for (const chunk of chunks) {
+1 -1
View File
@@ -21,7 +21,7 @@ function renderItem({
item,
index,
}: {
item: AppBskyActorDefs.ProfileViewBasic
item: AppBskyActorDefs.ProfileView
index: number
}) {
return (
+13 -6
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {$Typed, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -12,6 +12,7 @@ import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function PwiOptOut() {
const t = useTheme()
@@ -33,7 +34,10 @@ export function PwiOptOut() {
profile,
updates: existing => {
// create labels attr if needed
existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate(
existing.labels,
ComAtprotoLabelDefs.validateSelfLabels,
)
? existing.labels
: {
$type: 'com.atproto.label.defs#selfLabels',
@@ -41,23 +45,26 @@ export function PwiOptOut() {
}
// toggle the label
const hasLabel = existing.labels.values.some(
const hasLabel = labels.values.some(
l => l.val === '!no-unauthenticated',
)
if (hasLabel) {
wasAdded = false
existing.labels.values = existing.labels.values.filter(
labels.values = labels.values.filter(
l => l.val !== '!no-unauthenticated',
)
} else {
wasAdded = true
existing.labels.values.push({val: '!no-unauthenticated'})
labels.values.push({val: '!no-unauthenticated'})
}
// delete if no longer needed
if (existing.labels.values.length === 0) {
if (labels.values.length === 0) {
delete existing.labels
} else {
existing.labels = labels
}
return existing
},
checkCommitted: res => {
+5 -1
View File
@@ -26,6 +26,7 @@ import {Divider} from '#/components/Divider'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function Signup({onPressBack}: {onPressBack: () => void}) {
const {_} = useLingui()
@@ -95,7 +96,10 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
scrollable>
<View testID="createAccount" style={a.flex_1}>
{showStarterPackCard &&
AppBskyGraphStarterpack.isRecord(starterPack.record) ? (
bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
starterPack.record,
AppBskyGraphStarterpack.isRecord,
) ? (
<Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}>
<LinearGradientBackground
style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}>
@@ -38,6 +38,7 @@ import {Default as ProfileCard} from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
@@ -85,7 +86,12 @@ export function LandingScreen({
}
// Just for types, this cannot be hit
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
starterPack.record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
@@ -66,6 +66,7 @@ import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
import {ShareDialog} from '#/components/StarterPack/ShareDialog'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
type StarterPackScreeProps = NativeStackScreenProps<
CommonNavigatorParams,
@@ -387,7 +388,12 @@ function Header({
})
}
if (!AppBskyGraphStarterpack.isRecord(record)) {
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
+8 -9
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {
AppBskyActorDefs,
AppBskyGraphDefs,
AppBskyGraphStarterpack,
} from '@atproto/api'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import * as bsky from '#/types/bsky'
const steps = ['Details', 'Profiles', 'Feeds'] as const
type Step = (typeof steps)[number]
@@ -20,7 +17,7 @@ type Action =
| {type: 'SetCanNext'; canNext: boolean}
| {type: 'SetName'; name: string}
| {type: 'SetDescription'; description: string}
| {type: 'AddProfile'; profile: AppBskyActorDefs.ProfileViewBasic}
| {type: 'AddProfile'; profile: bsky.profile.AnyProfileView}
| {type: 'RemoveProfile'; profileDid: string}
| {type: 'AddFeed'; feed: GeneratorView}
| {type: 'RemoveFeed'; feedUri: string}
@@ -32,7 +29,7 @@ interface State {
currentStep: Step
name?: string
description?: string
profiles: AppBskyActorDefs.ProfileViewBasic[]
profiles: bsky.profile.AnyProfileView[]
feeds: GeneratorView[]
processing: boolean
error?: string
@@ -113,7 +110,6 @@ function reducer(state: State, action: Action): State {
return updatedState
}
// TODO supply the initial state to this component
export function Provider({
starterPack,
listItems,
@@ -126,7 +122,10 @@ export function Provider({
const {currentAccount} = useSession()
const createInitialState = (): State => {
if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) {
if (
starterPack &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
) {
return {
canNext: true,
currentStep: 'Details',
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
return item?.did ?? ''
@@ -50,7 +51,7 @@ export function StepProfiles({
const renderItem = ({
item,
}: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
}: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
return (
<WizardProfileCard
profile={item}
+5 -4
View File
@@ -54,6 +54,7 @@ import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {Provider} from './State'
export function Wizard({
@@ -141,7 +142,7 @@ function WizardInner({
}: {
currentStarterPack?: AppBskyGraphDefs.StarterPackView
currentListItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
}) {
const navigation = useNavigation<NavigationProp>()
@@ -363,7 +364,7 @@ function Footer({
onNext: () => void
nextBtnText: string
moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const t = useTheme()
@@ -577,10 +578,10 @@ function Footer({
)
}
function getName(item: AppBskyActorDefs.ProfileViewBasic | GeneratorView) {
function getName(item: bsky.profile.AnyProfileView | GeneratorView) {
if (typeof item.displayName === 'string') {
return enforceLen(sanitizeDisplayName(item.displayName), 28, true)
} else if (typeof item.handle === 'string') {
} else if ('handle' in item && typeof item.handle === 'string') {
return enforceLen(sanitizeHandle(item.handle), 28, true)
}
return ''
+7 -1
View File
@@ -90,6 +90,7 @@ import {ListFooter} from '#/components/Lists'
import * as Hider from '#/components/moderation/Hider'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber'
function createThreeVideoPlayers(
@@ -694,7 +695,12 @@ function Overlay({
)
const rkey = new AtUri(post.uri).rkey
const record = AppBskyFeedPost.isRecord(post.record) ? post.record : undefined
const record = bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? post.record
: undefined
const richText = new RichTextAPI({
text: record?.text || '',
facets: record?.facets,
+7 -6
View File
@@ -1,9 +1,9 @@
import {useEffect, useMemo, useState} from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates'
import * as bsky from '#/types/bsky'
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search'
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '../queries/known-followers'
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members'
@@ -20,6 +20,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '../queries/suggested-follows'
import {castAsShadow, Shadow} from './types'
export type {Shadow} from './types'
export interface ProfileShadow {
@@ -29,13 +30,13 @@ export interface ProfileShadow {
}
const shadows: WeakMap<
AppBskyActorDefs.ProfileView,
bsky.profile.AnyProfileView,
Partial<ProfileShadow>
> = new WeakMap()
const emitter = new EventEmitter()
export function useProfileShadow<
TProfileView extends AppBskyActorDefs.ProfileView,
TProfileView extends bsky.profile.AnyProfileView,
>(profile: TProfileView): Shadow<TProfileView> {
const [shadow, setShadow] = useState(() => shadows.get(profile))
const [prevPost, setPrevPost] = useState(profile)
@@ -68,7 +69,7 @@ export function useProfileShadow<
* This is useful for when the profile is not guaranteed to be loaded yet.
*/
export function useMaybeProfileShadow<
TProfileView extends AppBskyActorDefs.ProfileView,
TProfileView extends bsky.profile.AnyProfileView,
>(profile?: TProfileView): Shadow<TProfileView> | undefined {
const [shadow, setShadow] = useState(() =>
profile ? shadows.get(profile) : undefined,
@@ -115,7 +116,7 @@ export function updateProfileShadow(
})
}
function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
profile: TProfileView,
shadow: Partial<ProfileShadow>,
): Shadow<TProfileView> {
@@ -137,7 +138,7 @@ function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
function* findProfilesInCache(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<bsky.profile.AnyProfileView, void> {
yield* findAllProfilesInListMembersQueryData(queryClient, did)
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
+1
View File
@@ -69,6 +69,7 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
while (!cancelled) {
const threads = persisted.get('mutedThreads')
// @ts-ignore findLast is polyfilled - esb
const root = threads.findLast(uri => uri.includes(currentAccount.did))
if (!root) break
+13 -10
View File
@@ -1,6 +1,6 @@
import {
AppBskyActorDefs,
BskyAgent,
ChatBskyActorDefs,
ChatBskyConvoDefs,
ChatBskyConvoGetLog,
ChatBskyConvoSendMessage,
@@ -80,8 +80,8 @@ export class Convo {
convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
snapshot: ConvoState | undefined
constructor(params: ConvoParams) {
@@ -463,7 +463,7 @@ export class Convo {
throw new Error('Convo: could not find recipients in convo')
}
const userIsDisabled = this.sender.chatDisabled as boolean
const userIsDisabled = Boolean(this.sender.chatDisabled)
if (userIsDisabled) {
this.dispatch({event: ConvoDispatchEvent.Disable})
@@ -529,8 +529,8 @@ export class Convo {
private pendingFetchConvo:
| Promise<{
convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[]
}>
| undefined
async fetchConvo() {
@@ -538,8 +538,8 @@ export class Convo {
this.pendingFetchConvo = new Promise<{
convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[]
}>(async (resolve, reject) => {
try {
const response = await networkRetry(2, () => {
@@ -704,7 +704,7 @@ export class Convo {
* If there's a rev, we should handle it. If there's not a rev, we don't
* know what it is.
*/
if (typeof ev.rev === 'string') {
if ('rev' in ev && typeof ev.rev === 'string') {
const isUninitialized = !this.latestRev
const isNewEvent = this.latestRev && ev.rev > this.latestRev
@@ -1049,7 +1049,10 @@ export class Convo {
* `getItems` is only run in "active" status states, where
* `this.sender` is defined
*/
sender: this.sender!,
sender: {
$type: 'chat.bsky.convo.defs#messageViewSender',
did: this.sender!.did,
},
},
nextMessage: null,
prevMessage: null,
+13 -13
View File
@@ -1,6 +1,6 @@
import {
AppBskyActorDefs,
BskyAgent,
ChatBskyActorDefs,
ChatBskyConvoDefs,
ChatBskyConvoSendMessage,
} from '@atproto/api'
@@ -147,8 +147,8 @@ export type ConvoStateUninitialized = {
items: []
convo: ChatBskyConvoDefs.ConvoView | undefined
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
isFetchingHistory: false
deleteMessage: undefined
sendMessage: undefined
@@ -159,8 +159,8 @@ export type ConvoStateInitializing = {
items: []
convo: ChatBskyConvoDefs.ConvoView | undefined
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
isFetchingHistory: boolean
deleteMessage: undefined
sendMessage: undefined
@@ -171,8 +171,8 @@ export type ConvoStateReady = {
items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic
recipients: ChatBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean
deleteMessage: DeleteMessage
sendMessage: SendMessage
@@ -183,8 +183,8 @@ export type ConvoStateBackgrounded = {
items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic
recipients: ChatBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean
deleteMessage: DeleteMessage
sendMessage: SendMessage
@@ -195,8 +195,8 @@ export type ConvoStateSuspended = {
items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic
recipients: ChatBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean
deleteMessage: DeleteMessage
sendMessage: SendMessage
@@ -219,8 +219,8 @@ export type ConvoStateDisabled = {
items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView
error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
sender: ChatBskyActorDefs.ProfileViewBasic
recipients: ChatBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean
deleteMessage: DeleteMessage
sendMessage: SendMessage
+2 -5
View File
@@ -65,10 +65,7 @@ export class MessagesEventBus {
const handle = (event: MessagesEventBusEvent) => {
if (event.type === 'logs' && options.convoId) {
const filteredLogs = event.logs.filter(log => {
if (
typeof log.convoId === 'string' &&
log.convoId === options.convoId
) {
if ('convoId' in log && log.convoId === options.convoId) {
return log.convoId === options.convoId
}
return false
@@ -355,7 +352,7 @@ export class MessagesEventBus {
* If there's a rev, we should handle it. If there's not a rev, we don't
* know what it is.
*/
if (typeof ev.rev === 'string') {
if ('rev' in ev && typeof ev.rev === 'string') {
/*
* We only care about new events
*/
+7 -2
View File
@@ -1,11 +1,14 @@
import {Image as RNImage} from 'react-native-image-crop-picker'
import {
$Typed,
AppBskyGraphDefs,
AppBskyGraphGetList,
AppBskyGraphList,
AtUri,
BskyAgent,
ComAtprotoRepoApplyWrites,
Facet,
Un$Typed,
} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
@@ -68,7 +71,7 @@ export function useListCreateMutation() {
) {
throw new Error('Invalid list purpose: must be curatelist or modlist')
}
const record: AppBskyGraphList.Record = {
const record: Un$Typed<AppBskyGraphList.Record> = {
purpose,
name,
description,
@@ -212,7 +215,9 @@ export function useListDeleteMutation() {
}
// batch delete the list and listitem records
const createDel = (uri: string) => {
const createDel = (
uri: string,
): $Typed<ComAtprotoRepoApplyWrites.Delete> => {
const urip = new AtUri(uri)
return {
$type: 'com.atproto.repo.applyWrites#delete',
@@ -69,12 +69,10 @@ export function useDeleteActorDeclaration() {
return useMutation({
mutationFn: async () => {
if (!currentAccount) throw new Error('Not signed in')
// TODO(sam): remove validate: false once PDSes have the new lexicon
const result = await agent.api.com.atproto.repo.deleteRecord({
repo: currentAccount.did,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
validate: false,
})
return result
},
@@ -101,7 +101,7 @@ export function ListConvosProviderInner({
events => {
if (events.type !== 'logs') return
events.logs.forEach(log => {
for (const log of events.logs) {
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
@@ -110,30 +110,40 @@ export function ListConvosProviderInner({
)
} else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) =>
optimisticUpdate(log.convoId, old, convo =>
log.message.id === convo.lastMessage?.id
? {
...convo,
rev: log.rev,
lastMessage: log.message,
}
: convo,
),
optimisticUpdate(log.convoId, old, convo => {
if (
(ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
ChatBskyConvoDefs.isMessageView(log.message)) &&
(ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) ||
ChatBskyConvoDefs.isMessageView(convo.lastMessage))
) {
return log.message.id === convo.lastMessage.id
? {
...convo,
rev: log.rev,
lastMessage: log.message,
}
: convo
} else {
return convo
}
}),
)
} else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
// Store in a new var to avoid TS errors due to closures.
const logRef: ChatBskyConvoDefs.LogCreateMessage = log
queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
if (!old) return old
function updateConvo(convo: ChatBskyConvoDefs.ConvoView) {
if (!ChatBskyConvoDefs.isLogCreateMessage(log)) return convo
let unreadCount = convo.unreadCount
if (convo.id !== currentConvoId) {
if (
ChatBskyConvoDefs.isMessageView(log.message) ||
ChatBskyConvoDefs.isDeletedMessageView(log.message)
ChatBskyConvoDefs.isMessageView(logRef.message) ||
ChatBskyConvoDefs.isDeletedMessageView(logRef.message)
) {
if (log.message.sender.did !== currentAccount?.did) {
if (logRef.message.sender.did !== currentAccount?.did) {
unreadCount++
}
}
@@ -143,8 +153,8 @@ export function ListConvosProviderInner({
return {
...convo,
rev: log.rev,
lastMessage: log.message,
rev: logRef.rev,
lastMessage: logRef.message,
unreadCount,
}
}
@@ -152,10 +162,10 @@ export function ListConvosProviderInner({
function filterConvoFromPage(
convo: ChatBskyConvoDefs.ConvoView[],
) {
return convo.filter(c => c.id !== log.convoId)
return convo.filter(c => c.id !== logRef.convoId)
}
const existingConvo = getConvoFromQueryData(log.convoId, old)
const existingConvo = getConvoFromQueryData(logRef.convoId, old)
if (existingConvo) {
return {
@@ -186,7 +196,7 @@ export function ListConvosProviderInner({
}
})
}
})
}
},
{
// get events for all chats
+11 -7
View File
@@ -295,9 +295,11 @@ export function* findAllPostsInQueryData(
}
}
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost!)
if (AppBskyFeedDefs.isPostView(item.subject)) {
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost!)
}
}
}
}
@@ -307,7 +309,7 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: [RQKEY_ROOT],
})
@@ -323,9 +325,11 @@ export function* findAllProfilesInQueryData(
) {
yield item.subject.author
}
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
if (AppBskyFeedDefs.isPostView(item.subject)) {
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
}
}
}
}
+11 -7
View File
@@ -14,6 +14,7 @@ import {QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
import * as bsky from '#/types/bsky'
import {precacheProfile} from '../profile'
import {FeedNotification, FeedPage, NotificationType} from './types'
@@ -205,12 +206,9 @@ async function fetchSubjects(
),
)
const postsMap = new Map<string, AppBskyFeedDefs.PostView>()
const packsMap = new Map<string, AppBskyGraphDefs.StarterPackView>()
const packsMap = new Map<string, AppBskyGraphDefs.StarterPackViewBasic>()
for (const post of postsChunks.flat()) {
if (
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
if (AppBskyFeedPost.isRecord(post.record)) {
postsMap.set(post.uri, post)
}
}
@@ -255,8 +253,14 @@ function getSubjectUri(
return notif.uri
} else if (type === 'post-like' || type === 'repost') {
if (
AppBskyFeedRepost.isRecord(notif.record) ||
AppBskyFeedLike.isRecord(notif.record)
bsky.dangerousIsType<AppBskyFeedRepost.Record>(
notif.record,
AppBskyFeedRepost.isRecord,
) ||
bsky.dangerousIsType<AppBskyFeedLike.Record>(
notif.record,
AppBskyFeedLike.isRecord,
)
) {
return typeof notif.record.subject?.uri === 'string'
? notif.record.subject?.uri
+1 -1
View File
@@ -547,7 +547,7 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, undefined> {
): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
+1 -1
View File
@@ -70,7 +70,7 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>
>({
+7 -4
View File
@@ -18,6 +18,7 @@ import {
findAllProfilesInQueryData as findAllProfilesInSearchQueryData,
} from '#/state/queries/search-posts'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
import {
findAllPostsInQueryData as findAllPostsInNotifsQueryData,
findAllProfilesInQueryData as findAllProfilesInNotifsQueryData,
@@ -332,8 +333,10 @@ function responseToThreadNodes(
): ThreadNode {
if (
AppBskyFeedDefs.isThreadViewPost(node) &&
AppBskyFeedPost.isRecord(node.post.record) &&
AppBskyFeedPost.validateRecord(node.post.record).success
bsky.dangerousIsType<AppBskyFeedPost.Record>(
node.post.record,
AppBskyFeedPost.isRecord,
)
) {
const post = node.post
// These should normally be present. They're missing only for
@@ -364,7 +367,7 @@ function responseToThreadNodes(
depth,
isHighlightedPost: depth === 0,
hasMore:
direction === 'down' && !node.replies?.length && !!node.replyCount,
direction === 'down' && !node.replies?.length && !!post.replyCount,
isSelfThread: false, // populated `annotateSelfThread`
hasMoreSelfThread: false, // populated in `annotateSelfThread`
},
@@ -497,7 +500,7 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
queryKey: [RQKEY_ROOT],
})
+5 -1
View File
@@ -21,6 +21,7 @@ import {
POSTGATE_COLLECTION,
} from '#/state/queries/postgate/util'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
export async function getPostgateRecord({
agent,
@@ -60,7 +61,10 @@ export async function getPostgateRecord({
}),
)
if (data.value && AppBskyFeedPostgate.isRecord(data.value)) {
if (
data.value &&
bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)
) {
return data.value
} else {
return undefined
+8 -3
View File
@@ -1,4 +1,5 @@
import {
$Typed,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
@@ -45,8 +46,12 @@ export function mergePostgateRecords(
})
}
export function createEmbedViewDetachedRecord({uri}: {uri: string}) {
const record: AppBskyEmbedRecord.ViewDetached = {
export function createEmbedViewDetachedRecord({
uri,
}: {
uri: string
}): $Typed<AppBskyEmbedRecord.View> {
const record: $Typed<AppBskyEmbedRecord.ViewDetached> = {
$type: 'app.bsky.embed.record#viewDetached',
uri,
detached: true,
@@ -95,7 +100,7 @@ export function createMaybeDetachedQuoteEmbed({
export function createEmbedViewRecordFromPost(
post: AppBskyFeedDefs.PostView,
): AppBskyEmbedRecord.ViewRecord {
): $Typed<AppBskyEmbedRecord.ViewRecord> {
return {
$type: 'app.bsky.embed.record#viewRecord',
uri: post.uri,
+37 -37
View File
@@ -8,6 +8,7 @@ import {
AtUri,
BskyAgent,
ComAtprotoRepoUploadBlob,
Un$Typed,
} from '@atproto/api'
import {
keepPreviousData,
@@ -24,7 +25,12 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {
unstableCacheProfileView,
useUnstableProfileViewCache,
} from '#/state/queries/unstable-profile-cache'
import * as userActionHistory from '#/state/userActionHistory'
import * as bsky from '#/types/bsky'
import {updateProfileShadow} from '../cache/profile-shadow'
import {useAgent, useSession} from '../session'
import {
@@ -35,6 +41,12 @@ import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-conversations'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
export * from '#/state/queries/unstable-profile-cache'
/**
* @deprecated use {@link unstableCacheProfileView} instead
*/
export const precacheProfile = unstableCacheProfileView
const RQKEY_ROOT = 'profile'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
@@ -44,12 +56,6 @@ export const profilesQueryKey = (handles: string[]) => [
handles,
]
const profileBasicQueryKeyRoot = 'profileBasic'
export const profileBasicQueryKey = (didOrHandle: string) => [
profileBasicQueryKeyRoot,
didOrHandle,
]
export function useProfileQuery({
did,
staleTime = STALE.SECONDS.FIFTEEN,
@@ -57,8 +63,8 @@ export function useProfileQuery({
did: string | undefined
staleTime?: number
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
@@ -73,10 +79,7 @@ export function useProfileQuery({
},
placeholderData: () => {
if (!did) return
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
profileBasicQueryKey(did),
)
return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed
},
enabled: !!did,
})
@@ -121,10 +124,12 @@ export function usePrefetchProfileQuery() {
}
interface ProfileUpdateParams {
profile: AppBskyActorDefs.ProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
updates:
| AppBskyActorProfile.Record
| ((existing: AppBskyActorProfile.Record) => AppBskyActorProfile.Record)
| Un$Typed<AppBskyActorProfile.Record>
| ((
existing: Un$Typed<AppBskyActorProfile.Record>,
) => Un$Typed<AppBskyActorProfile.Record>)
newUserAvatar?: RNImage | undefined | null
newUserBanner?: RNImage | undefined | null
checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean
@@ -161,29 +166,29 @@ export function useProfileUpdateMutation() {
)
}
await agent.upsertProfile(async existing => {
existing = existing || {}
let next: Un$Typed<AppBskyActorProfile.Record> = existing || {}
if (typeof updates === 'function') {
existing = updates(existing)
next = updates(next)
} else {
existing.displayName = updates.displayName
existing.description = updates.description
next.displayName = updates.displayName
next.description = updates.description
if ('pinnedPost' in updates) {
existing.pinnedPost = updates.pinnedPost
next.pinnedPost = updates.pinnedPost
}
}
if (newUserAvatarPromise) {
const res = await newUserAvatarPromise
existing.avatar = res.data.blob
next.avatar = res.data.blob
} else if (newUserAvatar === null) {
existing.avatar = undefined
next.avatar = undefined
}
if (newUserBannerPromise) {
const res = await newUserBannerPromise
existing.banner = res.data.blob
next.banner = res.data.blob
} else if (newUserBanner === null) {
existing.banner = undefined
next.banner = undefined
}
return existing
return next
})
await whenAppViewReady(
agent,
@@ -228,7 +233,7 @@ export function useProfileUpdateMutation() {
}
export function useProfileFollowMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
profile: Shadow<bsky.profile.AnyProfileView>,
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:follow']['logContext'],
) {
@@ -302,7 +307,7 @@ export function useProfileFollowMutationQueue(
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
profile: Shadow<bsky.profile.AnyProfileView>,
) {
const {currentAccount} = useSession()
const agent = useAgent()
@@ -321,7 +326,10 @@ function useProfileFollowMutation(
didBecomeMutual: profile.viewer
? Boolean(profile.viewer.followedBy)
: undefined,
followeeClout: toClout(profile.followersCount),
followeeClout:
'followersCount' in profile
? toClout(profile.followersCount)
: undefined,
followerClout: toClout(ownProfile?.followersCount),
})
return await agent.follow(did)
@@ -342,7 +350,7 @@ function useProfileUnfollowMutation(
}
export function useProfileMuteMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
profile: Shadow<bsky.profile.AnyProfileView>,
) {
const queryClient = useQueryClient()
const did = profile.did
@@ -417,7 +425,7 @@ function useProfileUnmuteMutation() {
}
export function useProfileBlockMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
profile: Shadow<bsky.profile.AnyProfileView>,
) {
const queryClient = useQueryClient()
const did = profile.did
@@ -513,14 +521,6 @@ function useProfileUnblockMutation() {
})
}
export function precacheProfile(
queryClient: QueryClient,
profile: AppBskyActorDefs.ProfileViewBasic,
) {
queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
}
async function whenAppViewReady(
agent: BskyAgent,
actor: string,
+5 -14
View File
@@ -1,14 +1,9 @@
import {AppBskyActorDefs, AtUri} from '@atproto/api'
import {
QueryClient,
useQuery,
useQueryClient,
UseQueryResult,
} from '@tanstack/react-query'
import {AtUri} from '@atproto/api'
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
import {useUnstableProfileViewCache} from './profile'
const RQKEY_ROOT = 'resolved-did'
export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
@@ -28,8 +23,8 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const queryClient = useQueryClient()
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery<string, Error>({
staleTime: STALE.HOURS.ONE,
@@ -45,11 +40,7 @@ export function useResolveDidQuery(didOrHandle: string | undefined) {
initialData: () => {
// Return undefined if no did or handle
if (!didOrHandle) return
const profile =
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
RQKEY_PROFILE_BASIC(didOrHandle),
)
const profile = getUnstableProfile(didOrHandle)
return profile?.did
},
enabled: !!didOrHandle,
+1 -1
View File
@@ -174,7 +174,7 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, undefined> {
): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
>({
+1
View File
@@ -19,6 +19,7 @@ export function useServiceConfigQuery() {
const {data} = await agent.api.app.bsky.unspecced.getConfig()
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
// @ts-expect-error not included in types atm
topicsEnabled: Boolean(data.topicsEnabled),
}
} catch (e) {
+7 -4
View File
@@ -1,5 +1,4 @@
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyGraphGetStarterPack,
@@ -29,6 +28,7 @@ import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-pa
import {STALE} from '#/state/queries/index'
import {invalidateListMembersQuery} from '#/state/queries/list-members'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
const RQKEY_ROOT = 'starter-pack'
const RQKEY = ({
@@ -93,7 +93,7 @@ export async function invalidateStarterPack({
interface UseCreateStarterPackMutationParams {
name: string
description?: string
profiles: AppBskyActorDefs.ProfileViewBasic[]
profiles: bsky.profile.AnyProfileView[]
feeds?: AppBskyFeedDefs.GeneratorView[]
}
@@ -131,7 +131,7 @@ export function useCreateStarterPackMutation({
return await agent.app.bsky.graph.starterpack.create(
{
repo: agent.session?.did,
repo: agent.assertDid,
},
{
name,
@@ -366,7 +366,10 @@ export async function precacheStarterPack(
let starterPackView: AppBskyGraphDefs.StarterPackView | undefined
if (AppBskyGraphDefs.isStarterPackView(starterPack)) {
starterPackView = starterPack
} else if (AppBskyGraphDefs.isStarterPackViewBasic(starterPack)) {
} else if (
AppBskyGraphDefs.isStarterPackViewBasic(starterPack) &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
) {
const listView: AppBskyGraphDefs.ListViewBasic = {
uri: starterPack.record.list,
// This will be populated once the data from server is fetched
+5 -1
View File
@@ -20,6 +20,7 @@ import {
} from '#/state/queries/threadgate/util'
import {useAgent} from '#/state/session'
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
import * as bsky from '#/types/bsky'
export * from '#/state/queries/threadgate/types'
export * from '#/state/queries/threadgate/util'
@@ -138,7 +139,10 @@ export async function getThreadgateRecord({
}),
)
if (data.value && AppBskyFeedThreadgate.isRecord(data.value)) {
if (
data.value &&
bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord)
) {
return data.value
} else {
return null
+1 -1
View File
@@ -4,4 +4,4 @@ export type ThreadgateAllowUISetting =
| {type: 'mention'}
| {type: 'following'}
| {type: 'followers'}
| {type: 'list'; list: unknown}
| {type: 'list'; list: string}
+9 -12
View File
@@ -1,14 +1,15 @@
import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
import * as bsky from '#/types/bsky'
export function threadgateViewToAllowUISetting(
threadgateView: AppBskyFeedDefs.ThreadgateView | undefined,
): ThreadgateAllowUISetting[] {
// Validate the record for clarity, since backwards compat code is a little confusing
const threadgate =
threadgateView &&
AppBskyFeedThreadgate.isRecord(threadgateView.record) &&
AppBskyFeedThreadgate.validateRecord(threadgateView.record).success
bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord)
? threadgateView.record
: undefined
return threadgateRecordToAllowUISetting(threadgate)
@@ -39,14 +40,14 @@ export function threadgateRecordToAllowUISetting(
const settings: ThreadgateAllowUISetting[] = threadgate.allow
.map(allow => {
let setting: ThreadgateAllowUISetting | undefined
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
if (AppBskyFeedThreadgate.isMentionRule(allow)) {
setting = {type: 'mention'}
} else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
} else if (AppBskyFeedThreadgate.isFollowingRule(allow)) {
setting = {type: 'following'}
} else if (allow.$type === 'app.bsky.feed.threadgate#followerRule') {
setting = {type: 'followers'}
} else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
} else if (AppBskyFeedThreadgate.isListRule(allow)) {
setting = {type: 'list', list: allow.list}
} else if (AppBskyFeedThreadgate.isFollowerRule(allow)) {
setting = {type: 'followers'}
}
return setting
})
@@ -69,11 +70,7 @@ export function threadgateAllowUISettingToAllowRecordValue(
return undefined
}
let allow: (
| AppBskyFeedThreadgate.MentionRule
| AppBskyFeedThreadgate.FollowingRule
| AppBskyFeedThreadgate.ListRule
)[] = []
let allow: Exclude<AppBskyFeedThreadgate.Record['allow'], undefined> = []
if (!threadgate.find(v => v.type === 'nobody')) {
for (const rule of threadgate) {
@@ -0,0 +1,51 @@
import {useCallback} from 'react'
import {QueryClient, useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky'
const unstableProfileViewCacheQueryKeyRoot = 'unstableProfileViewCache'
export const unstableProfileViewCacheQueryKey = (didOrHandle: string) => [
unstableProfileViewCacheQueryKeyRoot,
didOrHandle,
]
/**
* Used as a rough cache of profile views to make loading snappier. This method
* accepts and stores any profile view type by both handle and DID.
*
* Access the cache via {@link useUnstableProfileViewCache}.
*/
export function unstableCacheProfileView(
queryClient: QueryClient,
profile: bsky.profile.AnyProfileView,
) {
queryClient.setQueryData(
unstableProfileViewCacheQueryKey(profile.handle),
profile,
)
queryClient.setQueryData(
unstableProfileViewCacheQueryKey(profile.did),
profile,
)
}
/**
* Hook to access the unstable profile view cache. This cache can return ANY
* profile view type, so if the object shape is important, you need to use the
* identity validators shipped in the atproto SDK e.g.
* `AppBskyActorDefs.isValidProfileViewBasic` to confirm before using.
*
* To cache a profile, use {@link unstableCacheProfileView}.
*/
export function useUnstableProfileViewCache() {
const qc = useQueryClient()
const getUnstableProfile = useCallback(
(didOrHandle: string) => {
return qc.getQueryData<bsky.profile.AnyProfileView>(
unstableProfileViewCacheQueryKey(didOrHandle),
)
},
[qc],
)
return {getUnstableProfile}
}
+11 -2
View File
@@ -8,6 +8,8 @@ import {
} from '@atproto/api'
import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
import * as bsky from '#/types/bsky'
export async function truncateAndInvalidate<T = any>(
queryClient: QueryClient,
queryKey: QueryKey,
@@ -44,7 +46,9 @@ export function didOrHandleUriMatches(
export function getEmbeddedPost(
v: unknown,
): AppBskyEmbedRecord.ViewRecord | undefined {
if (AppBskyEmbedRecord.isView(v)) {
if (
bsky.dangerousIsType<AppBskyEmbedRecord.View>(v, AppBskyEmbedRecord.isView)
) {
if (
AppBskyEmbedRecord.isViewRecord(v.record) &&
AppBskyFeedPost.isRecord(v.record.value)
@@ -52,7 +56,12 @@ export function getEmbeddedPost(
return v.record
}
}
if (AppBskyEmbedRecordWithMedia.isView(v)) {
if (
bsky.dangerousIsType<AppBskyEmbedRecordWithMedia.View>(
v,
AppBskyEmbedRecordWithMedia.isView,
)
) {
if (
AppBskyEmbedRecord.isViewRecord(v.record.record) &&
AppBskyFeedPost.isRecord(v.record.record.value)
+1 -2
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
AppBskyFeedDefs,
ModerationDecision,
} from '@atproto/api'
@@ -21,7 +20,7 @@ export interface ComposerOptsPostRef {
cid: string
text: string
author: AppBskyActorDefs.ProfileViewBasic
embed?: AppBskyEmbedRecord.ViewRecord['embed']
embed?: AppBskyFeedDefs.PostView['embed']
moderation?: ModerationDecision
}
+51
View File
@@ -0,0 +1,51 @@
import {ValidationResult} from '@atproto/lexicon'
export * as post from '#/types/bsky/post'
export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack'
/**
* Fast type checking without full schema validation, for use with data we
* trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
* utils do not assert the type of the entire object, only the `$type` string.
*
* For full validation of the object schema, use the `validate` export from
* this file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function dangerousIsType<R extends {$type?: string}>(
record: unknown,
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
): record is R {
return identity(record)
}
/**
* Fully validates the object schema, which has a performance cost.
*
* For faster checks with data we trust, like that from our app view, use the
* `dangerousIsType` export from this same file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function validate<R extends {$type?: string}>(
record: unknown,
validator: (v: unknown) => ValidationResult<R>,
): record is R {
return validator(record).success
}
+148
View File
@@ -0,0 +1,148 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyLabelerDefs,
} from '@atproto/api'
export type Embed =
| {
type: 'post'
view: AppBskyEmbedRecord.ViewRecord
}
| {
type: 'post_not_found'
view: AppBskyEmbedRecord.ViewNotFound
}
| {
type: 'post_blocked'
view: AppBskyEmbedRecord.ViewBlocked
}
| {
type: 'post_detached'
view: AppBskyEmbedRecord.ViewDetached
}
| {
type: 'feed'
view: AppBskyFeedDefs.GeneratorView
}
| {
type: 'list'
view: AppBskyGraphDefs.ListView
}
| {
type: 'labeler'
view: AppBskyLabelerDefs.LabelerView
}
| {
type: 'starter_pack'
view: AppBskyGraphDefs.StarterPackViewBasic
}
| {
type: 'images'
view: AppBskyEmbedImages.View
}
| {
type: 'link'
view: AppBskyEmbedExternal.View
}
| {
type: 'video'
view: AppBskyEmbedVideo.View
}
| {
type: 'post_with_media'
view: Embed
media: Embed
}
| {
type: 'unknown'
view: null
}
export type EmbedType<T extends Embed['type']> = Extract<Embed, {type: T}>
export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
if (AppBskyEmbedRecord.isViewRecord(record)) {
return {
type: 'post',
view: record,
}
} else if (AppBskyEmbedRecord.isViewNotFound(record)) {
return {
type: 'post_not_found',
view: record,
}
} else if (AppBskyEmbedRecord.isViewBlocked(record)) {
return {
type: 'post_blocked',
view: record,
}
} else if (AppBskyEmbedRecord.isViewDetached(record)) {
return {
type: 'post_detached',
view: record,
}
} else if (AppBskyFeedDefs.isGeneratorView(record)) {
return {
type: 'feed',
view: record,
}
} else if (AppBskyGraphDefs.isListView(record)) {
return {
type: 'list',
view: record,
}
} else if (AppBskyLabelerDefs.isLabelerView(record)) {
return {
type: 'labeler',
view: record,
}
} else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
return {
type: 'starter_pack',
view: record,
}
} else {
return {
type: 'unknown',
view: null,
}
}
}
export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed {
if (AppBskyEmbedImages.isView(embed)) {
return {
type: 'images',
view: embed,
}
} else if (AppBskyEmbedExternal.isView(embed)) {
return {
type: 'link',
view: embed,
}
} else if (AppBskyEmbedVideo.isView(embed)) {
return {
type: 'video',
view: embed,
}
} else if (AppBskyEmbedRecord.isView(embed)) {
return parseEmbedRecordView(embed)
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
return {
type: 'post_with_media',
view: parseEmbedRecordView(embed.record),
media: parseEmbed(embed.media),
}
} else {
return {
type: 'unknown',
view: null,
}
}
}
+10
View File
@@ -0,0 +1,10 @@
import {AppBskyActorDefs, ChatBskyActorDefs} from '@atproto/api'
/**
* Matches any profile view exported by our SDK
*/
export type AnyProfileView =
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewDetailed
| ChatBskyActorDefs.ProfileViewBasic
+11
View File
@@ -0,0 +1,11 @@
import {AppBskyGraphDefs} from '@atproto/api'
export const isBasicView = AppBskyGraphDefs.isStarterPackViewBasic
export const isView = AppBskyGraphDefs.isStarterPackView
/**
* Matches any starter pack view exported by our SDK
*/
export type AnyStarterPackView =
| AppBskyGraphDefs.StarterPackViewBasic
| AppBskyGraphDefs.StarterPackView
+4 -3
View File
@@ -1,6 +1,6 @@
import React, {useCallback} from 'react'
import {Dimensions, StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -11,6 +11,7 @@ import {useModalControls} from '#/state/modals'
import {useListMembersQuery} from '#/state/queries/list-members'
import {useSession} from '#/state/session'
import {ListFooter} from '#/components/Lists'
import * as bsky from '#/types/bsky'
import {ProfileCard} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
@@ -116,7 +117,7 @@ export function ListMembers({
}, [fetchNextPage])
const onPressEditMembership = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => {
(profile: bsky.profile.AnyProfileView) => {
openModal({
name: 'user-add-remove-lists',
subject: profile.did,
@@ -131,7 +132,7 @@ export function ListMembers({
// =
const renderMemberButton = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => {
(profile: bsky.profile.AnyProfileView) => {
if (!isOwner) {
return null
}
@@ -58,6 +58,7 @@ import * as MediaPreview from '#/components/MediaPreview'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
import {FeedSourceCard} from '../feeds/FeedSourceCard'
import {Post} from '../post/Post'
import {Link, TextLink} from '../util/Link'
@@ -71,7 +72,7 @@ const MAX_AUTHORS = 5
const EXPANDED_AUTHOR_EL_HEIGHT = 35
interface Author {
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileView
href: string
moderation: ModerationDecision
}
@@ -264,7 +265,10 @@ let NotificationFeedItem = ({
if (
item.notification.author.viewer?.following &&
AppBskyGraphFollow.isRecord(item.notification.record)
bsky.dangerousIsType<AppBskyGraphFollow.Record>(
item.notification.record,
AppBskyGraphFollow.isRecord,
)
) {
let followingTimestamp
try {
@@ -521,7 +525,7 @@ function ExpandListPressable({
}
}
function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) {
function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) {
const {_} = useLingui()
const agent = useAgent()
const navigation = useNavigation<NavigationProp>()
@@ -716,7 +720,13 @@ function ExpandedAuthorsList({
function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
const pal = usePalette('default')
if (post && AppBskyFeedPost.isRecord(post?.record)) {
if (
post &&
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post?.record,
AppBskyFeedPost.isRecord,
)
) {
const text = post.record.text
return (
+1 -1
View File
@@ -16,7 +16,7 @@ function renderItem({
item,
index,
}: {
item: ActorDefs.ProfileViewBasic
item: ActorDefs.ProfileView
index: number
}) {
return (
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {logger} from '#/logger'
import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
useProfileQuery,
@@ -35,8 +35,7 @@ function PostThreadFollowBtnLoaded({
const navigation = useNavigation()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
useProfileShadow(profileUnshadowed)
const profile = useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'PostThreadItem',
+5 -1
View File
@@ -58,6 +58,7 @@ import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {Text} from '#/components/Typography'
import {WhoCanReply} from '#/components/WhoCanReply'
import * as bsky from '#/types/bsky'
export function PostThreadItem({
post,
@@ -790,7 +791,10 @@ function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
const control = Prompt.usePromptControl()
const indexedAt = new Date(post.indexedAt)
const createdAt = AppBskyFeedPost.isRecord(post.record)
const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? new Date(post.record.createdAt)
: new Date(post.indexedAt)
+2 -2
View File
@@ -28,6 +28,7 @@ import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
import {ContentHider} from '../../../components/moderation/ContentHider'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
@@ -53,8 +54,7 @@ export function Post({
const moderationOpts = useModerationOpts()
const record = useMemo<AppBskyFeedPost.Record | undefined>(
() =>
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
bsky.validate(post.record, AppBskyFeedPost.validateRecord)
? post.record
: undefined,
[post],
+7 -2
View File
@@ -47,6 +47,7 @@ import {AppModerationCause} from '#/components/Pills'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
import {AviFollowButton} from './AviFollowButton'
@@ -232,8 +233,9 @@ let FeedItemInner = ({
* If `post[0]` in this slice is the actual root post (not an orphan thread),
* then we may have a threadgate record to reference
*/
const threadgateRecord = AppBskyFeedThreadgate.isRecord(
const threadgateRecord = bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
rootPost.threadgate?.record,
AppBskyFeedThreadgate.isRecord,
)
? rootPost.threadgate.record
: undefined
@@ -461,7 +463,10 @@ let PostContent = ({
})
const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
const rootPostUri = AppBskyFeedPost.isRecord(post.record)
const rootPostUri = bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? post.record?.reply?.root?.uri || post.uri
: undefined
const isControlledByViewer =
+2 -2
View File
@@ -1,10 +1,10 @@
import {StyleProp, TextStyle, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Shadow} from '#/state/cache/types'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import * as bsky from '#/types/bsky'
import {Button, ButtonType} from '../util/forms/Button'
import * as Toast from '../util/Toast'
@@ -18,7 +18,7 @@ export function FollowButton({
}: {
unfollowedType?: ButtonType
followedType?: ButtonType
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
profile: Shadow<bsky.profile.AnyProfileView>
labelStyle?: StyleProp<TextStyle>
logContext: 'ProfileCard' | 'StarterPackProfilesList'
onFollow?: () => void
+7 -5
View File
@@ -24,6 +24,7 @@ import {
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import * as Pills from '#/components/Pills'
import * as bsky from '#/types/bsky'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -41,12 +42,12 @@ export function ProfileCard({
showKnownFollowers,
}: {
testID?: string
profile: AppBskyActorDefs.ProfileViewBasic
profile: bsky.profile.AnyProfileView
noModFilter?: boolean
noBg?: boolean
noBorder?: boolean
renderButton?: (
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
profile: Shadow<bsky.profile.AnyProfileView>,
) => React.ReactNode
onPress?: () => void
style?: StyleProp<ViewStyle>
@@ -76,6 +77,7 @@ export function ProfileCard({
showKnownFollowers &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
moderationOpts
const hasDescription = 'description' in profile
return (
<Link
@@ -126,9 +128,9 @@ export function ProfileCard({
<View style={styles.layoutButton}>{renderButton(profile)}</View>
) : undefined}
</View>
{profile.description || knownFollowersVisible ? (
{hasDescription || knownFollowersVisible ? (
<View style={styles.details}>
{profile.description ? (
{hasDescription && profile.description ? (
<Text emoji style={pal.text} numberOfLines={4}>
{profile.description as string}
</Text>
@@ -139,7 +141,7 @@ export function ProfileCard({
a.flex_row,
a.align_center,
a.gap_sm,
!!profile.description && a.mt_md,
!!hasDescription && a.mt_md,
]}>
<KnownFollowers
minimal
+1 -1
View File
@@ -17,7 +17,7 @@ function renderItem({
item,
index,
}: {
item: ActorDefs.ProfileViewBasic
item: ActorDefs.ProfileView
index: number
}) {
return (
+1 -1
View File
@@ -17,7 +17,7 @@ function renderItem({
item,
index,
}: {
item: ActorDefs.ProfileViewBasic
item: ActorDefs.ProfileView
index: number
}) {
return (
+3 -2
View File
@@ -2,7 +2,7 @@ import React, {memo, useMemo} from 'react'
import {Image, Pressable, StyleSheet, View} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'
import Svg, {Circle, Path, Rect} from 'react-native-svg'
import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
import {ModerationUI} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -31,6 +31,7 @@ import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Menu from '#/components/Menu'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import * as bsky from '#/types/bsky'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
@@ -55,7 +56,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
moderation?: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
profile: bsky.profile.AnyProfileView
disableHoverCard?: boolean
onBeforePress?: () => void
}
+9 -4
View File
@@ -36,6 +36,7 @@ import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {PostAlerts} from '../../../../components/moderation/PostAlerts'
import {Link} from '../Link'
@@ -171,10 +172,14 @@ export function QuoteEmbed({
const itemTitle = `Post by ${quote.author.handle}`
const richText = React.useMemo(() => {
const text = AppBskyFeedPost.isRecord(quote.record) ? quote.record.text : ''
const facets = AppBskyFeedPost.isRecord(quote.record)
? quote.record.facets
: undefined
if (
!bsky.dangerousIsType<AppBskyFeedPost.Record>(
quote.record,
AppBskyFeedPost.isRecord,
)
)
return undefined
const {text, facets} = quote.record
return text.trim()
? new RichTextAPI({text: text, facets: facets})
: undefined
+2
View File
@@ -133,6 +133,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
})
mockedProfile.did = did
mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
// @ts-expect-error ProfileViewBasic is close enough -esb
mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile
@@ -922,6 +923,7 @@ function MockAccountScreen({
// @ts-ignore ProfileViewBasic is close enough -prf
profile={profile}
moderationOpts={moderationOpts}
// @ts-ignore ProfileViewBasic is close enough -esb
descriptionRT={new RichText({text: profile.description as string})}
/>
</ScreenHider>
+7 -6
View File
@@ -76,6 +76,7 @@ import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Glo
import * as Layout from '#/components/Layout'
import * as Menu from '#/components/Menu'
import {account, useStorage} from '#/storage'
import * as bsky from '#/types/bsky'
function Loader() {
return (
@@ -656,7 +657,7 @@ export function SearchScreenShell({
)
const updateProfileHistory = useCallback(
async (item: AppBskyActorDefs.ProfileViewBasic) => {
async (item: bsky.profile.AnyProfileView) => {
const newAccountHistory = [
item.did,
...accountHistory.filter(p => p !== item.did),
@@ -673,7 +674,7 @@ export function SearchScreenShell({
[termHistory, setTermHistory],
)
const deleteProfileHistoryItem = useCallback(
async (item: AppBskyActorDefs.ProfileViewBasic) => {
async (item: AppBskyActorDefs.ProfileViewDetailed) => {
setAccountHistory(accountHistory.filter(p => p !== item.did))
},
[accountHistory, setAccountHistory],
@@ -766,7 +767,7 @@ export function SearchScreenShell({
)
const handleProfileClick = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => {
(profile: bsky.profile.AnyProfileView) => {
// Slight delay to avoid updating during push nav animation.
setTimeout(() => {
updateProfileHistory(profile)
@@ -1013,11 +1014,11 @@ function SearchHistory({
onRemoveProfileClick,
}: {
searchHistory: string[]
selectedProfiles: AppBskyActorDefs.ProfileViewBasic[]
selectedProfiles: AppBskyActorDefs.ProfileViewDetailed[]
onItemClick: (item: string) => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
onRemoveItemClick: (item: string) => void
onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
}) {
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
+1 -1
View File
@@ -220,7 +220,7 @@ function SwitchMenuItems({
accounts:
| {
account: SessionAccount
profile?: AppBskyActorDefs.ProfileView
profile?: AppBskyActorDefs.ProfileViewDetailed
}[]
| undefined
signOutPromptControl: DialogControlProps