about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/List.web.tsx63
-rw-r--r--src/view/com/util/TimeElapsed.tsx6
-rw-r--r--src/view/com/util/UserAvatar.tsx35
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx45
-rw-r--r--src/view/com/util/images/ImageHorzList.tsx57
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx4
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}