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 webzio/stable
parent
03d152675e
commit
cc3fcb1645
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" "*"
|
||||||
|
|
Loading…
Reference in New Issue