diff --git a/src/lib/hooks/useIsKeyboardVisible.ts b/src/lib/hooks/useIsKeyboardVisible.ts new file mode 100644 index 0000000000..5b2a86eb0c --- /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 fb631c0bff..c5a710fff1 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 4b90025865..bce4e65905 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 3983c213f7..a61047e196 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 858225a6f4..e1c0b1f71a 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 c6a9ecd4a3..f2e3cbd630 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(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 6b3b98e474..a3fa78a59a 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 ( <> - + {MAX_GRAPHEME_LENGTH - count} diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx index 8c55e1c915..5014b54097 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 ( - + accessibilityHint=""> {store.preferences.postLanguages.length > 0 ? ( - - {store.preferences.postLanguages.join(', ')} + + {store.preferences.postLanguages + .map(lang => codeToLanguageName(lang)) + .join(', ')} ) : ( )} - + ) }) @@ -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 12ab0e9014..c380c9743a 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( + {needsTranslation && ( + + + + Translate this post + + + + )} winHeight) { + if (openUpwards || newY + estimatedMenuHeight > winHeight) { newY -= estimatedMenuHeight } createDropdownMenu( @@ -357,6 +369,14 @@ const DropdownItems = ({ return ( ) + } else if (isHeading(item)) { + return ( + + + {item.label} + + + ) } 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 0ab84fc0dd..4226e07e7a 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,