Skip to content

Commit

Permalink
Performance improvements: structural sharing & moderation opts context (
Browse files Browse the repository at this point in the history
#3785)

* Fix: correctly apply structural sharing to preferences object

* Move moderation opts into a context

* Fix import

* Remove log

* Pass userdid directly

* Pass moderationPrefs directly
  • Loading branch information
pfrazee authored May 1, 2024
1 parent 39807a8 commit 31cb3e5
Show file tree
Hide file tree
Showing 25 changed files with 231 additions and 120 deletions.
37 changes: 20 additions & 17 deletions src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useNotificationsListener} from 'lib/notifications/notifications'
Expand Down Expand Up @@ -79,23 +80,25 @@ function InnerApp() {
<QueryProvider currentDid={currentAccount?.did}>
<PushNotificationsListener>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
<ModerationOptsProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</ModerationOptsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
Expand Down
37 changes: 20 additions & 17 deletions src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
Expand Down Expand Up @@ -56,23 +57,25 @@ function InnerApp() {
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
<ModerationOptsProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</ModerationOptsProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProfileHoverCard/index.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/queries/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useProfileShadow} from 'state/cache/profile-shadow'
Expand Down
2 changes: 1 addition & 1 deletion src/components/dms/NewChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {useLingui} from '@lingui/react'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/queries/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
Expand Down
87 changes: 87 additions & 0 deletions src/lib/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,90 @@ export function dedupArray<T>(arr: T[]): T[] {
const s = new Set(arr)
return [...s]
}

/**
* Taken from @tanstack/query-core utils.ts
* Modified to support Date object comparisons
*
* This function returns `a` if `b` is deeply equal.
* If not, it will replace any deeply equal children of `b` with those of `a`.
* This can be used for structural sharing between JSON values for example.
*/
export function replaceEqualDeep(a: any, b: any): any {
if (a === b) {
return a
}

if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime() ? a : b
}

const array = isPlainArray(a) && isPlainArray(b)

if (array || (isPlainObject(a) && isPlainObject(b))) {
const aItems = array ? a : Object.keys(a)
const aSize = aItems.length
const bItems = array ? b : Object.keys(b)
const bSize = bItems.length
const copy: any = array ? [] : {}

let equalItems = 0

for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i]
if (
!array &&
a[key] === undefined &&
b[key] === undefined &&
aItems.includes(key)
) {
copy[key] = undefined
equalItems++
} else {
copy[key] = replaceEqualDeep(a[key], b[key])
if (copy[key] === a[key] && a[key] !== undefined) {
equalItems++
}
}
}

return aSize === bSize && equalItems === aSize ? a : copy
}

return b
}

export function isPlainArray(value: unknown) {
return Array.isArray(value) && value.length === Object.keys(value).length
}

// Copied from: https://github.com/jonschlinkert/is-plain-object
export function isPlainObject(o: any): o is Object {
if (!hasObjectPrototype(o)) {
return false
}

// If has no constructor
const ctor = o.constructor
if (ctor === undefined) {
return true
}

// If has modified prototype
const prot = ctor.prototype
if (!hasObjectPrototype(prot)) {
return false
}

// If constructor does not have an Object-specific method
if (!prot.hasOwnProperty('isPrototypeOf')) {
return false
}

// Most likely a plain Object
return true
}

function hasObjectPrototype(o: any): boolean {
return Object.prototype.toString.call(o) === '[object Object]'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react'
import {View, ViewStyle} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'

import {useTheme, atoms as a, flatten} from '#/alf'
import {Text} from '#/components/Typography'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, flatten, useTheme} from '#/alf'
import {useItemContext} from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {useModerationOpts} from '#/state/queries/preferences'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'

export function SuggestedAccountCard({
profile,
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Onboarding/StepSuggestedAccounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {capitalize} from '#/lib/strings/capitalize'
import {useModerationOpts} from '#/state/queries/preferences'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfilesQuery} from '#/state/queries/profile'
import {
DescriptionText,
Expand Down
61 changes: 61 additions & 0 deletions src/state/preferences/moderation-opts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, {createContext, useContext, useMemo} from 'react'
import {BSKY_LABELER_DID, ModerationOpts} from '@atproto/api'

import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
import {useSession} from '#/state/session'
import {usePreferencesQuery} from '../queries/preferences'

export const moderationOptsContext = createContext<ModerationOpts | undefined>(
undefined,
)

// used in the moderation state devtool
export const moderationOptsOverrideContext = createContext<
ModerationOpts | undefined
>(undefined)

export function useModerationOpts() {
return useContext(moderationOptsContext)
}

export function Provider({children}: React.PropsWithChildren<{}>) {
const override = useContext(moderationOptsOverrideContext)
const {currentAccount} = useSession()
const prefs = usePreferencesQuery()
const {labelDefs} = useLabelDefinitions()
const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs

const userDid = currentAccount?.did
const moderationPrefs = prefs.data?.moderationPrefs
const value = useMemo<ModerationOpts | undefined>(() => {
if (override) {
return override
}
if (!moderationPrefs) {
return undefined
}
return {
userDid,
prefs: {
...moderationPrefs,
labelers: moderationPrefs.labelers.length
? moderationPrefs.labelers
: [
{
did: BSKY_LABELER_DID,
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
},
],
hiddenPosts: hiddenPosts || [],
},
labelDefs,
}
}, [override, userDid, labelDefs, moderationPrefs, hiddenPosts])

return (
<moderationOptsContext.Provider value={value}>
{children}
</moderationOptsContext.Provider>
)
}
3 changes: 2 additions & 1 deletion src/state/queries/actor-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {isJustAMute} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
import {useModerationOpts} from '../preferences/moderation-opts'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences'

const DEFAULT_MOD_OPTS = {
userDid: undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/state/queries/notifications/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {

import {useMutedThreads} from '#/state/muted-threads'
import {useAgent} from '#/state/session'
import {useModerationOpts} from '../../preferences/moderation-opts'
import {STALE} from '..'
import {useModerationOpts} from '../preferences'
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
import {FeedPage} from './types'
import {useUnreadNotificationsApi} from './unread'
Expand Down
2 changes: 1 addition & 1 deletion src/state/queries/notifications/unread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useMutedThreads} from '#/state/muted-threads'
import {useAgent, useSession} from '#/state/session'
import {useModerationOpts} from '../preferences'
import {useModerationOpts} from '../../preferences/moderation-opts'
import {truncateAndInvalidate} from '../util'
import {RQKEY as RQKEY_NOTIFS} from './feed'
import {CachedFeedPage, FeedPage} from './types'
Expand Down
3 changes: 2 additions & 1 deletion src/state/queries/post-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts, usePreferencesQuery} from './preferences'
import {useModerationOpts} from '../preferences/moderation-opts'
import {usePreferencesQuery} from './preferences'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'

type ActorDid = string
Expand Down
Loading

0 comments on commit 31cb3e5

Please sign in to comment.