diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/List.web.tsx | 63 | ||||
-rw-r--r-- | src/view/com/util/TimeElapsed.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 35 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 45 | ||||
-rw-r--r-- | src/view/com/util/images/ImageHorzList.tsx | 57 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 4 |
6 files changed, 133 insertions, 77 deletions
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 6b0c17762..e917ab1d3 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -38,6 +38,7 @@ function ListImpl<ItemT>( { ListHeaderComponent, ListFooterComponent, + ListEmptyComponent, containWeb, contentContainerStyle, data, @@ -72,23 +73,35 @@ function ListImpl<ItemT>( ) } - let header: JSX.Element | null = null + const isEmpty = !data || data.length === 0 + + let headerComponent: JSX.Element | null = null if (ListHeaderComponent != null) { if (isValidElement(ListHeaderComponent)) { - header = ListHeaderComponent + headerComponent = ListHeaderComponent } else { // @ts-ignore Nah it's fine. - header = <ListHeaderComponent /> + headerComponent = <ListHeaderComponent /> } } - let footer: JSX.Element | null = null + let footerComponent: JSX.Element | null = null if (ListFooterComponent != null) { if (isValidElement(ListFooterComponent)) { - footer = ListFooterComponent + footerComponent = ListFooterComponent + } else { + // @ts-ignore Nah it's fine. + footerComponent = <ListFooterComponent /> + } + } + + let emptyComponent: JSX.Element | null = null + if (ListEmptyComponent != null) { + if (isValidElement(ListEmptyComponent)) { + emptyComponent = ListEmptyComponent } else { // @ts-ignore Nah it's fine. - footer = <ListFooterComponent /> + emptyComponent = <ListEmptyComponent /> } } @@ -323,36 +336,38 @@ function ListImpl<ItemT>( onVisibleChange={handleAboveTheFoldVisibleChange} style={[styles.aboveTheFoldDetector, {height: headerOffset}]} /> - {onStartReached && ( + {onStartReached && !isEmpty && ( <Visibility root={containWeb ? nativeRef : null} onVisibleChange={onHeadVisibilityChange} topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} /> )} - {header} - {(data as Array<ItemT>).map((item, index) => { - const key = keyExtractor!(item, index) - return ( - <Row<ItemT> - key={key} - item={item} - index={index} - renderItem={renderItem} - extraData={extraData} - onItemSeen={onItemSeen} - disableContentVisibility={disableContentVisibility} - /> - ) - })} - {onEndReached && ( + {headerComponent} + {isEmpty + ? emptyComponent + : (data as Array<ItemT>)?.map((item, index) => { + const key = keyExtractor!(item, index) + return ( + <Row<ItemT> + key={key} + item={item} + index={index} + renderItem={renderItem} + extraData={extraData} + onItemSeen={onItemSeen} + disableContentVisibility={disableContentVisibility} + /> + ) + })} + {onEndReached && !isEmpty && ( <Visibility root={containWeb ? nativeRef : null} onVisibleChange={onTailVisibilityChange} bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} /> )} - {footer} + {footerComponent} </View> </View> ) diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx index d939b3163..a49585182 100644 --- a/src/view/com/util/TimeElapsed.tsx +++ b/src/view/com/util/TimeElapsed.tsx @@ -15,12 +15,14 @@ export function TimeElapsed({ const ago = useGetTimeAgo() const format = timeToString ?? ago const tick = useTickEveryMinute() - const [timeElapsed, setTimeAgo] = React.useState(() => format(timestamp)) + const [timeElapsed, setTimeAgo] = React.useState(() => + format(timestamp, tick), + ) const [prevTick, setPrevTick] = React.useState(tick) if (prevTick !== tick) { setPrevTick(tick) - setTimeAgo(format(timestamp)) + setTimeAgo(format(timestamp, tick)) } return children({timeElapsed}) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 587b466a3..c212ea4c0 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -35,6 +35,7 @@ export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' interface BaseUserAvatarProps { type?: UserAvatarType + shape?: 'circle' | 'square' size: number avatar?: string | null } @@ -60,12 +61,16 @@ const BLUR_AMOUNT = isWeb ? 5 : 100 let DefaultAvatar = ({ type, + shape: overrideShape, size, }: { type: UserAvatarType + shape?: 'square' | 'circle' size: number }): React.ReactNode => { + const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') if (type === 'algo') { + // TODO: shape=circle // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. return ( <Svg @@ -84,6 +89,7 @@ let DefaultAvatar = ({ ) } if (type === 'list') { + // TODO: shape=circle // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. return ( <Svg @@ -117,14 +123,18 @@ let DefaultAvatar = ({ viewBox="0 0 32 32" fill="none" stroke="none"> - <Rect - x="0" - y="0" - width="32" - height="32" - rx="3" - fill={tokens.color.temp_purple} - /> + {finalShape === 'square' ? ( + <Rect + x="0" + y="0" + width="32" + height="32" + rx="3" + fill={tokens.color.temp_purple} + /> + ) : ( + <Circle cx="16" cy="16" r="16" fill={tokens.color.temp_purple} /> + )} <Path d="M24 9.75L16 7L8 9.75V15.9123C8 20.8848 12 23 16 25.1579C20 23 24 20.8848 24 15.9123V9.75Z" stroke="white" @@ -135,6 +145,7 @@ let DefaultAvatar = ({ </Svg> ) } + // TODO: shape=square return ( <Svg testID="userAvatarFallback" @@ -159,6 +170,7 @@ export {DefaultAvatar} let UserAvatar = ({ type = 'user', + shape: overrideShape, size, avatar, moderation, @@ -166,9 +178,10 @@ let UserAvatar = ({ }: UserAvatarProps): React.ReactNode => { const pal = usePalette('default') const backgroundColor = pal.colors.backgroundLight + const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') const aviStyle = useMemo(() => { - if (type === 'algo' || type === 'list' || type === 'labeler') { + if (finalShape === 'square') { return { width: size, height: size, @@ -182,7 +195,7 @@ let UserAvatar = ({ borderRadius: Math.floor(size / 2), backgroundColor, } - }, [type, size, backgroundColor]) + }, [finalShape, size, backgroundColor]) const alert = useMemo(() => { if (!moderation?.alert) { @@ -224,7 +237,7 @@ let UserAvatar = ({ </View> ) : ( <View style={{width: size, height: size}}> - <DefaultAvatar type={type} size={size} /> + <DefaultAvatar type={type} shape={finalShape} size={size} /> {alert} </View> ) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 2486b73d5..45e00e58c 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -7,7 +7,7 @@ import { } from 'react-native' import * as Clipboard from 'expo-clipboard' import { - AppBskyActorDefs, + AppBskyFeedDefs, AppBskyFeedPost, AtUri, RichText as RichTextAPI, @@ -22,12 +22,15 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' +import {Shadow} from '#/state/cache/post-shadow' import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {usePostDeleteMutation} from '#/state/queries/post' +import { + usePostDeleteMutation, + useThreadMuteMutationQueue, +} from '#/state/queries/post' import {useSession} from '#/state/session' import {getCurrentRoute} from 'lib/routes/helpers' import {shareUrl} from 'lib/sharing' @@ -62,9 +65,7 @@ import * as Toast from '../Toast' let PostDropdownBtn = ({ testID, - postAuthor, - postCid, - postUri, + post, postFeedContext, record, richText, @@ -74,9 +75,7 @@ let PostDropdownBtn = ({ timestamp, }: { testID: string - postAuthor: AppBskyActorDefs.ProfileViewBasic - postCid: string - postUri: string + post: Shadow<AppBskyFeedDefs.PostView> postFeedContext: string | undefined record: AppBskyFeedPost.Record richText: RichTextAPI @@ -92,8 +91,6 @@ let PostDropdownBtn = ({ const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const langPrefs = useLanguagePrefs() - const mutedThreads = useMutedThreads() - const toggleThreadMute = useToggleThreadMute() const postDeleteMutation = usePostDeleteMutation() const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() @@ -107,9 +104,15 @@ let PostDropdownBtn = ({ const loggedOutWarningPromptControl = useDialogControl() const embedPostControl = useDialogControl() const sendViaChatControl = useDialogControl() + const postUri = post.uri + const postCid = post.cid + const postAuthor = post.author const rootUri = record.reply?.root?.uri || postUri - const isThreadMuted = mutedThreads.includes(rootUri) + const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( + post, + rootUri, + ) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isAuthor = postAuthor.did === currentAccount?.did @@ -162,18 +165,22 @@ let PostDropdownBtn = ({ const onToggleThreadMute = React.useCallback(() => { try { - const muted = toggleThreadMute(rootUri) - if (muted) { + if (isThreadMuted) { + unmuteThread() + Toast.show(_(msg`You will now receive notifications for this thread`)) + } else { + muteThread() Toast.show( _(msg`You will no longer receive notifications for this thread`), ) - } else { - Toast.show(_(msg`You will now receive notifications for this thread`)) } - } catch (e) { - logger.error('Failed to toggle thread mute', {message: e}) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to toggle thread mute', {message: e}) + Toast.show(_(msg`Failed to toggle thread mute, please try again`)) + } } - }, [rootUri, toggleThreadMute, _]) + }, [isThreadMuted, unmuteThread, _, muteThread]) const onCopyPostText = React.useCallback(() => { const str = richTextToString(richText, true) diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 12eef14f7..bade2a444 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -2,39 +2,60 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' interface Props { images: AppBskyEmbedImages.ViewImage[] style?: StyleProp<ViewStyle> + gif?: boolean } -export function ImageHorzList({images, style}: Props) { +export function ImageHorzList({images, style, gif}: Props) { return ( - <View style={[styles.flexRow, style]}> + <View style={[a.flex_row, a.gap_xs, style]}> {images.map(({thumb, alt}) => ( - <Image + <View key={thumb} - source={{uri: thumb}} - style={styles.image} - accessible={true} - accessibilityIgnoresInvertColors - accessibilityHint={alt} - accessibilityLabel="" - /> + style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}> + <Image + key={thumb} + source={{uri: thumb}} + style={[a.flex_1, a.rounded_xs]} + accessible={true} + accessibilityIgnoresInvertColors + accessibilityHint={alt} + accessibilityLabel="" + /> + {gif && ( + <View style={styles.altContainer}> + <Text style={styles.alt}> + <Trans>GIF</Trans> + </Text> + </View> + )} + </View> ))} </View> ) } const styles = StyleSheet.create({ - flexRow: { - flexDirection: 'row', - gap: 5, + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + right: 5, + bottom: 5, + zIndex: 2, }, - image: { - maxWidth: 100, - aspectRatio: 1, - flex: 1, - borderRadius: 4, + alt: { + color: 'white', + fontSize: 7, + fontWeight: 'bold', }, }) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c389855e3..c0e743db4 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -319,9 +319,7 @@ let PostCtrls = ({ <View style={big ? a.align_center : [a.flex_1, a.align_start]}> <PostDropdownBtn testID="postDropdownBtn" - postAuthor={post.author} - postCid={post.cid} - postUri={post.uri} + post={post} postFeedContext={feedContext} record={record} richText={richText} |