[APP-690] better handling of post languages language filtering (#893)
* add SelectLangBtn * memoized objects that are created to reduce re-creation on re-render * add langs when uploading post * only send the top 3 languages otherwise backend will throw error * mv ContentLanguagesSettings to folder * add post languages settings modal and state * fix typos * modify feed manip to also check langs label on post * Fix tests * Remove log * Update feed-manip.ts * Fix syntax errors * UI tuneups * Show the currently selected languages in the composer * fix linting * Use a bcp-47 matching function * Fix a duplicate language issue * Fix web * Dont include lang in prompt * Make select language btn an observer * Keep device languages on top of language selection UIs * Fix android build settings * Enforce a max of 3 languages in posts * Fix tests * Fix types --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
9b19a95e63
commit
08804f265e
1
app.json
1
app.json
|
@ -79,6 +79,7 @@
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
{
|
{
|
||||||
"android": {
|
"android": {
|
||||||
|
"compileSdkVersion": 34,
|
||||||
"kotlinVersion": "1.8.0"
|
"kotlinVersion": "1.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"await-lock": "^2.2.2",
|
"await-lock": "^2.2.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
"bcp-47-match": "^2.0.3",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"eslint-plugin-react-native-a11y": "^3.3.0",
|
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||||
"expo": "~48.0.18",
|
"expo": "~48.0.18",
|
||||||
|
@ -207,7 +208,7 @@
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo)"
|
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"__tests__/.*/__mocks__",
|
"__tests__/.*/__mocks__",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import * as bcp47Match from 'bcp-47-match'
|
||||||
import lande from 'lande'
|
import lande from 'lande'
|
||||||
import {hasProp} from 'lib/type-guards'
|
import {hasProp} from 'lib/type-guards'
|
||||||
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
|
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
|
||||||
|
@ -236,44 +237,84 @@ export class FeedTuner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static preferredLangOnly(langsCode2: string[]) {
|
/**
|
||||||
const langsCode3 = langsCode2.map(l => LANGUAGES_MAP_CODE2[l]?.code3 || l)
|
* This function filters a list of FeedViewPostsSlice items based on whether they contain text in a
|
||||||
|
* preferred language.
|
||||||
|
* @param {string[]} preferredLangsCode2 - An array of prefered language codes in ISO 639-1 or ISO 639-2 format.
|
||||||
|
* @returns A function that takes in a `FeedTuner` and an array of `FeedViewPostsSlice` objects and
|
||||||
|
* returns an array of `FeedViewPostsSlice` objects.
|
||||||
|
*/
|
||||||
|
static preferredLangOnly(preferredLangsCode2: string[]) {
|
||||||
|
const langsCode3 = preferredLangsCode2.map(
|
||||||
|
l => LANGUAGES_MAP_CODE2[l]?.code3 || l,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
slices: FeedViewPostsSlice[],
|
slices: FeedViewPostsSlice[],
|
||||||
): FeedViewPostsSlice[] => {
|
): FeedViewPostsSlice[] => {
|
||||||
if (!langsCode2.length) {
|
// 1. Early return if no languages have been specified
|
||||||
|
if (!preferredLangsCode2.length || preferredLangsCode2.length === 0) {
|
||||||
return slices
|
return slices
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = slices.length - 1; i >= 0; i--) {
|
for (let i = slices.length - 1; i >= 0; i--) {
|
||||||
|
// 2. Set a flag to indicate whether the item has text in a preferred language
|
||||||
let hasPreferredLang = false
|
let hasPreferredLang = false
|
||||||
for (const item of slices[i].items) {
|
for (const item of slices[i].items) {
|
||||||
|
// 3. check if the post has a `langs` property and if it is in the list of preferred languages
|
||||||
|
// if it is, set the flag to true
|
||||||
|
// if language is declared, regardless of a match, break out of the loop
|
||||||
if (
|
if (
|
||||||
|
hasProp(item.post.record, 'langs') &&
|
||||||
|
Array.isArray(item.post.record.langs)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
bcp47Match.basicFilter(
|
||||||
|
item.post.record.langs,
|
||||||
|
preferredLangsCode2,
|
||||||
|
).length > 0
|
||||||
|
) {
|
||||||
|
hasPreferredLang = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 4. FALLBACK if no language declared :
|
||||||
|
// Get the most likely language of the text in the post from the `lande` library and
|
||||||
|
// check if it is in the list of preferred languages
|
||||||
|
// if it is, set the flag to true and break out of the loop
|
||||||
|
else if (
|
||||||
hasProp(item.post.record, 'text') &&
|
hasProp(item.post.record, 'text') &&
|
||||||
typeof item.post.record.text === 'string'
|
typeof item.post.record.text === 'string'
|
||||||
) {
|
) {
|
||||||
// Treat empty text the same as no text.
|
// Treat empty text the same as no text
|
||||||
if (item.post.record.text.length === 0) {
|
if (item.post.record.text.length === 0) {
|
||||||
hasPreferredLang = true
|
hasPreferredLang = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
const langsProbabilityMap = lande(item.post.record.text)
|
||||||
|
const mostLikelyLang = langsProbabilityMap[0][0]
|
||||||
|
// const secondMostLikelyLang = langsProbabilityMap[1][0]
|
||||||
|
// const thirdMostLikelyLang = langsProbabilityMap[2][0]
|
||||||
|
|
||||||
const res = lande(item.post.record.text)
|
// we check for code3 here because that is what the `lande` library returns
|
||||||
|
if (langsCode3.includes(mostLikelyLang)) {
|
||||||
if (langsCode3.includes(res[0][0])) {
|
|
||||||
hasPreferredLang = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no text? roll with it
|
|
||||||
hasPreferredLang = true
|
hasPreferredLang = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 5. no text? roll with it (eg: image-only posts, reposts, etc.)
|
||||||
|
else {
|
||||||
|
hasPreferredLang = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. if item does not fit preferred language, remove it
|
||||||
if (!hasPreferredLang) {
|
if (!hasPreferredLang) {
|
||||||
slices.splice(i, 1)
|
slices.splice(i, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 7. return the filtered list of items
|
||||||
return slices
|
return slices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ interface PostOpts {
|
||||||
images?: ImageModel[]
|
images?: ImageModel[]
|
||||||
knownHandles?: Set<string>
|
knownHandles?: Set<string>
|
||||||
onStateChange?: (state: string) => void
|
onStateChange?: (state: string) => void
|
||||||
|
langs?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post(store: RootStoreModel, opts: PostOpts) {
|
export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
|
@ -96,6 +97,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// add quote embed if present
|
||||||
if (opts.quote) {
|
if (opts.quote) {
|
||||||
embed = {
|
embed = {
|
||||||
$type: 'app.bsky.embed.record',
|
$type: 'app.bsky.embed.record',
|
||||||
|
@ -106,6 +108,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
} as AppBskyEmbedRecord.Main
|
} as AppBskyEmbedRecord.Main
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add image embed if present
|
||||||
if (opts.images?.length) {
|
if (opts.images?.length) {
|
||||||
const images: AppBskyEmbedImages.Image[] = []
|
const images: AppBskyEmbedImages.Image[] = []
|
||||||
for (const image of opts.images) {
|
for (const image of opts.images) {
|
||||||
|
@ -136,6 +139,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add external embed if present
|
||||||
if (opts.extLink && !opts.images?.length) {
|
if (opts.extLink && !opts.images?.length) {
|
||||||
if (opts.extLink.embed) {
|
if (opts.extLink.embed) {
|
||||||
embed = opts.extLink.embed
|
embed = opts.extLink.embed
|
||||||
|
@ -197,6 +201,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add replyTo if post is a reply to another post
|
||||||
if (opts.replyTo) {
|
if (opts.replyTo) {
|
||||||
const replyToUrip = new AtUri(opts.replyTo)
|
const replyToUrip = new AtUri(opts.replyTo)
|
||||||
const parentPost = await store.agent.getPost({
|
const parentPost = await store.agent.getPost({
|
||||||
|
@ -215,6 +220,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add top 3 languages from user preferences if langs is provided
|
||||||
|
let langs = opts.langs
|
||||||
|
if (opts.langs) {
|
||||||
|
langs = opts.langs.slice(0, 3)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opts.onStateChange?.('Posting...')
|
opts.onStateChange?.('Posting...')
|
||||||
return await store.agent.post({
|
return await store.agent.post({
|
||||||
|
@ -222,6 +233,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
facets: rt.facets,
|
facets: rt.facets,
|
||||||
reply,
|
reply,
|
||||||
embed,
|
embed,
|
||||||
|
langs,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Failed to create post: ${e.toString()}`)
|
console.error(`Failed to create post: ${e.toString()}`)
|
||||||
|
|
|
@ -4,3 +4,8 @@ export function choose<U, T extends Record<string, U>>(
|
||||||
): U {
|
): U {
|
||||||
return choices[value]
|
return choices[value]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dedupArray<T>(arr: T[]): T[] {
|
||||||
|
const s = new Set(arr)
|
||||||
|
return [...s]
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
|
import {getLocales} from 'expo-localization'
|
||||||
|
import {dedupArray} from 'lib/functions'
|
||||||
|
|
||||||
export const isIOS = Platform.OS === 'ios'
|
export const isIOS = Platform.OS === 'ios'
|
||||||
export const isAndroid = Platform.OS === 'android'
|
export const isAndroid = Platform.OS === 'android'
|
||||||
|
@ -10,3 +12,7 @@ export const isMobileWeb =
|
||||||
// @ts-ignore we know window exists -prf
|
// @ts-ignore we know window exists -prf
|
||||||
global.window.matchMedia(isMobileWebMediaQuery)?.matches
|
global.window.matchMedia(isMobileWebMediaQuery)?.matches
|
||||||
export const isDesktopWeb = isWeb && !isMobileWeb
|
export const isDesktopWeb = isWeb && !isMobileWeb
|
||||||
|
|
||||||
|
export const deviceLocales = dedupArray(
|
||||||
|
getLocales?.().map?.(locale => locale.languageCode),
|
||||||
|
)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {getLocales} from 'expo-localization'
|
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
|
@ -14,9 +13,8 @@ import {
|
||||||
ALWAYS_WARN_LABEL_GROUP,
|
ALWAYS_WARN_LABEL_GROUP,
|
||||||
} from 'lib/labeling/const'
|
} from 'lib/labeling/const'
|
||||||
import {DEFAULT_FEEDS} from 'lib/constants'
|
import {DEFAULT_FEEDS} from 'lib/constants'
|
||||||
import {isIOS} from 'platform/detection'
|
import {isIOS, deviceLocales} from 'platform/detection'
|
||||||
|
import {LANGUAGES} from '../../../locale/languages'
|
||||||
const deviceLocales = getLocales()
|
|
||||||
|
|
||||||
export type LabelPreference = 'show' | 'warn' | 'hide'
|
export type LabelPreference = 'show' | 'warn' | 'hide'
|
||||||
const LABEL_GROUPS = [
|
const LABEL_GROUPS = [
|
||||||
|
@ -46,8 +44,8 @@ export class LabelPreferencesModel {
|
||||||
|
|
||||||
export class PreferencesModel {
|
export class PreferencesModel {
|
||||||
adultContentEnabled = !isIOS
|
adultContentEnabled = !isIOS
|
||||||
contentLanguages: string[] =
|
contentLanguages: string[] = deviceLocales || []
|
||||||
deviceLocales?.map?.(locale => locale.languageCode) || []
|
postLanguages: string[] = deviceLocales || []
|
||||||
contentLabels = new LabelPreferencesModel()
|
contentLabels = new LabelPreferencesModel()
|
||||||
savedFeeds: string[] = []
|
savedFeeds: string[] = []
|
||||||
pinnedFeeds: string[] = []
|
pinnedFeeds: string[] = []
|
||||||
|
@ -66,6 +64,7 @@ export class PreferencesModel {
|
||||||
serialize() {
|
serialize() {
|
||||||
return {
|
return {
|
||||||
contentLanguages: this.contentLanguages,
|
contentLanguages: this.contentLanguages,
|
||||||
|
postLanguages: this.postLanguages,
|
||||||
contentLabels: this.contentLabels,
|
contentLabels: this.contentLabels,
|
||||||
savedFeeds: this.savedFeeds,
|
savedFeeds: this.savedFeeds,
|
||||||
pinnedFeeds: this.pinnedFeeds,
|
pinnedFeeds: this.pinnedFeeds,
|
||||||
|
@ -83,19 +82,33 @@ export class PreferencesModel {
|
||||||
*/
|
*/
|
||||||
hydrate(v: unknown) {
|
hydrate(v: unknown) {
|
||||||
if (isObj(v)) {
|
if (isObj(v)) {
|
||||||
|
// check if content languages in preferences exist, otherwise default to device languages
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'contentLanguages') &&
|
hasProp(v, 'contentLanguages') &&
|
||||||
Array.isArray(v.contentLanguages) &&
|
Array.isArray(v.contentLanguages) &&
|
||||||
typeof v.contentLanguages.every(item => typeof item === 'string')
|
typeof v.contentLanguages.every(item => typeof item === 'string')
|
||||||
) {
|
) {
|
||||||
this.contentLanguages = v.contentLanguages
|
this.contentLanguages = v.contentLanguages
|
||||||
}
|
|
||||||
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
|
|
||||||
Object.assign(this.contentLabels, v.contentLabels)
|
|
||||||
} else {
|
} else {
|
||||||
// default to the device languages
|
// default to the device languages
|
||||||
this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
|
this.contentLanguages = deviceLocales
|
||||||
}
|
}
|
||||||
|
// check if post languages in preferences exist, otherwise default to device languages
|
||||||
|
if (
|
||||||
|
hasProp(v, 'postLanguages') &&
|
||||||
|
Array.isArray(v.postLanguages) &&
|
||||||
|
typeof v.postLanguages.every(item => typeof item === 'string')
|
||||||
|
) {
|
||||||
|
this.postLanguages = v.postLanguages
|
||||||
|
} else {
|
||||||
|
// default to the device languages
|
||||||
|
this.postLanguages = deviceLocales
|
||||||
|
}
|
||||||
|
// check if content labels in preferences exist, then hydrate
|
||||||
|
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
|
||||||
|
Object.assign(this.contentLabels, v.contentLabels)
|
||||||
|
}
|
||||||
|
// check if saved feeds in preferences, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'savedFeeds') &&
|
hasProp(v, 'savedFeeds') &&
|
||||||
Array.isArray(v.savedFeeds) &&
|
Array.isArray(v.savedFeeds) &&
|
||||||
|
@ -103,6 +116,7 @@ export class PreferencesModel {
|
||||||
) {
|
) {
|
||||||
this.savedFeeds = v.savedFeeds
|
this.savedFeeds = v.savedFeeds
|
||||||
}
|
}
|
||||||
|
// check if pinned feeds in preferences exist, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'pinnedFeeds') &&
|
hasProp(v, 'pinnedFeeds') &&
|
||||||
Array.isArray(v.pinnedFeeds) &&
|
Array.isArray(v.pinnedFeeds) &&
|
||||||
|
@ -110,24 +124,28 @@ export class PreferencesModel {
|
||||||
) {
|
) {
|
||||||
this.pinnedFeeds = v.pinnedFeeds
|
this.pinnedFeeds = v.pinnedFeeds
|
||||||
}
|
}
|
||||||
|
// check if home feed replies are enabled in preferences, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'homeFeedRepliesEnabled') &&
|
hasProp(v, 'homeFeedRepliesEnabled') &&
|
||||||
typeof v.homeFeedRepliesEnabled === 'boolean'
|
typeof v.homeFeedRepliesEnabled === 'boolean'
|
||||||
) {
|
) {
|
||||||
this.homeFeedRepliesEnabled = v.homeFeedRepliesEnabled
|
this.homeFeedRepliesEnabled = v.homeFeedRepliesEnabled
|
||||||
}
|
}
|
||||||
|
// check if home feed replies threshold is enabled in preferences, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'homeFeedRepliesThreshold') &&
|
hasProp(v, 'homeFeedRepliesThreshold') &&
|
||||||
typeof v.homeFeedRepliesThreshold === 'number'
|
typeof v.homeFeedRepliesThreshold === 'number'
|
||||||
) {
|
) {
|
||||||
this.homeFeedRepliesThreshold = v.homeFeedRepliesThreshold
|
this.homeFeedRepliesThreshold = v.homeFeedRepliesThreshold
|
||||||
}
|
}
|
||||||
|
// check if home feed reposts are enabled in preferences, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'homeFeedRepostsEnabled') &&
|
hasProp(v, 'homeFeedRepostsEnabled') &&
|
||||||
typeof v.homeFeedRepostsEnabled === 'boolean'
|
typeof v.homeFeedRepostsEnabled === 'boolean'
|
||||||
) {
|
) {
|
||||||
this.homeFeedRepostsEnabled = v.homeFeedRepostsEnabled
|
this.homeFeedRepostsEnabled = v.homeFeedRepostsEnabled
|
||||||
}
|
}
|
||||||
|
// check if home feed quote posts are enabled in preferences, then hydrate
|
||||||
if (
|
if (
|
||||||
hasProp(v, 'homeFeedQuotePostsEnabled') &&
|
hasProp(v, 'homeFeedQuotePostsEnabled') &&
|
||||||
typeof v.homeFeedQuotePostsEnabled === 'boolean'
|
typeof v.homeFeedQuotePostsEnabled === 'boolean'
|
||||||
|
@ -245,7 +263,8 @@ export class PreferencesModel {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.contentLabels = new LabelPreferencesModel()
|
this.contentLabels = new LabelPreferencesModel()
|
||||||
this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
|
this.contentLanguages = deviceLocales
|
||||||
|
this.postLanguages = deviceLocales
|
||||||
this.savedFeeds = []
|
this.savedFeeds = []
|
||||||
this.pinnedFeeds = []
|
this.pinnedFeeds = []
|
||||||
})
|
})
|
||||||
|
@ -271,6 +290,26 @@ export class PreferencesModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasPostLanguage(code2: string) {
|
||||||
|
return this.postLanguages.includes(code2)
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePostLanguage(code2: string) {
|
||||||
|
if (this.hasPostLanguage(code2)) {
|
||||||
|
this.postLanguages = this.postLanguages.filter(lang => lang !== code2)
|
||||||
|
} else {
|
||||||
|
this.postLanguages = this.postLanguages.concat([code2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getReadablePostLanguages() {
|
||||||
|
const all = this.postLanguages.map(code2 => {
|
||||||
|
const lang = LANGUAGES.find(l => l.code2 === code2)
|
||||||
|
return lang ? lang.name : code2
|
||||||
|
})
|
||||||
|
return all.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
async setContentLabelPref(
|
async setContentLabelPref(
|
||||||
key: keyof LabelPreferencesModel,
|
key: keyof LabelPreferencesModel,
|
||||||
value: LabelPreference,
|
value: LabelPreference,
|
||||||
|
|
|
@ -111,6 +111,10 @@ export interface ContentLanguagesSettingsModal {
|
||||||
name: 'content-languages-settings'
|
name: 'content-languages-settings'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostLanguagesSettingsModal {
|
||||||
|
name: 'post-languages-settings'
|
||||||
|
}
|
||||||
|
|
||||||
export interface PreferencesHomeFeed {
|
export interface PreferencesHomeFeed {
|
||||||
name: 'preferences-home-feed'
|
name: 'preferences-home-feed'
|
||||||
}
|
}
|
||||||
|
@ -125,6 +129,7 @@ export type Modal =
|
||||||
// Curation
|
// Curation
|
||||||
| ContentFilteringSettingsModal
|
| ContentFilteringSettingsModal
|
||||||
| ContentLanguagesSettingsModal
|
| ContentLanguagesSettingsModal
|
||||||
|
| PostLanguagesSettingsModal
|
||||||
| PreferencesHomeFeed
|
| PreferencesHomeFeed
|
||||||
|
|
||||||
// Moderation
|
// Moderation
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {isDesktopWeb, isAndroid} from 'platform/detection'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
import {GalleryModel} from 'state/models/media/gallery'
|
||||||
import {Gallery} from './photos/Gallery'
|
import {Gallery} from './photos/Gallery'
|
||||||
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
||||||
|
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
||||||
|
|
||||||
type Props = ComposerOpts & {
|
type Props = ComposerOpts & {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
@ -71,6 +72,13 @@ export const ComposePost = observer(function ComposePost({
|
||||||
)
|
)
|
||||||
|
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
const viewStyles = useMemo(
|
||||||
|
() => ({
|
||||||
|
paddingBottom: isAndroid ? insets.bottom : 0,
|
||||||
|
paddingTop: isAndroid ? insets.top : isDesktopWeb ? 0 : 15,
|
||||||
|
}),
|
||||||
|
[insets],
|
||||||
|
)
|
||||||
|
|
||||||
// HACK
|
// HACK
|
||||||
// there's a bug with @mattermost/react-native-paste-input where if the input
|
// there's a bug with @mattermost/react-native-paste-input where if the input
|
||||||
|
@ -87,6 +95,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
}, [autocompleteView])
|
}, [autocompleteView])
|
||||||
|
|
||||||
|
// listen to escape key on desktop web
|
||||||
const onEscape = useCallback(
|
const onEscape = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
|
@ -109,7 +118,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
},
|
},
|
||||||
[store, onClose],
|
[store, onClose],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDesktopWeb) {
|
if (isDesktopWeb) {
|
||||||
window.addEventListener('keydown', onEscape)
|
window.addEventListener('keydown', onEscape)
|
||||||
|
@ -157,6 +165,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
extLink: extLink,
|
extLink: extLink,
|
||||||
onStateChange: setProcessingState,
|
onStateChange: setProcessingState,
|
||||||
knownHandles: autocompleteView.knownHandles,
|
knownHandles: autocompleteView.knownHandles,
|
||||||
|
langs: store.preferences.postLanguages,
|
||||||
})
|
})
|
||||||
track('Create Post', {
|
track('Create Post', {
|
||||||
imageCount: gallery.size,
|
imageCount: gallery.size,
|
||||||
|
@ -197,15 +206,13 @@ export const ComposePost = observer(function ComposePost({
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
|
const canPost = useMemo(
|
||||||
|
() => graphemeLength <= MAX_GRAPHEME_LENGTH,
|
||||||
|
[graphemeLength],
|
||||||
|
)
|
||||||
|
const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?`
|
||||||
|
|
||||||
const selectTextInputPlaceholder = replyTo ? 'Write your reply' : "What's up?"
|
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
|
||||||
|
|
||||||
const canSelectImages = gallery.size < 4
|
|
||||||
const viewStyles = {
|
|
||||||
paddingBottom: isAndroid ? insets.bottom : 0,
|
|
||||||
paddingTop: isAndroid ? insets.top : isDesktopWeb ? 0 : 15,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
|
@ -352,6 +359,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
|
<SelectLangBtn />
|
||||||
<CharProgress count={graphemeLength} />
|
<CharProgress count={graphemeLength} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React, {useCallback} from 'react'
|
||||||
|
import {TouchableOpacity, 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 {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}
|
||||||
|
|
||||||
|
export const SelectLangBtn = observer(function SelectLangBtn() {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
|
const onPress = useCallback(async () => {
|
||||||
|
if (isNative) {
|
||||||
|
if (Keyboard.isVisible()) {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.shell.openModal({name: 'post-languages-settings'})
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="selectLangBtn"
|
||||||
|
onPress={onPress}
|
||||||
|
style={styles.button}
|
||||||
|
hitSlop={HITSLOP}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Language selection"
|
||||||
|
accessibilityHint="Opens screen or modal to select language of post">
|
||||||
|
{store.preferences.postLanguages.length > 0 ? (
|
||||||
|
<Text type="lg-bold" style={pal.link}>
|
||||||
|
{store.preferences.postLanguages.join(', ')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="language"
|
||||||
|
style={pal.link as FontAwesomeIconStyle}
|
||||||
|
size={26}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,143 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {StyleSheet, Pressable, View} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {ScrollView} from './util'
|
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {ToggleButton} from '../util/forms/ToggleButton'
|
|
||||||
import {s, colors, gradients} from 'lib/styles'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
|
||||||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../locale/languages'
|
|
||||||
|
|
||||||
export const snapPoints = ['100%']
|
|
||||||
|
|
||||||
export function Component({}: {}) {
|
|
||||||
const store = useStores()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const onPressDone = React.useCallback(() => {
|
|
||||||
store.shell.closeModal()
|
|
||||||
}, [store])
|
|
||||||
|
|
||||||
const languages = React.useMemo(() => {
|
|
||||||
const langs = LANGUAGES.filter(
|
|
||||||
lang =>
|
|
||||||
!!lang.code2.trim() &&
|
|
||||||
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
|
|
||||||
)
|
|
||||||
// sort so that selected languages are on top, then alphabetically
|
|
||||||
langs.sort((a, b) => {
|
|
||||||
const hasA = store.preferences.hasContentLanguage(a.code2)
|
|
||||||
const hasB = store.preferences.hasContentLanguage(b.code2)
|
|
||||||
if (hasA === hasB) return a.name.localeCompare(b.name)
|
|
||||||
if (hasA) return -1
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
return langs
|
|
||||||
}, [store])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View testID="contentLanguagesModal" style={[pal.view, styles.container]}>
|
|
||||||
<Text style={[pal.text, styles.title]}>Content Languages</Text>
|
|
||||||
<Text style={[pal.text, styles.description]}>
|
|
||||||
Which languages would you like to see in the your feed? (Leave them all
|
|
||||||
unchecked to see any language.)
|
|
||||||
</Text>
|
|
||||||
<ScrollView style={styles.scrollContainer}>
|
|
||||||
{languages.map(lang => (
|
|
||||||
<LanguageToggle
|
|
||||||
key={lang.code2}
|
|
||||||
code2={lang.code2}
|
|
||||||
name={lang.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<View style={styles.bottomSpacer} />
|
|
||||||
</ScrollView>
|
|
||||||
<View style={[styles.btnContainer, pal.borderDark]}>
|
|
||||||
<Pressable
|
|
||||||
testID="sendReportBtn"
|
|
||||||
onPress={onPressDone}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Confirm content language settings"
|
|
||||||
accessibilityHint="">
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LanguageToggle = observer(
|
|
||||||
({code2, name}: {code2: string; name: string}) => {
|
|
||||||
const store = useStores()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
|
||||||
store.preferences.toggleContentLanguage(code2)
|
|
||||||
}, [store, code2])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleButton
|
|
||||||
label={name}
|
|
||||||
isSelected={store.preferences.contentLanguages.includes(code2)}
|
|
||||||
onPress={onPress}
|
|
||||||
style={[pal.border, styles.languageToggle]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
paddingTop: 20,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 24,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
scrollContainer: {
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
},
|
|
||||||
bottomSpacer: {
|
|
||||||
height: isDesktopWeb ? 0 : 60,
|
|
||||||
},
|
|
||||||
btnContainer: {
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingBottom: isDesktopWeb ? 0 : 40,
|
|
||||||
borderTopWidth: isDesktopWeb ? 0 : 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
languageToggle: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderRadius: 0,
|
|
||||||
paddingHorizontal: 0,
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: colors.gray1,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -23,7 +23,8 @@ import * as WaitlistModal from './Waitlist'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as AddAppPassword from './AddAppPasswords'
|
import * as AddAppPassword from './AddAppPasswords'
|
||||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||||
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||||
|
|
||||||
const DEFAULT_SNAPPOINTS = ['90%']
|
const DEFAULT_SNAPPOINTS = ['90%']
|
||||||
|
@ -106,6 +107,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'content-languages-settings') {
|
} else if (activeModal?.name === 'content-languages-settings') {
|
||||||
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
|
} else if (activeModal?.name === 'post-languages-settings') {
|
||||||
|
snapPoints = PostLanguagesSettingsModal.snapPoints
|
||||||
|
element = <PostLanguagesSettingsModal.Component />
|
||||||
} else if (activeModal?.name === 'preferences-home-feed') {
|
} else if (activeModal?.name === 'preferences-home-feed') {
|
||||||
snapPoints = PreferencesHomeFeed.snapPoints
|
snapPoints = PreferencesHomeFeed.snapPoints
|
||||||
element = <PreferencesHomeFeed.Component />
|
element = <PreferencesHomeFeed.Component />
|
||||||
|
|
|
@ -23,7 +23,9 @@ import * as WaitlistModal from './Waitlist'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as AddAppPassword from './AddAppPasswords'
|
import * as AddAppPassword from './AddAppPasswords'
|
||||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||||
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
|
|
||||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||||
|
|
||||||
export const ModalsContainer = observer(function ModalsContainer() {
|
export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
|
@ -94,6 +96,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <ContentFilteringSettingsModal.Component />
|
element = <ContentFilteringSettingsModal.Component />
|
||||||
} else if (modal.name === 'content-languages-settings') {
|
} else if (modal.name === 'content-languages-settings') {
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
|
} else if (modal.name === 'post-languages-settings') {
|
||||||
|
element = <PostLanguagesSettingsModal.Component />
|
||||||
} else if (modal.name === 'alt-text-image') {
|
} else if (modal.name === 'alt-text-image') {
|
||||||
element = <AltTextImageModal.Component {...modal} />
|
element = <AltTextImageModal.Component {...modal} />
|
||||||
} else if (modal.name === 'edit-image') {
|
} else if (modal.name === 'edit-image') {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, Text, View, Pressable} from 'react-native'
|
||||||
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
|
import {s, colors, gradients} from 'lib/styles'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
|
export const ConfirmLanguagesButton = ({
|
||||||
|
onPress,
|
||||||
|
extraText,
|
||||||
|
}: {
|
||||||
|
onPress: () => void
|
||||||
|
extraText?: string
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View style={[styles.btnContainer, pal.borderDark]}>
|
||||||
|
<Pressable
|
||||||
|
testID="confirmContentLanguagesBtn"
|
||||||
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Confirm content language settings"
|
||||||
|
accessibilityHint="">
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[styles.btn]}>
|
||||||
|
<Text style={[s.white, s.bold, s.f18]}>Done{extraText}</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
btnContainer: {
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingBottom: isDesktopWeb ? 0 : 40,
|
||||||
|
borderTopWidth: isDesktopWeb ? 0 : 1,
|
||||||
|
},
|
||||||
|
btn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 32,
|
||||||
|
padding: 14,
|
||||||
|
backgroundColor: colors.gray1,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export const snapPoints = ['100%']
|
||||||
|
|
||||||
|
export function Component({}: {}) {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const onPressDone = React.useCallback(() => {
|
||||||
|
store.shell.closeModal()
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const languages = React.useMemo(() => {
|
||||||
|
const langs = LANGUAGES.filter(
|
||||||
|
lang =>
|
||||||
|
!!lang.code2.trim() &&
|
||||||
|
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
|
||||||
|
)
|
||||||
|
// sort so that device & selected languages are on top, then alphabetically
|
||||||
|
langs.sort((a, b) => {
|
||||||
|
const hasA =
|
||||||
|
store.preferences.hasContentLanguage(a.code2) ||
|
||||||
|
deviceLocales.includes(a.code2)
|
||||||
|
const hasB =
|
||||||
|
store.preferences.hasContentLanguage(b.code2) ||
|
||||||
|
deviceLocales.includes(b.code2)
|
||||||
|
if (hasA === hasB) return a.name.localeCompare(b.name)
|
||||||
|
if (hasA) return -1
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
return langs
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const onPress = React.useCallback(
|
||||||
|
(code2: string) => {
|
||||||
|
store.preferences.toggleContentLanguage(code2)
|
||||||
|
},
|
||||||
|
[store],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="contentLanguagesModal" style={[pal.view, styles.container]}>
|
||||||
|
<Text style={[pal.text, styles.title]}>Content Languages</Text>
|
||||||
|
<Text style={[pal.text, styles.description]}>
|
||||||
|
Which languages would you like to see in your algorithmic feeds?
|
||||||
|
</Text>
|
||||||
|
<Text style={[pal.textLight, styles.description]}>
|
||||||
|
Leave them all unchecked to see any language.
|
||||||
|
</Text>
|
||||||
|
<ScrollView style={styles.scrollContainer}>
|
||||||
|
{languages.map(lang => (
|
||||||
|
<LanguageToggle
|
||||||
|
key={lang.code2}
|
||||||
|
code2={lang.code2}
|
||||||
|
langType="contentLanguages"
|
||||||
|
name={lang.name}
|
||||||
|
onPress={() => {
|
||||||
|
onPress(lang.code2)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<View style={styles.bottomSpacer} />
|
||||||
|
</ScrollView>
|
||||||
|
<ConfirmLanguagesButton onPress={onPressDone} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
bottomSpacer: {
|
||||||
|
height: isDesktopWeb ? 0 : 60,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet} from 'react-native'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
|
||||||
|
export const LanguageToggle = observer(
|
||||||
|
({
|
||||||
|
code2,
|
||||||
|
name,
|
||||||
|
onPress,
|
||||||
|
langType,
|
||||||
|
}: {
|
||||||
|
code2: string
|
||||||
|
name: string
|
||||||
|
onPress: () => void
|
||||||
|
langType: 'contentLanguages' | 'postLanguages'
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
|
const isSelected = store.preferences[langType].includes(code2)
|
||||||
|
|
||||||
|
// enforce a max of 3 selections for post languages
|
||||||
|
let isDisabled = false
|
||||||
|
if (
|
||||||
|
langType === 'postLanguages' &&
|
||||||
|
store.preferences[langType].length >= 3 &&
|
||||||
|
!isSelected
|
||||||
|
) {
|
||||||
|
isDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButton
|
||||||
|
label={name}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onPress={isDisabled ? undefined : onPress}
|
||||||
|
style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
languageToggle: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderRadius: 0,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
dimmed: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,97 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export const snapPoints = ['100%']
|
||||||
|
|
||||||
|
export function Component({}: {}) {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const onPressDone = React.useCallback(() => {
|
||||||
|
store.shell.closeModal()
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const languages = React.useMemo(() => {
|
||||||
|
const langs = LANGUAGES.filter(
|
||||||
|
lang =>
|
||||||
|
!!lang.code2.trim() &&
|
||||||
|
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
|
||||||
|
)
|
||||||
|
// sort so that device & selected languages are on top, then alphabetically
|
||||||
|
langs.sort((a, b) => {
|
||||||
|
const hasA =
|
||||||
|
store.preferences.hasPostLanguage(a.code2) ||
|
||||||
|
deviceLocales.includes(a.code2)
|
||||||
|
const hasB =
|
||||||
|
store.preferences.hasPostLanguage(b.code2) ||
|
||||||
|
deviceLocales.includes(b.code2)
|
||||||
|
if (hasA === hasB) return a.name.localeCompare(b.name)
|
||||||
|
if (hasA) return -1
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
return langs
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const onPress = React.useCallback(
|
||||||
|
(code2: string) => {
|
||||||
|
store.preferences.togglePostLanguage(code2)
|
||||||
|
},
|
||||||
|
[store],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="postLanguagesModal" style={[pal.view, styles.container]}>
|
||||||
|
<Text style={[pal.text, styles.title]}>Post Languages</Text>
|
||||||
|
<Text style={[pal.text, styles.description]}>
|
||||||
|
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)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<View style={styles.bottomSpacer} />
|
||||||
|
</ScrollView>
|
||||||
|
<ConfirmLanguagesButton onPress={onPressDone} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
bottomSpacer: {
|
||||||
|
height: isDesktopWeb ? 0 : 60,
|
||||||
|
},
|
||||||
|
})
|
|
@ -17,7 +17,7 @@ export function ToggleButton({
|
||||||
label: string
|
label: string
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
onPress: () => void
|
onPress?: () => void
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
|
const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
|
||||||
|
|
|
@ -6197,6 +6197,11 @@ batch@0.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||||
integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
|
integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
|
||||||
|
|
||||||
|
bcp-47-match@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-2.0.3.tgz#603226f6e5d3914a581408be33b28a53144b09d0"
|
||||||
|
integrity sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==
|
||||||
|
|
||||||
better-opn@~3.0.2:
|
better-opn@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
|
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
|
||||||
|
|
Loading…
Reference in New Issue