about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/composer/AltTextCounterWrapper.tsx33
-rw-r--r--src/view/com/composer/GifAltText.tsx137
-rw-r--r--src/view/com/composer/char-progress/CharProgress.tsx50
-rw-r--r--src/view/com/composer/photos/ImageAltTextDialog.tsx127
4 files changed, 230 insertions, 117 deletions
diff --git a/src/view/com/composer/AltTextCounterWrapper.tsx b/src/view/com/composer/AltTextCounterWrapper.tsx
new file mode 100644
index 000000000..d69252f4b
--- /dev/null
+++ b/src/view/com/composer/AltTextCounterWrapper.tsx
@@ -0,0 +1,33 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {MAX_ALT_TEXT} from '#/lib/constants'
+import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
+import {atoms as a, useTheme} from '#/alf'
+
+export function AltTextCounterWrapper({
+  altText,
+  children,
+}: {
+  altText?: string
+  children: React.ReactNode
+}) {
+  const t = useTheme()
+  return (
+    <View style={[a.flex_row]}>
+      <CharProgress
+        style={[
+          a.flex_col_reverse,
+          a.align_center,
+          a.mr_xs,
+          {minWidth: 50, gap: 1},
+        ]}
+        textStyle={[{marginRight: 0}, a.text_sm, t.atoms.text_contrast_medium]}
+        size={26}
+        count={altText?.length || 0}
+        max={MAX_ALT_TEXT}
+      />
+      {children}
+    </View>
+  )
+}
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index 3479fb973..90d20d94f 100644
--- a/src/view/com/composer/GifAltText.tsx
+++ b/src/view/com/composer/GifAltText.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useState} from 'react'
+import React, {useState} from 'react'
 import {TouchableOpacity, View} from 'react-native'
 import {AppBskyEmbedExternal} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
@@ -11,14 +11,16 @@ import {
   EmbedPlayerParams,
   parseEmbedPlayerFromUrl,
 } from '#/lib/strings/embed-player'
-import {enforceLen} from '#/lib/strings/helpers'
 import {isAndroid} from '#/platform/detection'
 import {Gif} from '#/state/queries/tenor'
+import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
 import {atoms as a, native, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
+import {DialogControlProps} from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
@@ -52,18 +54,11 @@ export function GifAltText({
     }
   }, [linkProp])
 
-  const onPressSubmit = useCallback(
-    (alt: string) => {
-      control.close(() => {
-        onSubmit(alt)
-      })
-    },
-    [onSubmit, control],
-  )
+  const parsedAlt = parseAltFromGIFDescription(link.description)
+  const [altText, setAltText] = useState(parsedAlt.alt)
 
   if (!gif || !params) return null
 
