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:
Paul Frazee 2023-11-01 16:15:40 -07:00 committed by GitHub
parent f9944b55e2
commit f57a8cf8ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 4090 additions and 1988 deletions

View file

@ -25,7 +25,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
name: 'trash',
},
android: 'ic_delete',
web: 'trash',
web: ['far', 'trash-can'],
},
},
]

View file

@ -83,19 +83,14 @@ export function PostLoadingPlaceholder({
export function PostFeedLoadingPlaceholder() {
return (
<>
<View>
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
</>
</View>
)
}

View file

@ -17,10 +17,10 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
import {UserPreviewLink} from './UserPreviewLink'
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
type Type = 'user' | 'algo' | 'list'
export type UserAvatarType = 'user' | 'algo' | 'list'
interface BaseUserAvatarProps {
type?: Type
type?: UserAvatarType
size: number
avatar?: string | null
}
@ -41,7 +41,7 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
const BLUR_AMOUNT = isWeb ? 5 : 100
function DefaultAvatar({type, size}: {type: Type; size: number}) {
function DefaultAvatar({type, size}: {type: UserAvatarType; size: number}) {
if (type === 'algo') {
// Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
return (
@ -261,7 +261,7 @@ export function EditableUserAvatar({
name: 'trash',
},
android: 'ic_delete',
web: 'trash',
web: ['far', 'trash-can'],
},
onPress: async () => {
onSelectNewAvatar(null)

View file

@ -91,7 +91,7 @@ export function UserBanner({
name: 'trash',
},
android: 'ic_delete',
web: 'trash',
web: ['far', 'trash-can'],
},
onPress: () => {
onSelectNewBanner?.(null)

View file

@ -124,7 +124,6 @@ function DesktopWebHeader({
<CenteredView
style={[
styles.header,
styles.headerFixed,
styles.desktopHeader,
pal.border,
{
@ -158,7 +157,6 @@ const Container = observer(function ContainerImpl({
<View
style={[
styles.header,
styles.headerFixed,
pal.view,
pal.border,
showBorder && styles.border,
@ -190,11 +188,6 @@ const styles = StyleSheet.create({
paddingVertical: 6,
width: '100%',
},
headerFixed: {
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
},
headerFloating: {
position: 'absolute',
top: 0,
@ -202,6 +195,9 @@ const styles = StyleSheet.create({
},
desktopHeader: {
paddingVertical: 12,
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
},
border: {
borderBottomWidth: 1,

1
src/view/com/util/Views.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {FlatList, ScrollView, View as CenteredView} from 'react-native'

View file

@ -0,0 +1,9 @@
import React from 'react'
import {View} from 'react-native'
import Animated from 'react-native-reanimated'
export const FlatList = Animated.FlatList
export const ScrollView = Animated.ScrollView
export function CenteredView(props) {
return <View {...props} />
}

View file

@ -1 +0,0 @@
export {View as CenteredView, FlatList, ScrollView} from 'react-native'

View file

@ -14,9 +14,7 @@
import React from 'react'
import {
FlatList as RNFlatList,
FlatListProps,
ScrollView as RNScrollView,
ScrollViewProps,
StyleSheet,
View,
@ -25,16 +23,29 @@ import {
import {addStyle} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import Animated from 'react-native-reanimated'
interface AddedProps {
desktopFixedHeight?: boolean
desktopFixedHeight?: boolean | number
}
export function CenteredView({
style,
sideBorders,
...props
}: React.PropsWithChildren<ViewProps>) {
style = addStyle(style, styles.container)
}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
if (!isMobile) {
style = addStyle(style, styles.container)
}
if (sideBorders) {
style = addStyle(style, {
borderLeftWidth: 1,
borderRightWidth: 1,
})
style = addStyle(style, pal.border)
}
return <View style={style} {...props} />
}
@ -46,14 +57,16 @@ export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
desktopFixedHeight,
...props
}: React.PropsWithChildren<FlatListProps<ItemT> & AddedProps>,
ref: React.Ref<RNFlatList>,
ref: React.Ref<Animated.FlatList<ItemT>>,
) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
)
if (!isMobile) {
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
)
}
if (contentOffset && contentOffset?.y !== 0) {
// NOTE
// we use paddingTop & contentOffset to space around the floating header
@ -68,7 +81,14 @@ export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
})
}
if (desktopFixedHeight) {
style = addStyle(style, styles.fixedHeight)
if (typeof desktopFixedHeight === 'number') {
// @ts-ignore Web only -prf
style = addStyle(style, {
height: `calc(100vh - ${desktopFixedHeight}px)`,
})
} else {
style = addStyle(style, styles.fixedHeight)
}
if (!isMobile) {
// NOTE
// react native web produces *three* wrapping divs
@ -85,7 +105,7 @@ export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
}
}
return (
<RNFlatList
<Animated.FlatList
ref={ref}
contentContainerStyle={[
contentContainerStyle,
@ -101,21 +121,25 @@ export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
export const ScrollView = React.forwardRef(function ScrollViewImpl(
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
ref: React.Ref<RNScrollView>,
ref: React.Ref<Animated.ScrollView>,
) {
const pal = usePalette('default')
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
)
const {isMobile} = useWebMediaQueries()
if (!isMobile) {
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
)
}
return (
<RNScrollView
<Animated.ScrollView
contentContainerStyle={[
contentContainerStyle,
pal.border,
styles.contentContainer,
]}
// @ts-ignore something is wrong with the reanimated types -prf
ref={ref}
{...props}
/>

View file

@ -10,6 +10,7 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import Animated from 'react-native-reanimated'
const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
import {isWeb} from 'platform/detection'
export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
onPress,
@ -47,7 +48,8 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
const styles = StyleSheet.create({
loadLatest: {
position: 'absolute',
// @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute',
left: 18,
bottom: 44,
borderWidth: 1,

View file

@ -74,7 +74,7 @@ export function PostHider({
accessibilityHint="">
<ShieldExclamation size={18} style={pal.text} />
</Pressable>
<Text type="lg" style={pal.text}>
<Text type="lg" style={[{flex: 1}, pal.text]} numberOfLines={1}>
{desc.name}
</Text>
{!moderation.noOverride && (

View file

@ -45,7 +45,7 @@ export function ProfileHeaderAlerts({
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<ShieldExclamation style={pal.text} size={24} />
<Text type="lg" style={pal.text}>
<Text type="lg" style={[{flex: 1}, pal.text]}>
{desc.name}
</Text>
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>

View file

@ -3,8 +3,8 @@ import {AppBskyFeedDefs} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {StyleSheet} from 'react-native'
import {useStores} from 'state/index'
import {CustomFeedModel} from 'state/models/feeds/custom-feed'
import {CustomFeed} from 'view/com/feeds/CustomFeed'
import {FeedSourceModel} from 'state/models/content/feed-source'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
export function CustomFeedEmbed({
record,
@ -13,12 +13,13 @@ export function CustomFeedEmbed({
}) {
const pal = usePalette('default')
const store = useStores()
const item = useMemo(
() => new CustomFeedModel(store, record),
[store, record],
)
const item = useMemo(() => {
const model = new FeedSourceModel(store, record.uri)
model.hydrateFeedGenerator(record)
return model
}, [store, record])
return (
<CustomFeed
<FeedSourceCard
item={item}
style={[pal.view, pal.border, styles.customFeedOuter]}
showLikes

View file

@ -75,7 +75,7 @@ export function PostEmbeds({
return <CustomFeedEmbed record={embed.record} />
}
// list embed (e.g. mute lists; i.e. ListView)
// list embed
if (AppBskyGraphDefs.isListView(embed.record)) {
return <ListEmbed item={embed.record} />
}