Adds profile media tab (#1137)

* add media tab

* fix loading state

* cleanup

* update naming

* upgrade api package

* fix load state

* add scroll view to tabs

* fix overflow on mobile web
zio/stable
Eric Bailey 2023-08-10 12:50:37 -05:00 committed by GitHub
parent 03d152675e
commit cc3fcb1645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 65 deletions

View File

@ -24,7 +24,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.6.0", "@atproto/api": "^0.6.1",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@expo/html-elements": "^0.4.2", "@expo/html-elements": "^0.4.2",

View File

@ -74,24 +74,6 @@ export class PostsFeedModel {
return this.hasLoaded && !this.hasContent return this.hasLoaded && !this.hasContent
} }
get nonReplyFeed() {
if (this.feedType === 'author') {
return this.slices.filter(slice => {
const params = this.params as GetAuthorFeed.QueryParams
const item = slice.rootItem
const isRepost =
item?.reasonRepost?.by?.handle === params.actor ||
item?.reasonRepost?.by?.did === params.actor
const allow =
!item.postRecord?.reply || // not a reply
isRepost // but allow if it's a repost
return allow
})
} else {
return this.slices
}
}
setHasNewLatest(v: boolean) { setHasNewLatest(v: boolean) {
this.hasNewLatest = v this.hasNewLatest = v
} }

View File

