Merge remote-tracking branch 'origin/main' into 3p-moderators

* origin/main: (33 commits)
  Improved server selector during account creation and signin (#2840)
  remove line height from text input for composer on ios (#2844)
  Add follow button to highlighted post (#2828)
  Cleaning up old codes pt_BR (#2809)
  Fix wrong translations in zh-CN localization  (#2812)
  Adjust Japanese translation (#2814)
  Update ja messages.po (#2821)
  change follow to follow all when !== 20 (#2831)
  ios adult content link fix 🤦 (#2845)
  improves build.md completeness (#2835)
  increase stroke width for active hashtag icon (#2829)
  Update `blueskyweb.xyz` links to `bsky.social` (#2830)
  eas nightlies (#2826)
  Always show post dropdown button at the bottom of the post, add share button to highlighted post (#2646)
  Measure header layout reliably (#2817)
  Round line height (#2824)
  Design system tweaks (#2822)
  Fix layout calculations (#2816)
  Fix flashes and jumps when opening profile (#2815)
  Show text cursor on web bio (#2813)
  ...
This commit is contained in:
Eric Bailey
2024-02-12 15:43:33 -06:00
90 changed files with 1781 additions and 1707 deletions
+13 -11
View File
@@ -22,15 +22,15 @@ export interface ProfileShadow {
blockingUri: string | undefined
}
type ProfileView =
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed
const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap()
const shadows: WeakMap<
AppBskyActorDefs.ProfileView,
Partial<ProfileShadow>
> = new WeakMap()
const emitter = new EventEmitter()
export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
export function useProfileShadow<
TProfileView extends AppBskyActorDefs.ProfileView,
>(profile: TProfileView): Shadow<TProfileView> {
const [shadow, setShadow] = useState(() => shadows.get(profile))
const [prevPost, setPrevPost] = useState(profile)
if (profile !== prevPost) {
@@ -70,10 +70,10 @@ export function updateProfileShadow(
})
}
function mergeShadow(
profile: ProfileView,
function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
profile: TProfileView,
shadow: Partial<ProfileShadow>,
): Shadow<ProfileView> {
): Shadow<TProfileView> {
return castAsShadow({
...profile,
viewer: {
@@ -89,7 +89,9 @@ function mergeShadow(
})
}
function* findProfilesInCache(did: string): Generator<ProfileView, void> {
function* findProfilesInCache(
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
yield* findAllProfilesInListMembersQueryData(queryClient, did)
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
-13
View File
@@ -26,17 +26,6 @@ export interface EditProfileModal {
onUpdate?: () => void
}
export interface ProfilePreviewModal {
name: 'profile-preview'
did: string
}
export interface ServerInputModal {
name: 'server-input'
initialService: string
onSelect: (url: string) => void
}
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
@@ -202,7 +191,6 @@ export type Modal =
| ChangeHandleModal
| DeleteAccountModal
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
@@ -228,7 +216,6 @@ export type Modal =
| AltTextImageModal
| CropImageModal
| EditImageModal
| ServerInputModal
| RepostModal
| SelfLabelModal
| ThreadgateModal
+1
View File
@@ -112,6 +112,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
hiddenPosts: defaults.hiddenPosts,
externalEmbeds: defaults.externalEmbeds,
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
pdsAddressHistory: defaults.pdsAddressHistory,
}
}
+2
View File
@@ -57,6 +57,7 @@ export const schema = z.object({
hiddenPosts: z.array(z.string()).optional(), // should move to server
useInAppBrowser: z.boolean().optional(),
lastSelectedHomeFeed: z.string().optional(),
pdsAddressHistory: z.array(z.string()).optional(),
})
export type Schema = z.infer<typeof schema>
@@ -91,4 +92,5 @@ export const defaults: Schema = {
hiddenPosts: [],
useInAppBrowser: undefined,
lastSelectedHomeFeed: undefined,
pdsAddressHistory: [],
}
+2 -2
View File
@@ -12,7 +12,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
import {precacheProfile} from '../profile'
import {NotificationType, FeedNotification, FeedPage} from './types'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
@@ -59,7 +59,7 @@ export async function fetchPage({
if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
precacheProfile(queryClient, notif.subject.author)
}
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
import {precacheFeedPostProfiles} from './profile'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation'
@@ -138,7 +138,7 @@ export function usePostFeedQuery(
}
const res = await api.fetch({cursor, limit: PAGE_SIZE})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
precacheFeedPostProfiles(queryClient, res.feed)
/*
* If this is a public view, we need to check if posts fail moderation.
+2 -2
View File
@@ -10,7 +10,7 @@ import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util'
export const RQKEY = (uri: string) => ['post-thread', uri]
@@ -71,7 +71,7 @@ export function usePostThreadQuery(uri: string | undefined) {
const res = await getAgent().getPostThread({uri: uri!})
if (res.success) {
const nodes = responseToThreadNodes(res.data.thread)
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
precacheThreadPostProfiles(queryClient, nodes)
return nodes
}
return {type: 'unknown', uri: uri!}
+77 -3
View File
@@ -4,6 +4,9 @@ import {
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyActorGetProfile,
AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
} from '@atproto/api'
import {
useQuery,
@@ -23,9 +26,14 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {STALE} from '#/state/queries'
import {track} from '#/lib/analytics/analytics'
import {ThreadNode} from './post-thread'
export const RQKEY = (did: string) => ['profile', did]
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
export const profileBasicQueryKey = (didOrHandle: string) => [
'profileBasic',
didOrHandle,
]
export function useProfileQuery({
did,
@@ -34,18 +42,26 @@ export function useProfileQuery({
did: string | undefined
staleTime?: number
}) {
return useQuery({
const queryClient = useQueryClient()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
// if you remove it, the UI infinite-loops
// -prf
staleTime,
refetchOnWindowFocus: true,
queryKey: RQKEY(did || ''),
queryKey: RQKEY(did ?? ''),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''})
const res = await getAgent().getProfile({actor: did ?? ''})
return res.data
},
placeholderData: () => {
if (!did) return
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
profileBasicQueryKey(did),
)
},
enabled: !!did,
})
}
@@ -405,6 +421,64 @@ function useProfileUnblockMutation() {
})
}
export function precacheProfile(
queryClient: QueryClient,
profile: AppBskyActorDefs.ProfileViewBasic,
) {
queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
}
export function precacheFeedPostProfiles(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
// Save the author of the post every time
precacheProfile(queryClient, post.post.author)
precachePostEmbedProfile(queryClient, post.post.embed)
// Cache parent author and embeds
const parent = post.reply?.parent
if (AppBskyFeedDefs.isPostView(parent)) {
precacheProfile(queryClient, parent.author)
precachePostEmbedProfile(queryClient, parent.embed)
}
}
}
function precachePostEmbedProfile(
queryClient: QueryClient,
embed: AppBskyFeedDefs.PostView['embed'],
) {
if (AppBskyEmbedRecord.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
precacheProfile(queryClient, embed.record.author)
}
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record.record)) {
precacheProfile(queryClient, embed.record.record.author)
}
}
}
export function precacheThreadPostProfiles(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPostProfiles(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPostProfiles(queryClient, reply)
}
}
}
}
async function whenAppViewReady(
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
+23 -49
View File
@@ -1,9 +1,9 @@
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
import {AtUri, AppBskyActorDefs} from '@atproto/api'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {ThreadNode} from './post-thread'
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
@@ -22,55 +22,29 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const queryClient = useQueryClient()
return useQuery<string, Error>({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle || ''),
async queryFn() {
if (!didOrHandle) {
return ''
}
if (!didOrHandle.startsWith('did:')) {
const res = await getAgent().resolveHandle({handle: didOrHandle})
didOrHandle = res.data.did
}
return didOrHandle
queryKey: RQKEY(didOrHandle ?? ''),
queryFn: async () => {
if (!didOrHandle) return ''
// Just return the did if it's already one
if (didOrHandle.startsWith('did:')) return didOrHandle
const res = await getAgent().resolveHandle({handle: didOrHandle})
return res.data.did
},
initialData: () => {
// Return undefined if no did or handle
if (!didOrHandle) return
const profile =
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
RQKEY_PROFILE_BASIC(didOrHandle),
)
return profile?.did
},
enabled: !!didOrHandle,
})
}
export function precacheProfile(
queryClient: QueryClient,
profile:
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
}
export function precacheFeedPosts(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
precacheProfile(queryClient, post.post.author)
}
}
export function precacheThreadPosts(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPosts(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPosts(queryClient, reply)
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {isWeb} from '#/platform/detection'
type StateContext = string
type SetContext = (v: string) => void
const stateContext = React.createContext<StateContext>('home')
const setContext = React.createContext<SetContext>((_: string) => {})
function getInitialFeed() {
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
const feedFromUrl = params.get('feed')
if (feedFromUrl) {
// If explicitly booted from a link like /?feed=..., prefer that.
return feedFromUrl
}
}
const feedFromSession = sessionStorage.getItem('lastSelectedHomeFeed')
if (feedFromSession) {
// Fall back to a previously chosen feed for this browser tab.
return feedFromSession
}
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
}
return 'home'
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(getInitialFeed)
const saveState = React.useCallback((feed: string) => {
setState(feed)
if (isWeb) {
try {
sessionStorage.setItem('lastSelectedHomeFeed', feed)
} catch {}
}
persisted.write('lastSelectedHomeFeed', feed)
}, [])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={saveState}>{children}</setContext.Provider>
</stateContext.Provider>
)
}
export function useSelectedFeed() {
return React.useContext(stateContext)
}
export function useSetSelectedFeed() {
return React.useContext(setContext)
}