about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-07-06 20:28:10 -0500
committerGitHub <noreply@github.com>2023-07-06 20:28:10 -0500
commite14c9783e0cea73ada1d20e8a798738c39319315 (patch)
tree41be4e050c1e7cf4ade0e0ff1c66342599618935 /src
parentf05c2f06d665cb3a9989154fbc82a2b0ea60669a (diff)
downloadvoidsky-e14c9783e0cea73ada1d20e8a798738c39319315.tar.zst
[APP-735] Post language improvements (#982)
* Fix composer character-counter bouncing around UI elements

* Fix composer toolbar padding when keyboard is dismissed on iOS

* Use the full name of the language in the composer footer

* Add headings to the DropdownButton

* Update the composer language control to use a simpler dropdown

* Fix lint

* Add translate link to Post component used in notifications

* Fix lint
Diffstat (limited to 'src')
-rw-r--r--src/lib/hooks/useIsKeyboardVisible.ts35
-rw-r--r--src/lib/styles.ts3
-rw-r--r--src/locale/helpers.ts5
-rw-r--r--src/locale/languages.ts2
-rw-r--r--src/state/models/ui/preferences.ts4
-rw-r--r--src/view/com/composer/Composer.tsx9
-rw-r--r--src/view/com/composer/char-progress/CharProgress.tsx2
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx78
-rw-r--r--src/view/com/post/Post.tsx29
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx44
-rw-r--r--src/view/index.ts6
11 files changed, 189 insertions, 28 deletions
diff --git a/src/lib/hooks/useIsKeyboardVisible.ts b/src/lib/hooks/useIsKeyboardVisible.ts
new file mode 100644
index 000000000..5b2a86eb0
--- /dev/null
+++ b/src/lib/hooks/useIsKeyboardVisible.ts
@@ -0,0 +1,35 @@
+import {useState, useEffect} from 'react'
+import {Keyboard} from 'react-native'
+import {isIOS} from 'platform/detection'
+
+export function useIsKeyboardVisible({
+  iosUseWillEvents,
+}: {
+  iosUseWillEvents?: boolean
+} = {}) {
+  const [isKeyboardVisible, setKeyboardVisible] = useState(false)
+
+  // NOTE
+  // only iOS suppose the "will" events
+  // -prf
+  const showEvent =
+    isIOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow'
+  const hideEvent =
+    isIOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide'
+
+  useEffect(() => {
+    const keyboardShowListener = Keyboard.addListener(showEvent, () =>
+      setKeyboardVisible(true),
+    )
+    const keyboardHideListener = Keyboard.addListener(hideEvent, () =>
+      setKeyboardVisible(false),
+    )
+
+    return () => {
+      keyboardHideListener.remove()
+      keyboardShowListener.remove()
+    }
+  }, [showEvent, hideEvent])
+
+  return [isKeyboardVisible]
+}
diff --git a/src/lib/styles.ts b/src/lib/styles.ts
index fb631c0bf..c5a710fff 100644
--- a/src/lib/styles.ts
+++ b/src/lib/styles.ts
@@ -89,6 +89,9 @@ export const s = StyleSheet.create({
   // text decoration
   underline: {textDecorationLine: 'underline'},
 
+  // font variants
+  tabularNum: {fontVariant: ['tabular-nums']},
+
   // font sizes
   f9: {fontSize: 9},
   f10: {fontSize: 10},
diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts
index 4b9002586..bce4e6590 100644
--- a/src/locale/helpers.ts
+++ b/src/locale/helpers.ts
@@ -18,6 +18,11 @@ export function code3ToCode2(lang: string): string {
   return lang
 }
 
+export function codeToLanguageName(lang: string): string {
+  const lang2 = code3ToCode2(lang)
+  return LANGUAGES_MAP_CODE2[lang2]?.name || lang
+}
+
 export function getPostLanguage(
   post: AppBskyFeedDefs.PostView,
 ): string | undefined {
diff --git a/src/locale/languages.ts b/src/locale/languages.ts
index 3983c213f..a61047e19 100644
--- a/src/locale/languages.ts
+++ b/src/locale/languages.ts
@@ -455,7 +455,7 @@ export const LANGUAGES: Language[] = [
   {code3: 'som', code2: 'so', name: 'Somali'},
   {code3: 'son', code2: ' ', name: 'Songhai languages'},
   {code3: 'sot', code2: 'st', name: 'Sotho, Southern'},
-  {code3: 'spa', code2: 'es', name: 'Spanish; Castilian'},
+  {code3: 'spa', code2: 'es', name: 'Spanish'},
   {code3: 'sqi', code2: 'sq', name: 'Albanian'},
   {code3: 'srd', code2: 'sc', name: 'Sardinian'},
   {code3: 'srn', code2: ' ', name: 'Sranan Tongo'},
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 858225a6f..e1c0b1f71 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -311,6 +311,10 @@ export class PreferencesModel {
     }
   }
 
+  setPostLanguage(code2: string) {
+    this.postLanguages = [code2]
+  }
+
   getReadablePostLanguages() {
     const all = this.postLanguages.map(code2 => {
       const lang = LANGUAGES.find(l => l.code2 === code2)
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index c6a9ecd4a..f2e3cbd63 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -16,6 +16,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {RichText} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
 import {ExternalEmbed} from './ExternalEmbed'
 import {Text} from '../util/text/Text'
 import * as Toast from '../util/Toast'
@@ -35,7 +36,7 @@ import {OpenCameraBtn} from './photos/OpenCameraBtn'
 import {usePalette} from 'lib/hooks/usePalette'
 import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
 import {useExternalLinkFetch} from './useExternalLinkFetch'
-import {isDesktopWeb, isAndroid} from 'platform/detection'
+import {isDesktopWeb, isAndroid, isIOS} from 'platform/detection'
 import {GalleryModel} from 'state/models/media/gallery'
 import {Gallery} from './photos/Gallery'
 import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
@@ -55,6 +56,7 @@ export const ComposePost = observer(function ComposePost({
   const pal = usePalette('default')
   const store = useStores()
   const textInput = useRef<TextInputRef>(null)
+  const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
   const [isProcessing, setIsProcessing] = useState(false)
   const [processingState, setProcessingState] = useState('')
   const [error, setError] = useState('')
@@ -75,10 +77,11 @@ export const ComposePost = observer(function ComposePost({
   const insets = useSafeAreaInsets()
   const viewStyles = useMemo(
     () => ({
-      paddingBottom: isAndroid ? insets.bottom : 0,
+      paddingBottom:
+        isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
       paddingTop: isAndroid ? insets.top : isDesktopWeb ? 0 : 15,
     }),
-    [insets],
+    [insets, isKeyboardVisible],
   )
 
   // HACK
diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx
index 6b3b98e47..a3fa78a59 100644
--- a/src/view/com/composer/char-progress/CharProgress.tsx
+++ b/src/view/com/composer/char-progress/CharProgress.tsx
@@ -17,7 +17,7 @@ export function CharProgress({count}: {count: number}) {
   const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link
   return (
     <>
-      <Text style={[s.mr10, {color: textColor}]}>
+      <Text style={[s.mr10, s.tabularNum, {color: textColor}]}>
         {MAX_GRAPHEME_LENGTH - count}
       </Text>
       <View>
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
index 8c55e1c91..5014b5409 100644
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ b/src/view/com/composer/select-language/SelectLangBtn.tsx
@@ -1,22 +1,27 @@
-import React, {useCallback} from 'react'
-import {TouchableOpacity, StyleSheet, Keyboard} from 'react-native'
+import React, {useCallback, useMemo} from 'react'
+import {StyleSheet, Keyboard} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {Text} from 'view/com/util/text/Text'
+import {
+  DropdownButton,
+  DropdownItem,
+  DropdownItemButton,
+} from 'view/com/util/forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {isNative} from 'platform/detection'
-
-const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
+import {codeToLanguageName} from '../../../../locale/helpers'
+import {deviceLocales} from 'platform/detection'
 
 export const SelectLangBtn = observer(function SelectLangBtn() {
   const pal = usePalette('default')
   const store = useStores()
 
-  const onPress = useCallback(async () => {
+  const onPressMore = useCallback(async () => {
     if (isNative) {
       if (Keyboard.isVisible()) {
         Keyboard.dismiss()
@@ -25,18 +30,62 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
     store.shell.openModal({name: 'post-languages-settings'})
   }, [store])
 
+  const postLanguagesPref = store.preferences.postLanguages
+  const items: DropdownItem[] = useMemo(() => {
+    let arr: DropdownItemButton[] = []
+
+    const add = (langCode: string) => {
+      const langName = codeToLanguageName(langCode)
+      if (arr.find((item: DropdownItemButton) => item.label === langName)) {
+        return
+      }
+      arr.push({
+        icon: store.preferences.hasPostLanguage(langCode)
+          ? ['fas', 'circle-check']
+          : ['far', 'circle'],
+        label: langName,
+        onPress() {
+          store.preferences.setPostLanguage(langCode)
+        },
+      })
+    }
+
+    for (const lang of postLanguagesPref) {
+      add(lang)
+    }
+    for (const lang of deviceLocales) {
+      add(lang)
+    }
+    add('en') // english
+    add('ja') // japanese
+    add('pt') // portugese
+    add('de') // german
+
+    return [
+      {heading: true, label: 'Post language'},
+      ...arr.slice(0, 6),
+      {sep: true},
+      {
+        label: 'Other...',
+        onPress: onPressMore,
+      },
+    ]
+  }, [store.preferences, postLanguagesPref, onPressMore])
+
   return (
-    <TouchableOpacity
+    <DropdownButton
+      type="bare"
       testID="selectLangBtn"
-      onPress={onPress}
+      items={items}
+      openUpwards
       style={styles.button}
-      hitSlop={HITSLOP}
-      accessibilityRole="button"
       accessibilityLabel="Language selection"
-      accessibilityHint="Opens screen or modal to select language of post">
+      accessibilityHint="">
       {store.preferences.postLanguages.length > 0 ? (
-        <Text type="lg-bold" style={pal.link}>
-          {store.preferences.postLanguages.join(', ')}
+        <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
+          {store.preferences.postLanguages
+            .map(lang => codeToLanguageName(lang))
+            .join(', ')}
         </Text>
       ) : (
         <FontAwesomeIcon
@@ -45,7 +94,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
           size={26}
         />
       )}
-    </TouchableOpacity>
+    </DropdownButton>
   )
 })
 
@@ -53,4 +102,7 @@ const styles = StyleSheet.create({
   button: {
     paddingHorizontal: 15,
   },
+  label: {
+    maxWidth: 100,
+  },
 })
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 12ab0e901..c380c9743 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react'
+import React, {useEffect, useState, useMemo} from 'react'
 import {
   ActivityIndicator,
   Linking,
@@ -29,7 +29,7 @@ import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {getTranslatorLink} from '../../../locale/helpers'
+import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
 
 export const Post = observer(function Post({
   uri,
@@ -134,6 +134,16 @@ const PostLoaded = observer(
       const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
       replyAuthorDid = urip.hostname
     }
+
+    const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
+    const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '')
+    const needsTranslation = useMemo(
+      () =>
+        store.preferences.contentLanguages.length > 0 &&
+        !isPostInLanguage(item.post, store.preferences.contentLanguages),
+      [item.post, store.preferences.contentLanguages],
+    )
+
     const onPressReply = React.useCallback(() => {
       store.shell.openComposer({
         replyTo: {
@@ -166,9 +176,6 @@ const PostLoaded = observer(
       Toast.show('Copied to clipboard')
     }, [record])
 
-    const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
-    const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '')
-
     const onOpenTranslate = React.useCallback(() => {
       Linking.openURL(translatorUrl)
     }, [translatorUrl])
@@ -263,6 +270,15 @@ const PostLoaded = observer(
               <ImageHider moderation={item.moderation.list} style={s.mb10}>
                 <PostEmbeds embed={item.post.embed} style={s.mb10} />
               </ImageHider>
+              {needsTranslation && (
+                <View style={[pal.borderDark, styles.translateLink]}>
+                  <Link href={translatorUrl} title="Translate">
+                    <Text type="sm" style={pal.link}>
+                      Translate this post
+                    </Text>
+                  </Link>
+                </View>
+              )}
             </ContentHider>
             <PostCtrls
               itemUri={itemUri}
@@ -320,6 +336,9 @@ const styles = StyleSheet.create({
     flexWrap: 'wrap',
     paddingBottom: 8,
   },
+  translateLink: {
+    marginBottom: 12,
+  },
   replyLine: {
     position: 'absolute',
     left: 36,
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index ad216d97e..046610b29 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -24,6 +24,7 @@ import {shareUrl} from 'lib/sharing'
 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
 const ESTIMATED_BTN_HEIGHT = 50
 const ESTIMATED_SEP_HEIGHT = 16
+const ESTIMATED_HEADING_HEIGHT = 60
 
 export interface DropdownItemButton {
   testID?: string
@@ -34,7 +35,14 @@ export interface DropdownItemButton {
 export interface DropdownItemSeparator {
   sep: true
 }
-export type DropdownItem = DropdownItemButton | DropdownItemSeparator
+export interface DropdownItemHeading {
+  heading: true
+  label: string
+}
+export type DropdownItem =
+  | DropdownItemButton
+  | DropdownItemSeparator
+  | DropdownItemHeading
 type MaybeDropdownItem = DropdownItem | false | undefined
 
 export type DropdownButtonType = ButtonType | 'bare'
@@ -48,6 +56,7 @@ interface DropdownButtonProps {
   menuWidth?: number
   children?: React.ReactNode
   openToRight?: boolean
+  openUpwards?: boolean
   rightOffset?: number
   bottomOffset?: number
   accessibilityLabel?: string
@@ -63,6 +72,7 @@ export function DropdownButton({
   menuWidth,
   children,
   openToRight = false,
+  openUpwards = false,
   rightOffset = 0,
   bottomOffset = 0,
   accessibilityLabel,
@@ -91,13 +101,15 @@ export function DropdownButton({
             estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
           } else if (item && isBtn(item)) {
             estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
+          } else if (item && isHeading(item)) {
+            estimatedMenuHeight += ESTIMATED_HEADING_HEIGHT
           }
         }
         const newX = openToRight
           ? pageX + width + rightOffset
           : pageX + width - menuWidth
         let newY = pageY + height + bottomOffset
-        if (newY + estimatedMenuHeight > winHeight) {
+        if (openUpwards || newY + estimatedMenuHeight > winHeight) {
           newY -= estimatedMenuHeight
         }
         createDropdownMenu(
@@ -357,6 +369,14 @@ const DropdownItems = ({
             return (
               <View key={index} style={[styles.separator, separatorColor]} />
             )
+          } else if (isHeading(item)) {
+            return (
+              <View style={[styles.heading, pal.border]} key={index}>
+                <Text style={[pal.text, styles.headingLabel]}>
+                  {item.label}
+                </Text>
+              </View>
+            )
           }
           return null
         })}
@@ -368,8 +388,11 @@ const DropdownItems = ({
 function isSep(item: DropdownItem): item is DropdownItemSeparator {
   return 'sep' in item && item.sep
 }
+function isHeading(item: DropdownItem): item is DropdownItemHeading {
+  return 'heading' in item && item.heading
+}
 function isBtn(item: DropdownItem): item is DropdownItemButton {
-  return !isSep(item)
+  return !isSep(item) && !isHeading(item)
 }
 
 const styles = StyleSheet.create({
@@ -403,7 +426,7 @@ const styles = StyleSheet.create({
     paddingTop: 12,
   },
   icon: {
-    marginLeft: 6,
+    marginLeft: 2,
     marginRight: 8,
   },
   label: {
@@ -413,4 +436,17 @@ const styles = StyleSheet.create({
     borderTopWidth: 1,
     marginVertical: 8,
   },
+  heading: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    paddingVertical: 10,
+    paddingLeft: 15,
+    paddingRight: 20,
+    borderBottomWidth: 1,
+    marginBottom: 6,
+  },
+  headingLabel: {
+    fontSize: 18,
+    fontWeight: '500',
+  },
 })
diff --git a/src/view/index.ts b/src/view/index.ts
index 0ab84fc0d..4226e07e7 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -24,7 +24,9 @@ import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faB
 import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar'
 import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera'
 import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
-import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
+import {faCircle} from '@fortawesome/free-regular-svg-icons/faCircle'
+import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
+import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck'
 import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation'
 import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser'
 import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
@@ -112,7 +114,9 @@ export function setup() {
     farCalendar,
     faCamera,
     faCheck,
+    faCircle,
     faCircleCheck,
+    farCircleCheck,
     faCircleExclamation,
     faCircleUser,
     faClone,