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