-  const parsedAlt = parseAltFromGIFDescription(link.description)
   return (
     <>
       <TouchableOpacity
@@ -99,13 +94,19 @@ export function GifAltText({
 
       <AltTextReminder />
 
-      <Dialog.Outer control={control} Portal={Portal}>
+      <Dialog.Outer
+        control={control}
+        onClose={() => {
+          onSubmit(altText)
+        }}
+        Portal={Portal}>
         <Dialog.Handle />
         <AltTextInner
-          onSubmit={onPressSubmit}
+          altText={altText}
+          setAltText={setAltText}
+          control={control}
           link={link}
           params={params}
-          initialValue={parsedAlt.isPreferred ? parsedAlt.alt : ''}
           key={link.uri}
         />
       </Dialog.Outer>
@@ -114,61 +115,83 @@ export function GifAltText({
 }
 
 function AltTextInner({
-  onSubmit,
+  altText,
+  setAltText,
+  control,
   link,
   params,
-  initialValue: initalValue,
 }: {
-  onSubmit: (text: string) => void
+  altText: string
+  setAltText: (text: string) => void
+  control: DialogControlProps
   link: AppBskyEmbedExternal.ViewExternal
   params: EmbedPlayerParams
-  initialValue: string
 }) {
-  const {_} = useLingui()
-  const [altText, setAltText] = useState(initalValue)
-  const control = Dialog.useDialogContext()
-
-  const onPressSubmit = useCallback(() => {
-    onSubmit(altText)
-  }, [onSubmit, altText])
+  const t = useTheme()
+  const {_, i18n} = useLingui()
 
   return (
     <Dialog.ScrollableInner label={_(msg`Add alt text`)}>
       <View style={a.flex_col_reverse}>
         <View style={[a.mt_md, a.gap_md]}>
-          <View>
-            <TextField.LabelText>
-              <Trans>Descriptive alt text</Trans>
-            </TextField.LabelText>
-            <TextField.Root>
-              <Dialog.Input
-                label={_(msg`Alt text`)}
-                placeholder={link.title}
-                onChangeText={text =>
-                  setAltText(enforceLen(text, MAX_ALT_TEXT))
-                }
-                value={altText}
-                multiline
-                numberOfLines={3}
-                autoFocus
-                onKeyPress={({nativeEvent}) => {
-                  if (nativeEvent.key === 'Escape') {
-                    control.close()
-                  }
-                }}
-              />
-            </TextField.Root>
+          <View style={[a.gap_sm]}>
+            <View style={[a.relative]}>
+              <TextField.LabelText>
+                <Trans>Descriptive alt text</Trans>
+              </TextField.LabelText>
+              <TextField.Root>
+                <Dialog.Input
+                  label={_(msg`Alt text`)}
+                  placeholder={link.title}
+                  onChangeText={text => {
+                    setAltText(text)
+                  }}
+                  defaultValue={altText}
+                  multiline
+                  numberOfLines={3}
+                  autoFocus
+                  onKeyPress={({nativeEvent}) => {
+                    if (nativeEvent.key === 'Escape') {
+                      control.close()
+                    }
+                  }}
+                />
+              </TextField.Root>
+            </View>
+
+            {altText.length > MAX_ALT_TEXT && (
+              <View style={[a.pb_sm, a.flex_row, a.gap_xs]}>
+                <CircleInfo fill={t.palette.negative_500} />
+                <Text
+                  style={[
+                    a.italic,
+                    a.leading_snug,
+                    t.atoms.text_contrast_medium,
+                  ]}>
+                  <Trans>
+                    Alt text will be truncated. Limit:{' '}
+                    {i18n.number(MAX_ALT_TEXT)} characters.
+                  </Trans>
+                </Text>
+              </View>
+            )}
           </View>
-          <Button
-            label={_(msg`Save`)}
-            size="large"
-            color="primary"
-            variant="solid"
-            onPress={onPressSubmit}>
-            <ButtonText>
-              <Trans>Save</Trans>
-            </ButtonText>
-          </Button>
+
+          <AltTextCounterWrapper altText={altText}>
+            <Button
+              label={_(msg`Save`)}
+              size="large"
+              color="primary"
+              variant="solid"
+              onPress={() => {
+                control.close()
+              }}
+              style={[a.flex_grow]}>
+              <ButtonText>
+                <Trans>Save</Trans>
+              </ButtonText>
+            </Button>
+          </AltTextCounterWrapper>
         </View>
         {/* below the text input to force tab order */}
         <View>
diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx
index a205fe096..c61f753f2 100644
--- a/src/view/com/composer/char-progress/CharProgress.tsx
+++ b/src/view/com/composer/char-progress/CharProgress.tsx
@@ -1,48 +1,56 @@
 import React from 'react'
-import {View} from 'react-native'
+import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
 // @ts-ignore no type definition -prf
 import ProgressCircle from 'react-native-progress/Circle'
 // @ts-ignore no type definition -prf
 import ProgressPie from 'react-native-progress/Pie'
 
-import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
-import {usePalette} from 'lib/hooks/usePalette'
-import {s} from 'lib/styles'
+import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {s} from '#/lib/styles'
 import {Text} from '../../util/text/Text'
 
-const DANGER_LENGTH = MAX_GRAPHEME_LENGTH
-
-export function CharProgress({count}: {count: number}) {
+export function CharProgress({
+  count,
+  max,
+  style,
+  textStyle,
+  size,
+}: {
+  count: number
+  max?: number
+  style?: StyleProp<ViewStyle>
+  textStyle?: StyleProp<TextStyle>
+  size?: number
+}) {
+  const maxLength = max || MAX_GRAPHEME_LENGTH
   const pal = usePalette('default')
-  const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text
-  const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link
+  const textColor = count > maxLength ? '#e60000' : pal.colors.text
+  const circleColor = count > maxLength ? '#e60000' : pal.colors.link
   return (
-    <>
-      <Text style={[s.mr10, s.tabularNum, {color: textColor}]}>
-        {MAX_GRAPHEME_LENGTH - count}
+    <View style={style}>
+      <Text style={[s.mr10, s.tabularNum, {color: textColor}, textStyle]}>
+        {maxLength - count}
       </Text>
       <View>
-        {count > DANGER_LENGTH ? (
+        {count > maxLength ? (
           <ProgressPie
-            size={30}
+            size={size ?? 30}
             borderWidth={4}
             borderColor={circleColor}
             color={circleColor}
-            progress={Math.min(
-              (count - MAX_GRAPHEME_LENGTH) / MAX_GRAPHEME_LENGTH,
-              1,
-            )}
+            progress={Math.min((count - maxLength) / maxLength, 1)}
           />
         ) : (
           <ProgressCircle
-            size={30}
+            size={size ?? 30}
             borderWidth={1}
             borderColor={pal.colors.border}
             color={circleColor}
-            progress={count / MAX_GRAPHEME_LENGTH}
+            progress={count / maxLength}
           />
         )}
       </View>
-    </>
+    </View>
   )
 }
diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx
index 16ce4351a..e9e8d4222 100644
--- a/src/view/com/composer/photos/ImageAltTextDialog.tsx
+++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx
@@ -5,12 +5,16 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {MAX_ALT_TEXT} from '#/lib/constants'
+import {enforceLen} from '#/lib/strings/helpers'
 import {isAndroid, isWeb} from '#/platform/detection'
 import {ComposerImage} from '#/state/gallery'
+import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
+import {DialogControlProps} from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
@@ -21,32 +25,50 @@ type Props = {
   Portal: PortalComponent
 }
 
-export const ImageAltTextDialog = (props: Props): React.ReactNode => {
+export const ImageAltTextDialog = ({
+  control,
+  image,
+  onChange,
+  Portal,
+}: Props): React.ReactNode => {
+  const [altText, setAltText] = React.useState(image.alt)
+
   return (
-    <Dialog.Outer control={props.control} Portal={props.Portal}>
+    <Dialog.Outer
+      control={control}
+      onClose={() => {
+        onChange({
+          ...image,
+          alt: enforceLen(altText, MAX_ALT_TEXT, true),
+        })
+      }}
+      Portal={Portal}>
       <Dialog.Handle />
-      <ImageAltTextInner {...props} />
+      <ImageAltTextInner
+        control={control}
+        image={image}
+        altText={altText}
+        setAltText={setAltText}
+      />
     </Dialog.Outer>
   )
 }
 
 const ImageAltTextInner = ({
+  altText,
+  setAltText,
   control,
   image,
-  onChange,
-}: Props): React.ReactNode => {
-  const {_} = useLingui()
+}: {
+  altText: string
+  setAltText: (text: string) => void
+  control: DialogControlProps
+  image: Props['image']
+}): React.ReactNode => {
+  const {_, i18n} = useLingui()
   const t = useTheme()
-
   const windim = useWindowDimensions()
 
-  const [altText, setAltText] = React.useState(image.alt)
-
-  const onPressSubmit = React.useCallback(() => {
-    control.close()
-    onChange({...image, alt: altText.trim()})
-  }, [control, image, altText, onChange])
-
   const imageStyle = React.useMemo<ImageStyle>(() => {
     const maxWidth = isWeb ? 450 : windim.width
     const source = image.transformed ?? image.source
@@ -90,32 +112,59 @@ const ImageAltTextInner = ({
       </View>
 
       <View style={[a.mt_md, a.gap_md]}>
-        <View>
-          <TextField.LabelText>
-            <Trans>Descriptive alt text</Trans>
-          </TextField.LabelText>
-          <TextField.Root>
-            <Dialog.Input
-              label={_(msg`Alt text`)}
-              onChangeText={text => setAltText(text)}
-              value={altText}
-              multiline
-              numberOfLines={3}
-              autoFocus
-            />
-          </TextField.Root>
+        <View style={[a.gap_sm]}>
+          <View style={[a.relative, {width: '100%'}]}>
+            <TextField.LabelText>
+              <Trans>Descriptive alt text</Trans>
+            </TextField.LabelText>
+            <TextField.Root>
+              <Dialog.Input
+                label={_(msg`Alt text`)}
+                onChangeText={text => {
+                  setAltText(text)
+                }}
+                defaultValue={altText}
+                multiline
+                numberOfLines={3}
+                autoFocus
+              />
+            </TextField.Root>
+          </View>
+
+          {altText.length > MAX_ALT_TEXT && (
+            <View style={[a.pb_sm, a.flex_row, a.gap_xs]}>
+              <CircleInfo fill={t.palette.negative_500} />
+              <Text
+                style={[
+                  a.italic,
+                  a.leading_snug,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>
+                  Alt text will be truncated. Limit: {i18n.number(MAX_ALT_TEXT)}{' '}
+                  characters.
+                </Trans>
+              </Text>
+            </View>
+          )}
         </View>
-        <Button
-          label={_(msg`Save`)}
-          disabled={altText.length > MAX_ALT_TEXT || altText === image.alt}
-          size="large"
-          color="primary"
-          variant="solid"
-          onPress={onPressSubmit}>
-          <ButtonText>
-            <Trans>Save</Trans>
-          </ButtonText>
-        </Button>
+
+        <AltTextCounterWrapper altText={altText}>
+          <Button
+            label={_(msg`Save`)}
+            disabled={altText === image.alt}
+            size="large"
+            color="primary"
+            variant="solid"
+            onPress={() => {
+              control.close()
+            }}
+            style={[a.flex_grow]}>
+            <ButtonText>
+              <Trans>Save</Trans>
+            </ButtonText>
+          </Button>
+        </AltTextCounterWrapper>
       </View>
       {/* Maybe fix this later -h */}
       {isAndroid ? <View style={{height: 300}} /> : null}