diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-07-06 20:28:10 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-06 20:28:10 -0500 |
commit | e14c9783e0cea73ada1d20e8a798738c39319315 (patch) | |
tree | 41be4e050c1e7cf4ade0e0ff1c66342599618935 | |
parent | f05c2f06d665cb3a9989154fbc82a2b0ea60669a (diff) | |
download | voidsky-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
-rw-r--r-- | src/lib/hooks/useIsKeyboardVisible.ts | 35 | ||||
-rw-r--r-- | src/lib/styles.ts | 3 | ||||
-rw-r--r-- | src/locale/helpers.ts | 5 | ||||
-rw-r--r-- | src/locale/languages.ts | 2 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 4 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 9 | ||||
-rw-r--r-- | src/view/com/composer/char-progress/CharProgress.tsx | 2 | ||||
-rw-r--r-- | src/view/com/composer/select-language/SelectLangBtn.tsx | 78 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 29 | ||||
-rw-r--r-- | src/view/com/util/forms/DropdownButton.tsx | 44 | ||||
-rw-r--r-- | src/view/index.ts | 6 |
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, |