@ -6,8 +6,9 @@ import {ActorFeedsModel} from '../lists/actor-feeds'
import {ListsListModel} from '../lists/lists-list' import {ListsListModel} from '../lists/lists-list'
export enum Sections { export enum Sections {
Posts = 'Posts', PostsNoReplies = 'Posts',
PostsWithReplies = 'Posts & replies', PostsWithReplies = 'Posts & replies',
PostsWithMedia = 'Media',
CustomAlgorithms = 'Feeds', CustomAlgorithms = 'Feeds',
Lists = 'Lists', Lists = 'Lists',
} }
@ -46,6 +47,7 @@ export class ProfileUiModel {
this.feed = new PostsFeedModel(rootStore, 'author', { this.feed = new PostsFeedModel(rootStore, 'author', {
actor: params.user, actor: params.user,
limit: 10, limit: 10,
filter: 'posts_no_replies',
}) })
this.algos = new ActorFeedsModel(rootStore, {actor: params.user}) this.algos = new ActorFeedsModel(rootStore, {actor: params.user})
this.lists = new ListsListModel(rootStore, params.user) this.lists = new ListsListModel(rootStore, params.user)
@ -53,8 +55,9 @@ export class ProfileUiModel {
get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel { get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel {
if ( if (
this.selectedView === Sections.Posts || this.selectedView === Sections.PostsNoReplies ||
this.selectedView === Sections.PostsWithReplies this.selectedView === Sections.PostsWithReplies ||
this.selectedView === Sections.PostsWithMedia
) { ) {
return this.feed return this.feed
} else if (this.selectedView === Sections.Lists) { } else if (this.selectedView === Sections.Lists) {
@ -76,7 +79,11 @@ export class ProfileUiModel {
} }
get selectorItems() { get selectorItems() {
const items = [Sections.Posts, Sections.PostsWithReplies] const items = [
Sections.PostsNoReplies,
Sections.PostsWithReplies,
Sections.PostsWithMedia,
]
if (this.algos.hasLoaded && !this.algos.isEmpty) { if (this.algos.hasLoaded && !this.algos.isEmpty) {
items.push(Sections.CustomAlgorithms) items.push(Sections.CustomAlgorithms)
} }
@ -90,7 +97,7 @@ export class ProfileUiModel {
// If, for whatever reason, the selected view index is not available, default back to posts // If, for whatever reason, the selected view index is not available, default back to posts
// This can happen when the user was focused on a view but performed an action that caused // This can happen when the user was focused on a view but performed an action that caused
// the view to disappear (e.g. deleting the last list in their list of lists https://imgflip.com/i/7txu1y) // the view to disappear (e.g. deleting the last list in their list of lists https://imgflip.com/i/7txu1y)
return this.selectorItems[this.selectedViewIndex] || Sections.Posts return this.selectorItems[this.selectedViewIndex] || Sections.PostsNoReplies
} }
get uiItems() { get uiItems() {
@ -109,16 +116,22 @@ export class ProfileUiModel {
} else { } else {
// not loading, no error, show content // not loading, no error, show content
if ( if (
this.selectedView === Sections.Posts || this.selectedView === Sections.PostsNoReplies ||
this.selectedView === Sections.PostsWithReplies || this.selectedView === Sections.PostsWithReplies ||
this.selectedView === Sections.PostsWithMedia ||
this.selectedView === Sections.CustomAlgorithms this.selectedView === Sections.CustomAlgorithms
) { ) {
if (this.feed.hasContent) { if (this.feed.hasContent) {
if (this.selectedView === Sections.CustomAlgorithms) { if (this.selectedView === Sections.CustomAlgorithms) {
arr = this.algos.feeds arr = this.algos.feeds
} else if (this.selectedView === Sections.Posts) { } else if (
arr = this.feed.nonReplyFeed this.selectedView === Sections.PostsNoReplies ||
this.selectedView === Sections.PostsWithReplies ||
this.selectedView === Sections.PostsWithMedia
) {
arr = this.feed.slices.slice()
} else { } else {
// posts with replies is also default
arr = this.feed.slices.slice() arr = this.feed.slices.slice()
} }
if (!this.feed.hasMore) { if (!this.feed.hasMore) {
@ -143,8 +156,9 @@ export class ProfileUiModel {
get showLoadingMoreFooter() { get showLoadingMoreFooter() {
if ( if (
this.selectedView === Sections.Posts || this.selectedView === Sections.PostsNoReplies ||
this.selectedView === Sections.PostsWithReplies this.selectedView === Sections.PostsWithReplies ||
this.selectedView === Sections.PostsWithMedia
) { ) {
return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading
} else if (this.selectedView === Sections.Lists) { } else if (this.selectedView === Sections.Lists) {
@ -157,7 +171,27 @@ export class ProfileUiModel {
// = // =
setSelectedViewIndex(index: number) { setSelectedViewIndex(index: number) {
// ViewSelector fires onSelectView on mount
if (index === this.selectedViewIndex) return
this.selectedViewIndex = index this.selectedViewIndex = index
let filter = 'posts_no_replies'
if (this.selectedView === Sections.PostsWithReplies) {
filter = 'posts_with_replies'
} else if (this.selectedView === Sections.PostsWithMedia) {
filter = 'posts_with_media'
}
this.feed = new PostsFeedModel(this.rootStore, 'author', {
actor: this.params.user,
limit: 10,
filter,
})
if (this.currentView instanceof PostsFeedModel) {
this.feed.setup()
}
} }
async setup() { async setup() {

View File

@ -1,5 +1,11 @@
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import {Pressable, RefreshControl, StyleSheet, View} from 'react-native' import {
Pressable,
RefreshControl,
StyleSheet,
View,
ScrollView,
} from 'react-native'
import {FlatList} from './Views' import {FlatList} from './Views'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
@ -131,6 +137,8 @@ export const ViewSelector = React.forwardRef<
}, },
) )
const SCROLLBAR_OFFSET = 12
export function Selector({ export function Selector({
selectedIndex, selectedIndex,
items, items,
@ -140,6 +148,8 @@ export function Selector({
items: string[] items: string[]
onSelect?: (index: number) => void onSelect?: (index: number) => void
}) { }) {
const [height, setHeight] = useState(0)
const pal = usePalette('default') const pal = usePalette('default')
const borderColor = useColorSchemeStyle( const borderColor = useColorSchemeStyle(
{borderColor: colors.black}, {borderColor: colors.black},
@ -151,37 +161,55 @@ export function Selector({
} }
return ( return (
<View style={[pal.view, styles.outer]}> <View
{items.map((item, i) => { style={{
const selected = i === selectedIndex width: '100%',
return ( position: 'relative',
<Pressable overflow: 'hidden',
testID={`selector-${i}`} marginTop: -SCROLLBAR_OFFSET,
key={item} height,
onPress={() => onPressItem(i)} }}>
accessibilityLabel={item} <ScrollView
accessibilityHint={`Selects ${item}`} horizontal
// TODO: Modify the component API such that lint fails style={{position: 'absolute', bottom: -SCROLLBAR_OFFSET}}>
// at the invocation site as well <View
> style={[pal.view, styles.outer, {paddingBottom: SCROLLBAR_OFFSET}]}
<View onLayout={e => {
style={[ const {height} = e.nativeEvent.layout
styles.item, setHeight(height || 60)
selected && styles.itemSelected, }}>
borderColor, {items.map((item, i) => {
]}> const selected = i === selectedIndex
<Text return (
style={ <Pressable
selected testID={`selector-${i}`}
? [styles.labelSelected, pal.text] key={item}
: [styles.label, pal.textLight] onPress={() => onPressItem(i)}
}> accessibilityLabel={item}
{item} accessibilityHint={`Selects ${item}`}
</Text> // TODO: Modify the component API such that lint fails
</View> // at the invocation site as well
</Pressable> >
) <View
})} style={[
styles.item,
selected && styles.itemSelected,
borderColor,
]}>
<Text
style={
selected
? [styles.labelSelected, pal.text]
: [styles.label, pal.textLight]
}>
{item}
</Text>
</View>
</Pressable>
)
})}
</View>
</ScrollView>
</View> </View>
) )
} }

View File

@ -40,10 +40,10 @@
tlds "^1.234.0" tlds "^1.234.0"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
"@atproto/api@^0.6.0": "@atproto/api@^0.6.1":
version "0.6.0" version "0.6.1"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.0.tgz#c4eea08ee4d1be522928cd016d7de8061d86e573" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.1.tgz#1a4794c4e379f3790dbc1c2cc69e0700c711f634"
integrity sha512-GkWHoGZfNneHarAYkIPJD1GGgKiI7OwnCtKS+J4AmlVKYijGEzOYgg1fY6rluT6XPT5TlQZiHUWpMlpqAkQIkQ== integrity sha512-Fwp3GxSxy04XCScLNb7gdYuITt3beUPM2gOmAaJJ/c0muvj3BS/lGeeEqHToSMlxyirfPQYiTHDGcDZgo6EpMQ==
dependencies: dependencies:
"@atproto/common-web" "*" "@atproto/common-web" "*"
"@atproto/uri" "*" "@atproto/uri" "*"