Merge branch 'log-session-events' into session-stress

This commit is contained in:
Dan Abramov
2024-07-09 20:08:08 +01:00
26 changed files with 165 additions and 136 deletions
+4 -4
View File
@@ -10,7 +10,7 @@ Get the app itself:
## Development Resources ## Development Resources
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto). This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
@@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
- Open an issue and give some time for discussion before submitting a PR. - Open an issue and give some time for discussion before submitting a PR.
- Stay away from PRs like... - Stay away from PRs like...
- Changing "Post" to "Skeet." - Changing "Post" to "Skeet."
- Refactoring the codebase, eg to replace mobx with redux or something. - Refactoring the codebase, e.g., to replace MobX with Redux or something.
- Adding entirely new features without prior discussion. - Adding entirely new features without prior discussion.
Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for! Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
## Forking guidelines ## Forking guidelines
@@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app.
## Are you a developer interested in building on atproto? ## Are you a developer interested in building on atproto?
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more. Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party integration can be as seamless as first-party through custom feeds, federated services, clients, and more.
## License (MIT) ## License (MIT)
+14 -6
View File
@@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const AVI_SIZE = 30 const AVI_SIZE = 30
const AVI_SIZE_SMALL = 20
const AVI_BORDER = 1 const AVI_BORDER = 1
/** /**
@@ -30,10 +31,12 @@ export function KnownFollowers({
profile, profile,
moderationOpts, moderationOpts,
onLinkPress, onLinkPress,
minimal,
}: { }: {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress'] onLinkPress?: LinkProps['onPress']
minimal?: boolean
}) { }) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>( const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(), new Map(),
@@ -59,6 +62,7 @@ export function KnownFollowers({
cachedKnownFollowers={cachedKnownFollowers} cachedKnownFollowers={cachedKnownFollowers}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
onLinkPress={onLinkPress} onLinkPress={onLinkPress}
minimal={minimal}
/> />
) )
} }
@@ -71,11 +75,13 @@ function KnownFollowersInner({
moderationOpts, moderationOpts,
cachedKnownFollowers, cachedKnownFollowers,
onLinkPress, onLinkPress,
minimal,
}: { }: {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress'] onLinkPress?: LinkProps['onPress']
minimal?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -110,6 +116,8 @@ function KnownFollowersInner({
*/ */
if (slice.length === 0) return null if (slice.length === 0) return null
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
return ( return (
<Link <Link
label={_( label={_(
@@ -120,7 +128,7 @@ function KnownFollowersInner({
style={[ style={[
a.flex_1, a.flex_1,
a.flex_row, a.flex_row,
a.gap_md, minimal ? a.gap_sm : a.gap_md,
a.align_center, a.align_center,
{marginLeft: -AVI_BORDER}, {marginLeft: -AVI_BORDER},
]}> ]}>
@@ -129,8 +137,8 @@ function KnownFollowersInner({
<View <View
style={[ style={[
{ {
height: AVI_SIZE, height: SIZE,
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap, width: SIZE + (slice.length - 1) * a.gap_md.gap,
}, },
pressed && { pressed && {
opacity: 0.5, opacity: 0.5,
@@ -145,14 +153,14 @@ function KnownFollowersInner({
{ {
borderWidth: AVI_BORDER, borderWidth: AVI_BORDER,
borderColor: t.atoms.bg.backgroundColor, borderColor: t.atoms.bg.backgroundColor,
width: AVI_SIZE + AVI_BORDER * 2, width: SIZE + AVI_BORDER * 2,
height: AVI_SIZE + AVI_BORDER * 2, height: SIZE + AVI_BORDER * 2,
left: i * a.gap_md.gap, left: i * a.gap_md.gap,
zIndex: AVI_BORDER - i, zIndex: AVI_BORDER - i,
}, },
]}> ]}>
<UserAvatar <UserAvatar
size={AVI_SIZE} size={SIZE}
avatar={prof.avatar} avatar={prof.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
/> />
+3 -1
View File
@@ -166,7 +166,9 @@ export function NameAndHandle({
return ( return (
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> <Text
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
numberOfLines={1}>
{name} {name}
</Text> </Text>
<Text <Text
@@ -462,7 +462,8 @@ function Inner({
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}> <Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
<View style={[a.pb_sm, a.flex_1]}> <View style={[a.pb_sm, a.flex_1]}>
<Text style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold]}> <Text
style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold, a.self_start]}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
@@ -78,7 +78,13 @@ function WizardListCard({
/> />
<View style={[a.flex_1, a.gap_2xs]}> <View style={[a.flex_1, a.gap_2xs]}>
<Text <Text
style={[a.flex_1, a.font_bold, a.text_md, a.leading_tight]} style={[
a.flex_1,
a.font_bold,
a.text_md,
a.leading_tight,
a.self_start,
]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
+6 -1
View File
@@ -168,7 +168,12 @@ function HeaderReady({
</View> </View>
<View style={a.flex_1}> <View style={a.flex_1}>
<Text <Text
style={[a.text_md, a.font_bold, web(a.leading_normal)]} style={[
a.text_md,
a.font_bold,
a.self_start,
web(a.leading_normal),
]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
@@ -395,7 +395,7 @@ function ProfileCard({
/> />
<View style={[a.flex_1, a.gap_2xs]}> <View style={[a.flex_1, a.gap_2xs]}>
<Text <Text
style={[t.atoms.text, a.font_bold, a.leading_tight]} style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
+1
View File
@@ -1,6 +1,7 @@
export type Gate = export type Gate =
// Keep this alphabetic please. // Keep this alphabetic please.
| 'debug_show_feedcontext' | 'debug_show_feedcontext'
| 'explore_page_profile_card_social_proof'
| 'native_pwi_disabled' | 'native_pwi_disabled'
| 'new_user_guided_tour' | 'new_user_guided_tour'
| 'new_user_progress_guide' | 'new_user_progress_guide'
+1
View File
@@ -0,0 +1 @@
export const NON_BREAKING_SPACE = '\u00A0'
@@ -386,10 +386,10 @@ export function MessagesList({
data={convoState.items} data={convoState.items}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
containWeb={true} disableFullWindowScroll={true}
// Prevents wrong position in Firefox when sending a message // Prevents wrong position in Firefox when sending a message
// as well as scroll getting stuck on Chome when scrolling upwards. // as well as scroll getting stuck on Chome when scrolling upwards.
disableContentVisibility={true} disableContainStyle={true}
disableVirtualization={true} disableVirtualization={true}
style={animatedListStyle} style={animatedListStyle}
// The extra two items account for the header and the footer components // The extra two items account for the header and the footer components
+1 -1
View File
@@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({
<View pointerEvents="none"> <View pointerEvents="none">
<Text <Text
testID="profileHeaderDisplayName" testID="profileHeaderDisplayName"
style={[t.atoms.text, a.text_4xl, {fontWeight: '500'}]}> style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
+1 -1
View File
@@ -101,7 +101,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
onEndReachedThreshold={2} onEndReachedThreshold={2}
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />} renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
containWeb={true} disableFullWindowScroll={true}
sideBorders={false} sideBorders={false}
style={{flex: 1}} style={{flex: 1}}
ListEmptyComponent={ ListEmptyComponent={
@@ -80,7 +80,7 @@ export function StepProfiles({
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />} renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
containWeb={true} disableFullWindowScroll={true}
sideBorders={false} sideBorders={false}
style={[a.flex_1]} style={[a.flex_1]}
onEndReached={ onEndReached={
+2
View File
@@ -12,6 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger' import {logger} from '#/logger'
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
import {addSessionEventLog} from './logging'
import { import {
configureModerationForAccount, configureModerationForAccount,
configureModerationForGuest, configureModerationForGuest,
@@ -194,6 +195,7 @@ async function prepareAgent(
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
agent.setPersistSessionHandler(event => { agent.setPersistSessionHandler(event => {
onSessionChange(agent, account.did, event) onSessionChange(agent, account.did, event)
addSessionEventLog(account.did, event)
}) })
return {agent, account} return {agent, account}
} }
+13 -1
View File
@@ -1,4 +1,4 @@
import {AtpSessionData} from '@atproto/api' import {AtpSessionData, AtpSessionEvent} from '@atproto/api'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
@@ -70,6 +70,18 @@ export function wrapSessionReducerForLogging(reducer: Reducer): Reducer {
let nextMessageIndex = 0 let nextMessageIndex = 0
const MAX_SLICE_LENGTH = 1000 const MAX_SLICE_LENGTH = 1000
// Not gated.
export function addSessionEventLog(did: string, event: AtpSessionEvent) {
try {
if (!Statsig.initializeCalled() || !Statsig.getStableID()) {
return
}
Statsig.logEvent('session:event', null, {did, event})
} catch (e) {
console.error(e)
}
}
export function addSessionDebugLog(log: Log) { export function addSessionDebugLog(log: Log) {
console.log(new Date(), log) console.log(new Date(), log)
try { try {
+5 -2
View File
@@ -58,6 +58,7 @@ import {useNavigation} from '@react-navigation/native'
import {parseTenorGif} from '#/lib/strings/embed-player' import {parseTenorGif} from '#/lib/strings/embed-player'
import {logger} from '#/logger' import {logger} from '#/logger'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {forceLTR} from 'lib/strings/bidi'
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const' import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
import {useAgent} from 'state/session' import {useAgent} from 'state/session'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -274,13 +275,15 @@ let FeedItem = ({
showDmButton={item.type === 'starterpack-joined' || isFollowBack} showDmButton={item.type === 'starterpack-joined' || isFollowBack}
/> />
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} /> <ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
<Text style={styles.meta}> <Text style={[styles.meta, a.self_start]}>
<TextLink <TextLink
key={authors[0].href} key={authors[0].href}
style={[pal.text, s.bold]} style={[pal.text, s.bold]}
href={authors[0].href} href={authors[0].href}
text={sanitizeDisplayName( text={forceLTR(
sanitizeDisplayName(
authors[0].profile.displayName || authors[0].profile.handle, authors[0].profile.displayName || authors[0].profile.handle,
),
)} )}
disableMismatchWarning disableMismatchWarning
/> />
+1 -1
View File
@@ -281,7 +281,7 @@ let PostThreadItemLoaded = ({
<Link style={s.flex1} href={authorHref} title={authorTitle}> <Link style={s.flex1} href={authorHref} title={authorTitle}>
<Text <Text
type="xl-bold" type="xl-bold"
style={[pal.text]} style={[pal.text, a.self_start]}
numberOfLines={1} numberOfLines={1}
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(
+35 -64
View File
@@ -5,7 +5,6 @@ import {
moderateProfile, moderateProfile,
ModerationDecision, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {Trans} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -19,12 +18,16 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile' import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {FollowButton} from './FollowButton' import {FollowButton} from './FollowButton'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
import {atoms as a} from '#/alf'
import * as Pills from '#/components/Pills' import * as Pills from '#/components/Pills'
export function ProfileCard({ export function ProfileCard({
@@ -33,22 +36,22 @@ export function ProfileCard({
noModFilter, noModFilter,
noBg, noBg,
noBorder, noBorder,
followers,
renderButton, renderButton,
onPress, onPress,
style, style,
showKnownFollowers,
}: { }: {
testID?: string testID?: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
noModFilter?: boolean noModFilter?: boolean
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
renderButton?: ( renderButton?: (
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>, profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
) => React.ReactNode ) => React.ReactNode
onPress?: () => void onPress?: () => void
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showKnownFollowers?: boolean
}) { }) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const pal = usePalette('default') const pal = usePalette('default')
@@ -70,6 +73,11 @@ export function ProfileCard({
return null return null
} }
const knownFollowersVisible =
showKnownFollowers &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
moderationOpts
return ( return (
<Link <Link
testID={testID} testID={testID}
@@ -97,7 +105,7 @@ export function ProfileCard({
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text <Text
type="lg" type="lg"
style={[s.bold, pal.text]} style={[s.bold, pal.text, a.self_start]}
numberOfLines={1} numberOfLines={1}
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(
@@ -118,14 +126,30 @@ export function ProfileCard({
<View style={styles.layoutButton}>{renderButton(profile)}</View> <View style={styles.layoutButton}>{renderButton(profile)}</View>
) : undefined} ) : undefined}
</View> </View>
{profile.description ? ( {profile.description || knownFollowersVisible ? (
<View style={styles.details}> <View style={styles.details}>
{profile.description ? (
<Text style={pal.text} numberOfLines={4}> <Text style={pal.text} numberOfLines={4}>
{profile.description as string} {profile.description as string}
</Text> </Text>
) : null}
{knownFollowersVisible ? (
<View
style={[
a.flex_row,
a.align_center,
a.gap_sm,
!!profile.description && a.mt_md,
]}>
<KnownFollowers
minimal
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
) : null}
</View> </View>
) : null} ) : null}
<FollowersList followers={followers} />
</Link> </Link>
) )
} }
@@ -155,73 +179,20 @@ export function ProfileCardPills({
) )
} }
function FollowersList({
followers,
}: {
followers?: AppBskyActorDefs.ProfileView[] | undefined
}) {
const pal = usePalette('default')
const moderationOpts = useModerationOpts()
const followersWithMods = React.useMemo(() => {
if (!followers || !moderationOpts) {
return []
}
return followers
.map(f => ({
f,
mod: moderateProfile(f, moderationOpts),
}))
.filter(({mod}) => !mod.ui('profileList').filter)
}, [followers, moderationOpts])
if (!followersWithMods?.length) {
return null
}
return (
<View style={styles.followedBy}>
<Text
type="sm"
style={[styles.followsByDesc, pal.textLight]}
numberOfLines={2}
lineHeight={1.2}>
<Trans>
Followed by{' '}
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
</Trans>
</Text>
{followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}>
<View style={[styles.followedByAvi, pal.view]}>
<PreviewableUserAvatar
size={32}
profile={f}
moderation={mod.ui('avatar')}
type={f.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
</View>
))}
</View>
)
}
export function ProfileCardWithFollowBtn({ export function ProfileCardWithFollowBtn({
profile, profile,
noBg, noBg,
noBorder, noBorder,
followers,
onPress, onPress,
logContext = 'ProfileCard', logContext = 'ProfileCard',
showKnownFollowers,
}: { }: {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileView
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
onPress?: () => void onPress?: () => void
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
showKnownFollowers?: boolean
}) { }) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did const isMe = profile.did === currentAccount?.did
@@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({
profile={profile} profile={profile}
noBg={noBg} noBg={noBg}
noBorder={noBorder} noBorder={noBorder}
followers={followers}
renderButton={ renderButton={
isMe isMe
? undefined ? undefined
@@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({
) )
} }
onPress={onPress} onPress={onPress}
showKnownFollowers={!isMe && showKnownFollowers}
/> />
) )
} }
+3 -2
View File
@@ -24,11 +24,12 @@ export type ListProps<ItemT> = Omit<
refreshing?: boolean refreshing?: boolean
onRefresh?: () => void onRefresh?: () => void
onItemSeen?: (item: ItemT) => void onItemSeen?: (item: ItemT) => void
containWeb?: boolean
desktopFixedHeight?: number | boolean desktopFixedHeight?: number | boolean
// Web only prop to contain the scroll to the container rather than the window
disableFullWindowScroll?: boolean
sideBorders?: boolean sideBorders?: boolean
// Web only prop to disable a perf optimization (which would otherwise be on). // Web only prop to disable a perf optimization (which would otherwise be on).
disableContentVisibility?: boolean disableContainStyle?: boolean
} }
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
+25 -22
View File
@@ -23,9 +23,11 @@ export type ListProps<ItemT> = Omit<
onRefresh?: () => void onRefresh?: () => void
onItemSeen?: (item: ItemT) => void onItemSeen?: (item: ItemT) => void
desktopFixedHeight?: number | boolean desktopFixedHeight?: number | boolean
containWeb?: boolean // Web only prop to contain the scroll to the container rather than the window
disableFullWindowScroll?: boolean
sideBorders?: boolean sideBorders?: boolean
disableContentVisibility?: boolean // Web only prop to disable a perf optimization (which would otherwise be on).
disableContainStyle?: boolean
} }
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
@@ -39,7 +41,7 @@ function ListImpl<ItemT>(
ListHeaderComponent, ListHeaderComponent,
ListFooterComponent, ListFooterComponent,
ListEmptyComponent, ListEmptyComponent,
containWeb, disableFullWindowScroll,
contentContainerStyle, contentContainerStyle,
data, data,
desktopFixedHeight, desktopFixedHeight,
@@ -58,7 +60,7 @@ function ListImpl<ItemT>(
extraData, extraData,
style, style,
sideBorders = true, sideBorders = true,
disableContentVisibility, disableContainStyle,
...props ...props
}: ListProps<ItemT>, }: ListProps<ItemT>,
ref: React.Ref<ListMethods>, ref: React.Ref<ListMethods>,
@@ -112,7 +114,7 @@ function ListImpl<ItemT>(
} }
const getScrollableNode = React.useCallback(() => { const getScrollableNode = React.useCallback(() => {
if (containWeb) { if (disableFullWindowScroll) {
const element = nativeRef.current as HTMLDivElement | null const element = nativeRef.current as HTMLDivElement | null
if (!element) return if (!element) return
@@ -182,7 +184,7 @@ function ListImpl<ItemT>(
}, },
} }
} }
}, [containWeb]) }, [disableFullWindowScroll])
const nativeRef = React.useRef<HTMLDivElement>(null) const nativeRef = React.useRef<HTMLDivElement>(null)
React.useImperativeHandle( React.useImperativeHandle(
@@ -267,7 +269,12 @@ function ListImpl<ItemT>(
return () => { return () => {
element?.removeEventListener('scroll', handleScroll) element?.removeEventListener('scroll', handleScroll)
} }
}, [isInsideVisibleTree, handleScroll, containWeb, getScrollableNode]) }, [
isInsideVisibleTree,
handleScroll,
disableFullWindowScroll,
getScrollableNode,
])
// --- onScrolledDownChange --- // --- onScrolledDownChange ---
const isScrolledDown = useRef(false) const isScrolledDown = useRef(false)
@@ -308,7 +315,7 @@ function ListImpl<ItemT>(
{...props} {...props}
style={[ style={[
style, style,
containWeb && { disableFullWindowScroll && {
flex: 1, flex: 1,
// @ts-expect-error web only // @ts-expect-error web only
'overflow-y': 'scroll', 'overflow-y': 'scroll',
@@ -332,13 +339,13 @@ function ListImpl<ItemT>(
pal.border, pal.border,
]}> ]}>
<Visibility <Visibility
root={containWeb ? nativeRef : null} root={disableFullWindowScroll ? nativeRef : null}
onVisibleChange={handleAboveTheFoldVisibleChange} onVisibleChange={handleAboveTheFoldVisibleChange}
style={[styles.aboveTheFoldDetector, {height: headerOffset}]} style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
/> />
{onStartReached && !isEmpty && ( {onStartReached && !isEmpty && (
<Visibility <Visibility
root={containWeb ? nativeRef : null} root={disableFullWindowScroll ? nativeRef : null}
onVisibleChange={onHeadVisibilityChange} onVisibleChange={onHeadVisibilityChange}
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
/> />
@@ -356,13 +363,13 @@ function ListImpl<ItemT>(
renderItem={renderItem} renderItem={renderItem}
extraData={extraData} extraData={extraData}
onItemSeen={onItemSeen} onItemSeen={onItemSeen}
disableContentVisibility={disableContentVisibility} disableContainStyle={disableContainStyle}
/> />
) )
})} })}
{onEndReached && !isEmpty && ( {onEndReached && !isEmpty && (
<Visibility <Visibility
root={containWeb ? nativeRef : null} root={disableFullWindowScroll ? nativeRef : null}
onVisibleChange={onTailVisibilityChange} onVisibleChange={onTailVisibilityChange}
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
key={data?.length} key={data?.length}
@@ -406,7 +413,7 @@ let Row = function RowImpl<ItemT>({
renderItem, renderItem,
extraData: _unused, extraData: _unused,
onItemSeen, onItemSeen,
disableContentVisibility, disableContainStyle,
}: { }: {
item: ItemT item: ItemT
index: number index: number
@@ -416,7 +423,7 @@ let Row = function RowImpl<ItemT>({
| ((data: {index: number; item: any; separators: any}) => React.ReactNode) | ((data: {index: number; item: any; separators: any}) => React.ReactNode)
extraData: any extraData: any
onItemSeen: ((item: any) => void) | undefined onItemSeen: ((item: any) => void) | undefined
disableContentVisibility?: boolean disableContainStyle?: boolean
}): React.ReactNode { }): React.ReactNode {
const rowRef = React.useRef(null) const rowRef = React.useRef(null)
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined) const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
@@ -465,14 +472,10 @@ let Row = function RowImpl<ItemT>({
return null return null
} }
const shouldDisableContentVisibility = disableContentVisibility || isSafari const shouldDisableContainStyle = disableContainStyle || isSafari
return ( return (
<View <View
style={ style={shouldDisableContainStyle ? undefined : styles.contain}
shouldDisableContentVisibility
? undefined
: styles.contentVisibilityAuto
}
ref={rowRef}> ref={rowRef}>
{renderItem({item, index, separators: null as any})} {renderItem({item, index, separators: null as any})}
</View> </View>
@@ -544,9 +547,9 @@ const styles = StyleSheet.create({
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}, },
contentVisibilityAuto: { contain: {
// @ts-ignore web only // @ts-ignore web only
contentVisibility: 'auto', contain: 'layout paint',
}, },
minHeightViewport: { minHeightViewport: {
// @ts-ignore web only // @ts-ignore web only
+5 -7
View File
@@ -6,6 +6,8 @@ import {useQueryClient} from '@tanstack/react-query'
import {precacheProfile} from '#/state/queries/profile' import {precacheProfile} from '#/state/queries/profile'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {forceLTR} from 'lib/strings/bidi'
import {NON_BREAKING_SPACE} from 'lib/strings/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {niceDate} from 'lib/strings/time' import {niceDate} from 'lib/strings/time'
@@ -32,8 +34,6 @@ interface PostMetaOpts {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
} }
const NON_BREAKING_SPACE = '\u00A0'
let PostMeta = (opts: PostMetaOpts): React.ReactNode => { let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const displayName = opts.author.displayName || opts.author.handle const displayName = opts.author.displayName || opts.author.handle
@@ -70,14 +70,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
style={[pal.text]} style={[pal.text]}
lineHeight={1.2} lineHeight={1.2}
disableMismatchWarning disableMismatchWarning
text={ text={forceLTR(
<> sanitizeDisplayName(
{sanitizeDisplayName(
displayName, displayName,
opts.moderation?.ui('displayName'), opts.moderation?.ui('displayName'),
),
)} )}
</>
}
href={profileLink} href={profileLink}
onBeforePress={onBeforePressAuthor} onBeforePress={onBeforePressAuthor}
/> />
+8 -4
View File
@@ -131,7 +131,7 @@ export function PostEmbeds({
const {alt, thumb, aspectRatio} = images[0] const {alt, thumb, aspectRatio} = images[0]
return ( return (
<ContentHider modui={moderation?.ui('contentMedia')}> <ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}> <View style={[styles.container, style]}>
<AutoSizedImage <AutoSizedImage
alt={alt} alt={alt}
uri={thumb} uri={thumb}
@@ -156,7 +156,7 @@ export function PostEmbeds({
return ( return (
<ContentHider modui={moderation?.ui('contentMedia')}> <ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}> <View style={[styles.container, style]}>
<ImageLayoutGrid <ImageLayoutGrid
images={embed.images} images={embed.images}
onPress={_openLightbox} onPress={_openLightbox}
@@ -174,7 +174,11 @@ export function PostEmbeds({
const link = embed.external const link = embed.external
return ( return (
<ContentHider modui={moderation?.ui('contentMedia')}> <ContentHider modui={moderation?.ui('contentMedia')}>
<ExternalLinkEmbed link={link} onOpen={onOpen} style={style} /> <ExternalLinkEmbed
link={link}
onOpen={onOpen}
style={[styles.container, style]}
/>
</ContentHider> </ContentHider>
) )
} }
@@ -183,7 +187,7 @@ export function PostEmbeds({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
imagesContainer: { container: {
marginTop: 8, marginTop: 8,
}, },
altContainer: { altContainer: {
+12 -3
View File
@@ -10,6 +10,7 @@ import {
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -241,7 +242,7 @@ type ExploreScreenItems =
| { | {
type: 'profile' type: 'profile'
key: string key: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileView
} }
| { | {
type: 'feed' type: 'feed'
@@ -291,6 +292,7 @@ export function Explore() {
error: feedsError, error: feedsError,
fetchNextPage: fetchNextFeedsPage, fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 10}) } = useGetPopularFeedsQuery({limit: 10})
const gate = useGate()
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
const onLoadMoreProfiles = React.useCallback(async () => { const onLoadMoreProfiles = React.useCallback(async () => {
@@ -492,7 +494,14 @@ export function Explore() {
case 'profile': { case 'profile': {
return ( return (
<View style={[a.border_b, t.atoms.border_contrast_low]}> <View style={[a.border_b, t.atoms.border_contrast_low]}>
<ProfileCardWithFollowBtn profile={item.profile} noBg noBorder /> <ProfileCardWithFollowBtn
profile={item.profile}
noBg
noBorder
showKnownFollowers={gate(
'explore_page_profile_card_social_proof',
)}
/>
</View> </View>
) )
} }
@@ -555,7 +564,7 @@ export function Explore() {
} }
} }
}, },
[t, moderationOpts], [t, moderationOpts, gate],
) )
return ( return (
+2 -1
View File
@@ -68,6 +68,7 @@ import {navigate, resetToTab} from '#/Navigation'
import {Email2FAToggle} from './Email2FAToggle' import {Email2FAToggle} from './Email2FAToggle'
import {ExportCarDialog} from './ExportCarDialog' import {ExportCarDialog} from './ExportCarDialog'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
import {atoms as a} from '#/alf'
function SettingsAccountCard({ function SettingsAccountCard({
account, account,
@@ -104,7 +105,7 @@ function SettingsAccountCard({
/> />
</View> </View>
<View style={[s.flex1]}> <View style={[s.flex1]}>
<Text type="md-bold" style={pal.text} numberOfLines={1}> <Text type="md-bold" style={[pal.text, a.self_start]} numberOfLines={1}>
{profile?.displayName || account.handle} {profile?.displayName || account.handle}
</Text> </Text>
<Text type="sm" style={pal.textLight} numberOfLines={1}> <Text type="sm" style={pal.textLight} numberOfLines={1}>
+1 -1
View File
@@ -47,7 +47,7 @@ export function ListContained() {
) )
}} }}
keyExtractor={item => item.id.toString()} keyExtractor={item => item.id.toString()}
containWeb={true} disableFullWindowScroll={true}
style={{flex: 1}} style={{flex: 1}}
onStartReached={() => { onStartReached={() => {
console.log('Start Reached') console.log('Start Reached')
+2 -1
View File
@@ -30,6 +30,7 @@ import {precacheProfile} from 'state/queries/profile'
import {Link} from '#/view/com/util/Link' import {Link} from '#/view/com/util/Link'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {atoms as a} from '#/alf'
let SearchLinkCard = ({ let SearchLinkCard = ({
label, label,
@@ -127,7 +128,7 @@ let SearchProfileCard = ({
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Text <Text
type="lg" type="lg"
style={[s.bold, pal.text]} style={[s.bold, pal.text, a.self_start]}
numberOfLines={1} numberOfLines={1}
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(