Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac15d69151 | |||
| a00672a7c6 | |||
| 703d13e3f7 | |||
| 55a7b2ec67 | |||
| 40eb45baad | |||
| 8cd6763811 | |||
| 5f4a0f2881 | |||
| d08ce0d473 | |||
| 0aee6390fa | |||
| 84796c1bcf | |||
| 6a2067a761 | |||
| c9d86de152 | |||
| 29b3b3db30 | |||
| 47cbdfecb3 | |||
| 763a5d5ab5 | |||
| 6a29296ae0 | |||
| c7d57b6374 | |||
| 91fffd3cd2 | |||
| b2c232e9fd | |||
| 2a575addba | |||
| 6d290591da | |||
| 1edebc5dc7 | |||
| 5929c2678c | |||
| 8cbe68306d | |||
| 95e1b21788 | |||
| b9db950d52 | |||
| eb9dfba9f4 | |||
| 7860b790dc | |||
| 8825d6b1a7 | |||
| 2c2d12b394 | |||
| 736ea3f49d | |||
| ce1b04b925 | |||
| 56a88098d2 | |||
| 61e97d6962 | |||
| 9d3bdbb2b9 | |||
| c6c6c91d7b | |||
| b0c36383a9 | |||
| 6c81090021 | |||
| 723bbfc58a | |||
| 5164ca6288 | |||
| b9406aa011 | |||
| 371e6ad333 | |||
| a21e2b45db | |||
| 8c43acc1d3 | |||
| 7782517a91 | |||
| a55444a53a | |||
| 8fc4f68e69 | |||
| 32bf8122e8 | |||
| b4d07c4112 | |||
| 1e168fd1e1 | |||
| 7e7910edf7 | |||
| af4db872b5 | |||
| 54d52b8172 | |||
| 1059088cf1 | |||
| 796d04b59f | |||
| 9bf7e278e7 | |||
| d0089a9335 | |||
| 21a5140704 | |||
| 468de4c6db | |||
| 260eb59530 | |||
| 69e5174057 | |||
| 9f070834dd | |||
| aa58216b6d | |||
| fdc3f0f17d | |||
| 0395ba3e79 | |||
| b892d8be08 | |||
| 9663b3e855 | |||
| f802f81fa6 | |||
| 30af267bb4 | |||
| b1c1c49897 | |||
| e428474478 | |||
| 4dd62e24ea | |||
| d29599fa6c | |||
| e9a792e4c1 | |||
| 631277dc3d | |||
| ac5b2cf31f | |||
| fee2f5daa2 | |||
| 523d1f01a5 | |||
| 93dec879ad | |||
| f139bcbcd4 | |||
| 2cb6e713bb | |||
| 6aa44d681d | |||
| 1b87351d38 | |||
| ef14faaa58 | |||
| 54a4a6c161 | |||
| 8754527aa6 | |||
| be429d5750 | |||
| 874a624e25 | |||
| 5910f78329 | |||
| b6fdde2f04 |
@@ -71,7 +71,7 @@ module.exports = function (config) {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
orientation: 'portrait',
|
||||
icon: './assets/icon.png',
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
splash: SPLASH_CONFIG,
|
||||
// hsl(211, 99%, 53%), same as palette.default.brandText
|
||||
@@ -162,7 +162,7 @@ module.exports = function (config) {
|
||||
backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
|
||||
},
|
||||
android: {
|
||||
icon: './assets/icon.png',
|
||||
icon: './assets/app-icons/android_icon_default_light.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-foreground.png',
|
||||
@@ -273,6 +273,73 @@ module.exports = function (config) {
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'@mozzius/expo-dynamic-app-icon',
|
||||
{
|
||||
/**
|
||||
* Default set
|
||||
*/
|
||||
default_light: {
|
||||
ios: './assets/app-icons/ios_icon_default_light.png',
|
||||
android: './assets/app-icons/android_icon_default_light.png',
|
||||
prerendered: true,
|
||||
},
|
||||
default_dark: {
|
||||
ios: './assets/app-icons/ios_icon_default_dark.png',
|
||||
android: './assets/app-icons/android_icon_default_dark.png',
|
||||
prerendered: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Bluesky+ core set
|
||||
*/
|
||||
core_aurora: {
|
||||
ios: './assets/app-icons/ios_icon_core_aurora.png',
|
||||
android: './assets/app-icons/android_icon_core_aurora.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_bonfire: {
|
||||
ios: './assets/app-icons/ios_icon_core_bonfire.png',
|
||||
android: './assets/app-icons/android_icon_core_bonfire.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_sunrise: {
|
||||
ios: './assets/app-icons/ios_icon_core_sunrise.png',
|
||||
android: './assets/app-icons/android_icon_core_sunrise.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_sunset: {
|
||||
ios: './assets/app-icons/ios_icon_core_sunset.png',
|
||||
android: './assets/app-icons/android_icon_core_sunset.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_midnight: {
|
||||
ios: './assets/app-icons/ios_icon_core_midnight.png',
|
||||
android: './assets/app-icons/android_icon_core_midnight.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_blue: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_blue.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_blue.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_white: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_white.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_white.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_black: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_black.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_black.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_classic: {
|
||||
ios: './assets/app-icons/ios_icon_core_classic.png',
|
||||
android: './assets/app-icons/android_icon_core_classic.png',
|
||||
prerendered: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
|
After Width: | Height: | Size: 783 KiB |
|
After Width: | Height: | Size: 714 KiB |
|
After Width: | Height: | Size: 947 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 671 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 511 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 947 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 452 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -257,6 +257,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
||||
e.GET("/settings/content-and-media", server.WebGeneric)
|
||||
e.GET("/settings/about", server.WebGeneric)
|
||||
e.GET("/settings/app-icon", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
e.GET("/sys/debug-mod", server.WebGeneric)
|
||||
e.GET("/sys/log", server.WebGeneric)
|
||||
|
||||
@@ -27,13 +27,21 @@
|
||||
|
||||
let player
|
||||
function onYouTubeIframeAPIReady() {
|
||||
let videoId = url.searchParams.get('videoId')
|
||||
videoId = decodeURIComponent(videoId)
|
||||
videoId = videoId.replace(/[^a-zA-Z0-9_-]/g, "")
|
||||
if (videoId.length !== 11) throw new Error("Invalid video ID")
|
||||
|
||||
let start = url.searchParams.get('start')
|
||||
start = start.replace(/[^0-9]/g, "")
|
||||
|
||||
player = new YT.Player('player', {
|
||||
width: "1000",
|
||||
height: "1000",
|
||||
videoId: url.searchParams.get('videoId'),
|
||||
videoId,
|
||||
playerVars: {
|
||||
autoplay: 1,
|
||||
start: url.searchParams.get('start'),
|
||||
start,
|
||||
rel: 0,
|
||||
loop: 0,
|
||||
playsinline: 1,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
module.exports = {
|
||||
locales: [
|
||||
'en',
|
||||
'ast',
|
||||
'an',
|
||||
'ca',
|
||||
'de',
|
||||
'en-GB',
|
||||
@@ -9,18 +11,21 @@ module.exports = {
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ru',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh-CN',
|
||||
'zh-HK',
|
||||
'zh-TW',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.95.0",
|
||||
"version": "1.96.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -54,7 +54,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.11",
|
||||
"@atproto/api": "^0.13.18",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -74,6 +74,7 @@
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.4.1",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "2.0.1",
|
||||
"@radix-ui/react-focus-guards": "^1.1.1",
|
||||
@@ -90,18 +91,18 @@
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
"@tiptap/core": "^2.6.6",
|
||||
"@tiptap/extension-document": "^2.6.6",
|
||||
"@tiptap/extension-hard-break": "^2.6.6",
|
||||
"@tiptap/extension-history": "^2.6.6",
|
||||
"@tiptap/extension-mention": "^2.6.6",
|
||||
"@tiptap/extension-paragraph": "^2.6.6",
|
||||
"@tiptap/extension-placeholder": "^2.6.6",
|
||||
"@tiptap/extension-text": "^2.6.6",
|
||||
"@tiptap/html": "^2.6.6",
|
||||
"@tiptap/pm": "^2.6.6",
|
||||
"@tiptap/react": "^2.6.6",
|
||||
"@tiptap/suggestion": "^2.6.6",
|
||||
"@tiptap/core": "^2.9.1",
|
||||
"@tiptap/extension-document": "^2.9.1",
|
||||
"@tiptap/extension-hard-break": "^2.9.1",
|
||||
"@tiptap/extension-history": "^2.9.1",
|
||||
"@tiptap/extension-mention": "^2.9.1",
|
||||
"@tiptap/extension-paragraph": "^2.9.1",
|
||||
"@tiptap/extension-placeholder": "^2.9.1",
|
||||
"@tiptap/extension-text": "^2.9.1",
|
||||
"@tiptap/html": "^2.9.1",
|
||||
"@tiptap/pm": "^2.9.1",
|
||||
"@tiptap/react": "^2.9.1",
|
||||
"@tiptap/suggestion": "^2.9.1",
|
||||
"@types/invariant": "^2.2.37",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^20.14.3",
|
||||
@@ -204,7 +205,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.55",
|
||||
"@atproto/dev-env": "^0.3.64",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
@@ -260,13 +261,12 @@
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@react-native/babel-preset": "0.74.1",
|
||||
"@types/react": "^18",
|
||||
"**/zeed-dom": "0.10.9",
|
||||
"**/zod": "3.23.8",
|
||||
"**/expo-constants": "16.0.1",
|
||||
"**/expo-device": "6.0.2",
|
||||
"@react-native/babel-preset": "0.74.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0"
|
||||
"**/zod": "3.23.8"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
index 5de0845..fa477e7 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
@@ -21,6 +21,7 @@ import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
+import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
@@ -678,6 +679,15 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
throw new Exception("Cannot select remote files");
|
||||
}
|
||||
BitmapFactory.Options original = validateImage(path);
|
||||
+ ExifInterface originalExif = new ExifInterface(path);
|
||||
+ int orientation = originalExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
|
||||
+ boolean invertDimensions = (
|
||||
+ orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
|
||||
+ orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
|
||||
+ orientation == ExifInterface.ORIENTATION_TRANSPOSE ||
|
||||
+ orientation == ExifInterface.ORIENTATION_TRANSVERSE
|
||||
+ );
|
||||
+
|
||||
|
||||
// if compression options are provided image will be compressed. If none options is provided,
|
||||
// then original image will be returned
|
||||
@@ -687,8 +697,8 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
long modificationDate = new File(path).lastModified();
|
||||
|
||||
image.putString("path", "file://" + compressedImagePath);
|
||||
- image.putInt("width", options.outWidth);
|
||||
- image.putInt("height", options.outHeight);
|
||||
+ image.putInt("width", invertDimensions ? options.outHeight : options.outWidth);
|
||||
+ image.putInt("height", invertDimensions ? options.outWidth : options.outHeight);
|
||||
image.putString("mime", options.outMimeType);
|
||||
image.putInt("size", (int) new File(compressedImagePath).length());
|
||||
image.putString("modificationDate", String.valueOf(modificationDate));
|
||||
@@ -4,16 +4,17 @@ import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
SafeAreaProvider,
|
||||
} from 'react-native-safe-area-context'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {
|
||||
initialize,
|
||||
@@ -24,6 +25,7 @@ import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
@@ -66,12 +68,15 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {AppProfiler} from '#/AppProfiler'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {AppProfiler} from './AppProfiler'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
if (isIOS) {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
@@ -188,7 +193,7 @@ function App() {
|
||||
<AppProfiler>
|
||||
<GeolocationProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
|
||||
<KeyboardControllerProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
@@ -217,7 +222,7 @@ function App() {
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardProvider>
|
||||
</KeyboardControllerProvider>
|
||||
</A11yProvider>
|
||||
</GeolocationProvider>
|
||||
</AppProfiler>
|
||||
|
||||
@@ -3,7 +3,6 @@ import '#/view/icons'
|
||||
import './style.css'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -102,54 +101,52 @@ function InnerApp() {
|
||||
if (!isReady || !hasCheckedReferrer) return null
|
||||
|
||||
return (
|
||||
<KeyboardProvider enabled={false}>
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</ComposerProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
</KeyboardProvider>
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</ComposerProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
@@ -362,6 +363,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AppIconSettings"
|
||||
getComponent={() => AppIconSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`App Icon`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Hashtag"
|
||||
getComponent={() => HashtagScreen}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import {ios, native, web} from '#/alf/util/platform'
|
||||
|
||||
export const atoms = {
|
||||
debug: {
|
||||
@@ -312,6 +312,12 @@ export const atoms = {
|
||||
border_r: {
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
curve_circular: ios({
|
||||
borderCurve: 'circular',
|
||||
}),
|
||||
curve_continuous: ios({
|
||||
borderCurve: 'continuous',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Shadow
|
||||
|
||||
@@ -20,6 +20,7 @@ export * from '#/alf/types'
|
||||
export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
export * from '#/alf/util/useGutterStyles'
|
||||
|
||||
export type Alf = {
|
||||
themeName: ThemeName
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as NavigationBar from 'expo-navigation-bar'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Theme} from '../types'
|
||||
|
||||
export function setNavigationBar(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
if (themeType === 'theme') {
|
||||
NavigationBar.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
|
||||
NavigationBar.setBorderColorAsync(t.atoms.bg.backgroundColor)
|
||||
NavigationBar.setButtonStyleAsync(t.name !== 'light' ? 'light' : 'dark')
|
||||
} else {
|
||||
NavigationBar.setBackgroundColorAsync('black')
|
||||
NavigationBar.setBorderColorAsync('black')
|
||||
NavigationBar.setButtonStyleAsync('light')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
|
||||
|
||||
export function useGutterStyles({
|
||||
top,
|
||||
bottom,
|
||||
}: {
|
||||
top?: boolean
|
||||
bottom?: boolean
|
||||
} = {}) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
return React.useMemo<ViewStyleProp['style']>(() => {
|
||||
return [
|
||||
a.px_lg,
|
||||
top && a.pt_md,
|
||||
bottom && a.pb_md,
|
||||
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
|
||||
]
|
||||
}, [gtMobile, top, bottom])
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export function AppLanguageDropdown(_props: ViewStyleProp) {
|
||||
return (
|
||||
<View style={a.relative}>
|
||||
<RNPickerSelect
|
||||
darkTheme={t.scheme === 'dark'}
|
||||
placeholder={{}}
|
||||
value={sanitizedLang}
|
||||
onValueChange={onChangeAppLanguage}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from 'react-native'
|
||||
import {
|
||||
KeyboardAwareScrollView,
|
||||
useKeyboardController,
|
||||
useKeyboardHandler,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {runOnJS} from 'react-native-reanimated'
|
||||
@@ -20,6 +19,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
@@ -199,27 +199,20 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||
const insets = useSafeAreaInsets()
|
||||
const {setEnabled} = useKeyboardController()
|
||||
|
||||
useEnableKeyboardController(isIOS)
|
||||
|
||||
const [keyboardHeight, setKeyboardHeight] = React.useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isIOS) {
|
||||
return
|
||||
}
|
||||
|
||||
setEnabled(true)
|
||||
return () => {
|
||||
setEnabled(false)
|
||||
}
|
||||
})
|
||||
|
||||
useKeyboardHandler({
|
||||
onEnd: e => {
|
||||
'worklet'
|
||||
runOnJS(setKeyboardHeight)(e.height)
|
||||
useKeyboardHandler(
|
||||
{
|
||||
onEnd: e => {
|
||||
'worklet'
|
||||
runOnJS(setKeyboardHeight)(e.height)
|
||||
},
|
||||
},
|
||||
})
|
||||
[],
|
||||
)
|
||||
|
||||
const basePading =
|
||||
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
|
||||
@@ -42,7 +41,6 @@ export function Outer({
|
||||
onClose,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
@@ -118,16 +116,7 @@ export function Outer({
|
||||
gtMobile ? a.p_lg : a.p_md,
|
||||
{overflowY: 'auto'},
|
||||
]}>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
// exiting={FadeOut.duration(150)}
|
||||
style={[
|
||||
web(a.fixed),
|
||||
a.inset_0,
|
||||
{opacity: 0.8, backgroundColor: t.palette.black},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Backdrop />
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
@@ -164,7 +153,7 @@ export function Inner({
|
||||
useFocusGuards()
|
||||
return (
|
||||
<FocusScope loop asChild trapped>
|
||||
<Animated.View
|
||||
<View
|
||||
role="dialog"
|
||||
aria-role="dialog"
|
||||
aria-label={label}
|
||||
@@ -174,8 +163,6 @@ export function Inner({
|
||||
onClick={stopPropagation}
|
||||
onStartShouldSetResponder={_ => true}
|
||||
onTouchEnd={stopPropagation}
|
||||
entering={FadeInDown.duration(100)}
|
||||
// exiting={FadeOut.duration(100)}
|
||||
style={flatten([
|
||||
a.relative,
|
||||
a.rounded_md,
|
||||
@@ -188,6 +175,8 @@ export function Inner({
|
||||
shadowColor: t.palette.black,
|
||||
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
|
||||
shadowRadius: 30,
|
||||
// @ts-ignore web only
|
||||
animation: 'fadeIn ease-out 0.1s',
|
||||
},
|
||||
flatten(style),
|
||||
])}>
|
||||
@@ -201,7 +190,7 @@ export function Inner({
|
||||
{children}
|
||||
</View>
|
||||
</DismissableLayer>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</FocusScope>
|
||||
)
|
||||
}
|
||||
@@ -268,3 +257,25 @@ export function Close() {
|
||||
export function Handle() {
|
||||
return null
|
||||
}
|
||||
|
||||
function Backdrop() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
opacity: 0.8,
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
{
|
||||
backgroundColor: t.palette.black,
|
||||
// @ts-ignore web only
|
||||
animation: 'fadeIn ease-out 0.15s',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import React, {useContext, useMemo} from 'react'
|
||||
import {View, ViewStyle} from 'react-native'
|
||||
import {StyleProp} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
// Every screen should have a Layout component wrapping it.
|
||||
// This component provides a default padding for the top of the screen.
|
||||
// This allows certain screens to avoid the top padding if they want to.
|
||||
|
||||
const LayoutContext = React.createContext({
|
||||
withinScreen: false,
|
||||
topPaddingDisabled: false,
|
||||
withinScrollView: false,
|
||||
})
|
||||
|
||||
/**
|
||||
* Every screen should have a Layout.Screen component wrapping it.
|
||||
* This component provides a default padding for the top of the screen
|
||||
* and height/minHeight
|
||||
*/
|
||||
let Screen = ({
|
||||
disableTopPadding = false,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View> & {
|
||||
disableTopPadding?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}): React.ReactNode => {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
withinScreen: true,
|
||||
topPaddingDisabled: disableTopPadding,
|
||||
withinScrollView: false,
|
||||
}),
|
||||
[disableTopPadding],
|
||||
)
|
||||
return (
|
||||
<LayoutContext.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
{paddingTop: disableTopPadding ? 0 : top},
|
||||
a.util_screen_outer,
|
||||
style,
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
Screen = React.memo(Screen)
|
||||
export {Screen}
|
||||
|
||||
let Header = (
|
||||
props: React.ComponentProps<typeof ViewHeader>,
|
||||
): React.ReactNode => {
|
||||
const {withinScrollView} = useContext(LayoutContext)
|
||||
if (!withinScrollView) {
|
||||
return (
|
||||
<CenteredView topBorder={false} sideBorders>
|
||||
<ViewHeader showOnDesktop showBorder {...props} />
|
||||
</CenteredView>
|
||||
)
|
||||
} else {
|
||||
return <ViewHeader showOnDesktop showBorder {...props} />
|
||||
}
|
||||
}
|
||||
Header = React.memo(Header)
|
||||
export {Header}
|
||||
|
||||
let Content = ({
|
||||
style,
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollView> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
}): React.ReactNode => {
|
||||
const context = useContext(LayoutContext)
|
||||
const newContext = useMemo(
|
||||
() => ({...context, withinScrollView: true}),
|
||||
[context],
|
||||
)
|
||||
return (
|
||||
<LayoutContext.Provider value={newContext}>
|
||||
<ScrollView
|
||||
style={[a.flex_1, style]}
|
||||
contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]}
|
||||
{...props}
|
||||
/>
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
Content = React.memo(Content)
|
||||
export {Content}
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
useGutterStyles,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
|
||||
const BUTTON_SIZE = 34 // small button
|
||||
|
||||
export function Outer({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
const gutter = useGutterStyles()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border_b,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
gutter,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<View style={[a.flex_1]}>{children}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Content({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.justify_center,
|
||||
a.py_xs,
|
||||
isIOS && a.align_center,
|
||||
{
|
||||
paddingHorizontal: BUTTON_SIZE + a.gap_sm.gap,
|
||||
minHeight: BUTTON_SIZE,
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Slot({
|
||||
children,
|
||||
style,
|
||||
}: {children?: React.ReactNode} & ViewStyleProp) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.z_50,
|
||||
{
|
||||
width: BUTTON_SIZE,
|
||||
right: 'auto',
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function BackButton() {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<Slot>
|
||||
<Button
|
||||
label={_(msg`Go back`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onPressBack}
|
||||
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
|
||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||
</Button>
|
||||
</Slot>
|
||||
)
|
||||
}
|
||||
|
||||
export function MenuButton() {
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
return gtMobile ? null : (
|
||||
<Slot>
|
||||
<Button
|
||||
label={_(msg`Open drawer menu`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onPress}
|
||||
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
|
||||
<ButtonIcon icon={Menu} size="lg" />
|
||||
</Button>
|
||||
</Slot>
|
||||
)
|
||||
}
|
||||
|
||||
export function TitleText({children}: {children: React.ReactNode}) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
a.text_lg,
|
||||
a.font_heavy,
|
||||
a.leading_snug,
|
||||
gtMobile && [a.text_xl],
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function SubtitleText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, a.leading_tight, t.atoms.text_contrast_medium]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, {useContext, useMemo} from 'react'
|
||||
import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native'
|
||||
import {StyleProp} from 'react-native'
|
||||
import {
|
||||
KeyboardAwareScrollView,
|
||||
KeyboardAwareScrollViewProps,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import Animated, {AnimatedScrollViewProps} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
|
||||
export * as Header from '#/components/Layout/Header'
|
||||
|
||||
// Every screen should have a Layout component wrapping it.
|
||||
// This component provides a default padding for the top of the screen.
|
||||
// This allows certain screens to avoid the top padding if they want to.
|
||||
|
||||
const LayoutContext = React.createContext({
|
||||
withinScreen: false,
|
||||
topPaddingDisabled: false,
|
||||
withinScrollView: false,
|
||||
})
|
||||
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
disableTopPadding?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
/**
|
||||
* Every screen should have a Layout.Screen component wrapping it.
|
||||
* This component provides a default padding for the top of the screen
|
||||
* and height/minHeight
|
||||
*/
|
||||
export const Screen = React.memo(function Screen({
|
||||
disableTopPadding = false,
|
||||
style,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
withinScreen: true,
|
||||
topPaddingDisabled: disableTopPadding,
|
||||
withinScrollView: false,
|
||||
}),
|
||||
[disableTopPadding],
|
||||
)
|
||||
return (
|
||||
<LayoutContext.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
{paddingTop: disableTopPadding ? 0 : top},
|
||||
a.util_screen_outer,
|
||||
style,
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
export type ContentProps = AnimatedScrollViewProps & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export const Content = React.memo(function Content({
|
||||
children,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: ContentProps) {
|
||||
const context = useContext(LayoutContext)
|
||||
const newContext = useMemo(
|
||||
() => ({...context, withinScrollView: true}),
|
||||
[context],
|
||||
)
|
||||
return (
|
||||
<LayoutContext.Provider value={newContext}>
|
||||
<Animated.ScrollView
|
||||
id="content"
|
||||
style={[
|
||||
styles.scrollViewCommonStyles,
|
||||
style,
|
||||
styles.scrollViewOverrideStyles,
|
||||
]}
|
||||
contentContainerStyle={[
|
||||
styles.scrollViewContentContainer,
|
||||
contentContainerStyle,
|
||||
]}
|
||||
{...props}>
|
||||
{isWeb ? (
|
||||
// @ts-ignore web only -esb
|
||||
<Center>{children}</Center>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
|
||||
children: React.ReactNode
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export const KeyboardAwareContent = React.forwardRef(function LayoutScrollView(
|
||||
{children, style, contentContainerStyle, ...props}: KeyboardAwareContentProps,
|
||||
_ref: React.Ref<typeof KeyboardAwareScrollView>,
|
||||
) {
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={[
|
||||
styles.scrollViewCommonStyles,
|
||||
style,
|
||||
styles.scrollViewOverrideStyles,
|
||||
]}
|
||||
contentContainerStyle={[
|
||||
styles.scrollViewContentContainer,
|
||||
contentContainerStyle,
|
||||
]}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
{...props}>
|
||||
{isWeb ? <Center>{children}</Center> : children}
|
||||
</KeyboardAwareScrollView>
|
||||
)
|
||||
})
|
||||
|
||||
export const Center = React.forwardRef(function LayoutContent(
|
||||
{children, style, ...props}: ViewProps,
|
||||
ref: React.Ref<View>,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
return (
|
||||
<>
|
||||
{gtMobile && (
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
a.border_l,
|
||||
a.border_r,
|
||||
t.atoms.border_contrast_low,
|
||||
{
|
||||
width: 602,
|
||||
left: '50%',
|
||||
transform: [
|
||||
{
|
||||
translateX: '-50%',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
ref={ref}
|
||||
style={[
|
||||
a.util_screen_outer,
|
||||
a.w_full,
|
||||
a.mx_auto,
|
||||
gtMobile && {
|
||||
maxWidth: 600,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
{...props}>
|
||||
{children}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollViewCommonStyles: {
|
||||
width: '100%',
|
||||
},
|
||||
scrollViewContentContainer: {
|
||||
paddingBottom: 100,
|
||||
},
|
||||
/**
|
||||
* Applied last to ensure they override any locally-defined styles. Use sparingly.
|
||||
*/
|
||||
scrollViewOverrideStyles: {
|
||||
/*
|
||||
* Ensures fixed items within `ScrollView` are fixed relative to window.
|
||||
*/
|
||||
transform: 'unset',
|
||||
},
|
||||
})
|
||||
@@ -223,7 +223,7 @@ export function Link({
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
rel: isExternal ? 'noopener' : undefined,
|
||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||
download,
|
||||
},
|
||||
dataSet: {
|
||||
@@ -307,7 +307,7 @@ export function InlineLinkText({
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
rel: isExternal ? 'noopener' : undefined,
|
||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||
download,
|
||||
},
|
||||
dataSet: {
|
||||
|
||||
@@ -302,8 +302,8 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
const animationStyle = {
|
||||
animation:
|
||||
currentState.stage === 'hiding'
|
||||
? `avatarHoverFadeOut ${HIDE_DURATION}ms both`
|
||||
: `avatarHoverFadeIn ${SHOW_DURATION}ms both`,
|
||||
? `fadeOut ${HIDE_DURATION}ms both`
|
||||
: `fadeIn ${SHOW_DURATION}ms both`,
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
ref,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const bottomBarOffset = useBottomBarOffset(300)
|
||||
const bottomBarOffset = useBottomBarOffset(headerHeight)
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const {currentAccount} = useSession()
|
||||
const {data, refetch, isError} = useAllListMembersQuery(listUri)
|
||||
|
||||
@@ -256,6 +256,9 @@ export function PostInteractionSettingsForm({
|
||||
} else {
|
||||
newSelected.splice(i, 1)
|
||||
}
|
||||
if (newSelected.length === 0) {
|
||||
newSelected.push({type: 'everybody'})
|
||||
}
|
||||
|
||||
onChangeThreadgateAllowUISettings(newSelected)
|
||||
}
|
||||
@@ -308,11 +311,7 @@ export function PostInteractionSettingsForm({
|
||||
onChange={onChangeQuotesEnabled}
|
||||
style={[a.justify_between, a.pt_xs]}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{quotesEnabled ? (
|
||||
<Trans>Quote posts enabled</Trans>
|
||||
) : (
|
||||
<Trans>Quote posts disabled</Trans>
|
||||
)}
|
||||
<Trans>Quote posts enabled</Trans>
|
||||
</Text>
|
||||
<Toggle.Switch />
|
||||
</Toggle.Item>
|
||||
|
||||
@@ -16,18 +16,21 @@ import {
|
||||
es,
|
||||
fi,
|
||||
fr,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
pl,
|
||||
ptBR,
|
||||
ru,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
vi,
|
||||
zhCN,
|
||||
zhHK,
|
||||
zhTW,
|
||||
@@ -41,6 +44,8 @@ import {useLanguagePrefs} from '#/state/preferences'
|
||||
*/
|
||||
const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
en: undefined,
|
||||
ast: undefined,
|
||||
an: undefined,
|
||||
ca,
|
||||
de,
|
||||
['en-GB']: enGB,
|
||||
@@ -48,18 +53,21 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
fi,
|
||||
fr,
|
||||
ga: undefined,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
pl,
|
||||
['pt-BR']: ptBR,
|
||||
ru,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
vi,
|
||||
['zh-CN']: zhCN,
|
||||
['zh-HK']: zhHK,
|
||||
['zh-TW']: zhTW,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
|
||||
})
|
||||
@@ -21,7 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
|
||||
// code and update this number with each release until we can get the
|
||||
// server route done.
|
||||
// -prf
|
||||
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
|
||||
export const JOINED_THIS_WEEK = 2880000 // estimate as of 11/26/24
|
||||
|
||||
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
|
||||
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
|
||||
@@ -29,6 +29,7 @@ export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
|
||||
'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
|
||||
'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
|
||||
'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
|
||||
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
|
||||
}
|
||||
|
||||
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
KeyboardProvider,
|
||||
useKeyboardController,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
const KeyboardControllerRefCountContext = createContext<{
|
||||
incrementRefCount: () => void
|
||||
decrementRefCount: () => void
|
||||
}>({
|
||||
incrementRefCount: () => {},
|
||||
decrementRefCount: () => {},
|
||||
})
|
||||
|
||||
export function KeyboardControllerProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<KeyboardProvider
|
||||
enabled={false}
|
||||
// I don't think this is necessary, but Chesterton's fence and all that -sfn
|
||||
statusBarTranslucent={true}>
|
||||
<KeyboardControllerProviderInner>
|
||||
{children}
|
||||
</KeyboardControllerProviderInner>
|
||||
</KeyboardProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function KeyboardControllerProviderInner({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const {setEnabled} = useKeyboardController()
|
||||
const refCount = useRef(0)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
incrementRefCount: () => {
|
||||
refCount.current++
|
||||
setEnabled(refCount.current > 0)
|
||||
},
|
||||
decrementRefCount: () => {
|
||||
refCount.current--
|
||||
setEnabled(refCount.current > 0)
|
||||
|
||||
if (IS_DEV && refCount.current < 0) {
|
||||
console.error('KeyboardController ref count < 0')
|
||||
}
|
||||
},
|
||||
}),
|
||||
[setEnabled],
|
||||
)
|
||||
|
||||
return (
|
||||
<KeyboardControllerRefCountContext.Provider value={value}>
|
||||
{children}
|
||||
</KeyboardControllerRefCountContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useEnableKeyboardController(shouldEnable: boolean) {
|
||||
const {incrementRefCount, decrementRefCount} = useContext(
|
||||
KeyboardControllerRefCountContext,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldEnable) {
|
||||
return
|
||||
}
|
||||
incrementRefCount()
|
||||
return () => {
|
||||
decrementRefCount()
|
||||
}
|
||||
}, [shouldEnable, incrementRefCount, decrementRefCount])
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `useEnableKeyboardController`, but using `useFocusEffect`
|
||||
*/
|
||||
export function useEnableKeyboardControllerScreen(shouldEnable: boolean) {
|
||||
const {incrementRefCount, decrementRefCount} = useContext(
|
||||
KeyboardControllerRefCountContext,
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!shouldEnable) {
|
||||
return
|
||||
}
|
||||
incrementRefCount()
|
||||
return () => {
|
||||
decrementRefCount()
|
||||
}
|
||||
}, [shouldEnable, incrementRefCount, decrementRefCount]),
|
||||
)
|
||||
}
|
||||
@@ -4,14 +4,12 @@ import * as WebBrowser from 'expo-web-browser'
|
||||
|
||||
import {
|
||||
createBskyAppAbsoluteUrl,
|
||||
isBskyAppUrl,
|
||||
isBskyRSSUrl,
|
||||
isRelativeUrl,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
|
||||
import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm'
|
||||
import {useTheme} from '#/alf'
|
||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
|
||||
@@ -20,7 +18,6 @@ export function useOpenLink() {
|
||||
const enabled = useInAppBrowser()
|
||||
const t = useTheme()
|
||||
const sheetWrapper = useSheetWrapper()
|
||||
const optOutOfUtm = useOptOutOfUtm()
|
||||
|
||||
const openLink = useCallback(
|
||||
async (url: string, override?: boolean) => {
|
||||
@@ -29,9 +26,6 @@ export function useOpenLink() {
|
||||
}
|
||||
|
||||
if (isNative && !url.startsWith('mailto:')) {
|
||||
if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) {
|
||||
url = addUtmSource(url)
|
||||
}
|
||||
if (override === undefined && enabled === undefined) {
|
||||
openModal({
|
||||
name: 'in-app-browser-consent',
|
||||
@@ -53,24 +47,8 @@ export function useOpenLink() {
|
||||
}
|
||||
Linking.openURL(url)
|
||||
},
|
||||
[enabled, openModal, t, sheetWrapper, optOutOfUtm],
|
||||
[enabled, openModal, t, sheetWrapper],
|
||||
)
|
||||
|
||||
return openLink
|
||||
}
|
||||
|
||||
function addUtmSource(url: string): string {
|
||||
let parsedUrl
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
if (!parsedUrl.searchParams.has('utm_source')) {
|
||||
parsedUrl.searchParams.set('utm_source', 'bluesky')
|
||||
if (!parsedUrl.searchParams.has('utm_medium')) {
|
||||
parsedUrl.searchParams.set('utm_medium', 'social')
|
||||
}
|
||||
}
|
||||
return parsedUrl.toString()
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export type CommonNavigatorParams = {
|
||||
PrivacyAndSecuritySettings: undefined
|
||||
ContentAndMediaSettings: undefined
|
||||
AboutSettings: undefined
|
||||
AppIconSettings: undefined
|
||||
Search: {q?: string}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
|
||||
@@ -2,4 +2,3 @@ export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext' // DISABLED DUE TO EME
|
||||
| 'post_feed_lang_window' // DISABLED DUE TO EME
|
||||
| 'suggested_feeds_interstitial'
|
||||
|
||||
@@ -11,4 +11,5 @@ test('sanitizeAppLanguageSetting', () => {
|
||||
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('vi')).toBe(AppLanguage.vi)
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ export function getTranslatorLink(text: string, lang: string): string {
|
||||
/**
|
||||
* Returns a valid `appLanguage` value from an arbitrary string.
|
||||
*
|
||||
* Contenxt: post-refactor, we populated some user's `appLanguage` setting with
|
||||
* Context: post-refactor, we populated some user's `appLanguage` setting with
|
||||
* `postLanguage`, which can be a comma-separated list of values. This breaks
|
||||
* `appLanguage` handling in the app, so we introduced this util to parse out a
|
||||
* valid `appLanguage` from the pre-populated `postLanguage` values.
|
||||
@@ -119,6 +119,10 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
switch (fixLegacyLanguageCode(lang)) {
|
||||
case 'en':
|
||||
return AppLanguage.en
|
||||
case 'an':
|
||||
return AppLanguage.an
|
||||
case 'ast':
|
||||
return AppLanguage.ast
|
||||
case 'ca':
|
||||
return AppLanguage.ca
|
||||
case 'de':
|
||||
@@ -133,6 +137,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.fr
|
||||
case 'ga':
|
||||
return AppLanguage.ga
|
||||
case 'gl':
|
||||
return AppLanguage.gl
|
||||
case 'hi':
|
||||
return AppLanguage.hi
|
||||
case 'hu':
|
||||
@@ -145,6 +151,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ja
|
||||
case 'ko':
|
||||
return AppLanguage.ko
|
||||
case 'nl':
|
||||
return AppLanguage.nl
|
||||
case 'pl':
|
||||
return AppLanguage.pl
|
||||
case 'pt-BR':
|
||||
@@ -157,6 +165,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.tr
|
||||
case 'uk':
|
||||
return AppLanguage.uk
|
||||
case 'vi':
|
||||
return AppLanguage.vi
|
||||
case 'zh-CN':
|
||||
return AppLanguage.zh_CN
|
||||
case 'zh-HK':
|
||||
|
||||
@@ -11,6 +11,8 @@ import {i18n} from '@lingui/core'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
@@ -19,18 +21,21 @@ import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
||||
import {messages as messagesGl} from '#/locale/locales/gl/messages'
|
||||
import {messages as messagesHi} from '#/locale/locales/hi/messages'
|
||||
import {messages as messagesHu} from '#/locale/locales/hu/messages'
|
||||
import {messages as messagesId} from '#/locale/locales/id/messages'
|
||||
import {messages as messagesIt} from '#/locale/locales/it/messages'
|
||||
import {messages as messagesJa} from '#/locale/locales/ja/messages'
|
||||
import {messages as messagesKo} from '#/locale/locales/ko/messages'
|
||||
import {messages as messagesNl} from '#/locale/locales/nl/messages'
|
||||
import {messages as messagesPl} from '#/locale/locales/pl/messages'
|
||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
||||
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
||||
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
||||
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||
import {messages as messagesVi} from '#/locale/locales/vi/messages'
|
||||
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages'
|
||||
import {messages as messagesZh_HK} from '#/locale/locales/zh-HK/messages'
|
||||
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages'
|
||||
@@ -41,6 +46,22 @@ import {useLanguagePrefs} from '#/state/preferences'
|
||||
*/
|
||||
export async function dynamicActivate(locale: AppLanguage) {
|
||||
switch (locale) {
|
||||
case AppLanguage.an: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAn})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/an'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ast: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAst})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/ast'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ast'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ca: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCa})
|
||||
await Promise.all([
|
||||
@@ -97,6 +118,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/gl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gl'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
await Promise.all([
|
||||
@@ -145,6 +174,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/nl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/nl'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.pl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPl})
|
||||
await Promise.all([
|
||||
@@ -193,6 +230,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.vi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesVi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/vi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/vi'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_CN})
|
||||
await Promise.all([
|
||||
|
||||
@@ -12,6 +12,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
let mod: any
|
||||
|
||||
switch (locale) {
|
||||
case AppLanguage.an: {
|
||||
mod = await import(`./locales/an/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ast: {
|
||||
mod = await import(`./locales/ast/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ca: {
|
||||
mod = await import(`./locales/ca/messages`)
|
||||
break
|
||||
@@ -40,6 +48,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ga/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
mod = await import(`./locales/gl/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
mod = await import(`./locales/hi/messages`)
|
||||
break
|
||||
@@ -64,6 +76,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ko/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
mod = await import(`./locales/nl/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.pl: {
|
||||
mod = await import(`./locales/pl/messages`)
|
||||
break
|
||||
@@ -88,6 +104,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/uk/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.vi: {
|
||||
mod = await import(`./locales/vi/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
mod = await import(`./locales/zh-CN/messages`)
|
||||
break
|
||||
|
||||
@@ -6,6 +6,8 @@ interface Language {
|
||||
|
||||
export enum AppLanguage {
|
||||
en = 'en',
|
||||
an = 'an',
|
||||
ast = 'ast',
|
||||
ca = 'ca',
|
||||
de = 'de',
|
||||
en_GB = 'en-GB',
|
||||
@@ -13,18 +15,21 @@ export enum AppLanguage {
|
||||
fi = 'fi',
|
||||
fr = 'fr',
|
||||
ga = 'ga',
|
||||
gl = 'gl',
|
||||
hi = 'hi',
|
||||
hu = 'hu',
|
||||
id = 'id',
|
||||
it = 'it',
|
||||
ja = 'ja',
|
||||
ko = 'ko',
|
||||
nl = 'nl',
|
||||
pl = 'pl',
|
||||
pt_BR = 'pt-BR',
|
||||
ru = 'ru',
|
||||
th = 'th',
|
||||
tr = 'tr',
|
||||
uk = 'uk',
|
||||
vi = 'vi',
|
||||
zh_CN = 'zh-CN',
|
||||
zh_HK = 'zh-HK',
|
||||
zh_TW = 'zh-TW',
|
||||
@@ -37,6 +42,8 @@ interface AppLanguageConfig {
|
||||
|
||||
export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.en, name: 'English'},
|
||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||
{code2: AppLanguage.ast, name: 'Asturianu - Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
@@ -44,18 +51,21 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||
{code2: AppLanguage.fr, name: 'Français – French'},
|
||||
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
|
||||
{code2: AppLanguage.gl, name: 'Galego - Galician'},
|
||||
{code2: AppLanguage.hi, name: 'हिंदी – Hindi'},
|
||||
{code2: AppLanguage.hu, name: 'magyar – Hungarian'},
|
||||
{code2: AppLanguage.id, name: 'Bahasa Indonesia – Indonesian'},
|
||||
{code2: AppLanguage.it, name: 'Italiano – Italian'},
|
||||
{code2: AppLanguage.ja, name: '日本語 – Japanese'},
|
||||
{code2: AppLanguage.ko, name: '한국어 – Korean'},
|
||||
{code2: AppLanguage.nl, name: 'Nederlands – Dutch'},
|
||||
{code2: AppLanguage.pl, name: 'Polski – Polish'},
|
||||
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
|
||||
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
||||
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
||||
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
||||
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
|
||||
{code2: AppLanguage.vi, name: 'Tiếng Việt – Vietnamese'},
|
||||
{code2: AppLanguage.zh_CN, name: '简体中文 – Simplified Chinese'},
|
||||
{code2: AppLanguage.zh_TW, name: '繁體中文 – Traditional Chinese'},
|
||||
{code2: AppLanguage.zh_HK, name: '粵文 – Cantonese'},
|
||||
@@ -469,8 +479,8 @@ export const LANGUAGES: Language[] = [
|
||||
{code3: 'roa', code2: '', name: 'Romance languages'},
|
||||
{code3: 'roh', code2: 'rm', name: 'Romansh'},
|
||||
{code3: 'rom', code2: '', name: 'Romany'},
|
||||
{code3: 'rum', code2: 'ro', name: 'Romanian; Moldavian; Moldovan'},
|
||||
{code3: 'ron', code2: 'ro', name: 'Romanian; Moldavian; Moldovan'},
|
||||
{code3: 'rum', code2: 'ro', name: 'Romanian'},
|
||||
{code3: 'ron', code2: 'ro', name: 'Romanian'},
|
||||
{code3: 'run', code2: 'rn', name: 'Rundi'},
|
||||
{code3: 'rup', code2: '', name: 'Aromanian; Arumanian; Macedo-Romanian'},
|
||||
{code3: 'rus', code2: 'ru', name: 'Russian'},
|
||||
|
||||
@@ -44,6 +44,7 @@ export const router = new Router({
|
||||
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
|
||||
ContentAndMediaSettings: '/settings/content-and-media',
|
||||
AboutSettings: '/settings/about',
|
||||
AppIconSettings: '/settings/app-icon',
|
||||
// support
|
||||
Support: '/support',
|
||||
PrivacyPolicy: '/support/privacy',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useKeyboardController} from 'react-native-keyboard-controller'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,6 +7,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -39,16 +39,7 @@ export function MessagesConversationScreen({route}: Props) {
|
||||
const convoId = route.params.conversation
|
||||
const {setCurrentConvoId} = useCurrentConvoId()
|
||||
|
||||
const {setEnabled} = useKeyboardController()
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (isWeb) return
|
||||
setEnabled(true)
|
||||
return () => {
|
||||
setEnabled(false)
|
||||
}
|
||||
}, [setEnabled]),
|
||||
)
|
||||
useEnableKeyboardControllerScreen(true)
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
|
||||
@@ -38,7 +38,7 @@ export function MessageInput({
|
||||
children?: React.ReactNode
|
||||
openEmojiPicker?: (pos: EmojiPickerPosition) => void
|
||||
}) {
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {getDraft, clearDraft} = useMessageDraft()
|
||||
@@ -212,7 +212,7 @@ export function MessageInput({
|
||||
onChange={onChange}
|
||||
// On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message
|
||||
// in these cases.
|
||||
onKeyDown={isTouchDevice && isTabletOrDesktop ? undefined : onKeyDown}
|
||||
onKeyDown={isTouchDevice && isMobile ? undefined : onKeyDown}
|
||||
/>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React, {useCallback, useRef} from 'react'
|
||||
import {FlatList, LayoutChangeEvent, View} from 'react-native'
|
||||
import {
|
||||
KeyboardStickyView,
|
||||
useKeyboardHandler,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {
|
||||
import {LayoutChangeEvent, View} from 'react-native'
|
||||
import {useKeyboardHandler} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
scrollTo,
|
||||
useAnimatedRef,
|
||||
@@ -33,7 +30,7 @@ import {
|
||||
EmojiPicker,
|
||||
EmojiPickerState,
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker.web'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {List, ListMethods} from '#/view/com/util/List'
|
||||
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
|
||||
import {MessageInput} from '#/screens/Messages/components/MessageInput'
|
||||
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
||||
@@ -94,7 +91,7 @@ export function MessagesList({
|
||||
const getPost = useGetPost()
|
||||
const {embedUri, setEmbed} = useMessageEmbed()
|
||||
|
||||
const flatListRef = useAnimatedRef<FlatList>()
|
||||
const flatListRef = useAnimatedRef<ListMethods>()
|
||||
|
||||
const [newMessagesPill, setNewMessagesPill] = React.useState({
|
||||
show: false,
|
||||
@@ -250,34 +247,44 @@ export function MessagesList({
|
||||
// We use this value to keep track of when we want to disable the animation.
|
||||
const layoutScrollWithoutAnimation = useSharedValue(false)
|
||||
|
||||
useKeyboardHandler({
|
||||
onStart: e => {
|
||||
'worklet'
|
||||
// Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
|
||||
// just update the height here instead of having the `onMove` event do it (that event will not fire!)
|
||||
if (e.duration === 0) {
|
||||
layoutScrollWithoutAnimation.set(true)
|
||||
useKeyboardHandler(
|
||||
{
|
||||
onStart: e => {
|
||||
'worklet'
|
||||
// Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
|
||||
// just update the height here instead of having the `onMove` event do it (that event will not fire!)
|
||||
if (e.duration === 0) {
|
||||
layoutScrollWithoutAnimation.set(true)
|
||||
keyboardHeight.set(e.height)
|
||||
} else {
|
||||
keyboardIsOpening.set(true)
|
||||
}
|
||||
},
|
||||
onMove: e => {
|
||||
'worklet'
|
||||
keyboardHeight.set(e.height)
|
||||
} else {
|
||||
keyboardIsOpening.set(true)
|
||||
}
|
||||
if (e.height > bottomOffset) {
|
||||
scrollTo(flatListRef, 0, 1e7, false)
|
||||
}
|
||||
},
|
||||
onEnd: e => {
|
||||
'worklet'
|
||||
keyboardHeight.set(e.height)
|
||||
if (e.height > bottomOffset) {
|
||||
scrollTo(flatListRef, 0, 1e7, false)
|
||||
}
|
||||
keyboardIsOpening.set(false)
|
||||
},
|
||||
},
|
||||
onMove: e => {
|
||||
'worklet'
|
||||
keyboardHeight.set(e.height)
|
||||
if (e.height > bottomOffset) {
|
||||
scrollTo(flatListRef, 0, 1e7, false)
|
||||
}
|
||||
},
|
||||
onEnd: () => {
|
||||
'worklet'
|
||||
keyboardIsOpening.set(false)
|
||||
},
|
||||
})
|
||||
[bottomOffset],
|
||||
)
|
||||
|
||||
const animatedListStyle = useAnimatedStyle(() => ({
|
||||
marginBottom:
|
||||
keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset,
|
||||
marginBottom: Math.max(keyboardHeight.get(), bottomOffset),
|
||||
}))
|
||||
|
||||
const animatedStickyViewStyle = useAnimatedStyle(() => ({
|
||||
transform: [{translateY: -Math.max(keyboardHeight.get(), bottomOffset)}],
|
||||
}))
|
||||
|
||||
// -- Message sending
|
||||
@@ -419,7 +426,7 @@ export function MessagesList({
|
||||
}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
<KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}>
|
||||
<Animated.View style={animatedStickyViewStyle}>
|
||||
{convoState.status === ConvoStatus.Disabled ? (
|
||||
<ChatDisabled />
|
||||
) : blocked ? (
|
||||
@@ -438,7 +445,7 @@ export function MessagesList({
|
||||
</MessageInput>
|
||||
</>
|
||||
)}
|
||||
</KeyboardStickyView>
|
||||
</Animated.View>
|
||||
|
||||
{isWeb && (
|
||||
<EmojiPicker
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -35,7 +36,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const scrollview = React.useRef<ScrollView>(null)
|
||||
const scrollview = React.useRef<Animated.ScrollView>(null)
|
||||
const prevActiveStep = React.useRef<string>(state.activeStep)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -21,8 +21,15 @@ export function AboutSettingsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`About`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>About</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.LinkItem
|
||||
to="https://bsky.social/about/support/tos"
|
||||
|
||||
@@ -39,8 +39,15 @@ export function AccessibilitySettingsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Accessibility`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Accessibility</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Group contentContainerStyle={[a.gap_sm]}>
|
||||
<SettingsList.ItemIcon icon={AccessibilityIcon} />
|
||||
|
||||
@@ -38,8 +38,16 @@ export function AccountSettingsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Account`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Account</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={EnvelopeIcon} />
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import React from 'react'
|
||||
import {Alert, View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {atoms as a, platform} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
|
||||
export function AppIconSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const sets = useAppIconSets()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Content
|
||||
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>App Icon</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
|
||||
<View style={[a.flex_row, a.flex_wrap]}>
|
||||
{sets.defaults.map(icon => (
|
||||
<View
|
||||
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
|
||||
key={icon.id}>
|
||||
<PressableScale
|
||||
accessibilityLabel={icon.name}
|
||||
accessibilityHint={_(msg`Tap to change app icon`)}
|
||||
targetScale={0.95}
|
||||
onPress={() => AppIcon.setAppIcon(icon.id)}>
|
||||
<Image
|
||||
source={platform({
|
||||
ios: icon.iosImage(),
|
||||
android: icon.androidImage(),
|
||||
})}
|
||||
style={[
|
||||
{width: 100, height: 100},
|
||||
platform({
|
||||
ios: {borderRadius: 20},
|
||||
android: a.rounded_full,
|
||||
}),
|
||||
a.curve_continuous,
|
||||
]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</PressableScale>
|
||||
<Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
|
||||
{icon.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
|
||||
<View style={[a.flex_row, a.flex_wrap]}>
|
||||
{sets.core.map(icon => (
|
||||
<View
|
||||
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
|
||||
key={icon.id}>
|
||||
<PressableScale
|
||||
accessibilityLabel={icon.name}
|
||||
accessibilityHint={_(msg`Tap to change app icon`)}
|
||||
targetScale={0.95}
|
||||
onPress={() => {
|
||||
if (isAndroid) {
|
||||
Alert.alert(
|
||||
_(msg`Change app icon to "${icon.name}"`),
|
||||
_(msg`The app will be restarted`),
|
||||
[
|
||||
{
|
||||
text: _(msg`Cancel`),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: _(msg`OK`),
|
||||
onPress: () => {
|
||||
AppIcon.setAppIcon(icon.id)
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
)
|
||||
} else {
|
||||
AppIcon.setAppIcon(icon.id)
|
||||
}
|
||||
}}>
|
||||
<Image
|
||||
source={platform({
|
||||
ios: icon.iosImage(),
|
||||
android: icon.androidImage(),
|
||||
})}
|
||||
style={[
|
||||
{width: 100, height: 100},
|
||||
platform({
|
||||
ios: {borderRadius: 20},
|
||||
android: a.rounded_full,
|
||||
}),
|
||||
a.curve_continuous,
|
||||
a.shadow_lg,
|
||||
]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</PressableScale>
|
||||
<Text
|
||||
style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
|
||||
// for Classic™
|
||||
emoji>
|
||||
{icon.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function useAppIconSets() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return React.useMemo(() => {
|
||||
const defaults = [
|
||||
{
|
||||
id: 'default_light',
|
||||
name: _('Light'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_default_light.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_default_light.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default_dark',
|
||||
name: _('Dark'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_default_dark.png`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Bluesky+
|
||||
*/
|
||||
const core = [
|
||||
{
|
||||
id: 'core_aurora',
|
||||
name: _('Aurora'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 'core_bonfire',
|
||||
// name: _('Bonfire'),
|
||||
// iosImage: () => {
|
||||
// return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
|
||||
// },
|
||||
// androidImage: () => {
|
||||
// return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'core_sunrise',
|
||||
name: _('Sunrise'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_sunset',
|
||||
name: _('Sunset'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_midnight',
|
||||
name: _('Midnight'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_blue',
|
||||
name: _('Flat Blue'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_white',
|
||||
name: _('Flat White'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_black',
|
||||
name: _('Flat Black'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_classic',
|
||||
name: _('Bluesky Classic™'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_classic.png`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
defaults,
|
||||
core,
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function AppIconSettingsScreen() {
|
||||
throw new Error('Not supported on web')
|
||||
}
|
||||
@@ -44,8 +44,15 @@ export function AppPasswordsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="AppPasswordsScreen">
|
||||
<Layout.Header title={_(msg`App Passwords`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>App Passwords</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
{error ? (
|
||||
<ErrorScreen
|
||||
title={_(msg`Oops!`)}
|
||||
|
||||
@@ -5,11 +5,15 @@ import Animated, {
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
@@ -70,11 +74,20 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
[fonts],
|
||||
)
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
<Layout.Screen testID="preferencesThreadsScreen">
|
||||
<Layout.Header title={_(msg`Appearance`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Appearance</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
<SettingsList.Container>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Color mode`)}
|
||||
@@ -121,6 +134,8 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
)}
|
||||
|
||||
<Animated.View layout={native(LinearTransition)}>
|
||||
<SettingsList.Divider />
|
||||
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font`)}
|
||||
description={_(
|
||||
@@ -161,6 +176,21 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
values={[fonts.scale]}
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
|
||||
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
<>
|
||||
<SettingsList.Divider />
|
||||
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/app-icon"
|
||||
label={_(msg`App Icon`)}>
|
||||
<SettingsList.ItemIcon icon={Logo} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>App Icon</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
</>
|
||||
)}
|
||||
</Animated.View>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
@@ -9,16 +9,9 @@ import {
|
||||
useInAppBrowser,
|
||||
useSetInAppBrowser,
|
||||
} from '#/state/preferences/in-app-browser'
|
||||
import {
|
||||
useOptOutOfUtm,
|
||||
useSetOptOutOfUtm,
|
||||
} from '#/state/preferences/opt-out-of-utm'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||
import {ChainLink3_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
|
||||
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
||||
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
||||
@@ -36,13 +29,18 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
const setAutoplayDisabledPref = useSetAutoplayDisabled()
|
||||
const inAppBrowserPref = useInAppBrowser()
|
||||
const setUseInAppBrowser = useSetInAppBrowser()
|
||||
const optOutOfUtm = useOptOutOfUtm()
|
||||
const setOptOutOfUtm = useSetOptOutOfUtm()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Content and Media`)} />
|
||||
<Layout.Content>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Content & Media</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
</Layout.Header.Outer>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/saved-feeds"
|
||||
@@ -77,19 +75,6 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<Toggle.Item
|
||||
name="disable_autoplay"
|
||||
label={_(msg`Autoplay videos and GIFs`)}
|
||||
value={!autoplayDisabledPref}
|
||||
onChange={value => setAutoplayDisabledPref(!value)}>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={PlayIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Autoplay videos and GIFs</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Platform />
|
||||
</SettingsList.Item>
|
||||
</Toggle.Item>
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
name="use_in_app_browser"
|
||||
@@ -105,31 +90,19 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
</SettingsList.Item>
|
||||
</Toggle.Item>
|
||||
)}
|
||||
{isNative && <SettingsList.Divider />}
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
name="allow_utm"
|
||||
label={_(msg`Specify Bluesky as a referer`)}
|
||||
value={!(optOutOfUtm ?? false)}
|
||||
onChange={value => setOptOutOfUtm(!value)}>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={ChainLinkIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Send Bluesky referrer</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Platform />
|
||||
</SettingsList.Item>
|
||||
</Toggle.Item>
|
||||
)}
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
name="disable_autoplay"
|
||||
label={_(msg`Autoplay videos and GIFs`)}
|
||||
value={!autoplayDisabledPref}
|
||||
onChange={value => setAutoplayDisabledPref(!value)}>
|
||||
<SettingsList.Item>
|
||||
<Admonition type="info" style={[a.flex_1]}>
|
||||
<Trans>
|
||||
Helps external sites estimate traffic from Bluesky.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
<SettingsList.ItemIcon icon={PlayIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Autoplay videos and GIFs</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Platform />
|
||||
</SettingsList.Item>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
|
||||