Merge branch 'log-session-events' into session-stress
This commit is contained in:
@@ -10,7 +10,7 @@ Get the app itself:
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
- Stay away from PRs like...
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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?
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
const AVI_BORDER = 1
|
||||
|
||||
/**
|
||||
@@ -30,10 +31,12 @@ export function KnownFollowers({
|
||||
profile,
|
||||
moderationOpts,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||
new Map(),
|
||||
@@ -59,6 +62,7 @@ export function KnownFollowers({
|
||||
cachedKnownFollowers={cachedKnownFollowers}
|
||||
moderationOpts={moderationOpts}
|
||||
onLinkPress={onLinkPress}
|
||||
minimal={minimal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -71,11 +75,13 @@ function KnownFollowersInner({
|
||||
moderationOpts,
|
||||
cachedKnownFollowers,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -110,6 +116,8 @@ function KnownFollowersInner({
|
||||
*/
|
||||
if (slice.length === 0) return null
|
||||
|
||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||
|
||||
return (
|
||||
<Link
|
||||
label={_(
|
||||
@@ -120,7 +128,7 @@ function KnownFollowersInner({
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
minimal ? a.gap_sm : a.gap_md,
|
||||
a.align_center,
|
||||
{marginLeft: -AVI_BORDER},
|
||||
]}>
|
||||
@@ -129,8 +137,8 @@ function KnownFollowersInner({
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: AVI_SIZE,
|
||||
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
height: SIZE,
|
||||
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
},
|
||||
pressed && {
|
||||
opacity: 0.5,
|
||||
@@ -145,14 +153,14 @@ function KnownFollowersInner({
|
||||
{
|
||||
borderWidth: AVI_BORDER,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
width: AVI_SIZE + AVI_BORDER * 2,
|
||||
height: AVI_SIZE + AVI_BORDER * 2,
|
||||
width: SIZE + AVI_BORDER * 2,
|
||||
height: SIZE + AVI_BORDER * 2,
|
||||
left: i * a.gap_md.gap,
|
||||
zIndex: AVI_BORDER - i,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={AVI_SIZE}
|
||||
size={SIZE}
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
|
||||
@@ -166,7 +166,9 @@ export function NameAndHandle({
|
||||
|
||||
return (
|
||||
<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}
|
||||
</Text>
|
||||
<Text
|
||||
|
||||
@@ -462,7 +462,8 @@ function Inner({
|
||||
|
||||
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
|
||||
<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(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
|
||||
@@ -78,7 +78,13 @@ function WizardListCard({
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<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}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -168,7 +168,12 @@ function HeaderReady({
|
||||
</View>
|
||||
<View style={a.flex_1}>
|
||||
<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}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -395,7 +395,7 @@ function ProfileCard({
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<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}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext'
|
||||
| 'explore_page_profile_card_social_proof'
|
||||
| 'native_pwi_disabled'
|
||||
| 'new_user_guided_tour'
|
||||
| 'new_user_progress_guide'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const NON_BREAKING_SPACE = '\u00A0'
|
||||
@@ -386,10 +386,10 @@ export function MessagesList({
|
||||
data={convoState.items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
// Prevents wrong position in Firefox when sending a message
|
||||
// as well as scroll getting stuck on Chome when scrolling upwards.
|
||||
disableContentVisibility={true}
|
||||
disableContainStyle={true}
|
||||
disableVirtualization={true}
|
||||
style={animatedListStyle}
|
||||
// The extra two items account for the header and the footer components
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({
|
||||
<View pointerEvents="none">
|
||||
<Text
|
||||
testID="profileHeaderDisplayName"
|
||||
style={[t.atoms.text, a.text_4xl, {fontWeight: '500'}]}>
|
||||
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
|
||||
@@ -101,7 +101,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||
onEndReachedThreshold={2}
|
||||
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
sideBorders={false}
|
||||
style={{flex: 1}}
|
||||
ListEmptyComponent={
|
||||
|
||||
@@ -80,7 +80,7 @@ export function StepProfiles({
|
||||
keyExtractor={keyExtractor}
|
||||
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
sideBorders={false}
|
||||
style={[a.flex_1]}
|
||||
onEndReached={
|
||||
|
||||
@@ -12,6 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
|
||||
import {addSessionEventLog} from './logging'
|
||||
import {
|
||||
configureModerationForAccount,
|
||||
configureModerationForGuest,
|
||||
@@ -194,6 +195,7 @@ async function prepareAgent(
|
||||
const account = agentToSessionAccountOrThrow(agent)
|
||||
agent.setPersistSessionHandler(event => {
|
||||
onSessionChange(agent, account.did, event)
|
||||
addSessionEventLog(account.did, event)
|
||||
})
|
||||
return {agent, account}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AtpSessionData} from '@atproto/api'
|
||||
import {AtpSessionData, AtpSessionEvent} from '@atproto/api'
|
||||
import {sha256} from 'js-sha256'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
|
||||
@@ -70,6 +70,18 @@ export function wrapSessionReducerForLogging(reducer: Reducer): Reducer {
|
||||
let nextMessageIndex = 0
|
||||
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) {
|
||||
console.log(new Date(), log)
|
||||
try {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
||||
import {logger} from '#/logger'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {forceLTR} from 'lib/strings/bidi'
|
||||
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
|
||||
import {useAgent} from 'state/session'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -274,13 +275,15 @@ let FeedItem = ({
|
||||
showDmButton={item.type === 'starterpack-joined' || isFollowBack}
|
||||
/>
|
||||
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
|
||||
<Text style={styles.meta}>
|
||||
<Text style={[styles.meta, a.self_start]}>
|
||||
<TextLink
|
||||
key={authors[0].href}
|
||||
style={[pal.text, s.bold]}
|
||||
href={authors[0].href}
|
||||
text={sanitizeDisplayName(
|
||||
authors[0].profile.displayName || authors[0].profile.handle,
|
||||
text={forceLTR(
|
||||
sanitizeDisplayName(
|
||||
authors[0].profile.displayName || authors[0].profile.handle,
|
||||
),
|
||||
)}
|
||||
disableMismatchWarning
|
||||
/>
|
||||
|
||||
@@ -281,7 +281,7 @@ let PostThreadItemLoaded = ({
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
type="xl-bold"
|
||||
style={[pal.text]}
|
||||
style={[pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
moderateProfile,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
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 {s} from 'lib/styles'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {
|
||||
KnownFollowers,
|
||||
shouldShowKnownFollowers,
|
||||
} from '#/components/KnownFollowers'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {FollowButton} from './FollowButton'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Pills from '#/components/Pills'
|
||||
|
||||
export function ProfileCard({
|
||||
@@ -33,22 +36,22 @@ export function ProfileCard({
|
||||
noModFilter,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
renderButton,
|
||||
onPress,
|
||||
style,
|
||||
showKnownFollowers,
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
noModFilter?: boolean
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
renderButton?: (
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
|
||||
) => React.ReactNode
|
||||
onPress?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
showKnownFollowers?: boolean
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const pal = usePalette('default')
|
||||
@@ -70,6 +73,11 @@ export function ProfileCard({
|
||||
return null
|
||||
}
|
||||
|
||||
const knownFollowersVisible =
|
||||
showKnownFollowers &&
|
||||
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
|
||||
moderationOpts
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
@@ -97,7 +105,7 @@ export function ProfileCard({
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
style={[s.bold, pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
@@ -118,14 +126,30 @@ export function ProfileCard({
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
{profile.description ? (
|
||||
{profile.description || knownFollowersVisible ? (
|
||||
<View style={styles.details}>
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
{profile.description ? (
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</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>
|
||||
) : null}
|
||||
<FollowersList followers={followers} />
|
||||
</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({
|
||||
profile,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
onPress,
|
||||
logContext = 'ProfileCard',
|
||||
showKnownFollowers,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
onPress?: () => void
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
showKnownFollowers?: boolean
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
@@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({
|
||||
profile={profile}
|
||||
noBg={noBg}
|
||||
noBorder={noBorder}
|
||||
followers={followers}
|
||||
renderButton={
|
||||
isMe
|
||||
? undefined
|
||||
@@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
showKnownFollowers={!isMe && showKnownFollowers}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ export type ListProps<ItemT> = Omit<
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
containWeb?: boolean
|
||||
desktopFixedHeight?: number | boolean
|
||||
// Web only prop to contain the scroll to the container rather than the window
|
||||
disableFullWindowScroll?: boolean
|
||||
sideBorders?: boolean
|
||||
// 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>
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ export type ListProps<ItemT> = Omit<
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
desktopFixedHeight?: number | boolean
|
||||
containWeb?: boolean
|
||||
// Web only prop to contain the scroll to the container rather than the window
|
||||
disableFullWindowScroll?: 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.
|
||||
|
||||
@@ -39,7 +41,7 @@ function ListImpl<ItemT>(
|
||||
ListHeaderComponent,
|
||||
ListFooterComponent,
|
||||
ListEmptyComponent,
|
||||
containWeb,
|
||||
disableFullWindowScroll,
|
||||
contentContainerStyle,
|
||||
data,
|
||||
desktopFixedHeight,
|
||||
@@ -58,7 +60,7 @@ function ListImpl<ItemT>(
|
||||
extraData,
|
||||
style,
|
||||
sideBorders = true,
|
||||
disableContentVisibility,
|
||||
disableContainStyle,
|
||||
...props
|
||||
}: ListProps<ItemT>,
|
||||
ref: React.Ref<ListMethods>,
|
||||
@@ -112,7 +114,7 @@ function ListImpl<ItemT>(
|
||||
}
|
||||
|
||||
const getScrollableNode = React.useCallback(() => {
|
||||
if (containWeb) {
|
||||
if (disableFullWindowScroll) {
|
||||
const element = nativeRef.current as HTMLDivElement | null
|
||||
if (!element) return
|
||||
|
||||
@@ -182,7 +184,7 @@ function ListImpl<ItemT>(
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [containWeb])
|
||||
}, [disableFullWindowScroll])
|
||||
|
||||
const nativeRef = React.useRef<HTMLDivElement>(null)
|
||||
React.useImperativeHandle(
|
||||
@@ -267,7 +269,12 @@ function ListImpl<ItemT>(
|
||||
return () => {
|
||||
element?.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [isInsideVisibleTree, handleScroll, containWeb, getScrollableNode])
|
||||
}, [
|
||||
isInsideVisibleTree,
|
||||
handleScroll,
|
||||
disableFullWindowScroll,
|
||||
getScrollableNode,
|
||||
])
|
||||
|
||||
// --- onScrolledDownChange ---
|
||||
const isScrolledDown = useRef(false)
|
||||
@@ -308,7 +315,7 @@ function ListImpl<ItemT>(
|
||||
{...props}
|
||||
style={[
|
||||
style,
|
||||
containWeb && {
|
||||
disableFullWindowScroll && {
|
||||
flex: 1,
|
||||
// @ts-expect-error web only
|
||||
'overflow-y': 'scroll',
|
||||
@@ -332,13 +339,13 @@ function ListImpl<ItemT>(
|
||||
pal.border,
|
||||
]}>
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||
/>
|
||||
{onStartReached && !isEmpty && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onHeadVisibilityChange}
|
||||
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
|
||||
/>
|
||||
@@ -356,13 +363,13 @@ function ListImpl<ItemT>(
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
disableContentVisibility={disableContentVisibility}
|
||||
disableContainStyle={disableContainStyle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{onEndReached && !isEmpty && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onTailVisibilityChange}
|
||||
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||
key={data?.length}
|
||||
@@ -406,7 +413,7 @@ let Row = function RowImpl<ItemT>({
|
||||
renderItem,
|
||||
extraData: _unused,
|
||||
onItemSeen,
|
||||
disableContentVisibility,
|
||||
disableContainStyle,
|
||||
}: {
|
||||
item: ItemT
|
||||
index: number
|
||||
@@ -416,7 +423,7 @@ let Row = function RowImpl<ItemT>({
|
||||
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||
extraData: any
|
||||
onItemSeen: ((item: any) => void) | undefined
|
||||
disableContentVisibility?: boolean
|
||||
disableContainStyle?: boolean
|
||||
}): React.ReactNode {
|
||||
const rowRef = React.useRef(null)
|
||||
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
|
||||
@@ -465,14 +472,10 @@ let Row = function RowImpl<ItemT>({
|
||||
return null
|
||||
}
|
||||
|
||||
const shouldDisableContentVisibility = disableContentVisibility || isSafari
|
||||
const shouldDisableContainStyle = disableContainStyle || isSafari
|
||||
return (
|
||||
<View
|
||||
style={
|
||||
shouldDisableContentVisibility
|
||||
? undefined
|
||||
: styles.contentVisibilityAuto
|
||||
}
|
||||
style={shouldDisableContainStyle ? undefined : styles.contain}
|
||||
ref={rowRef}>
|
||||
{renderItem({item, index, separators: null as any})}
|
||||
</View>
|
||||
@@ -544,9 +547,9 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
contentVisibilityAuto: {
|
||||
contain: {
|
||||
// @ts-ignore web only
|
||||
contentVisibility: 'auto',
|
||||
contain: 'layout paint',
|
||||
},
|
||||
minHeightViewport: {
|
||||
// @ts-ignore web only
|
||||
|
||||
@@ -6,6 +6,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
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 {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
@@ -32,8 +34,6 @@ interface PostMetaOpts {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
const NON_BREAKING_SPACE = '\u00A0'
|
||||
|
||||
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const displayName = opts.author.displayName || opts.author.handle
|
||||
@@ -70,14 +70,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
style={[pal.text]}
|
||||
lineHeight={1.2}
|
||||
disableMismatchWarning
|
||||
text={
|
||||
<>
|
||||
{sanitizeDisplayName(
|
||||
displayName,
|
||||
opts.moderation?.ui('displayName'),
|
||||
)}
|
||||
</>
|
||||
}
|
||||
text={forceLTR(
|
||||
sanitizeDisplayName(
|
||||
displayName,
|
||||
opts.moderation?.ui('displayName'),
|
||||
),
|
||||
)}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
/>
|
||||
|
||||
@@ -131,7 +131,7 @@ export function PostEmbeds({
|
||||
const {alt, thumb, aspectRatio} = images[0]
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<View style={[styles.container, style]}>
|
||||
<AutoSizedImage
|
||||
alt={alt}
|
||||
uri={thumb}
|
||||
@@ -156,7 +156,7 @@ export function PostEmbeds({
|
||||
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<View style={[styles.container, style]}>
|
||||
<ImageLayoutGrid
|
||||
images={embed.images}
|
||||
onPress={_openLightbox}
|
||||
@@ -174,7 +174,11 @@ export function PostEmbeds({
|
||||
const link = embed.external
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<ExternalLinkEmbed link={link} onOpen={onOpen} style={style} />
|
||||
<ExternalLinkEmbed
|
||||
link={link}
|
||||
onOpen={onOpen}
|
||||
style={[styles.container, style]}
|
||||
/>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -183,7 +187,7 @@ export function PostEmbeds({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imagesContainer: {
|
||||
container: {
|
||||
marginTop: 8,
|
||||
},
|
||||
altContainer: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -241,7 +242,7 @@ type ExploreScreenItems =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
@@ -291,6 +292,7 @@ export function Explore() {
|
||||
error: feedsError,
|
||||
fetchNextPage: fetchNextFeedsPage,
|
||||
} = useGetPopularFeedsQuery({limit: 10})
|
||||
const gate = useGate()
|
||||
|
||||
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
|
||||
const onLoadMoreProfiles = React.useCallback(async () => {
|
||||
@@ -492,7 +494,14 @@ export function Explore() {
|
||||
case 'profile': {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -555,7 +564,7 @@ export function Explore() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[t, moderationOpts],
|
||||
[t, moderationOpts, gate],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -68,6 +68,7 @@ import {navigate, resetToTab} from '#/Navigation'
|
||||
import {Email2FAToggle} from './Email2FAToggle'
|
||||
import {ExportCarDialog} from './ExportCarDialog'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
function SettingsAccountCard({
|
||||
account,
|
||||
@@ -104,7 +105,7 @@ function SettingsAccountCard({
|
||||
/>
|
||||
</View>
|
||||
<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}
|
||||
</Text>
|
||||
<Text type="sm" style={pal.textLight} numberOfLines={1}>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ListContained() {
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
style={{flex: 1}}
|
||||
onStartReached={() => {
|
||||
console.log('Start Reached')
|
||||
|
||||
@@ -30,6 +30,7 @@ import {precacheProfile} from 'state/queries/profile'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
let SearchLinkCard = ({
|
||||
label,
|
||||
@@ -127,7 +128,7 @@ let SearchProfileCard = ({
|
||||
<View style={{flex: 1}}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
style={[s.bold, pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
|
||||
Reference in New Issue
Block a user