From 74ab6530d427556c38eb04fe135467f4fa91404c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 19 Jan 2023 11:34:07 -0600 Subject: [PATCH] Add the ability to expand/collapse users in notifications --- src/view/com/notifications/FeedItem.tsx | 249 +++++++++++++++----- src/view/com/post-thread/PostThreadItem.tsx | 5 +- src/view/lib/hooks/usePalette.ts | 4 + src/view/lib/themes.ts | 2 +- 4 files changed, 202 insertions(+), 58 deletions(-) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 0c516a08..75b83d37 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -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(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 @@ -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,50 +160,45 @@ export const FeedItem = observer(function FeedItem({ )} - - {authors.slice(0, MAX_AUTHORS).map(author => ( - - - - ))} - {authors.length > MAX_AUTHORS ? ( - - +{authors.length - MAX_AUTHORS} - - ) : undefined} - - - - - {authors[0].displayName || authors[0].handle} - - - {authors.length > 1 ? ( - <> - and - - {authors.length - 1} {pluralize(authors.length - 1, 'other')} + 1 ? onToggleAuthorsExpanded : () => {}}> + + + {isAuthorsExpanded ? ( + <> + ) : ( + + )} + + + + {authors[0].displayName || authors[0].handle} + + + {authors.length > 1 ? ( + <> + and + + {authors.length - 1}{' '} + {pluralize(authors.length - 1, 'other')} + + + ) : undefined} + {action} + + {ago(item.indexedAt)} - - ) : undefined} - {action} - - {ago(item.indexedAt)} - - + + + {item.isUpvote || item.isRepost ? ( ) : ( @@ -198,6 +210,116 @@ export const FeedItem = observer(function FeedItem({ ) }) +function CondensedAuthorsList({authors}: {authors: Author[]}) { + const pal = usePalette('default') + if (authors.length === 1) { + return ( + + + + + + ) + } + return ( + + {authors.slice(0, MAX_AUTHORS).map(author => ( + + + + ))} + {authors.length > MAX_AUTHORS ? ( + + +{authors.length - MAX_AUTHORS} + + ) : undefined} + + + ) +} + +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 ( + + + + + Hide + + + {authors.map(author => ( + + + + + + + {author.displayName || author.handle} +   + {author.handle} + + + + ))} + + ) +} + 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, + }, }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8df6260c..2c7ab716 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -103,10 +103,7 @@ export const PostThreadItem = observer(function PostThreadItem({ if (deleted) { return ( - + This post has been deleted. ) diff --git a/src/view/lib/hooks/usePalette.ts b/src/view/lib/hooks/usePalette.ts index 9eb3e41a..890439f3 100644 --- a/src/view/lib/hooks/usePalette.ts +++ b/src/view/lib/hooks/usePalette.ts @@ -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, + }, } } diff --git a/src/view/lib/themes.ts b/src/view/lib/themes.ts index 429a7602..b9e2bdac 100644 --- a/src/view/lib/themes.ts +++ b/src/view/lib/themes.ts @@ -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,