mirror of
https://github.com/tooot-app/app
synced 2025-03-14 02:20:13 +01:00
Merge branch 'v2' into main
This commit is contained in:
commit
55401988c7
@ -5,6 +5,8 @@ export SENTRY_PROJECT=""
|
||||
export SENTRY_AUTH_TOKEN=""
|
||||
export SENTRY_DSN=""
|
||||
|
||||
export TRANSLATE_KEY=""
|
||||
|
||||
# Fastlane start
|
||||
export LC_ALL=""
|
||||
export LANG=""
|
||||
|
39
Gemfile.lock
39
Gemfile.lock
@ -15,7 +15,7 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.445.0)
|
||||
aws-partitions (1.455.0)
|
||||
aws-sdk-core (3.114.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
@ -24,7 +24,7 @@ GEM
|
||||
aws-sdk-kms (1.43.0)
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.93.1)
|
||||
aws-sdk-s3 (1.94.1)
|
||||
aws-sdk-core (~> 3, >= 3.112.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
@ -71,10 +71,9 @@ GEM
|
||||
cocoapods-try (1.2.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.1.8)
|
||||
connection_pool (2.2.5)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.3)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
@ -85,25 +84,23 @@ GEM
|
||||
escape (0.0.4)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
excon (0.80.1)
|
||||
faraday (1.4.0)
|
||||
faraday-excon (~> 1.0)
|
||||
excon (0.81.0)
|
||||
faraday (1.4.1)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-excon (1.0.0)
|
||||
excon (>= 0.27.4)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.0.3)
|
||||
net-http-persistent (>= 3.1)
|
||||
faraday-net_http_persistent (1.1.0)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.3)
|
||||
fastlane (2.180.1)
|
||||
fastlane (2.182.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@ -111,7 +108,7 @@ GEM
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
@ -122,7 +119,7 @@ GEM
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.37.0, < 0.39.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
@ -132,7 +129,6 @@ GEM
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
@ -185,14 +181,14 @@ GEM
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.16.1)
|
||||
googleauth (0.16.2)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
highline (1.7.10)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
@ -200,7 +196,7 @@ GEM
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.4.0)
|
||||
json (2.5.1)
|
||||
jwt (2.2.2)
|
||||
jwt (2.2.3)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.0)
|
||||
@ -211,8 +207,6 @@ GEM
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
os (1.1.1)
|
||||
plist (3.6.0)
|
||||
@ -237,7 +231,6 @@ GEM
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# [tooot](https://tooot.app/) app for Mastodon
|
||||
|
||||
[](LICENSE)    [](https://crowdin.tooot.app/project/tooot)
|
||||
[](LICENSE)    [](https://crowdin.tooot.app/project/tooot)
|
||||
|
||||
  
|
||||
  
|
||||
|
@ -8,8 +8,8 @@ public class BasePackageList {
|
||||
public List<Package> getPackageList() {
|
||||
return Arrays.<Package>asList(
|
||||
new expo.modules.application.ApplicationPackage(),
|
||||
new expo.modules.constants.ConstantsPackage(),
|
||||
new expo.modules.av.AVPackage(),
|
||||
new expo.modules.constants.ConstantsPackage(),
|
||||
new expo.modules.crypto.CryptoPackage(),
|
||||
new expo.modules.device.DevicePackage(),
|
||||
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
|
||||
@ -19,17 +19,15 @@ public class BasePackageList {
|
||||
new expo.modules.font.FontLoaderPackage(),
|
||||
new expo.modules.haptics.HapticsPackage(),
|
||||
new expo.modules.imageloader.ImageLoaderPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.imagemanipulator.ImageManipulatorPackage(),
|
||||
new expo.modules.imagepicker.ImagePickerPackage(),
|
||||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.lineargradient.LinearGradientPackage(),
|
||||
new expo.modules.localization.LocalizationPackage(),
|
||||
new expo.modules.location.LocationPackage(),
|
||||
new expo.modules.notifications.NotificationsPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.screencapture.ScreenCapturePackage(),
|
||||
new expo.modules.securestore.SecureStorePackage(),
|
||||
new expo.modules.splashscreen.SplashScreenPackage(),
|
||||
new expo.modules.sqlite.SQLitePackage(),
|
||||
new expo.modules.storereview.StoreReviewPackage(),
|
||||
new expo.modules.updates.UpdatesPackage(),
|
||||
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
|
||||
|
@ -4,8 +4,8 @@ buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "20.1.5948944"
|
||||
}
|
||||
repositories {
|
||||
|
@ -13,7 +13,8 @@ export default (): ExpoConfig => ({
|
||||
privacy: 'hidden',
|
||||
assetBundlePatterns: ['assets/*'],
|
||||
extra: {
|
||||
sentryDSN: process.env.SENTRY_DSN
|
||||
sentryDSN: process.env.SENTRY_DSN,
|
||||
translateKey: process.env.TRANSLATE_KEY
|
||||
},
|
||||
hooks: {
|
||||
postPublish: [
|
||||
|
@ -11,6 +11,7 @@ module.exports = function (api) {
|
||||
'@assets': './assets',
|
||||
'@root': './src',
|
||||
'@api': './src/api',
|
||||
'@helpers': './src/helpers',
|
||||
'@components': './src/components',
|
||||
'@screens': './src/screens',
|
||||
'@utils': './src/utils'
|
||||
|
@ -109,8 +109,8 @@ private_lane :build_ios do
|
||||
upload_to_app_store( ipa: IPA_FILE, app_version: VERSION )
|
||||
else
|
||||
if !is_ci
|
||||
match( type: "development", readonly: true )
|
||||
build_ios_app( export_method: "development", output_directory: BUILD_DIRECTORY, silent: true )
|
||||
match( type: "adhoc", readonly: true )
|
||||
build_ios_app( export_method: "ad-hoc", output_directory: BUILD_DIRECTORY, silent: true )
|
||||
install_on_device( skip_wifi: true )
|
||||
end
|
||||
end
|
||||
|
794
ios/Podfile.lock
794
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@ -346,7 +346,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2102022230;
|
||||
DEVELOPMENT_TEAM = 8EGBLQ2MA6;
|
||||
@ -366,7 +366,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
|
||||
PRODUCT_NAME = tooot;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match Development com.xmflsct.app.tooot";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
130
package.json
130
package.json
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "tooot",
|
||||
"versions": {
|
||||
"native": "210317",
|
||||
"major": 1,
|
||||
"minor": 1,
|
||||
"native": "210511",
|
||||
"major": 2,
|
||||
"minor": 0,
|
||||
"patch": 0,
|
||||
"expo": "40.0.0"
|
||||
"expo": "41.0.0"
|
||||
},
|
||||
"description": "tooot app for Mastodon",
|
||||
"author": "xmflsct <me@xmflsct.com>",
|
||||
@ -26,43 +26,45 @@
|
||||
"dependencies": {
|
||||
"@expo/react-native-action-sheet": "^3.9.0",
|
||||
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
|
||||
"@react-native-async-storage/async-storage": "^1.14.1",
|
||||
"@react-native-async-storage/async-storage": "^1.15.4",
|
||||
"@react-native-community/blur": "^3.6.0",
|
||||
"@react-native-community/cameraroll": "^4.0.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^6.0.0",
|
||||
"@react-native-community/cameraroll": "^4.0.4",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/segmented-control": "2.2.2",
|
||||
"@react-navigation/bottom-tabs": "^5.11.8",
|
||||
"@react-navigation/native": "^5.9.3",
|
||||
"@react-navigation/stack": "^5.14.3",
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"@sentry/react-native": "^2.3.0",
|
||||
"@sharcoux/slider": "^5.1.3",
|
||||
"@react-navigation/bottom-tabs": "^5.11.11",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@react-navigation/stack": "^5.14.5",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"@sentry/react-native": "^2.4.3",
|
||||
"@sharcoux/slider": "^5.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"expo": "^40.0.1",
|
||||
"expo-auth-session": "~3.1.0",
|
||||
"expo-av": "~9.0.0",
|
||||
"expo-crypto": "~9.0.0",
|
||||
"expo-firebase-analytics": "~3.0.0",
|
||||
"expo-haptics": "~9.0.0",
|
||||
"expo-image-picker": "~10.0.0",
|
||||
"expo-linking": "~2.1.1",
|
||||
"expo-localization": "~10.0.0",
|
||||
"expo-notifications": "~0.9.0",
|
||||
"expo-random": "~11.0.0",
|
||||
"expo-screen-capture": "^3.0.0",
|
||||
"expo-splash-screen": "~0.9.0",
|
||||
"expo-status-bar": "~1.0.3",
|
||||
"expo-store-review": "~3.0.0",
|
||||
"expo-video-thumbnails": "~5.0.0",
|
||||
"expo-web-browser": "~9.0.0",
|
||||
"i18next": "^19.9.2",
|
||||
"expo": "^41.0.1",
|
||||
"expo-auth-session": "~3.2.3",
|
||||
"expo-av": "~9.1.2",
|
||||
"expo-crypto": "~9.1.0",
|
||||
"expo-firebase-analytics": "~4.0.2",
|
||||
"expo-haptics": "~10.0.0",
|
||||
"expo-image-manipulator": "~9.1.0",
|
||||
"expo-image-picker": "~10.1.4",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-localization": "~10.1.0",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-random": "~11.1.2",
|
||||
"expo-screen-capture": "^3.1.0",
|
||||
"expo-secure-store": "~10.1.0",
|
||||
"expo-splash-screen": "~0.10.2",
|
||||
"expo-status-bar": "~1.0.4",
|
||||
"expo-store-review": "~4.0.2",
|
||||
"expo-video-thumbnails": "~5.1.0",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18next": "^20.3.0",
|
||||
"li": "^1.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-i18next": "^11.8.10",
|
||||
"react-native": "~0.64.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-native": "~0.64.1",
|
||||
"react-native-animated-spinkit": "^1.5.2",
|
||||
"react-native-blurhash": "^1.1.4",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
@ -70,56 +72,56 @@
|
||||
"react-native-flash-message": "^0.1.23",
|
||||
"react-native-gesture-handler": "~1.10.3",
|
||||
"react-native-htmlview": "^0.16.0",
|
||||
"react-native-pager-view": "^5.1.2",
|
||||
"react-native-reanimated": "^2.0.0",
|
||||
"react-native-pager-view": "5.1.9",
|
||||
"react-native-reanimated": "~2.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "~2.17.1",
|
||||
"react-native-svg": "12.1.0",
|
||||
"react-native-swipe-list-view": "^3.2.6",
|
||||
"react-native-tab-view": "^3.0.0",
|
||||
"react-native-unimodules": "~0.12.0",
|
||||
"react-query": "^3.12.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-native-screens": "~3.2.0",
|
||||
"react-native-svg": "12.1.1",
|
||||
"react-native-swipe-list-view": "^3.2.7",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-unimodules": "~0.13.3",
|
||||
"react-query": "^3.16.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-timeago": "^5.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rn-placeholder": "^3.0.3",
|
||||
"sentry-expo": "^3.0.5",
|
||||
"tslib": "^2.1.0",
|
||||
"sentry-expo": "^3.1.3",
|
||||
"tslib": "^2.2.0",
|
||||
"valid-url": "^1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.13.10",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
||||
"@babel/core": "~7.14.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.2",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@expo/config": "^3.3.31",
|
||||
"@expo/config": "^3.3.42",
|
||||
"@jest/types": "^26.6.2",
|
||||
"@testing-library/jest-native": "^4.0.1",
|
||||
"@testing-library/react-hooks": "^5.1.0",
|
||||
"@testing-library/react-hooks": "^5.1.2",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/react": "~17.0.3",
|
||||
"@types/react-dom": "~17.0.2",
|
||||
"@types/react-native": "~0.63.52",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"@types/react": "~17.0.6",
|
||||
"@types/react-dom": "~17.0.5",
|
||||
"@types/react-native": "~0.64.5",
|
||||
"@types/react-navigation": "^3.4.0",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/react-timeago": "^4.1.2",
|
||||
"@types/valid-url": "^1.0.3",
|
||||
"@welldone-software/why-did-you-render": "^6.1.1",
|
||||
"@welldone-software/why-did-you-render": "^6.1.4",
|
||||
"babel-jest": "~26.6.3",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"chalk": "^4.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"chalk": "^4.1.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-expo": "^40.0.2",
|
||||
"jest-expo": "^41.0.0",
|
||||
"nock": "^13.0.11",
|
||||
"react-native-clean-project": "^3.6.3",
|
||||
"react-native-clean-project": "^3.6.4",
|
||||
"react-navigation": "^4.4.4",
|
||||
"react-navigation-stack": "^2.10.4",
|
||||
"react-test-renderer": "^17.0.1",
|
||||
"typescript": "~4.2.3",
|
||||
"uri-scheme": "^1.0.68"
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"typescript": "~4.2.4",
|
||||
"uri-scheme": "^1.0.79"
|
||||
}
|
||||
}
|
||||
|
6
src/@types/react-navigation.d.ts
vendored
6
src/@types/react-navigation.d.ts
vendored
@ -1,6 +1,4 @@
|
||||
declare namespace Nav {
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
type RootStackParamList = {
|
||||
'Screen-Tabs': undefined
|
||||
'Screen-Actions':
|
||||
@ -151,8 +149,4 @@ declare namespace Nav {
|
||||
fields?: Mastodon.Source['fields']
|
||||
}
|
||||
}
|
||||
|
||||
type TabMePushStackParamList = {
|
||||
'Tab-Me-Push-Root': undefined
|
||||
}
|
||||
}
|
||||
|
1
src/@types/untyped.d.ts
vendored
1
src/@types/untyped.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
declare module 'gl-react-blurhash'
|
||||
declare module 'htmlparser2-without-node-native'
|
||||
declare module 'li'
|
||||
declare module 'react-native-feather'
|
||||
declare module 'react-native-htmlview'
|
||||
|
10
src/App.tsx
10
src/App.tsx
@ -1,4 +1,5 @@
|
||||
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import Screens from '@root/Screens'
|
||||
import audio from '@root/startup/audio'
|
||||
@ -14,8 +15,7 @@ import * as Notifications from 'expo-notifications'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AppState, LogBox, Platform } from 'react-native'
|
||||
import { enableScreens } from 'react-native-screens'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { QueryClientProvider } from 'react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import push from './startup/push'
|
||||
@ -29,12 +29,6 @@ sentry()
|
||||
audio()
|
||||
push()
|
||||
|
||||
log('log', 'react-query', 'initializing')
|
||||
export const queryClient = new QueryClient()
|
||||
|
||||
log('log', 'react-native-screens', 'initializing')
|
||||
enableScreens()
|
||||
|
||||
const App: React.FC = () => {
|
||||
log('log', 'App', 'rendering App')
|
||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { displayMessage, Message, removeMessage } from '@components/Message'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { useNetInfo } from '@react-native-community/netinfo'
|
||||
import {
|
||||
NavigationContainer,
|
||||
NavigationContainerRef
|
||||
} from '@react-navigation/native'
|
||||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import ScreenActions from '@screens/Actions'
|
||||
import ScreenAnnouncements from '@screens/Announcements'
|
||||
import ScreenCompose from '@screens/Compose'
|
||||
@ -19,7 +18,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { themes } from '@utils/styles/themes'
|
||||
import * as Analytics from 'expo-firebase-analytics'
|
||||
import { addScreenshotListener } from 'expo-screen-capture'
|
||||
import React, { createRef, useCallback, useEffect, useRef } from 'react'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Platform, StatusBar } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
@ -28,7 +27,6 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
|
||||
export const navigationRef = createRef<NavigationContainerRef>()
|
||||
|
||||
export interface Props {
|
||||
localCorrupt?: string
|
||||
@ -174,18 +172,30 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
<Stack.Screen
|
||||
name='Screen-Announcements'
|
||||
component={ScreenAnnouncements}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'transparentModal',
|
||||
stackAnimation: 'fade',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerHideShadow: true,
|
||||
headerTopInsetEnabled: false,
|
||||
headerStyle: { backgroundColor: 'transparent' },
|
||||
headerLeft: () => (
|
||||
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerTitle: t('screenAnnouncements:heading'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('screenAnnouncements:heading')} />
|
||||
)
|
||||
})
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose'
|
||||
component={ScreenCompose}
|
||||
options={{
|
||||
stackPresentation: 'fullScreenModal',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -194,7 +204,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
options={{
|
||||
stackPresentation: 'fullScreenModal',
|
||||
stackAnimation: 'fade',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
|
@ -69,12 +69,16 @@ const apiGeneral = async <T = unknown>({
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject(error.response)
|
||||
return Promise.reject(error.response.data.error)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API general '), ctx.bold('request'), error)
|
||||
console.error(
|
||||
ctx.bold(' API general '),
|
||||
ctx.bold('request'),
|
||||
error.request
|
||||
)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
|
@ -98,7 +98,7 @@ const apiInstance = async <T = unknown>({
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject(error.response)
|
||||
return Promise.reject(error.response.data.error)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
|
@ -4,7 +4,6 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, {
|
||||
createContext,
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
@ -13,44 +12,7 @@ import React, {
|
||||
useReducer
|
||||
} from 'react'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
|
||||
type EmojisState = {
|
||||
enabled: boolean
|
||||
active: boolean
|
||||
emojis: { title: string; data: Mastodon.Emoji[][] }[]
|
||||
shortcode: Mastodon.Emoji['shortcode'] | null
|
||||
}
|
||||
|
||||
type EmojisAction =
|
||||
| {
|
||||
type: 'load'
|
||||
payload: NonNullable<EmojisState['emojis']>
|
||||
}
|
||||
| {
|
||||
type: 'activate'
|
||||
payload: EmojisState['active']
|
||||
}
|
||||
| {
|
||||
type: 'shortcode'
|
||||
payload: EmojisState['shortcode']
|
||||
}
|
||||
|
||||
const emojisReducer = (state: EmojisState, action: EmojisAction) => {
|
||||
switch (action.type) {
|
||||
case 'activate':
|
||||
return { ...state, active: action.payload }
|
||||
case 'load':
|
||||
return { ...state, emojis: action.payload }
|
||||
case 'shortcode':
|
||||
return { ...state, shortcode: action.payload }
|
||||
}
|
||||
}
|
||||
|
||||
type ContextType = {
|
||||
emojisState: EmojisState
|
||||
emojisDispatch: Dispatch<EmojisAction>
|
||||
}
|
||||
const EmojisContext = createContext<ContextType>({} as ContextType)
|
||||
import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext'
|
||||
|
||||
const prefetchEmojis = (
|
||||
sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
|
||||
@ -163,4 +125,4 @@ const ComponentEmojis: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export { ComponentEmojis, EmojisContext, EmojisButton, EmojisList }
|
||||
export { ComponentEmojis, EmojisButton, EmojisList }
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { EmojisContext } from '@components/Emojis'
|
||||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { Pressable, StyleSheet } from 'react-native'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
|
||||
const EmojisButton = React.memo(
|
||||
() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { EmojisContext } from '@components/Emojis'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
@ -16,6 +15,7 @@ import {
|
||||
} from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import validUrl from 'valid-url'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
|
||||
const EmojisList = React.memo(
|
||||
() => {
|
||||
|
41
src/components/Emojis/helpers/EmojisContext.tsx
Normal file
41
src/components/Emojis/helpers/EmojisContext.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { createContext, Dispatch } from 'react'
|
||||
|
||||
export type EmojisState = {
|
||||
enabled: boolean
|
||||
active: boolean
|
||||
emojis: { title: string; data: Mastodon.Emoji[][] }[]
|
||||
shortcode: Mastodon.Emoji['shortcode'] | null
|
||||
}
|
||||
|
||||
export type EmojisAction =
|
||||
| {
|
||||
type: 'load'
|
||||
payload: NonNullable<EmojisState['emojis']>
|
||||
}
|
||||
| {
|
||||
type: 'activate'
|
||||
payload: EmojisState['active']
|
||||
}
|
||||
| {
|
||||
type: 'shortcode'
|
||||
payload: EmojisState['shortcode']
|
||||
}
|
||||
|
||||
type ContextType = {
|
||||
emojisState: EmojisState
|
||||
emojisDispatch: Dispatch<EmojisAction>
|
||||
}
|
||||
const EmojisContext = createContext<ContextType>({} as ContextType)
|
||||
|
||||
export const emojisReducer = (state: EmojisState, action: EmojisAction) => {
|
||||
switch (action.type) {
|
||||
case 'activate':
|
||||
return { ...state, active: action.payload }
|
||||
case 'load':
|
||||
return { ...state, emojis: action.payload }
|
||||
case 'shortcode':
|
||||
return { ...state, shortcode: action.payload }
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojisContext
|
@ -18,12 +18,8 @@ import {
|
||||
View
|
||||
} from 'react-native'
|
||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||
import {
|
||||
ComponentEmojis,
|
||||
EmojisButton,
|
||||
EmojisContext,
|
||||
EmojisList
|
||||
} from './Emojis'
|
||||
import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis'
|
||||
import EmojisContext from './Emojis/helpers/EmojisContext'
|
||||
|
||||
export interface Props {
|
||||
autoFocus?: boolean
|
||||
@ -114,7 +110,8 @@ const Input: React.FC<Props> = ({
|
||||
styles.base,
|
||||
{
|
||||
borderColor: theme.border,
|
||||
flexDirection: multiline ? 'column' : 'row'
|
||||
flexDirection: multiline ? 'column' : 'row',
|
||||
alignItems: 'stretch'
|
||||
}
|
||||
]}
|
||||
>
|
||||
@ -157,7 +154,7 @@ const Input: React.FC<Props> = ({
|
||||
{title}
|
||||
</Animated.Text>
|
||||
) : null}
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
|
||||
{options?.maxLength && value?.length ? (
|
||||
<Text style={[styles.maxLength, { color: theme.secondary }]}>
|
||||
{value?.length} / {options.maxLength}
|
||||
|
@ -76,96 +76,98 @@ const MenuRow: React.FC<Props> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
)}
|
||||
{badge ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: theme.red,
|
||||
borderRadius: 8,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primaryDefault }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{content || switchValue !== undefined || iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content ? (
|
||||
typeof content === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconBack}
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
)}
|
||||
{badge ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: theme.red,
|
||||
borderRadius: 8,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primaryDefault }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{content || switchValue !== undefined || iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content ? (
|
||||
typeof content === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
minHeight: 46,
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
minHeight: 50
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
front: {
|
||||
flex: 2,
|
||||
|
@ -4,7 +4,6 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { adaptiveScale } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -28,7 +27,6 @@ const ParseEmojis = React.memo(
|
||||
adaptiveSize = false,
|
||||
fontBold = false
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('componentParse')
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||
|
@ -162,7 +162,9 @@ export interface Props {
|
||||
showFullLink?: boolean
|
||||
numberOfLines?: number
|
||||
expandHint?: string
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
selectable?: boolean
|
||||
}
|
||||
|
||||
const ParseHTML = React.memo(
|
||||
@ -176,7 +178,9 @@ const ParseHTML = React.memo(
|
||||
showFullLink = false,
|
||||
numberOfLines = 10,
|
||||
expandHint,
|
||||
disableDetails = false
|
||||
highlighted = false,
|
||||
disableDetails = false,
|
||||
selectable = false
|
||||
}: Props) => {
|
||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||
const adaptedFontsize = adaptiveScale(
|
||||
@ -234,7 +238,7 @@ const ParseHTML = React.memo(
|
||||
const { t } = useTranslation('componentParse')
|
||||
|
||||
const [expandAllow, setExpandAllow] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [expanded, setExpanded] = useState(highlighted)
|
||||
|
||||
const onTextLayout = useCallback(({ nativeEvent }) => {
|
||||
if (
|
||||
@ -253,6 +257,7 @@ const ParseHTML = React.memo(
|
||||
numberOfLines={
|
||||
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
|
||||
}
|
||||
selectable={selectable}
|
||||
/>
|
||||
{expandAllow ? (
|
||||
<Pressable
|
||||
|
@ -19,6 +19,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import TimelineActionsUsers from './Shared/ActionsUsers'
|
||||
import TimelineFullConversation from './Shared/FullConversation'
|
||||
import TimelineTranslate from './Shared/Translate'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
|
||||
@ -128,11 +129,13 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
{!disableDetails && actualStatus.card && (
|
||||
<TimelineCard card={actualStatus.card} />
|
||||
)}
|
||||
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
|
||||
{!disableDetails ? (
|
||||
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
|
||||
) : null}
|
||||
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
|
||||
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
|
||||
</View>
|
||||
|
||||
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
|
||||
|
||||
{queryKey && !disableDetails && (
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
|
@ -340,7 +340,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4,
|
||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
|
||||
marginHorizontal: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ const TimelineActionsUsers = React.memo(
|
||||
'shared.actionsUsers.reblogged_by.accessibilityHint'
|
||||
)}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: theme.secondary }]}
|
||||
style={[styles.text, { color: theme.blue }]}
|
||||
onPress={() => {
|
||||
analytics('timeline_shared_actionsusers_press_boosted', {
|
||||
count: status.reblogs_count
|
||||
@ -68,7 +68,7 @@ const TimelineActionsUsers = React.memo(
|
||||
'shared.actionsUsers.favourited_by.accessibilityHint'
|
||||
)}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: theme.secondary }]}
|
||||
style={[styles.text, { color: theme.blue }]}
|
||||
onPress={() => {
|
||||
analytics('timeline_shared_actionsusers_press_boosted', {
|
||||
count: status.favourites_count
|
||||
@ -98,10 +98,9 @@ const styles = StyleSheet.create({
|
||||
base: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
pressable: { margin: StyleConstants.Spacing.M },
|
||||
text: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
padding: StyleConstants.Spacing.S * 1.5,
|
||||
...StyleConstants.FontStyle.M,
|
||||
padding: StyleConstants.Spacing.S,
|
||||
paddingLeft: 0,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ const TimelineContent = React.memo(
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
@ -41,7 +43,9 @@ const TimelineContent = React.memo(
|
||||
tags={status.tags}
|
||||
numberOfLines={1}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@ -54,6 +58,7 @@ const TimelineContent = React.memo(
|
||||
tags={status.tags}
|
||||
numberOfLines={highlighted ? 999 : numberOfLines}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -26,7 +26,7 @@ const TimelineFullConversation = React.memo(
|
||||
style={{
|
||||
...StyleConstants.FontStyle.S,
|
||||
color: theme.blue,
|
||||
marginTop: StyleConstants.Font.Size.S
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('shared.fullConversation')}
|
||||
|
131
src/components/Timeline/Shared/Translate.tsx
Normal file
131
src/components/Timeline/Shared/Translate.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
highlighted: boolean
|
||||
status: Mastodon.Status
|
||||
}
|
||||
|
||||
const TimelineTranslate = React.memo(
|
||||
({ highlighted, status }: Props) => {
|
||||
if (!highlighted) {
|
||||
return null
|
||||
}
|
||||
if (!status.language) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
|
||||
const tootLanguage = status.language.slice(0, 2)
|
||||
|
||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||
|
||||
if (settingsLanguage.includes(tootLanguage)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let text = status.spoiler_text
|
||||
? [status.spoiler_text, status.content]
|
||||
: [status.content]
|
||||
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, '')
|
||||
}
|
||||
}
|
||||
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||
uri: status.uri,
|
||||
source: status.language,
|
||||
target: settingsLanguage,
|
||||
text,
|
||||
options: { enabled }
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
style={[styles.button, { paddingBottom: isSuccess ? 0 : undefined }]}
|
||||
onPress={() => {
|
||||
if (enabled) {
|
||||
if (!isSuccess) {
|
||||
analytics('timeline_shared_translate_retry', {
|
||||
language: status.language
|
||||
})
|
||||
refetch()
|
||||
}
|
||||
} else {
|
||||
analytics('timeline_shared_translate', {
|
||||
language: status.language
|
||||
})
|
||||
setEnabled(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...StyleConstants.FontStyle.M,
|
||||
color:
|
||||
isLoading || isSuccess
|
||||
? theme.secondary
|
||||
: isError
|
||||
? theme.red
|
||||
: theme.blue
|
||||
}}
|
||||
>
|
||||
{isError
|
||||
? t('shared.translate.failed')
|
||||
: isSuccess
|
||||
? t('shared.translate.succeed', {
|
||||
provider: data?.provider,
|
||||
source: data?.sourceLanguage
|
||||
})
|
||||
: t('shared.translate.default')}
|
||||
{__DEV__ ? ` Source: ${status.language}` : undefined}
|
||||
</Text>
|
||||
{isLoading ? (
|
||||
<Circle
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={theme.disabled}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{data
|
||||
? data.text.map((d, i) => (
|
||||
<ParseHTML
|
||||
key={i}
|
||||
content={d}
|
||||
size={'M'}
|
||||
numberOfLines={999}
|
||||
selectable
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
export default TimelineTranslate
|
@ -1,13 +1,14 @@
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { Alert, Linking } from 'react-native'
|
||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||
import i18next from 'i18next'
|
||||
import analytics from '@components/analytics'
|
||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||
import * as ImageManipulator from 'expo-image-manipulator'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
||||
import i18next from 'i18next'
|
||||
import { Alert, Linking } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
mediaTypes?: ImagePicker.MediaTypeOptions
|
||||
uploader: (imageInfo: ImageInfo) => void
|
||||
resize?: { width?: number; height?: number } // Resize mode contain
|
||||
showActionSheetWithOptions: (
|
||||
options: ActionSheetOptions,
|
||||
callback: (i: number) => void
|
||||
@ -16,118 +17,134 @@ export interface Props {
|
||||
|
||||
const mediaSelector = async ({
|
||||
mediaTypes = ImagePicker.MediaTypeOptions.All,
|
||||
uploader,
|
||||
resize,
|
||||
showActionSheetWithOptions
|
||||
}: Props): Promise<any> => {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
title: i18next.t('componentMediaSelector:title'),
|
||||
options: [
|
||||
i18next.t('componentMediaSelector:options.library'),
|
||||
i18next.t('componentMediaSelector:options.photo'),
|
||||
i18next.t('componentMediaSelector:options.cancel')
|
||||
],
|
||||
cancelButtonIndex: 2
|
||||
},
|
||||
async buttonIndex => {
|
||||
if (buttonIndex === 0) {
|
||||
const {
|
||||
status
|
||||
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
i18next.t('componentMediaSelector:library.alert.title'),
|
||||
i18next.t('componentMediaSelector:library.alert.message'),
|
||||
[
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:library.alert.buttons.cancel'
|
||||
),
|
||||
style: 'cancel',
|
||||
onPress: () =>
|
||||
analytics('mediaSelector_nopermission', { action: 'cancel' })
|
||||
},
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:library.alert.buttons.settings'
|
||||
),
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
analytics('mediaSelector_nopermission', {
|
||||
action: 'settings'
|
||||
})
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes,
|
||||
exif: false
|
||||
})
|
||||
|
||||
if (!result.cancelled) {
|
||||
// https://github.com/expo/expo/issues/11214
|
||||
const fixResult = {
|
||||
...result,
|
||||
uri: result.uri.replace('file:/data', 'file:///data')
|
||||
}
|
||||
uploader(fixResult)
|
||||
return
|
||||
}: Props): Promise<ImageInfo> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resolveResult = async (result: ImageInfo) => {
|
||||
if (resize && result.type === 'image') {
|
||||
let newResult: ImageManipulator.ImageResult
|
||||
if (resize.width && resize.height) {
|
||||
if (resize.width / resize.height > result.width / result.height) {
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize: { width: resize.width } }
|
||||
])
|
||||
} else {
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize: { height: resize.height } }
|
||||
])
|
||||
}
|
||||
}
|
||||
} else if (buttonIndex === 1) {
|
||||
const { status } = await ImagePicker.requestCameraPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
i18next.t('componentMediaSelector:photo.alert.title'),
|
||||
i18next.t('componentMediaSelector:photo.alert.message'),
|
||||
[
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:photo.alert.buttons.cancel'
|
||||
),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
analytics('compose_addattachment_camera_nopermission', {
|
||||
action: 'cancel'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:photo.alert.buttons.settings'
|
||||
),
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
analytics('compose_addattachment_camera_nopermission', {
|
||||
action: 'settings'
|
||||
})
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
const result = await ImagePicker.launchCameraAsync({
|
||||
mediaTypes,
|
||||
exif: false
|
||||
})
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize }
|
||||
])
|
||||
}
|
||||
resolve(newResult)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.cancelled) {
|
||||
// https://github.com/expo/expo/issues/11214
|
||||
const fixResult = {
|
||||
...result,
|
||||
uri: result.uri.replace('file:/data', 'file:///data')
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
title: i18next.t('componentMediaSelector:title'),
|
||||
options: [
|
||||
i18next.t('componentMediaSelector:options.library'),
|
||||
i18next.t('componentMediaSelector:options.photo'),
|
||||
i18next.t('componentMediaSelector:options.cancel')
|
||||
],
|
||||
cancelButtonIndex: 2
|
||||
},
|
||||
async buttonIndex => {
|
||||
if (buttonIndex === 0) {
|
||||
const {
|
||||
status
|
||||
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
i18next.t('componentMediaSelector:library.alert.title'),
|
||||
i18next.t('componentMediaSelector:library.alert.message'),
|
||||
[
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:library.alert.buttons.cancel'
|
||||
),
|
||||
style: 'cancel',
|
||||
onPress: () =>
|
||||
analytics('mediaSelector_nopermission', {
|
||||
action: 'cancel'
|
||||
})
|
||||
},
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:library.alert.buttons.settings'
|
||||
),
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
analytics('mediaSelector_nopermission', {
|
||||
action: 'settings'
|
||||
})
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes,
|
||||
exif: false
|
||||
})
|
||||
|
||||
if (!result.cancelled) {
|
||||
await resolveResult(result)
|
||||
}
|
||||
}
|
||||
} else if (buttonIndex === 1) {
|
||||
const { status } = await ImagePicker.requestCameraPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
i18next.t('componentMediaSelector:photo.alert.title'),
|
||||
i18next.t('componentMediaSelector:photo.alert.message'),
|
||||
[
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:photo.alert.buttons.cancel'
|
||||
),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
analytics('compose_addattachment_camera_nopermission', {
|
||||
action: 'cancel'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:photo.alert.buttons.settings'
|
||||
),
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
analytics('compose_addattachment_camera_nopermission', {
|
||||
action: 'settings'
|
||||
})
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
const result = await ImagePicker.launchCameraAsync({
|
||||
mediaTypes,
|
||||
exif: false
|
||||
})
|
||||
|
||||
if (!result.cancelled) {
|
||||
await resolveResult(result)
|
||||
}
|
||||
uploader(fixResult)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default mediaSelector
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { NavigationProp, ParamListBase } from '@react-navigation/native'
|
||||
import { navigationRef } from '@root/Screens'
|
||||
import { store } from '@root/store'
|
||||
import { SearchResult } from '@utils/queryHooks/search'
|
||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||
|
6
src/helpers/navigationRef.ts
Normal file
6
src/helpers/navigationRef.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { NavigationContainerRef } from '@react-navigation/native'
|
||||
import { createRef } from 'react'
|
||||
|
||||
const navigationRef = createRef<NavigationContainerRef>()
|
||||
|
||||
export default navigationRef
|
5
src/helpers/queryClient.ts
Normal file
5
src/helpers/queryClient.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { QueryClient } from 'react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export default queryClient
|
@ -74,6 +74,11 @@
|
||||
"expandHint": "hidden content"
|
||||
},
|
||||
"fullConversation": "Read conversations",
|
||||
"translate": {
|
||||
"default": "Translate",
|
||||
"succeed": "Translated by {{provider}} from {{source}}",
|
||||
"failed": "Translation failed"
|
||||
},
|
||||
"header": {
|
||||
"shared": {
|
||||
"account": {
|
||||
|
@ -102,11 +102,11 @@
|
||||
},
|
||||
"avatar": {
|
||||
"title": "Avatar",
|
||||
"description": "Available in next version"
|
||||
"description": "Will be downscaled to 400x400px"
|
||||
},
|
||||
"banner": {
|
||||
"header": {
|
||||
"title": "Banner",
|
||||
"description": "Available in next version"
|
||||
"description": "Will be downscaled to 1500x500px"
|
||||
},
|
||||
"note": {
|
||||
"title": "Description"
|
||||
|
@ -1,7 +1,6 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import RelativeTime from '@components/RelativeTime'
|
||||
import { BlurView } from '@react-native-community/blur'
|
||||
@ -88,6 +87,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
|
||||
emojis={item.emojis}
|
||||
mentions={item.mentions}
|
||||
numberOfLines={999}
|
||||
selectable
|
||||
/>
|
||||
</ScrollView>
|
||||
{item.reactions?.length ? (
|
||||
@ -210,28 +210,6 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
|
||||
reducedTransparencyFallbackColor={theme.backgroundDefault}
|
||||
>
|
||||
<SafeAreaView style={styles.base}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexBasis: 44
|
||||
}}
|
||||
>
|
||||
<HeaderLeft
|
||||
content='X'
|
||||
native={false}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
<HeaderCenter content={t('screenAnnouncements:heading')} />
|
||||
<View style={{ opacity: 0 }} accessible={false}>
|
||||
<HeaderRight
|
||||
content='MoreHorizontal'
|
||||
native={false}
|
||||
onPress={() => {}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={query.data}
|
||||
|
@ -88,6 +88,14 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
return {
|
||||
...composeInitialState,
|
||||
timestamp: Date.now(),
|
||||
attachments: {
|
||||
...composeInitialState.attachments,
|
||||
sensitive:
|
||||
localAccount?.preferences &&
|
||||
localAccount?.preferences['posting:default:sensitive']
|
||||
? localAccount?.preferences['posting:default:sensitive']
|
||||
: false
|
||||
},
|
||||
visibility:
|
||||
localAccount?.preferences &&
|
||||
localAccount.preferences['posting:default:visibility']
|
||||
@ -397,12 +405,18 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-DraftsList'
|
||||
component={ComposeDraftsList}
|
||||
options={{ stackPresentation: 'modal', headerShown: false }}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment'
|
||||
component={ComposeEditAttachment}
|
||||
options={{ stackPresentation: 'modal', headerShown: false }}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</ComposeContext.Provider>
|
||||
|
@ -3,7 +3,7 @@ import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
@ -28,23 +28,26 @@ import ComposeContext from './utils/createContext'
|
||||
import ComposeDrafts from './Root/Drafts'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { ComposeState } from './utils/types'
|
||||
|
||||
const prefetchEmojis = (
|
||||
sortedEmojis: { title: string; data: Mastodon.Emoji[] }[],
|
||||
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
|
||||
reduceMotionEnabled: boolean
|
||||
) => {
|
||||
const prefetches: { uri: string }[] = []
|
||||
let requestedIndex = 0
|
||||
sortedEmojis.forEach(sorted => {
|
||||
sorted.data.forEach(emoji => {
|
||||
if (requestedIndex > 40) {
|
||||
return
|
||||
}
|
||||
prefetches.push({
|
||||
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
sorted.data.forEach(emojis =>
|
||||
emojis.forEach(emoji => {
|
||||
if (requestedIndex > 40) {
|
||||
return
|
||||
}
|
||||
prefetches.push({
|
||||
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
})
|
||||
requestedIndex++
|
||||
})
|
||||
requestedIndex++
|
||||
})
|
||||
)
|
||||
})
|
||||
try {
|
||||
FastImage.preload(prefetches)
|
||||
@ -90,10 +93,11 @@ const ComposeRoot = React.memo(
|
||||
const { data: emojisData } = useEmojisQuery({})
|
||||
useEffect(() => {
|
||||
if (emojisData && emojisData.length) {
|
||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
|
||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
|
||||
forEach(
|
||||
groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
|
||||
(value, key) => sortedEmojis.push({ title: key, data: value })
|
||||
(value, key) =>
|
||||
sortedEmojis.push({ title: key, data: chunk(value, 5) })
|
||||
)
|
||||
composeDispatch({
|
||||
type: 'emoji',
|
||||
|
@ -1,15 +1,8 @@
|
||||
import analytics from '@components/analytics'
|
||||
import haptics from '@components/haptics'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import React, { RefObject, useCallback, useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
@ -25,52 +18,15 @@ import validUrl from 'valid-url'
|
||||
import updateText from '../../updateText'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
|
||||
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||
const { t } = useTranslation()
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const onPress = useCallback(() => {
|
||||
analytics('compose_emoji_add')
|
||||
updateText({
|
||||
composeState,
|
||||
composeDispatch,
|
||||
newText: `:${emoji.shortcode}:`,
|
||||
type: 'emoji'
|
||||
})
|
||||
haptics('Light')
|
||||
}, [composeState])
|
||||
const children = useMemo(() => {
|
||||
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
if (validUrl.isHttpsUri(uri)) {
|
||||
return (
|
||||
<FastImage
|
||||
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
|
||||
emoji: emoji.shortcode
|
||||
})}
|
||||
accessibilityHint={t(
|
||||
'screenCompose:content.root.footer.emojis.accessibilityHint'
|
||||
)}
|
||||
source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<Pressable key={emoji.shortcode} onPress={onPress} children={children} />
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
accessibleRefEmojis: RefObject<SectionList>
|
||||
}
|
||||
|
||||
const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
|
||||
@ -86,21 +42,49 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
[]
|
||||
)
|
||||
|
||||
const emojiList = useCallback(
|
||||
section =>
|
||||
section.data.map((emoji: Mastodon.Emoji) => (
|
||||
<SingleEmoji key={emoji.shortcode} emoji={emoji} />
|
||||
)),
|
||||
[]
|
||||
)
|
||||
const listItem = useCallback(
|
||||
({ section, index }) =>
|
||||
index === 0 ? (
|
||||
<View key={section.title} style={styles.emojis}>
|
||||
{emojiList(section)}
|
||||
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
||||
return (
|
||||
<View key={index} style={styles.emojis}>
|
||||
{item.map(emoji => {
|
||||
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
if (validUrl.isHttpsUri(uri)) {
|
||||
return (
|
||||
<Pressable
|
||||
key={emoji.shortcode}
|
||||
onPress={() => {
|
||||
updateText({
|
||||
composeState,
|
||||
composeDispatch,
|
||||
newText: `:${emoji.shortcode}:`,
|
||||
type: 'emoji'
|
||||
})
|
||||
haptics('Light')
|
||||
}}
|
||||
>
|
||||
<FastImage
|
||||
accessibilityLabel={t(
|
||||
'common:customEmoji.accessibilityLabel',
|
||||
{
|
||||
emoji: emoji.shortcode
|
||||
}
|
||||
)}
|
||||
accessibilityHint={t(
|
||||
'screenCompose:content.root.footer.emojis.accessibilityHint'
|
||||
)}
|
||||
source={{ uri }}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
) : null,
|
||||
[]
|
||||
)
|
||||
},
|
||||
[composeState]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -111,7 +95,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
horizontal
|
||||
keyboardShouldPersistTaps='always'
|
||||
sections={composeState.emoji.emojis || []}
|
||||
keyExtractor={item => item.shortcode}
|
||||
keyExtractor={item => item[0].shortcode}
|
||||
renderSectionHeader={listHeader}
|
||||
renderItem={listItem}
|
||||
windowSize={2}
|
||||
|
@ -123,7 +123,8 @@ const addAttachment = async ({
|
||||
})
|
||||
}
|
||||
|
||||
mediaSelector({ uploader, showActionSheetWithOptions })
|
||||
const result = await mediaSelector({ showActionSheetWithOptions })
|
||||
await uploader(result)
|
||||
}
|
||||
|
||||
export default addAttachment
|
||||
|
@ -31,7 +31,10 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
||||
multiple: false,
|
||||
expire: '86400'
|
||||
},
|
||||
attachments: { sensitive: false, uploads: [] },
|
||||
attachments: {
|
||||
sensitive: false,
|
||||
uploads: []
|
||||
},
|
||||
visibility: 'public',
|
||||
visibilityLock: false,
|
||||
replyToStatus: undefined,
|
||||
|
2
src/screens/Compose/utils/types.d.ts
vendored
2
src/screens/Compose/utils/types.d.ts
vendored
@ -40,7 +40,7 @@ export type ComposeState = {
|
||||
}
|
||||
emoji: {
|
||||
active: boolean
|
||||
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
|
||||
emojis: { title: string; data: Mastodon.Emoji[][] }[] | undefined
|
||||
}
|
||||
poll: {
|
||||
active: boolean
|
||||
|
@ -109,16 +109,28 @@ const TabMe = React.memo(
|
||||
component={TabMeProfile}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Push'
|
||||
component={TabMePush}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerTitle: t('me.stacks.push.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.push.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Settings'
|
||||
@ -149,10 +161,22 @@ const TabMe = React.memo(
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Switch'
|
||||
component={TabMeSwitch}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerTitle: t('me.stacks.switch.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.switch.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
|
||||
{sharedScreens(Stack as any)}
|
||||
|
@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Profile-Root'
|
||||
component={TabMeProfileRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.profile.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
@ -45,7 +44,15 @@ const TabMeProfile: React.FC<StackScreenProps<
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{({ route, navigation }) => (
|
||||
<TabMeProfileRoot
|
||||
messageRef={messageRef}
|
||||
route={route}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Profile-Name'
|
||||
options={{
|
||||
|
@ -95,12 +95,13 @@ const TabMeProfileFields: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.note.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -77,12 +77,13 @@ const TabMeProfileName: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.name.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -77,12 +77,13 @@ const TabMeProfileNote: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.note.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -1,21 +1,30 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import React, { useCallback } from 'react'
|
||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import ProfileAvatarHeader from './Root/AvatarHeader'
|
||||
|
||||
const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
Nav.TabMeProfileStackParamList,
|
||||
'Tab-Me-Profile-Root'
|
||||
>> = ({ navigation }) => {
|
||||
> & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const { data, isLoading } = useProfileQuery({})
|
||||
const { mutate } = useProfileMutation()
|
||||
const { mutateAsync } = useProfileMutation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const onPressVisibility = useCallback(() => {
|
||||
showActionSheetWithOptions(
|
||||
@ -32,40 +41,185 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
mutate({ type: 'source[privacy]', data: 'public' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'public'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'public' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
case 1:
|
||||
mutate({ type: 'source[privacy]', data: 'unlisted' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'unlisted'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'unlisted' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
case 2:
|
||||
mutate({ type: 'source[privacy]', data: 'private' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'unlisted'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'private' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
}, [data?.source.privacy])
|
||||
|
||||
const onPressSensitive = useCallback(() => {
|
||||
if (data?.source.sensitive === undefined) {
|
||||
mutate({ type: 'source[sensitive]', data: true })
|
||||
analytics('me_profile_sensitive', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'source[sensitive]', data: true })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.sensitive.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'source[sensitive]', data: !data.source.sensitive })
|
||||
analytics('me_profile_sensitive', {
|
||||
current: data.source.sensitive,
|
||||
new: !data.source.sensitive
|
||||
})
|
||||
mutateAsync({
|
||||
type: 'source[sensitive]',
|
||||
data: !data.source.sensitive
|
||||
})
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.sensitive.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.source.sensitive])
|
||||
|
||||
const onPressLock = useCallback(() => {
|
||||
if (data?.locked === undefined) {
|
||||
mutate({ type: 'locked', data: true })
|
||||
analytics('me_profile_lock', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'locked', data: true }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.lock.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'locked', data: !data.locked })
|
||||
analytics('me_profile_lock', {
|
||||
current: data.locked,
|
||||
new: !data.locked
|
||||
})
|
||||
mutateAsync({ type: 'locked', data: !data.locked }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.lock.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.locked])
|
||||
|
||||
const onPressBot = useCallback(() => {
|
||||
if (data?.bot === undefined) {
|
||||
mutate({ type: 'bot', data: true })
|
||||
analytics('me_profile_bot', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'bot', data: true }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.bot.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'bot', data: !data?.bot })
|
||||
analytics('me_profile_bot', {
|
||||
current: data.bot,
|
||||
new: !data.bot
|
||||
})
|
||||
mutateAsync({ type: 'bot', data: !data?.bot }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.bot.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.bot])
|
||||
|
||||
@ -84,43 +238,18 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.profile.root.avatar.title')}
|
||||
description={t('me.profile.root.avatar.description')}
|
||||
// content={
|
||||
// <GracefullyImage
|
||||
// style={{ flex: 1 }}
|
||||
// uri={{
|
||||
// original: data?.avatar_static
|
||||
// }}
|
||||
// />
|
||||
// }
|
||||
// loading={isLoading}
|
||||
// iconBack='ChevronRight'
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.profile.root.banner.title')}
|
||||
description={t('me.profile.root.banner.description')}
|
||||
// content={
|
||||
// <GracefullyImage
|
||||
// style={{ flex: 1 }}
|
||||
// uri={{
|
||||
// original: data?.header_static
|
||||
// }}
|
||||
// />
|
||||
// }
|
||||
// loading={isLoading}
|
||||
// iconBack='ChevronRight'
|
||||
/>
|
||||
<ProfileAvatarHeader type='avatar' messageRef={messageRef} />
|
||||
<ProfileAvatarHeader type='header' messageRef={messageRef} />
|
||||
<MenuRow
|
||||
title={t('me.profile.root.note.title')}
|
||||
content={data?.source.note}
|
||||
loading={isLoading}
|
||||
iconBack='ChevronRight'
|
||||
onPress={() => {
|
||||
navigation.navigate('Tab-Me-Profile-Note', {
|
||||
note: data?.source?.note || ''
|
||||
})
|
||||
data &&
|
||||
navigation.navigate('Tab-Me-Profile-Note', {
|
||||
note: data.source?.note
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<MenuRow
|
||||
|
66
src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx
Normal file
66
src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import mediaSelector from '@components/mediaSelector'
|
||||
import { MenuRow } from '@components/Menu'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import React, { RefObject } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
||||
export interface Props {
|
||||
type: 'avatar' | 'header'
|
||||
messageRef: RefObject<FlashMessage>
|
||||
}
|
||||
|
||||
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const query = useProfileQuery({})
|
||||
const mutation = useProfileMutation()
|
||||
|
||||
return (
|
||||
<MenuRow
|
||||
title={t(`me.profile.root.${type}.title`)}
|
||||
description={t(`me.profile.root.${type}.description`)}
|
||||
loading={query.isLoading || mutation.isLoading}
|
||||
iconBack='ChevronRight'
|
||||
onPress={async () => {
|
||||
const image = await mediaSelector({
|
||||
showActionSheetWithOptions,
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
resize: { width: 400, height: 400 }
|
||||
})
|
||||
mutation
|
||||
.mutateAsync({ type, data: image.uri })
|
||||
.then(() =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.succeed', {
|
||||
type: t(`me.profile.root.${type}.title`)
|
||||
}),
|
||||
mode,
|
||||
type: 'success'
|
||||
})
|
||||
)
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t(`me.profile.root.${type}.title`)
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileAvatarHeader
|
@ -1,42 +1,173 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React from 'react'
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import {
|
||||
clearPushLoading,
|
||||
getInstanceAccount,
|
||||
getInstancePush,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import TabMePushRoot from './Push/Root'
|
||||
import { AppState, Linking, ScrollView } from 'react-native'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.TabMePushStackParamList>()
|
||||
|
||||
const TabMePush: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Push'
|
||||
>> = ({ navigation }) => {
|
||||
const TabMePush: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.acct === next?.acct
|
||||
)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||
const checkPush = async () => {
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkPush()
|
||||
AppState.addEventListener('change', checkPush)
|
||||
return () => {
|
||||
AppState.removeEventListener('change', checkPush)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPushLoading())
|
||||
}, [])
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
|
||||
'follow',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll'
|
||||
]).map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`me.push.${alert}.heading`)}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchOnValueChange={() => {
|
||||
analytics(`me_push_${alert}`, {
|
||||
current: instancePush?.alerts[alert].value,
|
||||
new: !instancePush?.alerts[alert].value
|
||||
})
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Push-Root'
|
||||
component={TabMePushRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.push.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.push.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
<ScrollView>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain
|
||||
? t('me.push.enable.direct')
|
||||
: t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
analytics('me_push_enabled_dialogue')
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
analytics('me_push_enabled_setting')
|
||||
Linking.openSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={!pushEnabled || isLoading}
|
||||
switchValue={
|
||||
pushEnabled === false ? false : instancePush?.global.value
|
||||
}
|
||||
switchOnValueChange={() => {
|
||||
analytics('me_push_global', {
|
||||
current: instancePush?.global.value,
|
||||
new: !instancePush?.global.value
|
||||
})
|
||||
dispatch(updateInstancePush(!instancePush?.global.value))
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush?.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() => {
|
||||
analytics('me_push_decode', {
|
||||
current: instancePush?.decode.value,
|
||||
new: !instancePush?.decode.value
|
||||
})
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={() => {
|
||||
analytics('me_push_howitworks')
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,163 +0,0 @@
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import {
|
||||
clearPushLoading,
|
||||
getInstanceAccount,
|
||||
getInstancePush,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import Button from '@components/Button'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { AppState, Linking } from 'react-native'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
|
||||
const TabMePushRoot: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Push'
|
||||
>> = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.acct === next?.acct
|
||||
)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||
const checkPush = async () => {
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkPush()
|
||||
AppState.addEventListener('change', checkPush)
|
||||
return () => {
|
||||
AppState.removeEventListener('change', checkPush)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPushLoading())
|
||||
}, [])
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
|
||||
'follow',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll'
|
||||
]).map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`me.push.${alert}.heading`)}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain
|
||||
? t('me.push.enable.direct')
|
||||
: t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
Linking.openSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={!pushEnabled || isLoading}
|
||||
switchValue={
|
||||
pushEnabled === false ? false : instancePush?.global.value
|
||||
}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePush(!instancePush?.global.value))
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush?.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={() =>
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabMePushRoot
|
@ -2,65 +2,25 @@ import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Collections: React.FC = () => {
|
||||
const { t, i18n } = useTranslation('screenTabs')
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const navigation = useNavigation()
|
||||
|
||||
const listsQuery = useListsQuery({
|
||||
options: {
|
||||
notifyOnChangeProps: []
|
||||
notifyOnChangeProps: ['data']
|
||||
}
|
||||
})
|
||||
const rowLists = useMemo(() => {
|
||||
if (listsQuery.isSuccess && listsQuery.data?.length) {
|
||||
return (
|
||||
<MenuRow
|
||||
iconFront='List'
|
||||
iconBack='ChevronRight'
|
||||
title={t('me.stacks.lists.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Lists')}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
|
||||
|
||||
const announcementsQuery = useAnnouncementQuery({
|
||||
showAll: true,
|
||||
options: {
|
||||
notifyOnChangeProps: []
|
||||
notifyOnChangeProps: ['data']
|
||||
}
|
||||
})
|
||||
const rowAnnouncements = useMemo(() => {
|
||||
if (announcementsQuery.isSuccess && announcementsQuery.data?.length) {
|
||||
const amount = announcementsQuery.data.filter(
|
||||
announcement => !announcement.read
|
||||
).length
|
||||
return (
|
||||
<MenuRow
|
||||
iconFront='Clipboard'
|
||||
iconBack='ChevronRight'
|
||||
title={t('screenAnnouncements:heading')}
|
||||
content={
|
||||
amount
|
||||
? t('me.root.announcements.content.unread', {
|
||||
amount
|
||||
})
|
||||
: t('me.root.announcements.content.read')
|
||||
}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Announcements', { showAll: true })
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
@ -82,8 +42,34 @@ const Collections: React.FC = () => {
|
||||
title={t('me.stacks.favourites.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Favourites')}
|
||||
/>
|
||||
{rowLists}
|
||||
{rowAnnouncements}
|
||||
{listsQuery.data?.length ? (
|
||||
<MenuRow
|
||||
iconFront='List'
|
||||
iconBack='ChevronRight'
|
||||
title={t('me.stacks.lists.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Lists')}
|
||||
/>
|
||||
) : null}
|
||||
{announcementsQuery.data?.length ? (
|
||||
<MenuRow
|
||||
iconFront='Clipboard'
|
||||
iconBack='ChevronRight'
|
||||
title={t('screenAnnouncements:heading')}
|
||||
content={
|
||||
announcementsQuery.data.filter(announcement => !announcement.read)
|
||||
.length
|
||||
? t('me.root.announcements.content.unread', {
|
||||
amount: announcementsQuery.data.filter(
|
||||
announcement => !announcement.read
|
||||
).length
|
||||
})
|
||||
: t('me.root.announcements.content.read')
|
||||
}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Announcements', { showAll: true })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
@ -1,47 +1,139 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React from 'react'
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import {
|
||||
getInstanceActive,
|
||||
getInstances,
|
||||
Instance,
|
||||
updateInstanceActive
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { KeyboardAvoidingView, Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import TabMeSwitchRoot from './Switch/Root'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
interface Props {
|
||||
instance: Instance
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const navigation = useNavigation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const TabMeSwitch: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Switch'
|
||||
>> = ({ navigation }) => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Screen-Me-Switch-Root'
|
||||
component={TabMeSwitchRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.switch.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.switch.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</KeyboardAvoidingView>
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={styles.button}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
dispatch(updateInstanceActive(instance))
|
||||
queryClient.clear()
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeSwitch: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { theme } = useTheme()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const instanceActive = useSelector(getInstanceActive, () => true)
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.base}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.existing')}
|
||||
</Text>
|
||||
<View style={styles.accountButtons}>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
const localAccount = instances[instanceActive!]
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
selected={
|
||||
instance.url === localAccount.url &&
|
||||
instance.token === localAccount.token &&
|
||||
instance.account.id === localAccount.account.id
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.secondSection}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.new')}
|
||||
</Text>
|
||||
<ComponentInstance
|
||||
scrollViewRef={scrollViewRef}
|
||||
disableHeaderImage
|
||||
goBack
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
},
|
||||
header: {
|
||||
...StyleConstants.FontStyle.M,
|
||||
textAlign: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
firstSection: {
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
secondSection: {
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
},
|
||||
accountButtons: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
button: {
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeSwitch
|
||||
|
@ -1,139 +0,0 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import {
|
||||
getInstanceActive,
|
||||
getInstances,
|
||||
Instance,
|
||||
updateInstanceActive
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
interface Props {
|
||||
instance: Instance
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const navigation = useNavigation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={styles.button}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
dispatch(updateInstanceActive(instance))
|
||||
queryClient.clear()
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeSwitchRoot: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { theme } = useTheme()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const instanceActive = useSelector(getInstanceActive, () => true)
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.base}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.existing')}
|
||||
</Text>
|
||||
<View style={styles.accountButtons}>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
const localAccount = instances[instanceActive!]
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
selected={
|
||||
instance.url === localAccount.url &&
|
||||
instance.token === localAccount.token &&
|
||||
instance.account.id === localAccount.account.id
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.secondSection}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.new')}
|
||||
</Text>
|
||||
<ComponentInstance
|
||||
scrollViewRef={scrollViewRef}
|
||||
disableHeaderImage
|
||||
goBack
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
},
|
||||
header: {
|
||||
...StyleConstants.FontStyle.M,
|
||||
textAlign: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
firstSection: {
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
secondSection: {
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
},
|
||||
accountButtons: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
button: {
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeSwitchRoot
|
@ -34,6 +34,7 @@ const AccountInformationFields = React.memo(
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
{field.verified_at ? (
|
||||
<Icon
|
||||
@ -51,6 +52,7 @@ const AccountInformationFields = React.memo(
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -58,7 +60,7 @@ const AccountInformationFields = React.memo(
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -28,11 +28,11 @@ const AccountInformationNote = React.memo(
|
||||
|
||||
return (
|
||||
<View style={styles.note}>
|
||||
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
|
||||
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -14,11 +14,11 @@ import { debounce } from 'lodash'
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript/native-stack'
|
||||
import {
|
||||
NativeStackNavigationEventMap,
|
||||
NativeStackNavigatorProps
|
||||
} from 'react-native-screens/lib/typescript/types'
|
||||
} from 'react-native-screens/lib/typescript/native-stack/types'
|
||||
|
||||
export type BaseScreens =
|
||||
| Nav.TabLocalStackParamList
|
||||
@ -150,17 +150,13 @@ const sharedScreens = (
|
||||
<View style={styles.searchBar}>
|
||||
<TextInput
|
||||
editable={false}
|
||||
children={
|
||||
<Text
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primaryDefault
|
||||
}
|
||||
]}
|
||||
children={t('shared.search.header.prefix')}
|
||||
/>
|
||||
}
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primaryDefault
|
||||
}
|
||||
]}
|
||||
defaultValue={t('shared.search.header.prefix')}
|
||||
/>
|
||||
<TextInput
|
||||
accessibilityRole='search'
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Constants from 'expo-constants'
|
||||
import * as Updates from 'expo-updates'
|
||||
import { Constants } from 'react-native-unimodules'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
import log from './log'
|
||||
|
||||
const sentry = () => {
|
||||
log('log', 'Sentry', 'initializing')
|
||||
Sentry.init({
|
||||
dsn: Constants.manifest.extra.sentryDSN,
|
||||
dsn: Constants.manifest?.extra?.sentryDSN,
|
||||
enableInExpoDevelopment: false,
|
||||
debug:
|
||||
__DEV__ ||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
|
||||
import { QueryKeyAccount } from './account'
|
||||
|
||||
type AccountWithSource = Mastodon.Account &
|
||||
Required<Pick<Mastodon.Account, 'source'>>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import haptics from '@components/haptics'
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { store } from '@root/store'
|
||||
import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
|
||||
import { AxiosError } from 'axios'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { InfiniteData } from 'react-query'
|
||||
import { MutationVarsTimelineDeleteItem } from '../timeline'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { findIndex } from 'lodash'
|
||||
import { InfiniteData } from 'react-query'
|
||||
import {
|
||||
|
63
src/utils/queryHooks/translate.ts
Normal file
63
src/utils/queryHooks/translate.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import { AxiosError } from 'axios'
|
||||
import { Buffer } from 'buffer'
|
||||
import Constants from 'expo-constants'
|
||||
import { useQuery, UseQueryOptions } from 'react-query'
|
||||
|
||||
type Translations = {
|
||||
provider: string
|
||||
sourceLanguage: string
|
||||
text: string[]
|
||||
}
|
||||
|
||||
export type QueryKeyTranslate = [
|
||||
'Translate',
|
||||
{
|
||||
uri: string
|
||||
source: string
|
||||
target: string
|
||||
text: string[]
|
||||
}
|
||||
]
|
||||
|
||||
export const TRANSLATE_SERVER = __DEV__
|
||||
? 'testtranslate.tooot.app'
|
||||
: 'translate.tooot.app'
|
||||
|
||||
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
|
||||
const key = Constants.manifest.extra?.translateKey
|
||||
if (!key) {
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const { uri, source, target, text } = queryKey[1]
|
||||
|
||||
const uriEncoded = Buffer.from(uri.replace(/https?:\/\//, ''))
|
||||
.toString('base64')
|
||||
.replace('+', '-')
|
||||
.replace('/', '_')
|
||||
.replace(/=+$/, '')
|
||||
const original = Buffer.from(JSON.stringify({ source, text })).toString(
|
||||
'base64'
|
||||
)
|
||||
|
||||
const res = await apiGeneral<Translations>({
|
||||
domain: TRANSLATE_SERVER,
|
||||
method: 'get',
|
||||
url: `v1/translate/${uriEncoded}/${target}`,
|
||||
headers: { key, original }
|
||||
})
|
||||
return res.body
|
||||
}
|
||||
|
||||
const useTranslateQuery = ({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKeyTranslate[1] & {
|
||||
options?: UseQueryOptions<Translations, AxiosError, Translations>
|
||||
}) => {
|
||||
const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }]
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
}
|
||||
|
||||
export { useTranslateQuery }
|
@ -1,6 +1,6 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@root/store'
|
||||
import apiGeneral from '@api/general'
|
||||
import { Constants } from 'react-native-unimodules'
|
||||
|
||||
export const retriveVersionLatest = createAsyncThunk(
|
||||
|
@ -9,10 +9,12 @@
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": false,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@api/*": ["./src/api/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@helpers/*": ["./src/helpers/*"],
|
||||
"@screens/*": ["./src/screens/*"],
|
||||
"@utils/*": ["./src/utils/*"],
|
||||
"@root/*": ["./src/*"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user