Add the ability to expand/collapse users in notifications

zio/stable
Paul Frazee 2023-01-19 11:34:07 -06:00
parent 1ed82b6c59
commit 74ab6530d4
4 changed files with 202 additions and 58 deletions

View File

@ -1,6 +1,12 @@
import React, {useMemo} from 'react'
import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, View} from 'react-native'
import {
Animated,
TouchableOpacity,
TouchableWithoutFeedback,
StyleSheet,
View,
} from 'react-native'
import {AppBskyEmbedImages} from '@atproto/api'
import {AtUri} from '../../../third-party/uri'
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
@ -16,16 +22,28 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {Post} from '../post/Post'
import {Link} from '../util/Link'
import {usePalette} from '../../lib/hooks/usePalette'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
const MAX_AUTHORS = 8
const EXPANDED_AUTHOR_EL_HEIGHT = 35
const EXPANDED_AUTHORS_CLOSE_EL_HEIGHT = 26
interface Author {
href: string
handle: string
displayName?: string
avatar?: string
}
export const FeedItem = observer(function FeedItem({
item,
}: {
item: NotificationsViewItemModel
}) {
const pal = usePalette('default')
const itemHref = useMemo(() => {
const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
const itemHref = React.useMemo(() => {
if (item.isUpvote || item.isRepost) {
const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/post/${urip.rkey}`
@ -37,7 +55,7 @@ export const FeedItem = observer(function FeedItem({
}
return ''
}, [item])
const itemTitle = useMemo(() => {
const itemTitle = React.useMemo(() => {
if (item.isUpvote || item.isRepost) {
return 'Post'
} else if (item.isFollow || item.isAssertion) {
@ -47,6 +65,10 @@ export const FeedItem = observer(function FeedItem({
}
}, [item])
const onToggleAuthorsExpanded = () => {
setAuthorsExpanded(!isAuthorsExpanded)
}
if (item.additionalPost?.notFound) {
// don't render anything if the target post was deleted or unfindable
return <View />
@ -93,12 +115,7 @@ export const FeedItem = observer(function FeedItem({
return <></>
}
let authors: {
href: string
handle: string
displayName?: string
avatar?: string
}[] = [
let authors: Author[] = [
{
href: `/profile/${item.author.handle}`,
handle: item.author.handle,
@ -143,27 +160,19 @@ export const FeedItem = observer(function FeedItem({
)}
</View>
<View style={styles.layoutContent}>
<View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => (
<Link
style={{marginRight: 5}}
key={author.href}
href={author.href}
title={`@${author.handle}`}>
<UserAvatar
size={35}
displayName={author.displayName}
handle={author.handle}
avatar={author.avatar}
<TouchableWithoutFeedback
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}>
<View>
<ExpandedAuthorsList
visible={isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
/>
</Link>
))}
{authors.length > MAX_AUTHORS ? (
<Text style={[styles.aviExtraCount, pal.textLight]}>
+{authors.length - MAX_AUTHORS}
</Text>
) : undefined}
</View>
{isAuthorsExpanded ? (
<></>
) : (
<CondensedAuthorsList authors={authors} />
)}
<View style={styles.meta}>
<Link
key={authors[0].href}
@ -178,7 +187,8 @@ export const FeedItem = observer(function FeedItem({
<>
<Text style={[styles.metaItem, pal.text]}>and</Text>
<Text style={[styles.metaItem, pal.text, s.bold]}>
{authors.length - 1} {pluralize(authors.length - 1, 'other')}
{authors.length - 1}{' '}
{pluralize(authors.length - 1, 'other')}
</Text>
</>
) : undefined}
@ -187,6 +197,8 @@ export const FeedItem = observer(function FeedItem({
{ago(item.indexedAt)}
</Text>
</View>
</View>
</TouchableWithoutFeedback>
{item.isUpvote || item.isRepost ? (
<AdditionalPostText additionalPost={item.additionalPost} />
) : (
@ -198,6 +210,116 @@ export const FeedItem = observer(function FeedItem({
)
})
function CondensedAuthorsList({authors}: {authors: Author[]}) {
const pal = usePalette('default')
if (authors.length === 1) {
return (
<View style={styles.avis}>
<Link
style={s.mr5}
href={authors[0].href}
title={`@${authors[0].handle}`}>
<UserAvatar
size={35}
displayName={authors[0].displayName}
handle={authors[0].handle}
avatar={authors[0].avatar}
/>
</Link>
</View>
)
}
return (
<View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => (
<View key={author.href} style={s.mr5}>
<UserAvatar
size={35}
displayName={author.displayName}
handle={author.handle}
avatar={author.avatar}
/>
</View>
))}
{authors.length > MAX_AUTHORS ? (
<Text style={[styles.aviExtraCount, pal.textLight]}>
+{authors.length - MAX_AUTHORS}
</Text>
) : undefined}
<FontAwesomeIcon
icon="angle-down"
size={18}
style={[styles.expandedAuthorsCloseBtnIcon, pal.icon]}
/>
</View>
)
}
function ExpandedAuthorsList({
visible,
authors,
onToggleAuthorsExpanded,
}: {
visible: boolean
authors: Author[]
onToggleAuthorsExpanded: () => void
}) {
const pal = usePalette('default')
const heightInterp = useAnimatedValue(visible ? 1 : 0)
const targetHeight =
authors.length * (EXPANDED_AUTHOR_EL_HEIGHT + 10) /*10=margin*/ +
EXPANDED_AUTHORS_CLOSE_EL_HEIGHT
const heightStyle = {
height: Animated.multiply(heightInterp, targetHeight),
overflow: 'hidden',
}
React.useEffect(() => {
Animated.timing(heightInterp, {
toValue: visible ? 1 : 0,
duration: 200,
useNativeDriver: false,
}).start()
}, [heightInterp, visible])
return (
<Animated.View style={(s.mb5, heightStyle)}>
<TouchableOpacity
style={styles.expandedAuthorsCloseBtn}
onPress={onToggleAuthorsExpanded}>
<FontAwesomeIcon
icon="angle-up"
size={18}
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
/>
<Text type="sm-medium" style={pal.text}>
Hide
</Text>
</TouchableOpacity>
{authors.map(author => (
<Link
href={author.href}
title={author.displayName || author.handle}
style={styles.expandedAuthor}>
<View style={styles.expandedAuthorAvi}>
<UserAvatar
size={35}
displayName={author.displayName}
handle={author.handle}
avatar={author.avatar}
/>
</View>
<View style={s.flex1}>
<Text type="lg-bold" numberOfLines={1}>
{author.displayName || author.handle}
&nbsp;
<Text style={[pal.textLight]}>{author.handle}</Text>
</Text>
</View>
</Link>
))}
</Animated.View>
)
}
function AdditionalPostText({
additionalPost,
}: {
@ -282,4 +404,25 @@ const styles = StyleSheet.create({
paddingTop: 4,
paddingLeft: 36,
},
expandedAuthorsCloseBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 8,
height: EXPANDED_AUTHORS_CLOSE_EL_HEIGHT,
overflow: 'hidden',
},
expandedAuthorsCloseBtnIcon: {
marginLeft: 4,
marginRight: 4,
},
expandedAuthor: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
height: EXPANDED_AUTHOR_EL_HEIGHT,
},
expandedAuthorAvi: {
marginRight: 5,
},
})

View File

@ -103,10 +103,7 @@ export const PostThreadItem = observer(function PostThreadItem({
if (deleted) {
return (
<View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}>
<FontAwesomeIcon
icon={['far', 'trash-can']}
style={{color: pal.colors.icon}}
/>
<FontAwesomeIcon icon={['far', 'trash-can']} style={pal.icon} />
<Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text>
</View>
)

View File

@ -10,6 +10,7 @@ export interface UsePaletteValue {
textLight: TextStyle
textInverted: TextStyle
link: TextStyle
icon: TextStyle
}
export function usePalette(color: PaletteColorName): UsePaletteValue {
const palette = useTheme().palette[color]
@ -36,5 +37,8 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
link: {
color: palette.link,
},
icon: {
color: palette.icon,
},
}
}

View File

@ -13,7 +13,7 @@ export const defaultTheme: Theme = {
textInverted: colors.white,
link: colors.blue3,
border: '#f0e9e9',
icon: colors.gray2,
icon: colors.gray3,
// non-standard
textVeryLight: colors.gray4,