phase 4 foundations: vendor missing lexicons, refreshSession api, toLex no-op purge, de-agent link-meta

This commit is contained in:
Samuel Newman
2026-07-17 11:55:07 +03:00
parent d13d1ca604
commit d052d14f2b
80 changed files with 649 additions and 450 deletions
+2 -3
View File
@@ -46,7 +46,7 @@ import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout'
import {IS_LIQUID_GLASS} from '#/env'
import {chat} from '#/lexicons'
import {isType, toLex} from '#/types/bsky'
import {isType} from '#/types/bsky'
import {ChatDisabled} from './components/ChatDisabled'
import {ChatEnded} from './components/ChatEnded'
import {ChatLocked} from './components/ChatLocked'
@@ -228,8 +228,7 @@ function InnerReady({
const moderationOpts = useModerationOpts()
const primaryMemberModeration = useMemo(() => {
if (!primaryMember || !moderationOpts) return null
// TODO(phase4): drop toLex once useMaybeProfileShadow emits #/lexicons views
return moderateProfile(toLex(primaryMember), moderationOpts)
return moderateProfile(primaryMember, moderationOpts)
}, [primaryMember, moderationOpts])
const header = <MessagesListHeader convo={convo} />
@@ -21,7 +21,6 @@ import * as ProfileCard from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {toLex} from '#/types/bsky'
import {MemberMenu} from './MemberMenu'
import {RemoveMemberPrompt} from './prompts'
import {StatusBadge} from './StatusBadge'
@@ -81,8 +80,7 @@ export function Member({
return <MemberPlaceholder />
}
// TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views
const moderation = moderateProfile(toLex(profile), moderationOpts)
const moderation = moderateProfile(profile, moderationOpts)
const isDeletedAccount = profile.handle === 'missing.invalid'
const displayName = isDeletedAccount
@@ -108,13 +106,10 @@ export function Member({
}
const joinedReason = profile.kind?.addedBy
? // TODO(phase4): drop toLex once addedBy emits #/lexicons views
l`Added by ${createSanitizedDisplayName(
? l`Added by ${createSanitizedDisplayName(
profile.kind.addedBy,
true,
moderateProfile(toLex(profile.kind.addedBy), moderationOpts).ui(
'displayName',
),
moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'),
)}`
: l`Added by invite link`
+4 -7
View File
@@ -187,9 +187,8 @@ export function JoinRequest({setScreenState}: Props) {
{createSanitizedDisplayName(
joinLinkPreview.owner,
true,
// TODO(phase4): drop toLex once join link preview emits #/lexicons views
moderateProfile(
bsky.toLex(joinLinkPreview.owner),
joinLinkPreview.owner,
moderationOpts,
).ui('displayName'),
)}
@@ -224,14 +223,12 @@ export function JoinRequest({setScreenState}: Props) {
? l`Sign in to request access to this group chat.`
: l`Sign in to accept invite.`}{' '}
{joinLinkPreview.joinRule === 'followedByOwner' &&
// TODO(phase4): drop toLex once join link preview emits #/lexicons views
l`Only people ${createSanitizedDisplayName(
joinLinkPreview.owner,
true,
moderateProfile(
bsky.toLex(joinLinkPreview.owner),
moderationOpts,
).ui('displayName'),
moderateProfile(joinLinkPreview.owner, moderationOpts).ui(
'displayName',
),
)} follows can join.`}
</Text>
<ActionButtons setScreenState={setScreenState} />
@@ -125,8 +125,7 @@ function DirectChatItem({
const {isWithinLeftPanel} = useIsWithinSplitView()
const moderation = useMemo(
// TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views
() => moderateProfile(bsky.toLex(profile), moderationOpts),
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
@@ -193,10 +192,7 @@ function GroupChatItem({
const moderation = useMemo(
() =>
groupOwner
? // TODO(phase4): drop toLex once useMaybeProfileShadow emits #/lexicons views
moderateProfile(bsky.toLex(groupOwner), moderationOpts)
: undefined,
groupOwner ? moderateProfile(groupOwner, moderationOpts) : undefined,
[groupOwner, moderationOpts],
)
@@ -423,15 +419,7 @@ function BaseChatItem({
for (const member of convo.view.members) {
unstableCacheProfileView(queryClient, member)
}
/*
* `convo.view` comes from the still-old-typed `#/components/dms/util`
* (migrates in a later task) while the convo cache is now keyed on the
* lexicon ConvoView. TODO(phase4): drop toLex once dms/util migrates.
*/
precacheConvoQuery(
queryClient,
bsky.toLex<chat.bsky.convo.defs.ConvoView>(convo.view),
)
precacheConvoQuery(queryClient, convo.view)
void decrementBadgeCount(convo.view.unreadCount)
if (isDeletedAccount) {
e.preventDefault()
@@ -525,10 +513,7 @@ function BaseChatItem({
}
onPressIn={() =>
// see onPress: old-typed dms/util view into the new-typed cache
precacheConvoQuery(
queryClient,
bsky.toLex<chat.bsky.convo.defs.ConvoView>(convo.view),
)
precacheConvoQuery(queryClient, convo.view)
}
onPress={onPress}
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
@@ -17,7 +17,6 @@ import {ProfileBadges} from '#/components/ProfileBadges'
import {usePromptControl} from '#/components/Prompt'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {toLex} from '#/types/bsky'
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
@@ -115,8 +114,7 @@ function InviterHeader({
const t = useTheme()
const profile = useProfileShadow(profileUnshadowed)
const moderation = useMemo(
// TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views
() => moderateProfile(toLex(profile), moderationOpts),
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
const displayName = createSanitizedDisplayName(
@@ -34,7 +34,6 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import {type chat} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {CopyTextButton} from './CopyTextButton'
import {EditTextButton} from './EditTextButton'
@@ -69,8 +68,7 @@ export function InviteLinkDialog({
const ownerName = createSanitizedDisplayName(
owner,
false,
// TODO(phase4): drop toLex once GroupConvoMember emits #/lexicons views
moderateProfile(toLex(owner), moderationOpts).ui('displayName'),
moderateProfile(owner, moderationOpts).ui('displayName'),
)
const {joinLink} = convo.details
@@ -38,7 +38,6 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
import {type chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {type MessageEmbedState} from './MessageInputEmbed'
const MIN_HEIGHT = 40
@@ -70,15 +69,7 @@ export function MessageComposer({
const {getDraft, clearDraft} = useMessageDraft()
const composerInternalApiRef = useComposerInternalApiRef()
const {replyTo: replyToOld, clearReply} = useMessageReplies()
/*
* `useMessageReplies` (`#/components/dms`, migrates in a later task) still
* emits the old `@atproto/api` MessageView; the send path is typed on the
* lexicon view. Structurally identical modulo branded strings.
* TODO(phase4): drop toLex once dms/MessageReplies migrates.
*/
const replyTo = bsky.toLex<chat.bsky.convo.defs.MessageView | null>(
replyToOld,
)
const replyTo = replyToOld
const [text, setText] = useState(getDraft)
useSaveMessageDraft(text)
@@ -569,24 +569,18 @@ export function MessagesList({
try {
const post = await getPost({uri: embedState.uri})
if (post) {
/*
* `post` comes from the still-old-typed `useGetPost` producer
* (migrates in a later task), so its uri/view shapes carry plain
* strings where the lexicon types are branded. TODO(phase4): drop
* toLex once that producer migrates.
*/
embed = bsky.toLex<$Typed<app.bsky.embed.record.Main>>({
embed = {
$type: 'app.bsky.embed.record',
record: {
uri: post.uri,
cid: post.cid,
},
})
}
embedView = bsky.toLex<$Typed<app.bsky.embed.record.View>>({
embedView = {
$type: 'app.bsky.embed.record#view',
record: createEmbedViewRecordFromPost(post),
})
}
stripLinkFacet(uri => {
if (!isBskyPostUrl(uri)) return false
@@ -611,14 +605,10 @@ export function MessagesList({
const joinLinkPreview = await getJoinLinkPreview({code, hasSession})
if (joinLinkPreview) {
/*
* The preview comes from the still-old-typed `join-links.ts` query
* (migrates in a later task). TODO(phase4): drop toLex once it does.
*/
embedView = bsky.toLex<$Typed<chat.bsky.embed.joinLink.View>>({
embedView = {
$type: 'chat.bsky.embed.joinLink#view',
joinLinkPreview,
})
}
}
stripLinkFacet(uri => getChatInviteCodeFromUrl(uri) === code)
@@ -16,7 +16,6 @@ import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/P
import {ProfileBadges} from '#/components/ProfileBadges'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import {toLex} from '#/types/bsky'
export function MessagesListInfoPanel({
convo,
@@ -35,11 +34,10 @@ export function MessagesListInfoPanel({
)[0]
const handle = sanitizeHandle(profile.handle, '@')
const displayName = moderationOpts
? // TODO(phase4): drop toLex once convo members emit #/lexicons views
createSanitizedDisplayName(
? createSanitizedDisplayName(
profile,
true,
moderateProfile(toLex(profile), moderationOpts).ui('displayName'),
moderateProfile(profile, moderationOpts).ui('displayName'),
)
: handle
const profileLink =
@@ -41,7 +41,6 @@ import * as Menu from '#/components/Menu'
import {ReportDialog} from '#/components/moderation/ReportDialog'
import * as Toast from '#/components/Toast'
import {type chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
export function RejectMenu({
convo,
@@ -229,18 +228,10 @@ export function AcceptChatButton({
onMutate: () => {
onAcceptConvo?.()
if (currentScreen === 'list') {
/*
* `convo` is the old-typed view threaded from `#/components/dms/util`
* consumers while the convo cache is now keyed on the lexicon
* ConvoView. TODO(phase4): drop toLex once this file's props flip.
*/
precacheConvoQuery(
queryClient,
bsky.toLex<chat.bsky.convo.defs.ConvoView>({
...convo,
status: 'accepted',
}),
)
precacheConvoQuery(queryClient, {
...convo,
status: 'accepted',
})
navigation.navigate('MessagesConversation', {
conversation: convo.id,
accept: true,
@@ -16,7 +16,6 @@ import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
const AVI_SIZE = 20
@@ -94,8 +93,7 @@ export function KnownLikers({post}: {post: AppBskyFeedDefs.PostView}) {
.map(like => like.actor)
.map(actor => ({
actor,
// TODO(phase4): drop toLex once useLikedBySampleQuery emits #/lexicons views
moderation: moderateProfile(toLex(actor), moderationOpts),
moderation: moderateProfile(actor, moderationOpts),
}))
.filter(({actor, moderation}) => {
const modui = moderation.ui('profileList')
@@ -33,7 +33,6 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {ProfileHeaderDisplayName} from './DisplayName'
import {EditProfileDialog} from './EditProfileDialog'
import {ProfileHeaderHandle} from './Handle'
@@ -67,7 +66,7 @@ let ProfileHeaderLabeler = ({
const isSelf = currentAccount?.did === profile.did
const moderation = useMemo(
() => moderateProfile(toLex(profile), moderationOpts),
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
@@ -40,7 +40,6 @@ import {IS_IOS, IS_NATIVE} from '#/env'
import {InviteFriendsDialog} from '#/features/inviteFriends'
import {useActorStatus} from '#/features/liveNow'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {GermButton} from '../components/GermButton'
import {ProfileHeaderDisplayName} from './DisplayName'
import {EditProfileDialog} from './EditProfileDialog'
@@ -69,7 +68,7 @@ let ProfileHeaderStandard = ({
const {currentAccount} = useSession()
const {_} = useLingui()
const moderation = useMemo(
() => moderateProfile(toLex(profile), moderationOpts),
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
const [, queueUnblock] = useProfileBlockMutationQueue(profile)
+1 -3
View File
@@ -22,7 +22,6 @@ import {Header} from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
import {IS_NATIVE} from '#/env'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {
HeaderLabelerButtons,
ProfileHeaderLabeler,
@@ -112,8 +111,7 @@ const MinimalHeader = memo(function MinimalHeader({
const profile = useProfileShadow(profileUnshadowed)
const moderationOpts = useModerationOpts()
const moderation = useMemo(
() =>
moderationOpts ? moderateProfile(toLex(profile), moderationOpts) : null,
() => (moderationOpts ? moderateProfile(profile, moderationOpts) : null),
[moderationOpts, profile],
)
const [visible, setVisible] = useState(false)
@@ -24,7 +24,6 @@ import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {MoreOptionsMenu} from './MoreOptionsMenu'
import {SubscribeMenu} from './SubscribeMenu'
@@ -130,8 +129,7 @@ export function Header({
list.description
? new RichTextAPI({
text: list.description,
// TODO(phase4): drop toLex once the list view producer emits #/lexicons facets
facets: toLex(list.descriptionFacets),
facets: list.descriptionFacets,
})
: undefined,
[list],
+1 -3
View File
@@ -39,7 +39,6 @@ import {Loader} from '#/components/Loader'
import * as Hider from '#/components/moderation/Hider'
import {IS_WEB} from '#/env'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {AboutSection} from './AboutSection'
import {ErrorScreen} from './components/ErrorScreen'
import {Header} from './components/Header'
@@ -167,8 +166,7 @@ function ProfileListScreenLoaded({
const [headerHeight, setHeaderHeight] = useState<number | null>(null)
const moderation = useMemo(() => {
// TODO(phase4): drop toLex once ListView prop emits #/lexicons views
return moderateUserList(toLex(list), moderationOpts)
return moderateUserList(list, moderationOpts)
}, [list, moderationOpts])
useSetTitle(isHidden ? _(msg`List Hidden`) : list.name)
@@ -22,7 +22,6 @@ import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
import {toLex} from '#/types/bsky'
export function SearchHistory({
searchHistory,
@@ -207,8 +206,7 @@ function RecentProfileItem({
const {t: l} = useLingui()
const width = 80
// TODO(phase4): drop toLex once profile prop emits #/lexicons views
const moderation = moderateProfile(toLex(profile), moderationOpts)
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
@@ -29,7 +29,6 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import * as ModuleHeader from '../components/ModuleHeader'
import {toLex} from '#/types/bsky'
const IMAGE_SIZE = 56
@@ -307,8 +306,7 @@ function useModerateTrendingActors(
return actors
.filter(actor => {
// TODO(phase4): drop toLex once TrendView actors emit #/lexicons views
const decision = moderateProfile(toLex(actor), moderationOpts)
const decision = moderateProfile(actor, moderationOpts)
return !decision.ui('avatar').filter && !decision.ui('avatar').blur
})
.slice(0, 3)
+2 -7
View File
@@ -70,7 +70,6 @@ import {useActorStatus} from '#/features/liveNow'
import {type app} from '#/lexicons'
import {device, useStorage} from '#/storage'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
import {toLex} from '#/types/bsky'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export function SettingsScreen({}: Props) {
@@ -331,8 +330,7 @@ function ProfilePreview({
if (!moderationOpts) return null
// TODO(phase4): drop toLex once ProfileViewDetailed prop emits #/lexicons views
const moderation = moderateProfile(toLex(profile), moderationOpts)
const moderation = moderateProfile(profile, moderationOpts)
const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
@@ -639,10 +637,7 @@ function AccountRow({
<UserAvatar
size={28}
avatar={profile.avatar}
// TODO(phase4): drop toLex once ProfileViewDetailed prop emits #/lexicons views
moderation={moderateProfile(toLex(profile), moderationOpts).ui(
'avatar',
)}
moderation={moderateProfile(profile, moderationOpts).ui('avatar')}
type={profile.associated?.labeler ? 'labeler' : 'user'}
live={live}
hideLiveBadge
@@ -411,8 +411,7 @@ function Header({
const richText = record.description
? new RichTextAPI({
text: record.description,
// TODO(phase4): drop toLex once the starterpack record producer emits #/lexicons facets
facets: bsky.toLex(record.descriptionFacets),
facets: record.descriptionFacets,
})
: undefined
+1 -2
View File
@@ -808,8 +808,7 @@ function Overlay({
: undefined
const richText = new RichTextAPI({
text: record?.text || '',
// TODO(phase4): drop toLex once the post record producer emits #/lexicons facets
facets: bsky.toLex(record?.facets),
facets: record?.facets,
})
const handle = sanitizeHandle(post.author.handle, '@')