Lists updates: curate lists and blocklists (#1689)
* Add lists screen * Update Lists screen and List create/edit modal to support curate lists * Rework the ProfileList screen and add curatelist support * More ProfileList progress * Update list modals * Rename mutelists to modlists * Layout updates/fixes * More layout fixes * Modal fixes * List list screen updates * Update feed page to give more info * Layout fixes to ListAddUser modal * Layout fixes to FlatList and Feed on desktop * Layout fix to LoadLatestBtn on Web * Handle did resolution before showing the ProfileList screen * Rename the CustomFeed routes to ProfileFeed for consistency * Fix layout issues with the pager and feeds * Factor out some common code * Fix UIs for mobile * Fix user list rendering * Fix: dont bubble custom feed errors in the merge feed * Refactor feed models to reduce usage of the SavedFeeds model * Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists * Add the ability to pin lists * Add pinned lists to mobile * Remove dead code * Rework the ProfileScreenHeader to create more real-estate for action buttons * Improve layout behavior on web mobile breakpoints * Refactor feed & list pages to use new Tabs layout component * Refactor to ProfileSubpageHeader * Implement modlist block and mute * Switch to new api and just modify state on modlist actions * Fix some UI overflows * Fix: dont show edit buttons on lists you dont own * Fix alignment issue on long titles * Improve loading and error states for feeds & lists * Update list dropdown icons for ios * Fetch feed display names in the mergefeed * Improve rendering off offline feeds in the feed-listing page * Update Feeds listing UI to react to changes in saved/pinned state * Refresh list and feed on posts tab press * Fix pinned feed ordering UI * Fixes to list pinning * Remove view=simple qp * Add list to feed tuners * Render richtext * Add list href * Add 'view avatar' * Remove unused import * Fix missing import * Correctly reflect block by list state * Replace the <Tabs> component with the more effective <PagerWithHeader> component * Improve the responsiveness of the PagerWithHeader * Fix visual jank in the feed loading state * Improve performance of the PagerWithHeader * Fix a case that would cause the header to animate too aggressively * Add the ability to scroll to top by tapping the selected tab * Fix unit test runner * Update modlists test * Add curatelist tests * Fix: remove link behavior in ListAddUser modal * Fix some layout jank in the PagerWithHeader on iOS * Simplify ListItems header rendering * Wait for the appview to recognize the list before proceeding with list creation * Fix glitch in the onPageSelecting index of the Pager * Fix until() * Copy fix Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
parent
f9944b55e2
commit
f57a8cf8ba
87 changed files with 4090 additions and 1988 deletions
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
|
@ -29,6 +29,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
|||
noBorder,
|
||||
followers,
|
||||
renderButton,
|
||||
style,
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
|
@ -36,6 +37,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
|||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -50,6 +52,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
|||
pal.border,
|
||||
noBorder && styles.outerNoBorder,
|
||||
!noBg && pal.view,
|
||||
style,
|
||||
]}
|
||||
href={makeProfileLink(profile)}
|
||||
title={profile.handle}
|
||||
|
@ -93,7 +96,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
|||
{profile.description as string}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
) : null}
|
||||
<FollowersList followers={followers} />
|
||||
</Link>
|
||||
)
|
||||
|
@ -220,10 +223,10 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
},
|
||||
layoutAvi: {
|
||||
alignSelf: 'baseline',
|
||||
width: 54,
|
||||
paddingLeft: 4,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
avi: {
|
||||
width: 40,
|
||||
|
|
|
@ -181,7 +181,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
const onPressAddRemoveLists = React.useCallback(() => {
|
||||
track('ProfileHeader:AddToListsButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'list-add-remove-user',
|
||||
name: 'user-add-remove-lists',
|
||||
subject: view.did,
|
||||
displayName: view.displayName || view.handle,
|
||||
})
|
||||
|
@ -276,21 +276,20 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
},
|
||||
},
|
||||
]
|
||||
if (!isMe) {
|
||||
items.push({label: 'separator'})
|
||||
// Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: 'Add to Lists',
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
items.push({label: 'separator'})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: 'Add to Lists',
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
})
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
},
|
||||
})
|
||||
if (!isMe) {
|
||||
if (!view.viewer.blocking) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
|
@ -307,20 +306,22 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
|
||||
onPress: view.viewer.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
if (!view.viewer.blockingByList) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
|
||||
onPress: view.viewer.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: 'Report Account',
|
||||
|
@ -339,6 +340,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
isMe,
|
||||
view.viewer.muted,
|
||||
view.viewer.blocking,
|
||||
view.viewer.blockingByList,
|
||||
onPressShare,
|
||||
onPressUnmuteAccount,
|
||||
onPressMuteAccount,
|
||||
|
@ -371,17 +373,19 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
|||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : view.viewer.blocking ? (
|
||||
<TouchableOpacity
|
||||
testID="unblockBtn"
|
||||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Unblock"
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
Unblock
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
view.viewer.blockingByList ? null : (
|
||||
<TouchableOpacity
|
||||
testID="unblockBtn"
|
||||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Unblock"
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
Unblock
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
) : !view.viewer.blockedBy ? (
|
||||
<>
|
||||
{!isProfilePreview && (
|
||||
|
|
194
src/view/com/profile/ProfileSubpageHeader.tsx
Normal file
194
src/view/com/profile/ProfileSubpageHeader.tsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {ImagesLightbox} from 'state/models/ui/shell'
|
||||
|
||||
export const ProfileSubpageHeader = observer(function HeaderImpl({
|
||||
isLoading,
|
||||
href,
|
||||
title,
|
||||
avatar,
|
||||
isOwner,
|
||||
creator,
|
||||
avatarType,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
isLoading?: boolean
|
||||
href: string
|
||||
title: string | undefined
|
||||
avatar: string | undefined
|
||||
isOwner: boolean | undefined
|
||||
creator:
|
||||
| {
|
||||
did: string
|
||||
handle: string
|
||||
}
|
||||
| undefined
|
||||
avatarType: UserAvatarType
|
||||
}>) {
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const canGoBack = navigation.canGoBack()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
store.shell.openDrawer()
|
||||
}, [store])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
if (
|
||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
store.shell.openLightbox(new ImagesLightbox([{uri: avatar}], 0))
|
||||
}
|
||||
}, [store, avatar])
|
||||
|
||||
return (
|
||||
<CenteredView style={pal.view}>
|
||||
{isMobile && (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
paddingTop: isNative ? 0 : 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
},
|
||||
pal.border,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="headerDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||
accessibilityHint="">
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="bars"
|
||||
style={[styles.backIcon, pal.textLight]}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
<View style={{flex: 1}} />
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 10,
|
||||
paddingTop: 14,
|
||||
paddingBottom: 6,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
}}>
|
||||
<Pressable
|
||||
testID="headerAviButton"
|
||||
onPress={onPressAvi}
|
||||
accessibilityRole="image"
|
||||
accessibilityLabel="View the avatar"
|
||||
accessibilityHint=""
|
||||
style={{width: 58}}>
|
||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||
</Pressable>
|
||||
<View style={{flex: 1}}>
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder
|
||||
width={200}
|
||||
height={32}
|
||||
style={{marginVertical: 6}}
|
||||
/>
|
||||
) : (
|
||||
<TextLink
|
||||
testID="headerTitle"
|
||||
type="title-xl"
|
||||
href={href}
|
||||
style={[pal.text, {fontWeight: 'bold'}]}
|
||||
text={title || ''}
|
||||
onPress={() => store.emitScreenSoftReset()}
|
||||
numberOfLines={4}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder width={50} height={8} />
|
||||
) : (
|
||||
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
|
||||
by{' '}
|
||||
{!creator ? (
|
||||
'—'
|
||||
) : isOwner ? (
|
||||
'you'
|
||||
) : (
|
||||
<TextLink
|
||||
text={sanitizeHandle(creator.handle, '@')}
|
||||
href={makeProfileLink(creator)}
|
||||
style={pal.textLight}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{!isMobile && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backBtn: {
|
||||
width: 20,
|
||||
height: 30,
|
||||
},
|
||||
backBtnWide: {
|
||||
width: 20,
|
||||
height: 30,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
backIcon: {
|
||||
marginTop: 6,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue