about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/models/ui/preferences.ts75
-rw-r--r--src/view/com/composer/Composer.tsx1
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx48
-rw-r--r--src/view/com/modals/lang-settings/PostLanguagesSettings.tsx53
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx3
-rw-r--r--src/view/index.ts2
6 files changed, 135 insertions, 47 deletions
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 23668a3dc..e9ffe28c2 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -33,6 +33,9 @@ const LABEL_GROUPS = [
   'impersonation',
 ]
 const VISIBILITY_VALUES = ['show', 'warn', 'hide']
+const DEFAULT_LANG_CODES = (deviceLocales || [])
+  .concat(['en', 'ja', 'pt', 'de'])
+  .slice(0, 6)
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -51,7 +54,8 @@ export class LabelPreferencesModel {
 export class PreferencesModel {
   adultContentEnabled = !isIOS
   contentLanguages: string[] = deviceLocales || []
-  postLanguages: string[] = deviceLocales || []
+  postLanguage: string = deviceLocales[0] || 'en'
+  postLanguageHistory: string[] = DEFAULT_LANG_CODES
   contentLabels = new LabelPreferencesModel()
   savedFeeds: string[] = []
   pinnedFeeds: string[] = []
@@ -71,7 +75,8 @@ export class PreferencesModel {
   serialize() {
     return {
       contentLanguages: this.contentLanguages,
-      postLanguages: this.postLanguages,
+      postLanguage: this.postLanguage,
+      postLanguageHistory: this.postLanguageHistory,
       contentLabels: this.contentLabels,
       savedFeeds: this.savedFeeds,
       pinnedFeeds: this.pinnedFeeds,
@@ -101,16 +106,23 @@ export class PreferencesModel {
         // default to the device languages
         this.contentLanguages = deviceLocales
       }
-      // check if post languages in preferences exist, otherwise default to device languages
+      if (hasProp(v, 'postLanguage') && typeof v.postLanguage === 'string') {
+        this.postLanguage = v.postLanguage
+      } else {
+        // default to the device languages
+        this.postLanguage = deviceLocales[0] || 'en'
+      }
       if (
-        hasProp(v, 'postLanguages') &&
-        Array.isArray(v.postLanguages) &&
-        typeof v.postLanguages.every(item => typeof item === 'string')
+        hasProp(v, 'postLanguageHistory') &&
+        Array.isArray(v.postLanguageHistory) &&
+        typeof v.postLanguageHistory.every(item => typeof item === 'string')
       ) {
-        this.postLanguages = v.postLanguages
+        this.postLanguageHistory = v.postLanguageHistory
+          .concat(DEFAULT_LANG_CODES)
+          .slice(0, 6)
       } else {
-        // default to the device languages
-        this.postLanguages = deviceLocales
+        // default to a starter set
+        this.postLanguageHistory = DEFAULT_LANG_CODES
       }
       // check if content labels in preferences exist, then hydrate
       if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
@@ -279,7 +291,8 @@ export class PreferencesModel {
       runInAction(() => {
         this.contentLabels = new LabelPreferencesModel()
         this.contentLanguages = deviceLocales
-        this.postLanguages = deviceLocales
+        this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
+        this.postLanguageHistory = DEFAULT_LANG_CODES
         this.savedFeeds = []
         this.pinnedFeeds = []
       })
@@ -305,20 +318,54 @@ export class PreferencesModel {
     }
   }
 
+  /**
+   * A getter that splits `this.postLanguage` into an array of strings.
+   *
+   * This was previously the main field on this model, but now we're
+   * concatenating lang codes to make multi-selection a little better.
+   */
+  get postLanguages() {
+    // filter out empty strings if exist
+    return this.postLanguage.split(',').filter(Boolean)
+  }
+
   hasPostLanguage(code2: string) {
     return this.postLanguages.includes(code2)
   }
 
   togglePostLanguage(code2: string) {
     if (this.hasPostLanguage(code2)) {
-      this.postLanguages = this.postLanguages.filter(lang => lang !== code2)
+      this.postLanguage = this.postLanguages
+        .filter(lang => lang !== code2)
+        .join(',')
     } else {
-      this.postLanguages = this.postLanguages.concat([code2])
+      // sort alphabetically for deterministic comparison in context menu
+      this.postLanguage = this.postLanguages
+        .concat([code2])
+        .sort((a, b) => a.localeCompare(b))
+        .join(',')
     }
   }
 
-  setPostLanguage(code2: string) {
-    this.postLanguages = [code2]
+  setPostLanguage(commaSeparatedLangCodes: string) {
+    this.postLanguage = commaSeparatedLangCodes
+  }
+
+  /**
+   * Saves whatever language codes are currently selected into a history array,
+   * which is then used to populate the language selector menu.
+   */
+  savePostLanguageToHistory() {
+    // filter out duplicate `this.postLanguage` if exists, and prepend
+    // value to start of array
+    this.postLanguageHistory = [this.postLanguage]
+      .concat(
+        this.postLanguageHistory.filter(
+          commaSeparatedLangCodes =>
+            commaSeparatedLangCodes !== this.postLanguage,
+        ),
+      )
+      .slice(0, 6)
   }
 
   getReadablePostLanguages() {
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index cb66cc909..f6308c394 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -212,6 +212,7 @@ export const ComposePost = observer(function ComposePost({
     if (!replyTo) {
       store.me.mainFeed.onPostCreated()
     }
+    store.preferences.savePostLanguageToHistory()
     onPost?.()
     onClose()
     Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
index 5014b5409..4faac3750 100644
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ b/src/view/com/composer/select-language/SelectLangBtn.tsx
@@ -15,7 +15,6 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {isNative} from 'platform/detection'
 import {codeToLanguageName} from '../../../../locale/helpers'
-import {deviceLocales} from 'platform/detection'
 
 export const SelectLangBtn = observer(function SelectLangBtn() {
   const pal = usePalette('default')
@@ -31,35 +30,48 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
   }, [store])
 
   const postLanguagesPref = store.preferences.postLanguages
+  const postLanguagePref = store.preferences.postLanguage
   const items: DropdownItem[] = useMemo(() => {
     let arr: DropdownItemButton[] = []
 
-    const add = (langCode: string) => {
-      const langName = codeToLanguageName(langCode)
+    function add(commaSeparatedLangCodes: string) {
+      const langCodes = commaSeparatedLangCodes.split(',')
+      const langName = langCodes
+        .map(code => codeToLanguageName(code))
+        .join(' + ')
+
+      /*
+       * Filter out any duplicates
+       */
       if (arr.find((item: DropdownItemButton) => item.label === langName)) {
         return
       }
+
       arr.push({
-        icon: store.preferences.hasPostLanguage(langCode)
-          ? ['fas', 'circle-check']
-          : ['far', 'circle'],
+        icon:
+          langCodes.every(code => store.preferences.hasPostLanguage(code)) &&
+          langCodes.length === postLanguagesPref.length
+            ? ['fas', 'circle-dot']
+            : ['far', 'circle'],
         label: langName,
         onPress() {
-          store.preferences.setPostLanguage(langCode)
+          store.preferences.setPostLanguage(commaSeparatedLangCodes)
         },
       })
     }
 
-    for (const lang of postLanguagesPref) {
-      add(lang)
+    if (postLanguagesPref.length) {
+      /*
+       * Re-join here after sanitization bc postLanguageHistory is an array of
+       * comma-separated strings too
+       */
+      add(postLanguagePref)
     }
-    for (const lang of deviceLocales) {
+
+    // comma-separted strings of lang codes that have been used in the past
+    for (const lang of store.preferences.postLanguageHistory) {
       add(lang)
     }
-    add('en') // english
-    add('ja') // japanese
-    add('pt') // portugese
-    add('de') // german
 
     return [
       {heading: true, label: 'Post language'},
@@ -70,7 +82,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
         onPress: onPressMore,
       },
     ]
-  }, [store.preferences, postLanguagesPref, onPressMore])
+  }, [store.preferences, onPressMore, postLanguagePref, postLanguagesPref])
 
   return (
     <DropdownButton
@@ -81,11 +93,9 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
       style={styles.button}
       accessibilityLabel="Language selection"
       accessibilityHint="">
-      {store.preferences.postLanguages.length > 0 ? (
+      {postLanguagesPref.length > 0 ? (
         <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
-          {store.preferences.postLanguages
-            .map(lang => codeToLanguageName(lang))
-            .join(', ')}
+          {postLanguagesPref.map(lang => codeToLanguageName(lang)).join(', ')}
         </Text>
       ) : (
         <FontAwesomeIcon
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
index 3dc35e9ed..0f336e7bc 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -1,17 +1,18 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
 import {ScrollView} from '../util'
 import {useStores} from 'state/index'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isDesktopWeb, deviceLocales} from 'platform/detection'
 import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
-import {LanguageToggle} from './LanguageToggle'
 import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
 
 export const snapPoints = ['100%']
 
-export function Component({}: {}) {
+export const Component = observer(() => {
   const store = useStores()
   const pal = usePalette('default')
   const onPressDone = React.useCallback(() => {
@@ -53,23 +54,38 @@ export function Component({}: {}) {
         Which languages are used in this post?
       </Text>
       <ScrollView style={styles.scrollContainer}>
-        {languages.map(lang => (
-          <LanguageToggle
-            key={lang.code2}
-            code2={lang.code2}
-            langType="postLanguages"
-            name={lang.name}
-            onPress={() => {
-              onPress(lang.code2)
-            }}
-          />
-        ))}
+        {languages.map(lang => {
+          const isSelected = store.preferences.hasPostLanguage(lang.code2)
+
+          // enforce a max of 3 selections for post languages
+          let isDisabled = false
+          if (
+            store.preferences.postLanguage.split(',').length >= 3 &&
+            !isSelected
+          ) {
+            isDisabled = true
+          }
+
+          return (
+            <ToggleButton
+              key={lang.code2}
+              label={lang.name}
+              isSelected={isSelected}
+              onPress={() => (isDisabled ? undefined : onPress(lang.code2))}
+              style={[
+                pal.border,
+                styles.languageToggle,
+                isDisabled && styles.dimmed,
+              ]}
+            />
+          )
+        })}
         <View style={styles.bottomSpacer} />
       </ScrollView>
       <ConfirmLanguagesButton onPress={onPressDone} />
     </View>
   )
-}
+})
 
 const styles = StyleSheet.create({
   container: {
@@ -94,4 +110,13 @@ const styles = StyleSheet.create({
   bottomSpacer: {
     height: isDesktopWeb ? 0 : 60,
   },
+  languageToggle: {
+    borderTopWidth: 1,
+    borderRadius: 0,
+    paddingHorizontal: 6,
+    paddingVertical: 12,
+  },
+  dimmed: {
+    opacity: 0.5,
+  },
 })
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index a1ee3d589..d5b254bb9 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -319,9 +319,12 @@ const styles = StyleSheet.create({
   icon: {
     marginLeft: 2,
     marginRight: 8,
+    flexShrink: 0,
   },
   label: {
     fontSize: 18,
+    flexShrink: 1,
+    flexGrow: 1,
   },
   separator: {
     borderTopWidth: 1,
diff --git a/src/view/index.ts b/src/view/index.ts
index 4294508de..27655e900 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -29,6 +29,7 @@ import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-ico
 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 {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot'
 import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
 import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone'
 import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
@@ -122,6 +123,7 @@ export function setup() {
     farCircleCheck,
     faCircleExclamation,
     faCircleUser,
+    faCircleDot,
     faClone,
     farClone,
     faComment,