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