Post UI updates (Profile Preview on mobile) (#990)
* Update postmeta to put the timestamp on the right side on mobile * Drop the two-line PostMeta mode * Add ProfilePreview modal * Tune PostMeta to give the best behavior possible for a given platform * Remove old showFollowBtn attributes * Fix style issue * Switch the follow button in the profile header to use the inverted color for consistency with the rest of the app * Fix lint * Fix darkmode * Tune the profile preview footer * Better analytics choicezio/stable
parent
df7552135a
commit
6f69157269
|
@ -129,6 +129,7 @@ interface ScreenPropertiesMap {
|
||||||
Feed: {}
|
Feed: {}
|
||||||
Notifications: {}
|
Notifications: {}
|
||||||
Profile: {}
|
Profile: {}
|
||||||
|
'Profile:Preview': {}
|
||||||
Settings: {}
|
Settings: {}
|
||||||
AppPasswords: {}
|
AppPasswords: {}
|
||||||
Moderation: {}
|
Moderation: {}
|
||||||
|
|
|
@ -31,6 +31,11 @@ export interface EditProfileModal {
|
||||||
onUpdate?: () => void
|
onUpdate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfilePreviewModal {
|
||||||
|
name: 'profile-preview'
|
||||||
|
did: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerInputModal {
|
export interface ServerInputModal {
|
||||||
name: 'server-input'
|
name: 'server-input'
|
||||||
initialService: string
|
initialService: string
|
||||||
|
@ -128,6 +133,7 @@ export type Modal =
|
||||||
| ChangeHandleModal
|
| ChangeHandleModal
|
||||||
| DeleteAccountModal
|
| DeleteAccountModal
|
||||||
| EditProfileModal
|
| EditProfileModal
|
||||||
|
| ProfilePreviewModal
|
||||||
|
|
||||||
// Curation
|
// Curation
|
||||||
| ContentFilteringSettingsModal
|
| ContentFilteringSettingsModal
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
import * as ConfirmModal from './Confirm'
|
import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
|
import * as ProfilePreviewModal from './ProfilePreview'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './report/ReportPost'
|
import * as ReportPostModal from './report/ReportPost'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
|
@ -62,6 +63,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'edit-profile') {
|
} else if (activeModal?.name === 'edit-profile') {
|
||||||
snapPoints = EditProfileModal.snapPoints
|
snapPoints = EditProfileModal.snapPoints
|
||||||
element = <EditProfileModal.Component {...activeModal} />
|
element = <EditProfileModal.Component {...activeModal} />
|
||||||
|
} else if (activeModal?.name === 'profile-preview') {
|
||||||
|
snapPoints = ProfilePreviewModal.snapPoints
|
||||||
|
element = <ProfilePreviewModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'server-input') {
|
} else if (activeModal?.name === 'server-input') {
|
||||||
snapPoints = ServerInputModal.snapPoints
|
snapPoints = ServerInputModal.snapPoints
|
||||||
element = <ServerInputModal.Component {...activeModal} />
|
element = <ServerInputModal.Component {...activeModal} />
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {isMobileWeb} from 'platform/detection'
|
||||||
|
|
||||||
import * as ConfirmModal from './Confirm'
|
import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
|
import * as ProfilePreviewModal from './ProfilePreview'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './report/ReportPost'
|
import * as ReportPostModal from './report/ReportPost'
|
||||||
import * as ReportAccountModal from './report/ReportAccount'
|
import * as ReportAccountModal from './report/ReportAccount'
|
||||||
|
@ -68,6 +69,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <ConfirmModal.Component {...modal} />
|
element = <ConfirmModal.Component {...modal} />
|
||||||
} else if (modal.name === 'edit-profile') {
|
} else if (modal.name === 'edit-profile') {
|
||||||
element = <EditProfileModal.Component {...modal} />
|
element = <EditProfileModal.Component {...modal} />
|
||||||
|
} else if (modal.name === 'profile-preview') {
|
||||||
|
element = <ProfilePreviewModal.Component {...modal} />
|
||||||
} else if (modal.name === 'server-input') {
|
} else if (modal.name === 'server-input') {
|
||||||
element = <ServerInputModal.Component {...modal} />
|
element = <ServerInputModal.Component {...modal} />
|
||||||
} else if (modal.name === 'report-post') {
|
} else if (modal.name === 'report-post') {
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {useNavigation, StackActions} from '@react-navigation/native'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {ProfileModel} from 'state/models/content/profile'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {ProfileHeader} from '../profile/ProfileHeader'
|
||||||
|
import {Button} from '../util/forms/Button'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
|
export const snapPoints = [560]
|
||||||
|
|
||||||
|
export const Component = observer(({did}: {did: string}) => {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const palInverted = usePalette('inverted')
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const [model] = useState(new ProfileModel(store, {actor: did}))
|
||||||
|
const {screen} = useAnalytics()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screen('Profile:Preview')
|
||||||
|
model.setup()
|
||||||
|
}, [model, screen])
|
||||||
|
|
||||||
|
const onPressViewProfile = useCallback(() => {
|
||||||
|
navigation.dispatch(StackActions.push('Profile', {name: model.handle}))
|
||||||
|
store.shell.closeModal()
|
||||||
|
}, [navigation, store, model])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={pal.view}>
|
||||||
|
<View style={styles.headerWrapper}>
|
||||||
|
<ProfileHeader view={model} hideBackButton onRefreshAll={() => {}} />
|
||||||
|
</View>
|
||||||
|
<View style={[styles.buttonsContainer, pal.view]}>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
type="inverted"
|
||||||
|
style={[styles.button, styles.buttonWide]}
|
||||||
|
onPress={onPressViewProfile}
|
||||||
|
accessibilityLabel="View profile"
|
||||||
|
accessibilityHint="">
|
||||||
|
<Text type="button-lg" style={palInverted.text}>
|
||||||
|
View Profile
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
style={styles.button}
|
||||||
|
onPress={() => store.shell.closeModal()}
|
||||||
|
accessibilityLabel="Close this preview"
|
||||||
|
accessibilityHint="">
|
||||||
|
<Text type="button-lg" style={pal.text}>
|
||||||
|
Close
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerWrapper: {
|
||||||
|
height: 440,
|
||||||
|
},
|
||||||
|
buttonsContainer: {
|
||||||
|
height: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 2,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
buttonWide: {
|
||||||
|
flex: 3,
|
||||||
|
},
|
||||||
|
})
|
|
@ -13,7 +13,7 @@ import {RichText} from '../util/text/RichText'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {PostDropdownBtn} from '../util/forms/DropdownButton'
|
import {PostDropdownBtn} from '../util/forms/DropdownButton'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {ago, niceDate} from 'lib/strings/time'
|
import {ago, niceDate} from 'lib/strings/time'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
@ -163,22 +163,17 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<PostSandboxWarning />
|
<PostSandboxWarning />
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
<Link
|
<PreviewableUserAvatar
|
||||||
href={authorHref}
|
size={52}
|
||||||
title={authorTitle}
|
did={item.post.author.did}
|
||||||
asAnchor
|
handle={item.post.author.handle}
|
||||||
accessibilityLabel={`${item.post.author.handle}'s avatar`}
|
avatar={item.post.author.avatar}
|
||||||
accessibilityHint="">
|
moderation={item.moderation.avatar}
|
||||||
<UserAvatar
|
/>
|
||||||
size={52}
|
|
||||||
avatar={item.post.author.avatar}
|
|
||||||
moderation={item.moderation.avatar}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<View style={[styles.meta, styles.metaExpandedLine1]}>
|
<View style={[styles.meta, styles.metaExpandedLine1]}>
|
||||||
<View style={[s.flexRow, s.alignBaseline]}>
|
<View style={[s.flexRow]}>
|
||||||
<Link
|
<Link
|
||||||
style={styles.metaItem}
|
style={styles.metaItem}
|
||||||
href={authorHref}
|
href={authorHref}
|
||||||
|
@ -353,13 +348,13 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<PostSandboxWarning />
|
<PostSandboxWarning />
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
<Link href={authorHref} title={authorTitle} asAnchor>
|
<PreviewableUserAvatar
|
||||||
<UserAvatar
|
size={52}
|
||||||
size={52}
|
did={item.post.author.did}
|
||||||
avatar={item.post.author.avatar}
|
handle={item.post.author.handle}
|
||||||
moderation={item.moderation.avatar}
|
avatar={item.post.author.avatar}
|
||||||
/>
|
moderation={item.moderation.avatar}
|
||||||
</Link>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<PostMeta
|
<PostMeta
|
||||||
|
@ -368,7 +363,6 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
authorHasWarning={!!item.post.author.labels?.length}
|
authorHasWarning={!!item.post.author.labels?.length}
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
|
||||||
/>
|
/>
|
||||||
<ContentHider
|
<ContentHider
|
||||||
moderation={item.moderation.thread}
|
moderation={item.moderation.thread}
|
||||||
|
|
|
@ -229,7 +229,6 @@ const PostLoaded = observer(
|
||||||
authorHasWarning={!!item.post.author.labels?.length}
|
authorHasWarning={!!item.post.author.labels?.length}
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
|
||||||
/>
|
/>
|
||||||
{replyAuthorDid !== '' && (
|
{replyAuthorDid !== '' && (
|
||||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||||
|
|
|
@ -28,7 +28,6 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
feed,
|
feed,
|
||||||
style,
|
style,
|
||||||
showPostFollowBtn,
|
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
@ -41,7 +40,6 @@ export const Feed = observer(function Feed({
|
||||||
}: {
|
}: {
|
||||||
feed: PostsFeedModel
|
feed: PostsFeedModel
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showPostFollowBtn?: boolean
|
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
onScroll?: OnScrollCb
|
onScroll?: OnScrollCb
|
||||||
|
@ -138,15 +136,9 @@ export const Feed = observer(function Feed({
|
||||||
} else if (item === LOADING_ITEM) {
|
} else if (item === LOADING_ITEM) {
|
||||||
return <PostFeedLoadingPlaceholder />
|
return <PostFeedLoadingPlaceholder />
|
||||||
}
|
}
|
||||||
return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} />
|
return <FeedSlice slice={item} />
|
||||||
},
|
},
|
||||||
[
|
[feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
|
||||||
feed,
|
|
||||||
onPressTryAgain,
|
|
||||||
onPressRetryLoadMore,
|
|
||||||
showPostFollowBtn,
|
|
||||||
renderEmptyState,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeedFooter = React.useCallback(
|
const FeedFooter = React.useCallback(
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {ImageHider} from '../util/moderation/ImageHider'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
@ -33,14 +33,12 @@ export const FeedItem = observer(function ({
|
||||||
item,
|
item,
|
||||||
isThreadChild,
|
isThreadChild,
|
||||||
isThreadParent,
|
isThreadParent,
|
||||||
showFollowBtn,
|
|
||||||
ignoreMuteFor,
|
ignoreMuteFor,
|
||||||
}: {
|
}: {
|
||||||
item: PostsFeedItemModel
|
item: PostsFeedItemModel
|
||||||
isThreadChild?: boolean
|
isThreadChild?: boolean
|
||||||
isThreadParent?: boolean
|
isThreadParent?: boolean
|
||||||
showReplyLine?: boolean
|
showReplyLine?: boolean
|
||||||
showFollowBtn?: boolean
|
|
||||||
ignoreMuteFor?: string
|
ignoreMuteFor?: string
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -55,7 +53,6 @@ export const FeedItem = observer(function ({
|
||||||
return `/profile/${item.post.author.handle}/post/${urip.rkey}`
|
return `/profile/${item.post.author.handle}/post/${urip.rkey}`
|
||||||
}, [item.post.uri, item.post.author.handle])
|
}, [item.post.uri, item.post.author.handle])
|
||||||
const itemTitle = `Post by ${item.post.author.handle}`
|
const itemTitle = `Post by ${item.post.author.handle}`
|
||||||
const authorHref = `/profile/${item.post.author.handle}`
|
|
||||||
const replyAuthorDid = useMemo(() => {
|
const replyAuthorDid = useMemo(() => {
|
||||||
if (!record?.reply) {
|
if (!record?.reply) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -214,13 +211,13 @@ export const FeedItem = observer(function ({
|
||||||
<PostSandboxWarning />
|
<PostSandboxWarning />
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
<Link href={authorHref} title={item.post.author.handle} asAnchor>
|
<PreviewableUserAvatar
|
||||||
<UserAvatar
|
size={52}
|
||||||
size={52}
|
did={item.post.author.did}
|
||||||
avatar={item.post.author.avatar}
|
handle={item.post.author.handle}
|
||||||
moderation={item.moderation.avatar}
|
avatar={item.post.author.avatar}
|
||||||
/>
|
moderation={item.moderation.avatar}
|
||||||
</Link>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<PostMeta
|
<PostMeta
|
||||||
|
@ -229,8 +226,6 @@ export const FeedItem = observer(function ({
|
||||||
authorHasWarning={!!item.post.author.labels?.length}
|
authorHasWarning={!!item.post.author.labels?.length}
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
|
||||||
showFollowBtn={showFollowBtn}
|
|
||||||
/>
|
/>
|
||||||
{!isThreadChild && replyAuthorDid !== '' && (
|
{!isThreadChild && replyAuthorDid !== '' && (
|
||||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||||
|
@ -357,9 +352,9 @@ const styles = StyleSheet.create({
|
||||||
layout: {
|
layout: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
|
gap: 10,
|
||||||
},
|
},
|
||||||
layoutAvi: {
|
layoutAvi: {
|
||||||
width: 70,
|
|
||||||
paddingLeft: 8,
|
paddingLeft: 8,
|
||||||
},
|
},
|
||||||
layoutContent: {
|
layoutContent: {
|
||||||
|
|
|
@ -11,11 +11,9 @@ import {ModerationBehaviorCode} from 'lib/labeling/types'
|
||||||
|
|
||||||
export function FeedSlice({
|
export function FeedSlice({
|
||||||
slice,
|
slice,
|
||||||
showFollowBtn,
|
|
||||||
ignoreMuteFor,
|
ignoreMuteFor,
|
||||||
}: {
|
}: {
|
||||||
slice: PostsFeedSliceModel
|
slice: PostsFeedSliceModel
|
||||||
showFollowBtn?: boolean
|
|
||||||
ignoreMuteFor?: string
|
ignoreMuteFor?: string
|
||||||
}) {
|
}) {
|
||||||
if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
|
if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
|
||||||
|
@ -32,7 +30,6 @@ export function FeedSlice({
|
||||||
item={slice.items[0]}
|
item={slice.items[0]}
|
||||||
isThreadParent={slice.isThreadParentAt(0)}
|
isThreadParent={slice.isThreadParentAt(0)}
|
||||||
isThreadChild={slice.isThreadChildAt(0)}
|
isThreadChild={slice.isThreadChildAt(0)}
|
||||||
showFollowBtn={showFollowBtn}
|
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
ignoreMuteFor={ignoreMuteFor}
|
||||||
/>
|
/>
|
||||||
<FeedItem
|
<FeedItem
|
||||||
|
@ -40,7 +37,6 @@ export function FeedSlice({
|
||||||
item={slice.items[1]}
|
item={slice.items[1]}
|
||||||
isThreadParent={slice.isThreadParentAt(1)}
|
isThreadParent={slice.isThreadParentAt(1)}
|
||||||
isThreadChild={slice.isThreadChildAt(1)}
|
isThreadChild={slice.isThreadChildAt(1)}
|
||||||
showFollowBtn={showFollowBtn}
|
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
ignoreMuteFor={ignoreMuteFor}
|
||||||
/>
|
/>
|
||||||
<ViewFullThread slice={slice} />
|
<ViewFullThread slice={slice} />
|
||||||
|
@ -49,7 +45,6 @@ export function FeedSlice({
|
||||||
item={slice.items[last]}
|
item={slice.items[last]}
|
||||||
isThreadParent={slice.isThreadParentAt(last)}
|
isThreadParent={slice.isThreadParentAt(last)}
|
||||||
isThreadChild={slice.isThreadChildAt(last)}
|
isThreadChild={slice.isThreadChildAt(last)}
|
||||||
showFollowBtn={showFollowBtn}
|
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
ignoreMuteFor={ignoreMuteFor}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -64,7 +59,6 @@ export function FeedSlice({
|
||||||
item={item}
|
item={item}
|
||||||
isThreadParent={slice.isThreadParentAt(i)}
|
isThreadParent={slice.isThreadParentAt(i)}
|
||||||
isThreadChild={slice.isThreadChildAt(i)}
|
isThreadChild={slice.isThreadChildAt(i)}
|
||||||
showFollowBtn={showFollowBtn}
|
|
||||||
ignoreMuteFor={ignoreMuteFor}
|
ignoreMuteFor={ignoreMuteFor}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import {CogIcon} from 'lib/icons'
|
||||||
export const MultiFeed = observer(function Feed({
|
export const MultiFeed = observer(function Feed({
|
||||||
multifeed,
|
multifeed,
|
||||||
style,
|
style,
|
||||||
showPostFollowBtn,
|
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
onScroll,
|
onScroll,
|
||||||
scrollEventThrottle,
|
scrollEventThrottle,
|
||||||
|
@ -38,7 +37,6 @@ export const MultiFeed = observer(function Feed({
|
||||||
}: {
|
}: {
|
||||||
multifeed: PostsMultiFeedModel
|
multifeed: PostsMultiFeedModel
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showPostFollowBtn?: boolean
|
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
onScroll?: OnScrollCb
|
onScroll?: OnScrollCb
|
||||||
|
@ -105,9 +103,7 @@ export const MultiFeed = observer(function Feed({
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
} else if (item.type === 'feed-slice') {
|
} else if (item.type === 'feed-slice') {
|
||||||
return (
|
return <FeedSlice slice={item.slice} />
|
||||||
<FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} />
|
|
||||||
)
|
|
||||||
} else if (item.type === 'feed-loading') {
|
} else if (item.type === 'feed-loading') {
|
||||||
return <PostFeedLoadingPlaceholder />
|
return <PostFeedLoadingPlaceholder />
|
||||||
} else if (item.type === 'feed-error') {
|
} else if (item.type === 'feed-error') {
|
||||||
|
@ -139,7 +135,7 @@ export const MultiFeed = observer(function Feed({
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
[showPostFollowBtn, pal],
|
[pal],
|
||||||
)
|
)
|
||||||
|
|
||||||
const ListFooter = React.useCallback(
|
const ListFooter = React.useCallback(
|
||||||
|
|
|
@ -6,10 +6,7 @@ import {
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconStyle,
|
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {BlurView} from '../util/BlurView'
|
import {BlurView} from '../util/BlurView'
|
||||||
import {ProfileModel} from 'state/models/content/profile'
|
import {ProfileModel} from 'state/models/content/profile'
|
||||||
|
@ -102,6 +99,7 @@ export const ProfileHeader = observer(
|
||||||
const ProfileHeaderLoaded = observer(
|
const ProfileHeaderLoaded = observer(
|
||||||
({view, onRefreshAll, hideBackButton = false}: Props) => {
|
({view, onRefreshAll, hideBackButton = false}: Props) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const palInverted = usePalette('inverted')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
@ -351,15 +349,15 @@ const ProfileHeaderLoaded = observer(
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="followBtn"
|
testID="followBtn"
|
||||||
onPress={onPressToggleFollow}
|
onPress={onPressToggleFollow}
|
||||||
style={[styles.btn, styles.primaryBtn]}
|
style={[styles.btn, styles.mainBtn, palInverted.view]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`Follow ${view.handle}`}
|
accessibilityLabel={`Follow ${view.handle}`}
|
||||||
accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
|
accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="plus"
|
icon="plus"
|
||||||
style={[s.white as FontAwesomeIconStyle, s.mr5]}
|
style={[palInverted.text, s.mr5]}
|
||||||
/>
|
/>
|
||||||
<Text type="button" style={[s.white, s.bold]}>
|
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||||
Follow
|
Follow
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -609,7 +607,6 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
description: {
|
description: {
|
||||||
flex: 1,
|
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Platform,
|
Platform,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
TextProps,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
@ -144,7 +145,7 @@ export const TextLink = observer(function TextLink({
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
lineHeight?: number
|
lineHeight?: number
|
||||||
dataSet?: any
|
dataSet?: any
|
||||||
}) {
|
} & TextProps) {
|
||||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
@ -186,16 +187,7 @@ export const TextLink = observer(function TextLink({
|
||||||
/**
|
/**
|
||||||
* Only acts as a link on desktop web
|
* Only acts as a link on desktop web
|
||||||
*/
|
*/
|
||||||
export const DesktopWebTextLink = observer(function DesktopWebTextLink({
|
interface DesktopWebTextLinkProps extends TextProps {
|
||||||
testID,
|
|
||||||
type = 'md',
|
|
||||||
style,
|
|
||||||
href,
|
|
||||||
text,
|
|
||||||
numberOfLines,
|
|
||||||
lineHeight,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
testID?: string
|
testID?: string
|
||||||
type?: TypographyVariant
|
type?: TypographyVariant
|
||||||
style?: StyleProp<TextStyle>
|
style?: StyleProp<TextStyle>
|
||||||
|
@ -206,7 +198,17 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({
|
||||||
accessible?: boolean
|
accessible?: boolean
|
||||||
accessibilityLabel?: string
|
accessibilityLabel?: string
|
||||||
accessibilityHint?: string
|
accessibilityHint?: string
|
||||||
}) {
|
}
|
||||||
|
export const DesktopWebTextLink = observer(function DesktopWebTextLink({
|
||||||
|
testID,
|
||||||
|
type = 'md',
|
||||||
|
style,
|
||||||
|
href,
|
||||||
|
text,
|
||||||
|
numberOfLines,
|
||||||
|
lineHeight,
|
||||||
|
...props
|
||||||
|
}: DesktopWebTextLinkProps) {
|
||||||
if (isDesktopWeb) {
|
if (isDesktopWeb) {
|
||||||
return (
|
return (
|
||||||
<TextLink
|
<TextLink
|
||||||
|
|
|
@ -4,12 +4,10 @@ import {Text} from './text/Text'
|
||||||
import {DesktopWebTextLink} from './Link'
|
import {DesktopWebTextLink} from './Link'
|
||||||
import {ago, niceDate} from 'lib/strings/time'
|
import {ago, niceDate} from 'lib/strings/time'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {UserAvatar} from './UserAvatar'
|
import {UserAvatar} from './UserAvatar'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FollowButton} from '../profile/FollowButton'
|
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
import {isAndroid, isIOS} from 'platform/detection'
|
||||||
|
|
||||||
interface PostMetaOpts {
|
interface PostMetaOpts {
|
||||||
authorAvatar?: string
|
authorAvatar?: string
|
||||||
|
@ -18,88 +16,17 @@ interface PostMetaOpts {
|
||||||
authorHasWarning: boolean
|
authorHasWarning: boolean
|
||||||
postHref: string
|
postHref: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
did?: string
|
|
||||||
showFollowBtn?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostMeta = observer(function (opts: PostMetaOpts) {
|
export const PostMeta = observer(function (opts: PostMetaOpts) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const displayName = opts.authorDisplayName || opts.authorHandle
|
const displayName = opts.authorDisplayName || opts.authorHandle
|
||||||
const handle = opts.authorHandle
|
const handle = opts.authorHandle
|
||||||
const store = useStores()
|
|
||||||
const isMe = opts.did === store.me.did
|
|
||||||
const followState =
|
|
||||||
typeof opts.did === 'string'
|
|
||||||
? store.me.follows.getFollowState(opts.did)
|
|
||||||
: FollowState.Unknown
|
|
||||||
|
|
||||||
const [didFollow, setDidFollow] = React.useState(false)
|
|
||||||
const onToggleFollow = React.useCallback(() => {
|
|
||||||
setDidFollow(true)
|
|
||||||
}, [setDidFollow])
|
|
||||||
|
|
||||||
if (
|
|
||||||
opts.showFollowBtn &&
|
|
||||||
!isMe &&
|
|
||||||
(followState === FollowState.NotFollowing || didFollow) &&
|
|
||||||
opts.did
|
|
||||||
) {
|
|
||||||
// two-liner with follow button
|
|
||||||
return (
|
|
||||||
<View style={styles.metaTwoLine}>
|
|
||||||
<View style={styles.metaTwoLineLeft}>
|
|
||||||
<View style={styles.metaTwoLineTop}>
|
|
||||||
<DesktopWebTextLink
|
|
||||||
type="lg-bold"
|
|
||||||
style={pal.text}
|
|
||||||
numberOfLines={1}
|
|
||||||
lineHeight={1.2}
|
|
||||||
text={sanitizeDisplayName(displayName)}
|
|
||||||
href={`/profile/${opts.authorHandle}`}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
type="md"
|
|
||||||
style={pal.textLight}
|
|
||||||
lineHeight={1.2}
|
|
||||||
accessible={false}>
|
|
||||||
·
|
|
||||||
</Text>
|
|
||||||
<DesktopWebTextLink
|
|
||||||
type="md"
|
|
||||||
style={[styles.metaItem, pal.textLight]}
|
|
||||||
lineHeight={1.2}
|
|
||||||
text={ago(opts.timestamp)}
|
|
||||||
accessibilityLabel={niceDate(opts.timestamp)}
|
|
||||||
accessibilityHint=""
|
|
||||||
href={opts.postHref}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<DesktopWebTextLink
|
|
||||||
type="md"
|
|
||||||
style={[styles.metaItem, pal.textLight]}
|
|
||||||
lineHeight={1.2}
|
|
||||||
numberOfLines={1}
|
|
||||||
text={`@${handle}`}
|
|
||||||
href={`/profile/${opts.authorHandle}`}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<FollowButton
|
|
||||||
unfollowedType="default"
|
|
||||||
did={opts.did}
|
|
||||||
onToggleFollow={onToggleFollow}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// one-liner
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.meta}>
|
<View style={styles.metaOneLine}>
|
||||||
{typeof opts.authorAvatar !== 'undefined' && (
|
{typeof opts.authorAvatar !== 'undefined' && (
|
||||||
<View style={[styles.metaItem, styles.avatar]}>
|
<View style={styles.avatar}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={opts.authorAvatar}
|
avatar={opts.authorAvatar}
|
||||||
size={16}
|
size={16}
|
||||||
|
@ -107,7 +34,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={[styles.metaItem, styles.maxWidth]}>
|
<View style={styles.maxWidth}>
|
||||||
<DesktopWebTextLink
|
<DesktopWebTextLink
|
||||||
type="lg-bold"
|
type="lg-bold"
|
||||||
style={pal.text}
|
style={pal.text}
|
||||||
|
@ -128,12 +55,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
|
||||||
href={`/profile/${opts.authorHandle}`}
|
href={`/profile/${opts.authorHandle}`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text type="md" style={pal.textLight} lineHeight={1.2} accessible={false}>
|
{!isAndroid && (
|
||||||
·
|
<Text
|
||||||
</Text>
|
type="md"
|
||||||
|
style={pal.textLight}
|
||||||
|
lineHeight={1.2}
|
||||||
|
accessible={false}>
|
||||||
|
·
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<DesktopWebTextLink
|
<DesktopWebTextLink
|
||||||
type="md"
|
type="md"
|
||||||
style={[styles.metaItem, pal.textLight]}
|
style={pal.textLight}
|
||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
text={ago(opts.timestamp)}
|
text={ago(opts.timestamp)}
|
||||||
accessibilityLabel={niceDate(opts.timestamp)}
|
accessibilityLabel={niceDate(opts.timestamp)}
|
||||||
|
@ -145,32 +78,16 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
meta: {
|
metaOneLine: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingBottom: 2,
|
paddingBottom: 2,
|
||||||
},
|
gap: 4,
|
||||||
metaTwoLine: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
paddingBottom: 4,
|
|
||||||
},
|
|
||||||
metaTwoLineLeft: {
|
|
||||||
flex: 1,
|
|
||||||
paddingRight: 40,
|
|
||||||
},
|
|
||||||
metaTwoLineTop: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
},
|
|
||||||
metaItem: {
|
|
||||||
paddingRight: 5,
|
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
maxWidth: '80%',
|
flex: isAndroid ? 1 : undefined,
|
||||||
|
maxWidth: isIOS ? '80%' : undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -12,13 +12,31 @@ import {
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {DropdownButton} from './forms/DropdownButton'
|
import {DropdownButton} from './forms/DropdownButton'
|
||||||
|
import {Link} from './Link'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {AvatarModeration} from 'lib/labeling/types'
|
import {AvatarModeration} from 'lib/labeling/types'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
type Type = 'user' | 'algo' | 'list'
|
type Type = 'user' | 'algo' | 'list'
|
||||||
|
|
||||||
|
interface BaseUserAvatarProps {
|
||||||
|
type?: Type
|
||||||
|
size: number
|
||||||
|
avatar?: string | null
|
||||||
|
moderation?: AvatarModeration
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserAvatarProps extends BaseUserAvatarProps {
|
||||||
|
onSelectNewAvatar?: (img: RNImage | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||||
|
did: string
|
||||||
|
handle: string
|
||||||
|
}
|
||||||
|
|
||||||
const BLUR_AMOUNT = isWeb ? 5 : 100
|
const BLUR_AMOUNT = isWeb ? 5 : 100
|
||||||
|
|
||||||
function DefaultAvatar({type, size}: {type: Type; size: number}) {
|
function DefaultAvatar({type, size}: {type: Type; size: number}) {
|
||||||
|
@ -91,13 +109,7 @@ export function UserAvatar({
|
||||||
avatar,
|
avatar,
|
||||||
moderation,
|
moderation,
|
||||||
onSelectNewAvatar,
|
onSelectNewAvatar,
|
||||||
}: {
|
}: UserAvatarProps) {
|
||||||
type?: Type
|
|
||||||
size: number
|
|
||||||
avatar?: string | null
|
|
||||||
moderation?: AvatarModeration
|
|
||||||
onSelectNewAvatar?: (img: RNImage | null) => void
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
|
@ -244,6 +256,32 @@ export function UserAvatar({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
|
if (isDesktopWeb) {
|
||||||
|
return (
|
||||||
|
<Link href={`/profile/${props.handle}`} title={props.handle} asAnchor>
|
||||||
|
<UserAvatar {...props} />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() =>
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'profile-preview',
|
||||||
|
did: props.did,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={props.handle}
|
||||||
|
accessibilityHint="">
|
||||||
|
<UserAvatar {...props} />
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
editButtonContainer: {
|
editButtonContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
|
@ -106,7 +106,6 @@ export const FeedsScreen = withAuthRequired(
|
||||||
onScroll={onMainScroll}
|
onScroll={onMainScroll}
|
||||||
scrollEventThrottle={100}
|
scrollEventThrottle={100}
|
||||||
headerOffset={HEADER_OFFSET}
|
headerOffset={HEADER_OFFSET}
|
||||||
showPostFollowBtn
|
|
||||||
/>
|
/>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
title="My Feeds"
|
title="My Feeds"
|
||||||
|
|
|
@ -266,7 +266,6 @@ const FeedPage = observer(
|
||||||
key="default"
|
key="default"
|
||||||
feed={feed}
|
feed={feed}
|
||||||
scrollElRef={scrollElRef}
|
scrollElRef={scrollElRef}
|
||||||
showPostFollowBtn
|
|
||||||
onPressTryAgain={onPressTryAgain}
|
onPressTryAgain={onPressTryAgain}
|
||||||
onScroll={onMainScroll}
|
onScroll={onMainScroll}
|
||||||
scrollEventThrottle={100}
|
scrollEventThrottle={100}
|
||||||
|
|
Loading…
Reference in New Issue