Add the ability to expand/collapse users in notifications
parent
1ed82b6c59
commit
74ab6530d4
|
@ -1,6 +1,12 @@
|
||||||
import React, {useMemo} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
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 {AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {AtUri} from '../../../third-party/uri'
|
import {AtUri} from '../../../third-party/uri'
|
||||||
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
|
||||||
|
@ -16,16 +22,28 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {Post} from '../post/Post'
|
import {Post} from '../post/Post'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
|
||||||
|
|
||||||
const MAX_AUTHORS = 8
|
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({
|
export const FeedItem = observer(function FeedItem({
|
||||||
item,
|
item,
|
||||||
}: {
|
}: {
|
||||||
item: NotificationsViewItemModel
|
item: NotificationsViewItemModel
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const itemHref = useMemo(() => {
|
const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
|
||||||
|
const itemHref = React.useMemo(() => {
|
||||||
if (item.isUpvote || item.isRepost) {
|
if (item.isUpvote || item.isRepost) {
|
||||||
const urip = new AtUri(item.subjectUri)
|
const urip = new AtUri(item.subjectUri)
|
||||||
return `/profile/${urip.host}/post/${urip.rkey}`
|
return `/profile/${urip.host}/post/${urip.rkey}`
|
||||||
|
@ -37,7 +55,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}, [item])
|
}, [item])
|
||||||
const itemTitle = useMemo(() => {
|
const itemTitle = React.useMemo(() => {
|
||||||
if (item.isUpvote || item.isRepost) {
|
if (item.isUpvote || item.isRepost) {
|
||||||
return 'Post'
|
return 'Post'
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
} else if (item.isFollow || item.isAssertion) {
|
||||||
|
@ -47,6 +65,10 @@ export const FeedItem = observer(function FeedItem({
|
||||||
}
|
}
|
||||||
}, [item])
|
}, [item])
|
||||||
|
|
||||||
|
const onToggleAuthorsExpanded = () => {
|
||||||
|
setAuthorsExpanded(!isAuthorsExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
if (item.additionalPost?.notFound) {
|
if (item.additionalPost?.notFound) {
|
||||||
// don't render anything if the target post was deleted or unfindable
|
// don't render anything if the target post was deleted or unfindable
|
||||||
return <View />
|
return <View />
|
||||||
|
@ -93,12 +115,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
let authors: {
|
let authors: Author[] = [
|
||||||
href: string
|
|
||||||
handle: string
|
|
||||||
displayName?: string
|
|
||||||
avatar?: string
|
|
||||||
}[] = [
|
|
||||||
{
|
{
|
||||||
href: `/profile/${item.author.handle}`,
|
href: `/profile/${item.author.handle}`,
|
||||||
handle: item.author.handle,
|
handle: item.author.handle,
|
||||||
|
@ -143,27 +160,19 @@ export const FeedItem = observer(function FeedItem({
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<View style={styles.avis}>
|
<TouchableWithoutFeedback
|
||||||
{authors.slice(0, MAX_AUTHORS).map(author => (
|
onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}>
|
||||||
<Link
|
<View>
|
||||||
style={{marginRight: 5}}
|
<ExpandedAuthorsList
|
||||||
key={author.href}
|
visible={isAuthorsExpanded}
|
||||||
href={author.href}
|
authors={authors}
|
||||||
title={`@${author.handle}`}>
|
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
|
||||||
<UserAvatar
|
|
||||||
size={35}
|
|
||||||
displayName={author.displayName}
|
|
||||||
handle={author.handle}
|
|
||||||
avatar={author.avatar}
|
|
||||||
/>
|
/>
|
||||||
</Link>
|
{isAuthorsExpanded ? (
|
||||||
))}
|
<></>
|
||||||
{authors.length > MAX_AUTHORS ? (
|
) : (
|
||||||
<Text style={[styles.aviExtraCount, pal.textLight]}>
|
<CondensedAuthorsList authors={authors} />
|
||||||
+{authors.length - MAX_AUTHORS}
|
)}
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<Link
|
<Link
|
||||||
key={authors[0].href}
|
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]}>and</Text>
|
||||||
<Text style={[styles.metaItem, pal.text, s.bold]}>
|
<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>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -187,6 +197,8 @@ export const FeedItem = observer(function FeedItem({
|
||||||
{ago(item.indexedAt)}
|
{ago(item.indexedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
{item.isUpvote || item.isRepost ? (
|
{item.isUpvote || item.isRepost ? (
|
||||||
<AdditionalPostText additionalPost={item.additionalPost} />
|
<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}
|
||||||
|
|
||||||
|
<Text style={[pal.textLight]}>{author.handle}</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function AdditionalPostText({
|
function AdditionalPostText({
|
||||||
additionalPost,
|
additionalPost,
|
||||||
}: {
|
}: {
|
||||||
|
@ -282,4 +404,25 @@ const styles = StyleSheet.create({
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingLeft: 36,
|
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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -103,10 +103,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}>
|
<View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={['far', 'trash-can']} style={pal.icon} />
|
||||||
icon={['far', 'trash-can']}
|
|
||||||
style={{color: pal.colors.icon}}
|
|
||||||
/>
|
|
||||||
<Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text>
|
<Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface UsePaletteValue {
|
||||||
textLight: TextStyle
|
textLight: TextStyle
|
||||||
textInverted: TextStyle
|
textInverted: TextStyle
|
||||||
link: TextStyle
|
link: TextStyle
|
||||||
|
icon: TextStyle
|
||||||
}
|
}
|
||||||
export function usePalette(color: PaletteColorName): UsePaletteValue {
|
export function usePalette(color: PaletteColorName): UsePaletteValue {
|
||||||
const palette = useTheme().palette[color]
|
const palette = useTheme().palette[color]
|
||||||
|
@ -36,5 +37,8 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
|
||||||
link: {
|
link: {
|
||||||
color: palette.link,
|
color: palette.link,
|
||||||
},
|
},
|
||||||
|
icon: {
|
||||||
|
color: palette.icon,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const defaultTheme: Theme = {
|
||||||
textInverted: colors.white,
|
textInverted: colors.white,
|
||||||
link: colors.blue3,
|
link: colors.blue3,
|
||||||
border: '#f0e9e9',
|
border: '#f0e9e9',
|
||||||
icon: colors.gray2,
|
icon: colors.gray3,
|
||||||
|
|
||||||
// non-standard
|
// non-standard
|
||||||
textVeryLight: colors.gray4,
|
textVeryLight: colors.gray4,
|
||||||
|
|
Loading…
Reference in New Issue