diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73628202..909d6f6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,42 @@ on: - release jobs: - build: + build-ios: + runs-on: macos-12 + steps: + - name: -- Step 0 -- Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: branch + - name: -- Step 1 -- Checkout code + uses: actions/checkout@v2 + - name: -- Step 2 -- Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: -- Step 3 -- Install node dependencies + run: yarn install + - name: -- Step 4 -- Install ruby dependencies + run: bundle install + - name: -- Step 5 -- Run fastlane + env: + DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer + ENVIRONMENT: ${{ steps.branch.outputs.branch }} + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + SENTRY_ORGANIZATION: ${{ secrets.SENTRY_ORGANIZATION }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + run: yarn app:build ios + + build-android: runs-on: macos-12 steps: - name: -- Step 0 -- Extract branch name @@ -24,36 +59,50 @@ jobs: with: distribution: 'zulu' java-version: '11' - - name: -- Step 4 -- Use Expo action - uses: expo/expo-github-action@v7 - with: - expo-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - name: -- Step 5 -- Install node dependencies + - name: -- Step 4 -- Install node dependencies run: yarn install - - name: -- Step 6 -- Install ruby dependencies + - name: -- Step 5 -- Install ruby dependencies run: bundle install - - name: -- Step 7 -- Run fastlane + - name: -- Step 6 -- Run fastlane env: - DEVELOPER_DIR: /Applications/Xcode_13.3.1.app/Contents/Developer ENVIRONMENT: ${{ steps.branch.outputs.branch }} LC_ALL: en_US.UTF-8 LANG: en_US.UTF-8 - SENTRY_ORGANIZATION: ${{ secrets.SENTRY_ORGANIZATION }} - SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - FASTLANE_USER: ${{ secrets.FASTLANE_USER }} - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} - MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }} ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }} SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }} + run: yarn app:build android + + create-release: + runs-on: macos-12 + needs: [build-ios, build-android] + steps: + - name: -- Step 0 -- Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: branch + - name: -- Step 1 -- Checkout code + uses: actions/checkout@v2 + - name: -- Step 2 -- Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: -- Step 3 -- Use Expo action + uses: expo/expo-github-action@v7 + with: + expo-version: latest + token: ${{ secrets.EXPO_TOKEN }} + - name: -- Step 4 -- Install node dependencies + run: yarn install + - name: -- Step 5 -- Install ruby dependencies + run: bundle install + - name: -- Step 6 -- Run fastlane + env: + ENVIRONMENT: ${{ steps.branch.outputs.branch }} + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 GH_PAT_GET_RELEASE: ${{ secrets.GH_PAT_GET_RELEASE }} FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }} - run: yarn app:build + run: yarn app:build release diff --git a/.gitignore b/.gitignore index a2022620..2173fcc9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,44 @@ coverage/ builds/ -# @generated expo-cli sync-28e2ab0e9ece60556eaf932abe52d017ec33db50 +# @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8 # The following patterns were generated by expo-cli # OSX +# .DS_Store +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof + # node.js +# node_modules/ npm-debug.log yarn-error.log @@ -25,9 +56,12 @@ buck-out/ # Bundle artifacts *.jsbundle +# CocoaPods +/ios/Pods/ + # Expo -.expo/* -.expo-shared/* +.expo/ web-build/ +dist/ # @end expo-cli \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 4a89e4dc..ec69c7a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,16 +17,16 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.582.0) - aws-sdk-core (3.130.2) + aws-partitions (1.598.0) + aws-sdk-core (3.131.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.56.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.2) + aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -106,8 +106,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -116,7 +116,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.205.2) + fastlane (2.206.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -163,9 +163,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.19.0) + google-apis-androidpublisher_v3 (0.21.0) google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-core (0.5.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -178,7 +178,7 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-playcustomapp_v1 (0.7.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.13.0) + google-apis-storage_v1 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -202,14 +202,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.10.0) concurrent-ruby (~> 1.0) jmespath (1.6.1) - json (2.6.1) - jwt (2.3.0) + json (2.6.2) + jwt (2.4.1) memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) @@ -226,7 +226,7 @@ GEM plist (3.6.0) public_suffix (4.0.7) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -260,7 +260,7 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8.1) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) diff --git a/README.md b/README.md index c4c48d1b..0e64ec9f 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,5 @@ [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation + +[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound diff --git a/VERSIONING.md b/VERSIONING.md index 6d9e4861..7a500bd4 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -22,11 +22,3 @@ ## OTA release channels * `MAJOR.MINOR-environment`. Environments include `release`, `candidate` and `development`. - -## Major versions mapping to native module versions - -| Version | Native module version | Expo version | -| :------:| :-------------------: | :----------: | -| `0-` | `210201` | `40.0.0` | -| `1-` | `210317` | `40.0.0` | -| `2.2` | `210916` | `41.0.0` | \ No newline at end of file diff --git a/android/app/src/main/res/raw/boop.mp3 b/android/app/src/main/res/raw/boop.mp3 new file mode 100644 index 00000000..bf9c3c1a Binary files /dev/null and b/android/app/src/main/res/raw/boop.mp3 differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 1f97fdc0..df082f69 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,4 +1,3 @@ - tooot contain diff --git a/app.config.ts b/app.config.ts index 73f78825..5a8c343b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -35,5 +35,13 @@ export default (): ExpoConfig => ({ googleServicesFile: './configs/google-services.json', permissions: ['CAMERA', 'VIBRATE'], blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT'] - } + }, + plugins: [ + [ + 'expo-notifications', + { + sounds: ['./assets/sounds/boop.mp3'] + } + ] + ] }) diff --git a/assets/sounds/boop.mp3 b/assets/sounds/boop.mp3 new file mode 100644 index 00000000..bf9c3c1a Binary files /dev/null and b/assets/sounds/boop.mp3 differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7a7cb227..fd746971 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,10 +1,5 @@ -fastlane_version "2.203.0" skip_docs -ensure_env_vars( - env_vars: ["ENVIRONMENT", "SENTRY_ORGANIZATION", "SENTRY_PROJECT", "SENTRY_AUTH_TOKEN"] -) - VERSIONS = read_json( json_path: "./package.json" )[:versions] ENVIRONMENT = ENV["ENVIRONMENT"] VERSION = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}" @@ -192,8 +187,8 @@ private_lane :build_android do task: 'assemble', build_type: 'release', project_dir: "./android", - print_command: false, - print_command_output: false, + print_command: true, + print_command_output: true, properties: { "expoSDK" => VERSIONS[:expo], "releaseChannel" => RELEASE_CHANNEL, @@ -218,15 +213,32 @@ private_lane :build_android do end end -lane :build do +lane :ios do + releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_token: ENV['GH_PAT_GET_RELEASE']) + if releaseExists + puts("Release #{GITHUB_RELEASE} exists. Continue with building React Native only.") + else + puts("Release #{GITHUB_RELEASE} does not exist.") + cocoapods(clean_install: true, podfile: "./ios/Podfile") + build_ios + end + rocket +end + +lane :android do releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_token: ENV['GH_PAT_GET_RELEASE']) if releaseExists puts("Release #{GITHUB_RELEASE} exists. Continue with building React Native only.") else puts("Release #{GITHUB_RELEASE} does not exist. Create new release as well as new native build.") - cocoapods(clean_install: true, podfile: "./ios/Podfile") - build_ios build_android + end + rocket +end + +lane :release do + releaseExists = get_github_release(url: GITHUB_REPO, version: "v#{VERSION}", api_token: ENV['GH_PAT_GET_RELEASE']) + if !releaseExists case ENVIRONMENT when "candidate" github_release(prerelease: true) diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 2859b8c8..bd412548 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,5 +1,2 @@ -Support editing toot -Integrated into system's share menu -Follow system's font weight setting (iOS) -Upload gif from keyboard (Android) -Added German and Italian +Support selecting multiple images +Long press toot to show options \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index ebf42983..69de255f 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,5 +1,2 @@ -支持编辑嘟文 -支持系统图片分享 -跟随系统字体粗细(苹果) -键盘上传gif图片(安卓) -新增德语和意大利语 +支持同时上传多张图片 +长按嘟文弹出嘟文选项 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d455be6c..b00f4cf7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,7 +35,7 @@ PODS: - EXJSONUtils - EXNotifications (0.15.2): - ExpoModulesCore - - Expo (45.0.4): + - Expo (45.0.5): - ExpoModulesCore - ExpoCrypto (10.2.0): - ExpoModulesCore @@ -55,7 +55,7 @@ PODS: - ReactCommon/turbomodule/core - ExpoRandom (12.2.0): - ExpoModulesCore - - ExpoWebBrowser (10.2.0): + - ExpoWebBrowser (10.2.1): - ExpoModulesCore - EXScreenCapture (4.2.0): - ExpoModulesCore @@ -67,7 +67,7 @@ PODS: - EXStoreReview (5.2.0): - ExpoModulesCore - EXStructuredHeaders (2.2.1) - - EXUpdates (0.13.1): + - EXUpdates (0.13.2): - ASN1Decoder (~> 1.8) - EASClient - EXManifests @@ -415,13 +415,18 @@ PODS: - React-Core - react-native-cameraroll (4.1.2): - React-Core - - react-native-image-keyboard (2.2.0): + - react-native-context-menu-view (1.5.4): - React - - react-native-netinfo (8.3.0): + - react-native-language-detection (0.1.0): + - React + - react-native-netinfo (9.0.0): - React-Core - react-native-pager-view (5.4.11): - React-Core - - react-native-safe-area-context (4.2.5): + - react-native-paste-input (0.4.2): + - React-Core + - Swime (= 3.0.6) + - react-native-safe-area-context (4.3.1): - RCT-Folly - RCTRequired - RCTTypeSafety @@ -523,7 +528,7 @@ PODS: - React-logger (= 0.68.2) - React-perflogger (= 0.68.2) - ReactCommon/turbomodule/core (= 0.68.2) - - RNCAsyncStorage (1.17.4): + - RNCAsyncStorage (1.17.6): - React-Core - RNFastImage (8.5.11): - React-Core @@ -531,6 +536,15 @@ PODS: - SDWebImageWebPCoder (~> 0.8.4) - RNGestureHandler (2.4.2): - React-Core + - RNImageCropPicker (0.37.3): + - React-Core + - React-RCTImage + - RNImageCropPicker/QBImagePickerController (= 0.37.3) + - TOCropViewController + - RNImageCropPicker/QBImagePickerController (0.37.3): + - React-Core + - React-RCTImage + - TOCropViewController - RNReanimated (2.8.0): - DoubleConversion - FBLazyVector @@ -561,7 +575,7 @@ PODS: - RNScreens (3.13.1): - React-Core - React-RCTImage - - RNSentry (3.4.2): + - RNSentry (3.4.3): - React-Core - Sentry (= 7.11.0) - RNShareMenu (5.0.5): @@ -577,6 +591,8 @@ PODS: - Sentry (7.11.0): - Sentry/Core (= 7.11.0) - Sentry/Core (7.11.0) + - Swime (3.0.6) + - TOCropViewController (2.6.1) - Yoga (1.14.0) DEPENDENCIES: @@ -638,9 +654,11 @@ DEPENDENCIES: - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - react-native-blurhash (from `../node_modules/react-native-blurhash`) - "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)" - - react-native-image-keyboard (from `../node_modules/react-native-image-keyboard`) + - react-native-context-menu-view (from `../node_modules/react-native-context-menu-view`) + - react-native-language-detection (from `../node_modules/react-native-language-detection`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) + - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-segmented-control (from `../node_modules/@react-native-community/segmented-control`)" - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -658,6 +676,7 @@ DEPENDENCIES: - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNFastImage (from `../node_modules/react-native-fast-image`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - "RNSentry (from `../node_modules/@sentry/react-native`)" @@ -685,6 +704,8 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - Sentry + - Swime + - TOCropViewController EXTERNAL SOURCES: boost: @@ -795,12 +816,16 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-blurhash" react-native-cameraroll: :path: "../node_modules/@react-native-community/cameraroll" - react-native-image-keyboard: - :path: "../node_modules/react-native-image-keyboard" + react-native-context-menu-view: + :path: "../node_modules/react-native-context-menu-view" + react-native-language-detection: + :path: "../node_modules/react-native-language-detection" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-pager-view: :path: "../node_modules/react-native-pager-view" + react-native-paste-input: + :path: "../node_modules/@mattermost/react-native-paste-input" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-segmented-control: @@ -835,6 +860,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-fast-image" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNImageCropPicker: + :path: "../node_modules/react-native-image-crop-picker" RNReanimated: :path: "../node_modules/react-native-reanimated" RNScreens: @@ -866,7 +893,7 @@ SPEC CHECKSUMS: EXJSONUtils: 2a74b8f40f1523cc3f92af99c91aa78201737a77 EXManifests: 0c6134b7b6f3236a93a778c3f44ba1cfb3f9fa3d EXNotifications: ea9fc56d27d1fee229489c5d8f452c7f367c237e - Expo: 64d52669fa3b9342919b5b44b2b4f15f19b0cf76 + Expo: b9fff0a1eac0f424fc68ea49b4347fb308e52e17 ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17 ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8 @@ -875,13 +902,13 @@ SPEC CHECKSUMS: ExpoLocalization: 8f619bb6eec64575cd5220bfabbd7b4e2d6f33f8 ExpoModulesCore: e4278a668e8c13c0269ed8b8a4200989deea2973 ExpoRandom: 14df0976aa363a71a730ceb7655250f3047c0e42 - ExpoWebBrowser: 818c519c3519cdd79780228039938fbd8236c885 + ExpoWebBrowser: 4b5f9633e5f169dc948587cb6d26d2d1d1406187 EXScreenCapture: cbee2204f313038a1819d31ad99a31e15f8e0f59 EXSecureStore: aaae919d83aec2faf031e99398807edac0313285 EXSplashScreen: 34f460788db8d682883871708dddbfac72095bb7 EXStoreReview: e61fbd500624ee7363ab134ee247cff380a8b254 EXStructuredHeaders: 5d86829469399370a9fc7cb1e4391b09de87681d - EXUpdates: 08c3931d6a5f39686091130a10310266ae3ba5f9 + EXUpdates: 08d69031f9ed1e918d50f041fa505fe67d6b4809 EXUpdatesInterface: 0b101ace1dbfa0f64260a5df31c71d03c66cca54 EXVideoThumbnails: 19e055dc3245b53c536da9e0ef9c618fd2118297 FBLazyVector: a7a655862f6b09625d11c772296b01cd5164b648 @@ -918,10 +945,12 @@ SPEC CHECKSUMS: react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7 react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866 - react-native-image-keyboard: adbf5996b8592a7d8cb8d3e431a9607f9cf3b270 - react-native-netinfo: 3671b091c4843fda5e153612866ef4024b8f5d62 + react-native-context-menu-view: b0beca02aad4bd9f9d7d932bf437e0a03baa69ef + react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d + react-native-netinfo: 5b664b2945a8f02102b296f0f812bddd6827ed9c react-native-pager-view: 7f00d63688f7df9fad86dfb0154814419cc5eb8d - react-native-safe-area-context: ebf8c413eb8b5f7c392a036a315eb7b46b96845f + react-native-paste-input: efbf0b08fa1673f0e3131da6ea01678c1bb8003e + react-native-safe-area-context: 6c12e3859b6f27b25de4fee8201cfb858432d8de react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 React-perflogger: a18b4f0bd933b8b24ecf9f3c54f9bf65180f3fe6 React-RCTActionSheet: 547fe42fdb4b6089598d79f8e1d855d7c23e2162 @@ -935,17 +964,20 @@ SPEC CHECKSUMS: React-RCTVibration: 79040b92bfa9c3c2d2cb4f57e981164ec7ab9374 React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23 ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2 - RNCAsyncStorage: 9367a646dc24e3ab7b6874d79bc1bfd0832dce58 + RNCAsyncStorage: 466b9df1a14bccda91da86e0b7d9a345d78e1673 RNFastImage: 945abf54742505d790d9024d230c69b1e866bc88 RNGestureHandler: 61628a2c859172551aa2100d3e73d1e57878392f + RNImageCropPicker: 44e2807bc410741f35d4c45b6586aedfe3da39d2 RNReanimated: 64573e25e078ae6bec03b891586d50b9ec284393 RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19 - RNSentry: 2cd1daa124b0d9fd0dfc2cb6094fdd168cb579bc + RNSentry: 85f6525b5fe8d2ada065858026b338605b3c09da RNShareMenu: c69282e50ac439737a86949a55c7b023b90027c8 RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8 SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e SDWebImageWebPCoder: f93010f3f6c031e2f8fb3081ca4ee6966c539815 Sentry: 0c5cd63d714187b4a39c331c1f0eb04ba7868341 + Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b + TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 Yoga: 99652481fcd320aefa4a7ef90095b95acd181952 PODFILE CHECKSUM: d6d20fa7c51228cebc309aed987ed7d8f4274844 diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json index cb338dda..b1bd45da 100644 --- a/ios/Podfile.properties.json +++ b/ios/Podfile.properties.json @@ -1,3 +1,3 @@ { "expo.jsEngine": "hermes" -} \ No newline at end of file +} diff --git a/ios/en.lproj/InfoPlist.strings b/ios/en.lproj/InfoPlist.strings index 1787913e..f9728054 100644 --- a/ios/en.lproj/InfoPlist.strings +++ b/ios/en.lproj/InfoPlist.strings @@ -6,6 +6,5 @@ */ -"NSCameraUsageDescription" = "Allow tooot to capture photo or video and attach it to your toot"; "NSPhotoLibraryAddUsageDescription" = "Allow tooot to save an image to your camera roll"; "NSPhotoLibraryUsageDescription" = "Allow tooot to access your camera roll to attach photos or videos to your toot"; diff --git a/ios/tooot.xcodeproj/project.pbxproj b/ios/tooot.xcodeproj/project.pbxproj index ffe81c03..d4f2fab3 100644 --- a/ios/tooot.xcodeproj/project.pbxproj +++ b/ios/tooot.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 34A37A6C820725DC6DDAA0EE /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B3640FCDF7C4396A68A74D1 /* libPods-ShareExtension.a */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 4986628FD0DD4630BFE5F388 /* boop.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = DF8133F098604A10B0D94952 /* boop.mp3 */; }; 5E36538325C9B8BD009F93EE /* RootViewColor.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */; }; 5EE088C926297820007E5FEC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5EE088CB26297820007E5FEC /* InfoPlist.strings */; }; 5EE44DD62600124E00A9BCED /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE44DD52600124E00A9BCED /* File.swift */; }; @@ -71,6 +72,7 @@ AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = tooot/SplashScreen.storyboard; sourceTree = ""; }; B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "tooot/GoogleService-Info.plist"; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + DF8133F098604A10B0D94952 /* boop.mp3 */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = boop.mp3; path = tooot/boop.mp3; sourceTree = ""; }; E613A80A28282A01003C97D6 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = tooot/AppDelegate.mm; sourceTree = ""; }; E633A420281EAEAB000E540F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -115,6 +117,7 @@ 5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */, B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */, 5EE088CB26297820007E5FEC /* InfoPlist.strings */, + DF8133F098604A10B0D94952 /* boop.mp3 */, ); name = tooot; sourceTree = ""; @@ -310,6 +313,7 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */, + 4986628FD0DD4630BFE5F388 /* boop.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -386,13 +390,17 @@ "${PODS_ROOT}/Target Support Files/Pods-tooot/Pods-tooot-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/ios/tooot/Info.plist b/ios/tooot/Info.plist index 2f610bc1..d6741b51 100644 --- a/ios/tooot/Info.plist +++ b/ios/tooot/Info.plist @@ -1,92 +1,90 @@ - - CFBundleAllowMixedLocalizations - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - tooot - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.2 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - com.xmflsct.app.tooot - CFBundleURLSchemes - - tooot-share - tooot - - - - CFBundleVersion - 2102022230 - ITSAppUsesNonExemptEncryption - - LSApplicationCategoryType - public.app-category.social-networking - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSExceptionDomains - - localhost - - NSExceptionAllowsInsecureHTTPLoads - - - - - NSCameraUsageDescription - Allow $(PRODUCT_NAME) to capture photo or video and attach it to your toot - NSLocationWhenInUseUsageDescription - - NSMainNibFile - LaunchScreen - NSMicrophoneUsageDescription - $(PRODUCT_NAME) DOES NOT need microphone permission. Please reject this request. - NSPhotoLibraryAddUsageDescription - Allow $(PRODUCT_NAME) to save an image to your camera roll - NSPhotoLibraryUsageDescription - Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - armv7 - - UIRequiresFullScreen - - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Automatic - UIViewControllerBasedStatusBarAppearance - - + + CFBundleAllowMixedLocalizations + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + tooot + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.2 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.xmflsct.app.tooot + CFBundleURLSchemes + + tooot-share + tooot + + + + CFBundleVersion + 2102022230 + ITSAppUsesNonExemptEncryption + + LSApplicationCategoryType + public.app-category.social-networking + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + NSLocationWhenInUseUsageDescription + + NSMainNibFile + LaunchScreen + NSMicrophoneUsageDescription + $(PRODUCT_NAME) DOES NOT need microphone permission. Please reject this request. + NSPhotoLibraryAddUsageDescription + Allow $(PRODUCT_NAME) to save an image to your camera roll + NSPhotoLibraryUsageDescription + Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Automatic + UIViewControllerBasedStatusBarAppearance + + diff --git a/ios/tooot/Supporting/Expo.plist b/ios/tooot/Supporting/Expo.plist index bba13d22..ab06f3de 100644 --- a/ios/tooot/Supporting/Expo.plist +++ b/ios/tooot/Supporting/Expo.plist @@ -1,18 +1,18 @@ - - EXUpdatesCheckOnLaunch - WIFI_ONLY - EXUpdatesEnabled - - EXUpdatesLaunchWaitMs - 0 - EXUpdatesReleaseChannel - 0-development - EXUpdatesSDKVersion - 0 - EXUpdatesURL - https://exp.host/@xmflsct/tooot - - + + EXUpdatesCheckOnLaunch + WIFI_ONLY + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + EXUpdatesReleaseChannel + 0-development + EXUpdatesSDKVersion + 0 + EXUpdatesURL + https://exp.host/@xmflsct/tooot + + \ No newline at end of file diff --git a/ios/tooot/boop.mp3 b/ios/tooot/boop.mp3 new file mode 100644 index 00000000..bf9c3c1a Binary files /dev/null and b/ios/tooot/boop.mp3 differ diff --git a/ios/tooot/tooot.entitlements b/ios/tooot/tooot.entitlements index 9dadf2e3..574f838f 100644 --- a/ios/tooot/tooot.entitlements +++ b/ios/tooot/tooot.entitlements @@ -1,24 +1,24 @@ - - aps-environment - development - com.apple.security.app-sandbox - - com.apple.security.application-groups - - group.com.xmflsct.app.tooot - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.network.client - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - - - + + aps-environment + development + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.xmflsct.app.tooot + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.network.client + + com.apple.security.personal-information.location + + com.apple.security.personal-information.photos-library + + + \ No newline at end of file diff --git a/ios/zh-Hans.lproj/InfoPlist.strings b/ios/zh-Hans.lproj/InfoPlist.strings index 41bee912..67025bd4 100644 --- a/ios/zh-Hans.lproj/InfoPlist.strings +++ b/ios/zh-Hans.lproj/InfoPlist.strings @@ -6,6 +6,5 @@ */ -"NSCameraUsageDescription" = "允许tooot拍摄图片或视频,以添加嘟文附件"; "NSPhotoLibraryAddUsageDescription" = "允许tooot保存图片至相册"; "NSPhotoLibraryUsageDescription" = "允许tooot读取相册图片或视频,以添加嘟文附件"; diff --git a/package.json b/package.json index 4f4b11fb..aa1d3a1c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "tooot", "versions": { - "native": "220508", + "native": "220603", "major": 4, - "minor": 0, - "patch": 5, + "minor": 1, + "patch": 0, "expo": "45.0.0" }, "description": "tooot app for Mastodon", @@ -19,34 +19,35 @@ "android": "react-native run-android", "iphone": "react-native run-ios", "ipad": "react-native run-ios --simulator 'iPad mini (6th generation)'", - "app:build": "bundle exec fastlane build", + "app:build": "bundle exec fastlane", "release": "scripts/release.sh", "clean": "react-native-clean-project", "postinstall": "patch-package" }, "dependencies": { "@expo/react-native-action-sheet": "3.13.0", - "@formatjs/intl-datetimeformat": "^6.0.1", - "@formatjs/intl-getcanonicallocales": "^2.0.1", - "@formatjs/intl-locale": "^3.0.1", - "@formatjs/intl-numberformat": "^8.0.1", - "@formatjs/intl-pluralrules": "^5.0.1", - "@formatjs/intl-relativetimeformat": "^11.0.1", + "@formatjs/intl-datetimeformat": "^6.0.2", + "@formatjs/intl-getcanonicallocales": "^2.0.2", + "@formatjs/intl-locale": "^3.0.2", + "@formatjs/intl-numberformat": "^8.0.2", + "@formatjs/intl-pluralrules": "^5.0.2", + "@formatjs/intl-relativetimeformat": "^11.0.2", + "@mattermost/react-native-paste-input": "^0.4.2", "@neverdull-agency/expo-unlimited-secure-store": "1.0.10", - "@react-native-async-storage/async-storage": "1.17.4", + "@react-native-async-storage/async-storage": "1.17.6", "@react-native-community/blur": "3.6.0", "@react-native-community/cameraroll": "4.1.2", - "@react-native-community/netinfo": "8.3.0", + "@react-native-community/netinfo": "9.0.0", "@react-native-community/segmented-control": "2.2.2", "@react-navigation/bottom-tabs": "6.3.1", "@react-navigation/native": "6.0.10", "@react-navigation/native-stack": "6.6.2", "@react-navigation/stack": "6.2.1", "@reduxjs/toolkit": "1.8.2", - "@sentry/react-native": "3.4.2", + "@sentry/react-native": "3.4.3", "@sharcoux/slider": "6.0.3", "axios": "0.27.2", - "expo": "45.0.4", + "expo": "45.0.5", "expo-auth-session": "3.6.1", "expo-av": "11.2.3", "expo-constants": "^13.1.1", @@ -55,7 +56,7 @@ "expo-file-system": "14.0.0", "expo-firebase-analytics": "7.0.0", "expo-haptics": "11.2.0", - "expo-image-manipulator": "10.3.1", + "expo-image-manipulator": "^10.3.1", "expo-image-picker": "13.1.1", "expo-linking": "3.1.0", "expo-localization": "13.0.0", @@ -65,29 +66,31 @@ "expo-secure-store": "11.2.0", "expo-splash-screen": "0.15.1", "expo-store-review": "5.2.0", - "expo-updates": "0.13.1", + "expo-updates": "0.13.2", "expo-video-thumbnails": "6.3.0", - "expo-web-browser": "10.2.0", + "expo-web-browser": "10.2.1", "i18next": "21.8.8", "li": "1.3.0", "lodash": "4.17.21", "react": "17.0.2", "react-dom": "17.0.2", "react-i18next": "11.17.0", - "react-intl": "^6.0.3", + "react-intl": "^6.0.4", "react-native": "0.68.2", "react-native-animated-spinkit": "1.5.2", "react-native-base64": "^0.2.1", "react-native-blurhash": "1.1.10", + "react-native-context-menu-view": "xmflsct/react-native-context-menu-view", "react-native-fast-image": "8.5.11", "react-native-feather": "1.1.2", "react-native-flash-message": "0.2.1", "react-native-gesture-handler": "2.4.2", "react-native-htmlview": "0.16.0", - "react-native-image-keyboard": "^2.2.0", + "react-native-image-crop-picker": "^0.37.3", + "react-native-language-detection": "^0.1.0", "react-native-pager-view": "5.4.11", "react-native-reanimated": "2.8.0", - "react-native-safe-area-context": "4.2.5", + "react-native-safe-area-context": "4.3.1", "react-native-screens": "3.13.1", "react-native-share-menu": "^5.0.5", "react-native-svg": "12.3.0", @@ -97,7 +100,7 @@ "react-redux": "8.0.2", "redux-persist": "6.0.0", "rn-placeholder": "3.0.3", - "sentry-expo": "4.1.1", + "sentry-expo": "4.2.0", "tslib": "2.4.0", "valid-url": "1.0.9" }, @@ -110,7 +113,7 @@ "@types/lodash": "4.14.182", "@types/react": "17.0.43", "@types/react-dom": "17.0.14", - "@types/react-native": "0.67.7", + "@types/react-native": "0.67.8", "@types/react-native-base64": "^0.2.0", "@types/react-native-share-menu": "^5.0.2", "@types/react-timeago": "4.1.3", diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index 489f987e..0db454c2 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -13,11 +13,4 @@ declare namespace App { | 'Conversations' | 'Bookmarks' | 'Favourites' - - interface IImageInfo { - uri: string - width: number - height: number - type?: 'image' | 'video' - } } diff --git a/src/App.tsx b/src/App.tsx index 496f2c71..96433aeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,6 @@ import * as SplashScreen from 'expo-splash-screen' import React, { useCallback, useEffect, useState } from 'react' import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' -import 'react-native-image-keyboard' import { enableFreeze } from 'react-native-screens' import { QueryClientProvider } from 'react-query' import { Provider } from 'react-redux' diff --git a/src/Screens.tsx b/src/Screens.tsx index 805129ff..b5e1c889 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -178,8 +178,49 @@ const Screens: React.FC = ({ localCorrupt }) => { } let text: string | undefined = undefined - let images: { type: string; uri: string }[] = [] - let video: { type: string; uri: string } | undefined = undefined + let media: { uri: string; mime: string }[] = [] + + const typesImage = ['png', 'jpg', 'jpeg', 'gif'] + const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg'] + const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => { + if (mime.startsWith('image/')) { + if (!typesImage.includes(mime.split('/')[1])) { + console.warn('Image type not supported:', mime.split('/')[1]) + displayMessage({ + message: t('shareError.imageNotSupported', { + type: mime.split('/')[1] + }), + type: 'error', + theme + }) + return + } + media.push({ uri, mime }) + } else if (mime.startsWith('video/')) { + if (!typesVideo.includes(mime.split('/')[1])) { + console.warn('Video type not supported:', mime.split('/')[1]) + displayMessage({ + message: t('shareError.videoNotSupported', { + type: mime.split('/')[1] + }), + type: 'error', + theme + }) + return + } + media.push({ uri, mime }) + } else { + if (typesImage.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'image/jpg' }) + return + } + if (typesVideo.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'video/mp4' }) + return + } + text = !text ? uri : text.concat(text, `\n${uri}`) + } + } switch (Platform.OS) { case 'ios': @@ -187,55 +228,11 @@ const Screens: React.FC = ({ localCorrupt }) => { return } - item.data.forEach(d => { - if (typeof d === 'string') return - const typesImage = ['png', 'jpg', 'jpeg', 'gif'] - const typesVideo = ['mp4', 'm4v', 'mov', 'webm'] - const { mimeType, data } = d - if (mimeType.startsWith('image/')) { - if (!typesImage.includes(mimeType.split('/')[1])) { - console.warn( - 'Image type not supported:', - mimeType.split('/')[1] - ) - displayMessage({ - message: t('shareError.imageNotSupported', { - type: mimeType.split('/')[1] - }), - type: 'error', - theme - }) - return - } - images.push({ type: mimeType.split('/')[1], uri: data }) - } else if (mimeType.startsWith('video/')) { - if (!typesVideo.includes(mimeType.split('/')[1])) { - console.warn( - 'Video type not supported:', - mimeType.split('/')[1] - ) - displayMessage({ - message: t('shareError.videoNotSupported', { - type: mimeType.split('/')[1] - }), - type: 'error', - theme - }) - return - } - video = { type: mimeType.split('/')[1], uri: data } - } else { - if (typesImage.includes(data.split('.').pop() || '')) { - images.push({ type: data.split('.').pop()!, uri: data }) - return - } - if (typesVideo.includes(data.split('.').pop() || '')) { - video = { type: data.split('.').pop()!, uri: data } - return - } - text = !text ? data : text.concat(text, `\n${data}`) + for (const d of item.data) { + if (typeof d !== 'string') { + filterMedia({ uri: d.data, mime: d.mimeType }) } - }) + } break case 'android': if (!item.mimeType) { @@ -247,65 +244,16 @@ const Screens: React.FC = ({ localCorrupt }) => { } else { tempData = item.data } - tempData.forEach(d => { - const typesImage = ['png', 'jpg', 'jpeg', 'gif'] - const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg'] - if (item.mimeType!.startsWith('image/')) { - if (!typesImage.includes(item.mimeType.split('/')[1])) { - console.warn( - 'Image type not supported:', - item.mimeType.split('/')[1] - ) - displayMessage({ - message: t('shareError.imageNotSupported', { - type: item.mimeType.split('/')[1] - }), - type: 'error', - theme - }) - return - } - images.push({ type: item.mimeType.split('/')[1], uri: d }) - } else if (item.mimeType.startsWith('video/')) { - if (!typesVideo.includes(item.mimeType.split('/')[1])) { - console.warn( - 'Video type not supported:', - item.mimeType.split('/')[1] - ) - displayMessage({ - message: t('shareError.videoNotSupported', { - type: item.mimeType.split('/')[1] - }), - type: 'error', - theme - }) - return - } - video = { type: item.mimeType.split('/')[1], uri: d } - } else { - if (typesImage.includes(d.split('.').pop() || '')) { - images.push({ type: d.split('.').pop()!, uri: d }) - return - } - if (typesVideo.includes(d.split('.').pop() || '')) { - video = { type: d.split('.').pop()!, uri: d } - return - } - text = !text ? d : text.concat(text, `\n${d}`) - } - }) + for (const d of item.data) { + filterMedia({ uri: d, mime: item.mimeType }) + } break } - if (!text && (!images || !images.length) && !video) { + if (!text && !media.length) { return } else { - navigationRef.navigate('Screen-Compose', { - type: 'share', - text, - images, - video - }) + navigationRef.navigate('Screen-Compose', { type: 'share', text, media }) } }, [instanceActive] diff --git a/src/api/general.ts b/src/api/general.ts index a39f815e..cbcc784f 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -68,8 +68,8 @@ const apiGeneral = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/api/instance.ts b/src/api/instance.ts index 86d9a367..883302e7 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -110,8 +110,8 @@ const apiInstance = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/api/tooot.ts b/src/api/tooot.ts index b4264a76..9c029b53 100644 --- a/src/api/tooot.ts +++ b/src/api/tooot.ts @@ -66,7 +66,7 @@ const apiTooot = async ({ }) }) .catch(error => { - if (sentry && Math.random() < 0.1) { + if (sentry) { Sentry.Native.setExtras({ API: 'tooot', ...(error?.response && { response: error.response }), @@ -85,8 +85,8 @@ const apiTooot = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/components/ContextMenu/account.ts b/src/components/ContextMenu/account.ts new file mode 100644 index 00000000..d1289679 --- /dev/null +++ b/src/components/ContextMenu/account.ts @@ -0,0 +1,164 @@ +import analytics from '@components/analytics' +import { displayMessage } from '@components/Message' +import { + MutationVarsTimelineUpdateAccountProperty, + QueryKeyTimeline, + useTimelineMutation +} from '@utils/queryHooks/timeline' +import { getInstanceAccount } from '@utils/slices/instancesSlice' +import { useTheme } from '@utils/styles/ThemeManager' +import { useTranslation } from 'react-i18next' +import { Platform } from 'react-native' +import { ContextMenuAction } from 'react-native-context-menu-view' +import { useQueryClient } from 'react-query' +import { useSelector } from 'react-redux' + +export interface Props { + actions: ContextMenuAction[] + queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline + id: Mastodon.Account['id'] +} + +const contextMenuAccount = ({ + actions, + queryKey, + rootQueryKey, + id: accountId +}: Props) => { + const { theme } = useTheme() + const { t } = useTranslation('componentContextMenu') + + const queryClient = useQueryClient() + const mutateion = useTimelineMutation({ + onSuccess: (_, params) => { + const theParams = params as MutationVarsTimelineUpdateAccountProperty + displayMessage({ + theme, + type: 'success', + message: t('common:message.success.message', { + function: t(`account.${theParams.payload.property}.action`) + }) + }) + }, + onError: (err: any, params) => { + const theParams = params as MutationVarsTimelineUpdateAccountProperty + displayMessage({ + theme, + type: 'error', + message: t('common:message.error.message', { + function: t(`account.${theParams.payload.property}.action`) + }), + ...(err.status && + typeof err.status === 'number' && + err.data && + err.data.error && + typeof err.data.error === 'string' && { + description: err.data.error + }) + }) + }, + onSettled: () => { + queryKey && queryClient.invalidateQueries(queryKey) + rootQueryKey && queryClient.invalidateQueries(rootQueryKey) + } + }) + + const instanceAccount = useSelector( + getInstanceAccount, + (prev, next) => prev.id === next.id + ) + const ownAccount = instanceAccount?.id === accountId + + if (!ownAccount) { + switch (Platform.OS) { + case 'ios': + actions.push({ + id: 'account', + title: t('account.title'), + inlineChildren: true, + actions: [ + { + id: 'account-mute', + title: t('account.mute.action'), + systemIcon: 'eye.slash' + }, + { + id: 'account-block', + title: t('account.block.action'), + systemIcon: 'xmark.circle', + destructive: true + }, + { + id: 'account-reports', + title: t('account.reports.action'), + systemIcon: 'flag', + destructive: true + } + ] + }) + break + default: + actions.push( + { + id: 'account-mute', + title: t('account.mute.action'), + systemIcon: 'eye.slash' + }, + { + id: 'account-block', + title: t('account.block.action'), + systemIcon: 'xmark.circle', + destructive: true + }, + { + id: 'account-reports', + title: t('account.reports.action'), + systemIcon: 'flag', + destructive: true + } + ) + break + } + } + + return (id: string) => { + switch (id) { + case 'account-mute': + analytics('timeline_shared_headeractions_account_mute_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'mute' } + }) + break + case 'account-block': + analytics('timeline_shared_headeractions_account_block_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'block' } + }) + break + case 'account-report': + analytics('timeline_shared_headeractions_account_reports_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'reports' } + }) + break + } + } +} + +export default contextMenuAccount diff --git a/src/components/ContextMenu/instance.ts b/src/components/ContextMenu/instance.ts new file mode 100644 index 00000000..676bc8ad --- /dev/null +++ b/src/components/ContextMenu/instance.ts @@ -0,0 +1,108 @@ +import analytics from '@components/analytics' +import { displayMessage } from '@components/Message' +import { + QueryKeyTimeline, + useTimelineMutation +} from '@utils/queryHooks/timeline' +import { getInstanceUrl } from '@utils/slices/instancesSlice' +import { useTheme } from '@utils/styles/ThemeManager' +import { useTranslation } from 'react-i18next' +import { Alert, Platform } from 'react-native' +import { ContextMenuAction } from 'react-native-context-menu-view' +import { useQueryClient } from 'react-query' +import { useSelector } from 'react-redux' + +export interface Props { + actions: ContextMenuAction[] + status: Mastodon.Status + queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline +} + +const contextMenuInstance = ({ + actions, + status, + queryKey, + rootQueryKey +}: Props) => { + const { t } = useTranslation('componentContextMenu') + const { theme } = useTheme() + + const currentInstance = useSelector(getInstanceUrl) + const instance = status.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1] + + const queryClient = useQueryClient() + const mutation = useTimelineMutation({ + onSettled: () => { + displayMessage({ + theme, + type: 'success', + message: t('common:message.success.message', { + function: t(`instance.block.action`, { instance }) + }) + }) + queryClient.invalidateQueries(queryKey) + rootQueryKey && queryClient.invalidateQueries(rootQueryKey) + } + }) + + if (currentInstance !== instance && instance) { + switch (Platform.OS) { + case 'ios': + actions.push({ + id: 'instance', + title: t('instance.title'), + actions: [ + { + id: 'instance-block', + title: t('instance.block.action', { instance }), + destructive: true + } + ] + }) + break + default: + actions.push({ + id: 'instance-block', + title: t('instance.block.action', { instance }), + destructive: true + }) + break + } + } + + return (id: string) => { + switch (id) { + case 'instance-block': + analytics('timeline_shared_headeractions_domain_block_press', { + page: queryKey[1].page + }) + Alert.alert( + t('instance.block.alert.title', { instance }), + t('instance.block.alert.message'), + [ + { + text: t('instance.block.alert.buttons.confirm'), + style: 'destructive', + onPress: () => { + analytics( + 'timeline_shared_headeractions_domain_block_confirm', + { page: queryKey && queryKey[1].page } + ) + mutation.mutate({ + type: 'domainBlock', + queryKey, + domain: instance + }) + } + }, + { + text: t('common:buttons.cancel') + } + ] + ) + } + } +} + +export default contextMenuInstance diff --git a/src/components/ContextMenu/share.ts b/src/components/ContextMenu/share.ts new file mode 100644 index 00000000..4f7b25ec --- /dev/null +++ b/src/components/ContextMenu/share.ts @@ -0,0 +1,38 @@ +import analytics from '@components/analytics' +import { useTranslation } from 'react-i18next' +import { Platform, Share } from 'react-native' +import { ContextMenuAction } from 'react-native-context-menu-view' + +export interface Props { + actions: ContextMenuAction[] + type: 'status' | 'account' + url: string +} + +const contextMenuShare = ({ actions, type, url }: Props) => { + const { t } = useTranslation('componentContextMenu') + + actions.push({ + id: 'share', + title: t(`share.${type}.action`), + systemIcon: 'square.and.arrow.up' + }) + + return (id: string) => { + switch (id) { + case 'share': + analytics('timeline_shared_headeractions_share_press') + switch (Platform.OS) { + case 'ios': + Share.share({ url }) + break + case 'android': + Share.share({ message: url }) + break + } + break + } + } +} + +export default contextMenuShare diff --git a/src/components/ContextMenu/status.ts b/src/components/ContextMenu/status.ts new file mode 100644 index 00000000..8867b01a --- /dev/null +++ b/src/components/ContextMenu/status.ts @@ -0,0 +1,286 @@ +import apiInstance from '@api/instance' +import analytics from '@components/analytics' +import { displayMessage } from '@components/Message' +import { useNavigation } from '@react-navigation/native' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { RootStackParamList } from '@utils/navigation/navigators' +import { + MutationVarsTimelineUpdateStatusProperty, + QueryKeyTimeline, + useTimelineMutation +} from '@utils/queryHooks/timeline' +import { + checkInstanceFeature, + getInstanceAccount +} from '@utils/slices/instancesSlice' +import { useTheme } from '@utils/styles/ThemeManager' +import { useTranslation } from 'react-i18next' +import { Alert, Platform } from 'react-native' +import { ContextMenuAction } from 'react-native-context-menu-view' +import { useQueryClient } from 'react-query' +import { useSelector } from 'react-redux' + +export interface Props { + actions: ContextMenuAction[] + status: Mastodon.Status + queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline +} + +const contextMenuStatus = ({ + actions, + status, + queryKey, + rootQueryKey +}: Props) => { + const navigation = + useNavigation< + NativeStackNavigationProp + >() + const { theme } = useTheme() + const { t } = useTranslation('componentContextMenu') + + const queryClient = useQueryClient() + const mutation = useTimelineMutation({ + onMutate: true, + onError: (err: any, params, oldData) => { + const theFunction = (params as MutationVarsTimelineUpdateStatusProperty) + .payload + ? (params as MutationVarsTimelineUpdateStatusProperty).payload.property + : 'delete' + displayMessage({ + theme, + type: 'error', + message: t('common:message.error.message', { + function: t(`status.${theFunction}.action`) + }), + ...(err.status && + typeof err.status === 'number' && + err.data && + err.data.error && + typeof err.data.error === 'string' && { + description: err.data.error + }) + }) + queryClient.setQueryData(queryKey, oldData) + } + }) + + const instanceAccount = useSelector( + getInstanceAccount, + (prev, next) => prev.id === next.id + ) + const ownAccount = instanceAccount?.id === status.account.id + + if (ownAccount) { + const accountMenuItems: ContextMenuAction[] = [ + { + id: 'status-delete', + title: t('status.delete.action'), + systemIcon: 'trash', + destructive: true + }, + { + id: 'status-delete-edit', + title: t('status.deleteEdit.action'), + systemIcon: 'pencil.and.outline', + destructive: true + }, + { + id: 'status-mute', + title: t('status.mute.action-muted', { + context: status.muted.toString() + }), + systemIcon: status.muted ? 'speaker' : 'speaker.slash' + } + ] + + const canEditPost = useSelector(checkInstanceFeature('edit_post')) + if (canEditPost) { + accountMenuItems.unshift({ + id: 'status-edit', + title: t('status.edit.action'), + systemIcon: 'square.and.pencil' + }) + } + + if (status.visibility === 'public' || status.visibility === 'unlisted') { + accountMenuItems.push({ + id: 'status-pin', + title: t('status.pin.action-pinned', { + context: status.pinned.toString() + }), + systemIcon: status.pinned ? 'pin.slash' : 'pin' + }) + } + + switch (Platform.OS) { + case 'ios': + actions.push({ + id: 'status', + title: t('status.title'), + inlineChildren: true, + actions: accountMenuItems + }) + break + default: + actions.push(...accountMenuItems) + break + } + } + + return async (id: string) => { + switch (id) { + case 'status-delete': + analytics('timeline_shared_headeractions_status_delete_press', { + page: queryKey && queryKey[1].page + }) + Alert.alert( + t('status.delete.alert.title'), + t('status.delete.alert.message'), + [ + { + text: t('status.delete.alert.buttons.confirm'), + style: 'destructive', + onPress: async () => { + analytics( + 'timeline_shared_headeractions_status_delete_confirm', + { + page: queryKey && queryKey[1].page + } + ) + mutation.mutate({ + type: 'deleteItem', + source: 'statuses', + queryKey, + rootQueryKey, + id: status.id + }) + } + }, + { + text: t('common:buttons.cancel') + } + ] + ) + break + case 'status-delete-edit': + analytics('timeline_shared_headeractions_status_deleteedit_press', { + page: queryKey && queryKey[1].page + }) + Alert.alert( + t('status.deleteEdit.alert.title'), + t('status.deleteEdit.alert.message'), + [ + { + text: t('status.deleteEdit.alert.buttons.confirm'), + style: 'destructive', + onPress: async () => { + analytics( + 'timeline_shared_headeractions_status_deleteedit_confirm', + { + page: queryKey && queryKey[1].page + } + ) + let replyToStatus: Mastodon.Status | undefined = undefined + if (status.in_reply_to_id) { + replyToStatus = await apiInstance({ + method: 'get', + url: `statuses/${status.in_reply_to_id}` + }).then(res => res.body) + } + mutation + .mutateAsync({ + type: 'deleteItem', + source: 'statuses', + queryKey, + id: status.id + }) + .then(res => { + navigation.navigate('Screen-Compose', { + type: 'deleteEdit', + incomingStatus: res.body as Mastodon.Status, + ...(replyToStatus && { replyToStatus }), + queryKey + }) + }) + } + }, + { + text: t('common:buttons.cancel') + } + ] + ) + break + case 'status-mute': + analytics('timeline_shared_headeractions_status_mute_press', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'updateStatusProperty', + queryKey, + rootQueryKey, + id: status.id, + payload: { + property: 'muted', + currentValue: status.muted, + propertyCount: undefined, + countValue: undefined + } + }) + break + case 'status-edit': + analytics('timeline_shared_headeractions_status_edit_press', { + page: queryKey && queryKey[1].page + }) + let replyToStatus: Mastodon.Status | undefined = undefined + if (status.in_reply_to_id) { + replyToStatus = await apiInstance({ + method: 'get', + url: `statuses/${status.in_reply_to_id}` + }).then(res => res.body) + } + apiInstance<{ + id: Mastodon.Status['id'] + text: NonNullable + spoiler_text: Mastodon.Status['spoiler_text'] + }>({ + method: 'get', + url: `statuses/${status.id}/source` + }).then(res => { + navigation.navigate('Screen-Compose', { + type: 'edit', + incomingStatus: { + ...status, + text: res.body.text, + spoiler_text: res.body.spoiler_text + }, + ...(replyToStatus && { replyToStatus }), + queryKey, + rootQueryKey + }) + }) + break + case 'status-pin': + // Also note that reblogs cannot be pinned. + analytics('timeline_shared_headeractions_status_pin_press', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'updateStatusProperty', + queryKey, + rootQueryKey, + id: status.id, + payload: { + property: 'pinned', + currentValue: status.pinned, + propertyCount: undefined, + countValue: undefined + } + }) + break + } + } +} + +export default contextMenuStatus diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx index 5e8c1c75..1158fe76 100644 --- a/src/components/Instance.tsx +++ b/src/components/Instance.tsx @@ -1,6 +1,5 @@ import Button from '@components/Button' import Icon from '@components/Icon' -import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAppsQuery } from '@utils/queryHooks/apps' import { useInstanceQuery } from '@utils/queryHooks/instance' import { getInstances } from '@utils/slices/instancesSlice' @@ -9,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import * as WebBrowser from 'expo-web-browser' import { debounce } from 'lodash' import React, { RefObject, useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { Alert, Image, @@ -39,7 +38,6 @@ const ComponentInstance: React.FC = ({ }) => { const { t } = useTranslation('componentInstance') const { colors, mode } = useTheme() - const { screenReaderEnabled } = useAccessibility() const instances = useSelector(getInstances, () => true) const [domain, setDomain] = useState() @@ -247,7 +245,7 @@ const ComponentInstance: React.FC = ({ style={{ flexDirection: 'row', marginHorizontal: StyleConstants.Spacing.Global.PagePadding, - marginVertical: StyleConstants.Spacing.M + marginTop: StyleConstants.Spacing.M }} > = ({ { - if (screenReaderEnabled) { - analytics('view_privacy') - WebBrowser.openBrowserAsync( - 'https://tooot.app/privacy-policy' - ) - } - }} > {t('server.disclaimer.base')} - { - analytics('view_privacy') - WebBrowser.openBrowserAsync( - 'https://tooot.app/privacy-policy' - ) - }} - > - {t('server.disclaimer.privacy')} - + + + + + + { + analytics('view_privacy') + WebBrowser.openBrowserAsync( + 'https://tooot.app/privacy-policy' + ) + }} + />, + { + analytics('view_tos') + WebBrowser.openBrowserAsync( + 'https://tooot.app/terms-of-service' + ) + }} + /> + ]} + /> diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index c4c0ff62..3c1b26f4 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -49,7 +49,7 @@ const renderNode = ({ const href = node.attribs.href if (classes) { if (classes.includes('hashtag')) { - const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) + const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const differentTag = routeParams?.hashtag ? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2] : true @@ -107,7 +107,7 @@ const renderNode = ({ ) } } else { - const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/)) + const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/)) // Need example here const content = node.children && node.children[0] && node.children[0].data @@ -124,11 +124,15 @@ const renderNode = ({ }} onPress={async () => { analytics('status_link_press') - !disableDetails && !shouldBeTag - ? await openLink(href, navigation) - : navigation.push('Tab-Shared-Hashtag', { + if (!disableDetails) { + if (shouldBeTag) { + navigation.push('Tab-Shared-Hashtag', { hashtag: content.substring(1) }) + } else { + await openLink(href, navigation) + } + } }} > {(content && content !== href && content) || diff --git a/src/components/RelativeTime.tsx b/src/components/RelativeTime.tsx new file mode 100644 index 00000000..7dfd3c6a --- /dev/null +++ b/src/components/RelativeTime.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react' +import { FormattedRelativeTime } from 'react-intl' +import { AppState } from 'react-native' + +export interface Props { + type: 'past' | 'future' + time: string | number +} + +const RelativeTime: React.FC = ({ type, time }) => { + const [now, setNow] = useState(new Date().getTime()) + useEffect(() => { + const appStateListener = AppState.addEventListener('change', state => { + setNow(new Date().getTime()) + }) + + return () => { + appStateListener.remove() + } + }, []) + + return ( + + ) +} + +export default RelativeTime diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index b18aec52..6908a321 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -5,18 +5,11 @@ import { getInstanceActive } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { RefObject, useCallback, useRef } from 'react' -import { - FlatList, - FlatListProps, - Platform, - RefreshControl, - StyleSheet -} from 'react-native' +import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' -import { useQueryClient } from 'react-query' import { useSelector } from 'react-redux' import TimelineEmpty from './Timeline/Empty' import TimelineFooter from './Timeline/Footer' @@ -149,7 +142,6 @@ const Timeline: React.FC = ({ data={flattenData} initialNumToRender={6} maxToRenderPerBatch={3} - style={styles.flatList} onEndReached={onEndReached} onEndReachedThreshold={0.75} ListFooterComponent={ @@ -160,9 +152,13 @@ const Timeline: React.FC = ({ } ListEmptyComponent={} ItemSeparatorComponent={ItemSeparatorComponent} - maintainVisibleContentPosition={{ - minIndexForVisible: 0 - }} + maintainVisibleContentPosition={ + isFetching + ? { + minIndexForVisible: 0 + } + : undefined + } {...androidRefreshControl} {...customProps} /> @@ -170,10 +166,4 @@ const Timeline: React.FC = ({ ) } -const styles = StyleSheet.create({ - flatList: { - minHeight: '100%' - } -}) - export default Timeline diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 9d863144..6c84aec3 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -18,6 +18,7 @@ import { uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, View } from 'react-native' import { useSelector } from 'react-redux' +import TimelineContextMenu from './Shared/ContextMenu' import TimelineFeedback from './Shared/Feedback' import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' @@ -73,96 +74,108 @@ const TimelineDefault: React.FC = ({ }, []) return ( - - {item.reblog ? ( - - ) : item._pinned ? ( - - ) : null} - - - - - - - {}} > - {typeof actualStatus.content === 'string' && - actualStatus.content.length > 0 ? ( - + ) : item._pinned ? ( + + ) : null} + + + + - ) : null} - {queryKey && actualStatus.poll ? ( - + + + {typeof actualStatus.content === 'string' && + actualStatus.content.length > 0 ? ( + + ) : null} + {queryKey && actualStatus.poll ? ( + + ) : null} + {!disableDetails && + Array.isArray(actualStatus.media_attachments) && + actualStatus.media_attachments.length ? ( + + ) : null} + {!disableDetails && actualStatus.card ? ( + + ) : null} + {!disableDetails ? ( + + ) : null} + + + + + {queryKey && !disableDetails ? ( + d?.id !== instanceAccount?.id), + d => d?.id + ).map(d => d?.acct)} reblog={item.reblog ? true : false} - sameAccount={ownAccount} /> ) : null} - {!disableDetails && - Array.isArray(actualStatus.media_attachments) && - actualStatus.media_attachments.length ? ( - - ) : null} - {!disableDetails && actualStatus.card ? ( - - ) : null} - {!disableDetails ? ( - - ) : null} - - - - - {queryKey && !disableDetails ? ( - d?.id !== instanceAccount?.id), - d => d?.id - ).map(d => d?.acct)} - reblog={item.reblog ? true : false} - /> - ) : null} - + + ) } diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index c4df686f..80be1816 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -18,6 +18,7 @@ import { isEqual, uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, View } from 'react-native' import { useSelector } from 'react-redux' +import TimelineContextMenu from './Shared/ContextMenu' import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' @@ -58,103 +59,105 @@ const TimelineNotifications = React.memo( }, []) return ( - - {notification.type !== 'mention' ? ( - - ) : null} - - + {}} > - - - + ) : null} + + + + + + + + {notification.status ? ( + + {notification.status.content.length > 0 ? ( + + ) : null} + {notification.status.poll ? ( + + ) : null} + {notification.status.media_attachments.length > 0 ? ( + + ) : null} + {notification.status.card ? ( + + ) : null} + + + ) : null} {notification.status ? ( - - {notification.status.content.length > 0 ? ( - - ) : null} - {notification.status.poll ? ( - - ) : null} - {notification.status.media_attachments.length > 0 ? ( - - ) : null} - {notification.status.card ? ( - - ) : null} - - + d?.id !== instanceAccount?.id), + d => d?.id + ).map(d => d?.acct)} + reblog={false} + /> ) : null} - - - {notification.status ? ( - d?.id !== instanceAccount?.id), - d => d?.id - ).map(d => d?.acct)} - reblog={false} - /> - ) : null} - + + ) }, (prev, next) => isEqual(prev.notification, next.notification) diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx index 0ff986e3..59db82b9 100644 --- a/src/components/Timeline/Shared/Actioned.tsx +++ b/src/components/Timeline/Shared/Actioned.tsx @@ -22,7 +22,7 @@ const TimelineActioned = React.memo( const { colors } = useTheme() const navigation = useNavigation>() - const name = account?.display_name || account.username + const name = account?.display_name || account?.username const iconColor = colors.primaryDefault const content = (content: string) => ( diff --git a/src/components/Timeline/Shared/ContextMenu.tsx b/src/components/Timeline/Shared/ContextMenu.tsx new file mode 100644 index 00000000..f11af4b4 --- /dev/null +++ b/src/components/Timeline/Shared/ContextMenu.tsx @@ -0,0 +1,82 @@ +import contextMenuAccount from '@components/ContextMenu/account' +import contextMenuInstance from '@components/ContextMenu/instance' +import contextMenuShare from '@components/ContextMenu/share' +import contextMenuStatus from '@components/ContextMenu/status' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import React from 'react' +import { createContext } from 'react' +import ContextMenu, { + ContextMenuAction, + ContextMenuProps +} from 'react-native-context-menu-view' + +export interface Props { + status?: Mastodon.Status + queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline +} + +export const ContextMenuContext = createContext([]) + +const TimelineContextMenu: React.FC = ({ + children, + status, + queryKey, + rootQueryKey, + ...props +}) => { + if (!status || !queryKey) { + return <>{children} + } + + const actions: ContextMenuAction[] = [] + + const shareOnPress = + status.visibility !== 'direct' + ? contextMenuShare({ + actions, + type: 'status', + url: status.url || status.uri + }) + : null + const statusOnPress = contextMenuStatus({ + actions, + status, + queryKey, + rootQueryKey + }) + const accountOnPress = contextMenuAccount({ + actions, + queryKey, + rootQueryKey, + id: status.account.id + }) + const instanceOnPress = contextMenuInstance({ + actions, + status, + queryKey, + rootQueryKey + }) + + return ( + + { + for (const on of [ + shareOnPress, + statusOnPress, + accountOnPress, + instanceOnPress + ]) { + on && on(id) + } + }} + children={children} + {...props} + /> + + ) +} + +export default TimelineContextMenu diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index b4db437b..80a1c1bb 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -1,13 +1,12 @@ import Icon from '@components/Icon' -import { useNavigation } from '@react-navigation/native' -import { StackNavigationProp } from '@react-navigation/stack' -import { RootStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React from 'react' +import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, View } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { ContextMenuContext } from './ContextMenu' import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedCreated from './HeaderShared/Created' @@ -16,24 +15,19 @@ import HeaderSharedVisibility from './HeaderShared/Visibility' export interface Props { queryKey?: QueryKeyTimeline - rootQueryKey?: QueryKeyTimeline status: Mastodon.Status highlighted: boolean } -const TimelineHeaderDefault = ({ - queryKey, - rootQueryKey, - status, - highlighted -}: Props) => { - const { t } = useTranslation('componentTimeline') - const navigation = useNavigation>() +const TimelineHeaderDefault = ({ queryKey, status, highlighted }: Props) => { + const { t } = useTranslation('componentContextMenu') const { colors } = useTheme() + const contextMenuContext = useContext(ContextMenuContext) + return ( - + - navigation.navigate('Screen-Actions', { - queryKey, - rootQueryKey, - status, - type: 'status' - }) - } - children={ - - } - /> + > + {}} + children={ + + } + /> + ) : null} ) diff --git a/src/components/Timeline/Shared/HeaderNotification.tsx b/src/components/Timeline/Shared/HeaderNotification.tsx index 20bc735a..a9b30f2e 100644 --- a/src/components/Timeline/Shared/HeaderNotification.tsx +++ b/src/components/Timeline/Shared/HeaderNotification.tsx @@ -3,14 +3,12 @@ import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship' -import { useNavigation } from '@react-navigation/native' -import { StackNavigationProp } from '@react-navigation/stack' -import { RootStackParamList } from '@utils/navigation/navigators' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useMemo } from 'react' +import React, { useContext, useMemo } from 'react' import { Pressable, View } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { ContextMenuContext } from './ContextMenu' import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedCreated from './HeaderShared/Created' @@ -18,14 +16,14 @@ import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedVisibility from './HeaderShared/Visibility' export interface Props { - queryKey: QueryKeyTimeline notification: Mastodon.Notification } -const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { - const navigation = useNavigation>() +const TimelineHeaderNotification = ({ notification }: Props) => { const { colors } = useTheme() + const contextMenuContext = useContext(ContextMenuContext) + const actions = useMemo(() => { switch (notification.type) { case 'follow': @@ -42,18 +40,18 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { justifyContent: 'center', paddingBottom: StyleConstants.Spacing.S }} - onPress={() => - navigation.navigate('Screen-Actions', { - queryKey, - status: notification.status!, - type: 'status' - }) - } children={ - {}} + children={ + + } /> } /> diff --git a/src/components/Timeline/Shared/HeaderShared/Account.tsx b/src/components/Timeline/Shared/HeaderShared/Account.tsx index a3a1f84d..5efe3bf9 100644 --- a/src/components/Timeline/Shared/HeaderShared/Account.tsx +++ b/src/components/Timeline/Shared/HeaderShared/Account.tsx @@ -27,7 +27,7 @@ const HeaderSharedAccount = React.memo( numberOfLines={1} > diff --git a/src/components/Timeline/Shared/HeaderShared/Created.tsx b/src/components/Timeline/Shared/HeaderShared/Created.tsx index 9b4fe066..9830d72a 100644 --- a/src/components/Timeline/Shared/HeaderShared/Created.tsx +++ b/src/components/Timeline/Shared/HeaderShared/Created.tsx @@ -1,10 +1,11 @@ import Icon from '@components/Icon' +import RelativeTime from '@components/RelativeTime' import CustomText from '@components/Text' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' -import { FormattedDate, FormattedRelativeTime } from 'react-intl' +import { FormattedDate } from 'react-intl' export interface Props { created_at: Mastodon.Status['created_at'] | number @@ -31,12 +32,7 @@ const HeaderSharedCreated = React.memo( /> ) : ( - + )} {edited_at ? ( diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index bab34fa4..fddc8a8d 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -4,6 +4,7 @@ import haptics from '@components/haptics' import Icon from '@components/Icon' import { displayMessage } from '@components/Message' import { ParseEmojis } from '@components/Parse' +import RelativeTime from '@components/RelativeTime' import CustomText from '@components/Text' import { MutationVarsTimelineUpdateStatusProperty, @@ -16,8 +17,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import { maxBy } from 'lodash' import React, { useCallback, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { FormattedRelativeTime } from 'react-intl' -import { Pressable, StyleSheet, Text, View } from 'react-native' +import { Pressable, View } from 'react-native' import { useQueryClient } from 'react-query' export interface Props { @@ -289,15 +289,7 @@ const TimelinePoll: React.FC = ({ return ( - ]} + components={[]} /> ) } diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index d2361aef..f699bf8e 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -6,10 +6,11 @@ import { getSettingsLanguage } from '@utils/slices/settingsSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import * as Localization from 'expo-localization' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Pressable } from 'react-native' import { Circle } from 'react-native-animated-spinkit' +import detectLanguage from 'react-native-language-detection' import { useSelector } from 'react-redux' export interface Props { @@ -25,42 +26,55 @@ const TimelineTranslate = React.memo( if (!highlighted) { return null } - if (!status.language) { - return null - } const { t } = useTranslation('componentTimeline') const { colors } = useTheme() - const tootLanguage = status.language.slice(0, 2) - - const settingsLanguage = useSelector(getSettingsLanguage) - - if (Localization.locale.includes(tootLanguage)) { - return null - } - if (settingsLanguage?.includes(tootLanguage)) { - return null - } - - let text = status.spoiler_text + const 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}:`, '') + text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ') } + text[i] = text[i] + .replace(/(<([^>]+)>)/gi, ' ') + .replace(/@.*? /gi, ' ') + .replace(/#.*? /gi, ' ') + .replace(/http(s):\/\/.*? /gi, ' ') } + const [detectedLanguage, setDetectedLanguage] = useState('') + useEffect(() => { + const detect = async () => { + const result = await detectLanguage(text.join(`\n\n`)) + setDetectedLanguage(result.detected.slice(0, 2)) + } + detect() + }, []) + + const settingsLanguage = useSelector(getSettingsLanguage) + const targetLanguage = settingsLanguage || Localization.locale || 'en' + const [enabled, setEnabled] = useState(false) const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ - source: status.language, - target: Localization.locale || settingsLanguage || 'en', + source: detectedLanguage, + target: targetLanguage, text, options: { enabled } }) + if (!detectedLanguage) { + return null + } + if (Localization.locale.slice(0, 2).includes(detectedLanguage)) { + return null + } + if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) { + return null + } + return ( <> {__DEV__ - ? ` Source: ${status.language}; Target: ${ - Localization.locale || settingsLanguage || 'en' - }` + ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined} {isLoading ? ( @@ -138,7 +150,6 @@ const TimelineTranslate = React.memo( ) }, (prev, next) => - prev.status.language === next.status.language && prev.status.content === next.status.content && prev.status.spoiler_text === next.status.spoiler_text ) diff --git a/src/components/mediaSelector.ts b/src/components/mediaSelector.ts index 50ad1adc..4d18cb78 100644 --- a/src/components/mediaSelector.ts +++ b/src/components/mediaSelector.ts @@ -1,17 +1,21 @@ 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, - UIImagePickerPresentationStyle -} from 'expo-image-picker/build/ImagePicker.types' +import { store } from '@root/store' +import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice' +import { manipulateAsync, SaveFormat } from 'expo-image-manipulator' +import * as ExpoImagePicker from 'expo-image-picker' import i18next from 'i18next' import { Alert, Linking, Platform } from 'react-native' +import ImagePicker, { + Image, + ImageOrVideo +} from 'react-native-image-crop-picker' export interface Props { - mediaTypes?: ImagePicker.MediaTypeOptions - resize?: { width?: number; height?: number } // Resize mode contain + mediaType?: 'photo' | 'video' + resize?: { width?: number; height?: number } + maximum?: number + indicateMaximum?: boolean showActionSheetWithOptions: ( options: ActionSheetOptions, callback: (i?: number | undefined) => void | Promise @@ -19,134 +23,194 @@ export interface Props { } const mediaSelector = async ({ - mediaTypes = ImagePicker.MediaTypeOptions.All, + mediaType, resize, + maximum, + indicateMaximum = false, showActionSheetWithOptions -}: Props): Promise => { - 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 } } - ]) +}: Props): Promise<({ uri: string } & Omit)[]> => { + const checkLibraryPermission = async (): Promise => { + const { status } = + await ExpoImagePicker.requestMediaLibraryPermissionsAsync() + if (status !== 'granted') { + Alert.alert( + i18next.t('componentMediaSelector:library.alert.title'), + i18next.t('componentMediaSelector:library.alert.message'), + [ + { + text: i18next.t('common: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:') + } + } + ] + ) + return false + } else { + return true + } + } + + const _maximum = + maximum || + getInstanceConfigurationStatusMaxAttachments(store.getState()) || + 4 + + const options = () => { + switch (mediaType) { + case 'photo': + return [ + i18next.t( + 'componentMediaSelector:options.image', + indicateMaximum ? { context: 'max', max: _maximum } : undefined + ), + i18next.t('common:buttons.cancel') + ] + case 'video': + return [ + i18next.t( + 'componentMediaSelector:options.video', + indicateMaximum ? { context: 'max', max: 1 } : undefined + ), + i18next.t('common:buttons.cancel') + ] + default: + return [ + i18next.t( + 'componentMediaSelector:options.image', + indicateMaximum ? { context: 'max', max: _maximum } : undefined + ), + i18next.t( + 'componentMediaSelector:options.video', + indicateMaximum ? { context: 'max', max: 1 } : undefined + ), + i18next.t('common:buttons.cancel') + ] + } + } + + return new Promise((resolve, reject) => { + const selectImage = async () => { + const images = await ImagePicker.openPicker({ + mediaType: 'photo', + includeExif: false, + multiple: true, + minFiles: 1, + maxFiles: _maximum, + smartAlbums: ['UserLibrary'], + writeTempFile: false, + loadingLabelText: '' + }).catch(() => {}) + + if (!images) { + return reject() + } + + // react-native-image-crop-picker may return HEIC as JPG that causes upload failure + if (Platform.OS === 'ios') { + for (const [index, image] of images.entries()) { + if (image.mime === 'image/heic') { + const converted = await manipulateAsync(image.sourceURL!, [], { + base64: false, + compress: 0.8, + format: SaveFormat.JPEG + }) + images[index] = { + ...images[index], + sourceURL: converted.uri, + mime: 'image/jpeg' + } } - } else { - newResult = await ImageManipulator.manipulateAsync(result.uri, [ - { resize } - ]) } - resolve({ ...newResult, cancelled: false }) + } + + if (!resize) { + return resolve( + images.map(image => ({ + ...image, + uri: image.sourceURL || `file://${image.path}` + })) + ) } else { - resolve(result) + const croppedImages: Image[] = [] + for (const image of images) { + const croppedImage = await ImagePicker.openCropper({ + mediaType: 'photo', + path: image.path, + width: resize.width, + height: resize.height, + cropperChooseText: i18next.t('common:buttons.apply'), + cropperCancelText: i18next.t('common:buttons.cancel'), + hideBottomControls: true + }).catch(() => {}) + croppedImage && croppedImages.push(croppedImage) + } + return resolve( + croppedImages.map(image => ({ + ...image, + uri: `file://${image.path}` + })) + ) } } + const selectVideo = async () => { + const video = await ImagePicker.openPicker({ + mediaType: 'video', + includeExif: false, + loadingLabelText: '' + }).catch(() => {}) + if (video) { + return resolve([ + { ...video, uri: video.sourceURL || `file://${video.path}` } + ]) + } else { + return reject() + } + } showActionSheetWithOptions( { title: i18next.t('componentMediaSelector:title'), - options: [ - i18next.t('componentMediaSelector:options.library'), - i18next.t('componentMediaSelector:options.photo'), - i18next.t('componentMediaSelector:options.cancel') - ], - cancelButtonIndex: 2 + options: options(), + cancelButtonIndex: mediaType ? 1 : 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, - presentationStyle: - Platform.OS === 'ios' && parseInt(Platform.Version) < 13 - ? UIImagePickerPresentationStyle.FULL_SCREEN - : UIImagePickerPresentationStyle.AUTOMATIC - }) + if (!(await checkLibraryPermission())) { + return reject() + } - if (!result.cancelled) { - await resolveResult(result) + switch (mediaType) { + case 'photo': + if (buttonIndex === 0) { + await selectImage() } - } - } 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) + break + case 'video': + if (buttonIndex === 0) { + await selectVideo() } - } + break + default: + if (buttonIndex === 0) { + await selectImage() + } else if (buttonIndex === 1) { + await selectVideo() + } + break } } ) diff --git a/src/components/mediaTransformation.ts b/src/components/mediaTransformation.ts new file mode 100644 index 00000000..29bb542d --- /dev/null +++ b/src/components/mediaTransformation.ts @@ -0,0 +1,149 @@ +import { store } from '@root/store' +import { getInstanceConfigurationMediaAttachments } from '@utils/slices/instancesSlice' +import { Action, manipulateAsync, SaveFormat } from 'expo-image-manipulator' +import i18next from 'i18next' +import { Platform } from 'react-native' +import ImagePicker from 'react-native-image-crop-picker' + +export interface Props { + type: 'image' | 'video' + uri: string // This can be pure path or uri starting with file:// + mime?: string + transform: { + imageFormat?: SaveFormat.JPEG | SaveFormat.PNG + resize?: boolean + width?: number + height?: number + } +} + +const getFileExtension = (uri: string) => { + const extension = uri.split('.').pop() + // Using mime type standard of jpeg + return extension === 'jpg' ? 'jpeg' : extension +} + +const mediaTransformation = async ({ + type, + uri, + mime, + transform +}: Props): Promise<{ + uri: string + mime: string + width: number + height: number +}> => { + const configurationMediaAttachments = + getInstanceConfigurationMediaAttachments(store.getState()) + + const fileExtension = getFileExtension(uri) + + switch (type) { + case 'image': + if (mime === 'image/gif' || fileExtension === 'gif') { + return Promise.reject('GIFs should not be transformed') + } + let targetFormat: SaveFormat.JPEG | SaveFormat.PNG = SaveFormat.JPEG + + const supportedImageTypes = + configurationMediaAttachments.supported_mime_types.filter(mime => + mime.startsWith('image/') + ) + + // @ts-ignore + const transformations: Action[] = [ + !transform.resize && (transform.width || transform.height) + ? { + resize: { width: transform.width, height: transform.height } + } + : null + ].filter(t => !!t) + + if (mime) { + if ( + mime !== `image/${fileExtension}` || + !supportedImageTypes.includes(mime) + ) { + targetFormat = transform.imageFormat || SaveFormat.JPEG + } else { + targetFormat = mime.split('/').pop() as any + } + } else { + if (!fileExtension) { + return Promise.reject('Unable to get file extension') + } + if (!supportedImageTypes.includes(`image/${fileExtension}`)) { + targetFormat = transform.imageFormat || SaveFormat.JPEG + } else { + targetFormat = fileExtension as any + } + } + + const converted = await manipulateAsync(uri, transformations, { + base64: false, + compress: Platform.OS === 'ios' ? 0.8 : 1, + format: targetFormat + }) + + if (transform.resize) { + const resized = await ImagePicker.openCropper({ + mediaType: 'photo', + path: converted.uri, + width: transform.width, + height: transform.height, + cropperChooseText: i18next.t('common:buttons.apply'), + cropperCancelText: i18next.t('common:buttons.cancel'), + hideBottomControls: true + }) + if (!resized) { + return Promise.reject('Resize failed') + } else { + return { + uri: resized.path, + mime: resized.mime, + width: resized.width, + height: resized.height + } + } + } else { + return { + uri: converted.uri, + mime: transform.imageFormat || SaveFormat.JPEG, + width: converted.width, + height: converted.height + } + } + case 'video': + const supportedVideoTypes = + configurationMediaAttachments.supported_mime_types.filter(mime => + mime.startsWith('video/') + ) + + if (mime) { + if (mime !== `video/${fileExtension}`) { + console.warn('Video mime type and file extension does not match') + } + if (!supportedVideoTypes.includes(mime)) { + return Promise.reject('Video file type is not supported') + } + } else { + if (!fileExtension) { + return Promise.reject('Unable to get file extension') + } + if (!supportedVideoTypes.includes(`video/${fileExtension}`)) { + return Promise.reject('Video file type is not supported') + } + } + + return { + uri: uri, + mime: mime || `video/${fileExtension}`, + width: 0, + height: 0 + } + break + } +} + +export default mediaTransformation diff --git a/src/i18n/de/components/contextMenu.json b/src/i18n/de/components/contextMenu.json new file mode 100644 index 00000000..3738d471 --- /dev/null +++ b/src/i18n/de/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "Funktionen für diesen Tröt - wie z. B. Autor und Originaltröt", + "account": { + "title": "", + "mute": { + "action": "Profil stummschalten" + }, + "block": { + "action": "Nutzer blockieren" + }, + "reports": { + "action": "User melden" + } + }, + "instance": { + "title": "", + "block": { + "action": "Instanz {{instance}} blockieren", + "alert": { + "title": "", + "message": "Üblicherweise kannst du einen User stummschalten oder blockieren.\nBlockierst du hingegegen eine Instanz, wird deren gesamter Inhalt samt Usern, die dir von dieser Instanz folgen, entfernt!", + "buttons": { + "confirm": "Bestätigen" + } + } + } + }, + "share": { + "status": { + "action": "Tröt teilen" + }, + "account": { + "action": "User verlinken" + } + }, + "status": { + "title": "", + "edit": { + "action": "Tröt bearbeiten" + }, + "delete": { + "action": "Tröt löschen", + "alert": { + "title": "Löschen bestätigen?", + "message": "", + "buttons": { + "confirm": "Bestätigen" + } + } + }, + "deleteEdit": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "Bestätigen" + } + } + }, + "mute": { + "action-muted_false": "", + "action-muted_true": "" + }, + "pin": { + "action-pinned_false": "", + "action-pinned_true": "" + } + } +} \ No newline at end of file diff --git a/src/i18n/de/components/instance.json b/src/i18n/de/components/instance.json index 9794585d..a3d49c32 100644 --- a/src/i18n/de/components/instance.json +++ b/src/i18n/de/components/instance.json @@ -11,8 +11,10 @@ "domains": "Domains" }, "disclaimer": { - "base": "Der Login erfolgt über den Browser, so dass Ihre Kontoinformationen für die Toot-App nicht sichtbar sind. Weitere Informationen", - "privacy": "Datenschutzbestimmungen" + "base": "Der Login erfolgt über den Browser, so dass Ihre Kontoinformationen für die Toot-App nicht sichtbar sind." + }, + "terms": { + "base": "" } }, "update": { diff --git a/src/i18n/de/components/mediaSelector.json b/src/i18n/de/components/mediaSelector.json index d460f005..8789b337 100644 --- a/src/i18n/de/components/mediaSelector.json +++ b/src/i18n/de/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "Datenquelle auswählen", "options": { - "library": "Hochladen", - "photo": "Bild aufnehmen", - "cancel": "$t(common:buttons.cancel)" + "image": "", + "image_max": "", + "video": "", + "video_max": "" }, "library": { "alert": { "title": "Kein Zugriff", "message": "Für den Upload ist eine Zugriffsgenehmigung erforderlich", "buttons": { - "settings": "Einstellungen bestätigen", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "Zugriff verweigert", - "message": "Zugriff auf die Kamera erforderlich", - "buttons": { - "settings": "Einstellungen übernehmen", - "cancel": "$t(common:buttons.cancel)" + "settings": "Einstellungen bestätigen" } } } diff --git a/src/i18n/de/components/timeline.json b/src/i18n/de/components/timeline.json index 0945c559..993af2ff 100644 --- a/src/i18n/de/components/timeline.json +++ b/src/i18n/de/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "Nachricht löschen" } - }, - "actions": { - "accessibilityHint": "Funktionen für diesen Tröt - wie z. B. Autor und Originaltröt", - "account": { - "heading": "Über die Nutzerin/den Nutzer", - "mute": { - "function": "Profil stummschalten", - "button": "@{{acct}} stummschalten" - }, - "block": { - "function": "Nutzer blockieren", - "button": "@{{acct}} blockieren" - }, - "reports": { - "function": "User melden", - "button": "@{{acct}} melden" - } - }, - "domain": { - "heading": "Über diese Instanz", - "block": { - "function": "Instanz blockieren", - "button": "Instanz {{domain}} blockieren" - }, - "alert": { - "title": "{{domain}} wirklich blockieren?", - "message": "Üblicherweise kannst du einen User stummschalten oder blockieren.\nBlockierst du hingegegen eine Instanz, wird deren gesamter Inhalt samt Usern, die dir von dieser Instanz folgen, entfernt!", - "buttons": { - "confirm": "Blockierung bestätigen", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "Tröt teilen", - "button": "Link zu diesem Tröt teilen" - }, - "account": { - "heading": "User verlinken", - "button": "Link zu diesem Benutzer teilen" - } - }, - "status": { - "heading": "Über Toot", - "edit": { - "function": "Tröt bearbeiten", - "button": "Diesen Tröt bearbeiten" - }, - "delete": { - "function": "Tröt löschen", - "button": "Diesen Tröt löschen", - "alert": { - "title": "Tröt sicher löschen?", - "message": "Bist du wirklich sicher, diesen Tröt löschen zu wollen? Sämtliche Boosts und Sterne werden samt der Antworten entfernt.", - "buttons": { - "confirm": "Löschen bestätigen", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "Tröt löschen", - "button": "Diesen Tröt neu entwerfen", - "alert": { - "title": "Tröt sicher löschen?", - "message": "Bist du wirklich sicher, diesen Tröt neu zu entwerfen? Alle Boosts und Sterne werden entfernt - samt der Antworten.", - "buttons": { - "confirm": "Löschen bestätigen", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "Tröt stummschalten", - "button": { - "positive": "Diesen Tröt sowie die Antworten stummschalten", - "negative": "Diesen Tröt sowie die Antworten nicht mehr stummschalten" - } - }, - "pin": { - "function": "Anheften", - "button": { - "positive": "Diesen Tröt anheften", - "negative": "Diesen Tröt nicht mehr anheften" - } - } - } } }, "poll": { diff --git a/src/i18n/en/_all.ts b/src/i18n/en/_all.ts index fa81be2e..5f2a7a82 100644 --- a/src/i18n/en/_all.ts +++ b/src/i18n/en/_all.ts @@ -8,6 +8,7 @@ export default { screenImageViewer: require('./screens/imageViewer'), screenTabs: require('./screens/tabs'), + componentContextMenu: require('./components/contextMenu'), componentEmojis: require('./components/emojis'), componentInstance: require('./components/instance'), componentMediaSelector: require('./components/mediaSelector'), diff --git a/src/i18n/en/components/contextMenu.json b/src/i18n/en/components/contextMenu.json new file mode 100644 index 00000000..538c7a4f --- /dev/null +++ b/src/i18n/en/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "Actions for this toot, such as its posted user, toot itself", + "account": { + "title": "User actions", + "mute": { + "action": "Mute user" + }, + "block": { + "action": "Block user" + }, + "reports": { + "action": "Report user" + } + }, + "instance": { + "title": "Instance action", + "block": { + "action": "Block instance {{instance}}", + "alert": { + "title": "Confirm blocking instance {{instance}} ?", + "message": "Mostly you can mute or block certain user.\n\nAfter blocking instance, all its content including followers from this instance will be removed!", + "buttons": { + "confirm": "Confirm" + } + } + } + }, + "share": { + "status": { + "action": "Share toot" + }, + "account": { + "action": "Share user" + } + }, + "status": { + "title": "Toot actions", + "edit": { + "action": "Edit toot" + }, + "delete": { + "action": "Delete toot", + "alert": { + "title": "Confirm deleting?", + "message": "All boosts and favourites will be cleared, including all replies.", + "buttons": { + "confirm": "Confirm" + } + } + }, + "deleteEdit": { + "action": "Delete toot and repost", + "alert": { + "title": "Confirm deleting and repost?", + "message": "All boosts and favourites will be cleared, including all replies.", + "buttons": { + "confirm": "Confirm" + } + } + }, + "mute": { + "action-muted_false": "Mute toot and replies", + "action-muted_true": "Unmute toot and replies" + }, + "pin": { + "action-pinned_false": "Pin toot", + "action-pinned_true": "Unpin toot" + } + } +} \ No newline at end of file diff --git a/src/i18n/en/components/instance.json b/src/i18n/en/components/instance.json index c9d1f43e..73412475 100644 --- a/src/i18n/en/components/instance.json +++ b/src/i18n/en/components/instance.json @@ -11,8 +11,10 @@ "domains": "Universes" }, "disclaimer": { - "base": "Logging in process uses system broswer that, your account information won't be visible to tooot app. Read more ", - "privacy": "privacy policy" + "base": "Logging in process uses system broswer that, your account information won't be visible to tooot app." + }, + "terms": { + "base": "By logging in, you agree to the <0>privacy policy and <1>terms of service." } }, "update": { diff --git a/src/i18n/en/components/mediaSelector.json b/src/i18n/en/components/mediaSelector.json index 6f509ea7..adc0fa47 100644 --- a/src/i18n/en/components/mediaSelector.json +++ b/src/i18n/en/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "Select media source", "options": { - "library": "Upload from library", - "photo": "Take a photo", - "cancel": "$t(common:buttons.cancel)" + "image": "Upload photos", + "image_max": "Upload photos (max {{max}})", + "video": "Upload video", + "video_max": "Upload video (max {{max}})" }, "library": { "alert": { "title": "No permission", "message": "Require photo library read permission to upload", "buttons": { - "settings": "Update setting", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "No permission", - "message": "Require camera usage permission to upload", - "buttons": { - "settings": "Update setting", - "cancel": "$t(common:buttons.cancel)" + "settings": "Update setting" } } } diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index caeb15ed..6a3341a1 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "Delete direct message" } - }, - "actions": { - "accessibilityHint": "Actions for this toot, such as its posted user, toot itself", - "account": { - "heading": "About user", - "mute": { - "function": "Mute user", - "button": "Mute @{{acct}}" - }, - "block": { - "function": "Block user", - "button": "Block @{{acct}}" - }, - "reports": { - "function": "Report user", - "button": "Report @{{acct}}" - } - }, - "domain": { - "heading": "About instance", - "block": { - "function": "Block instance", - "button": "Block instance {{domain}}" - }, - "alert": { - "title": "Confirm blocking {{domain}} ?", - "message": "Mostly you can mute or block certain user.\n\nAfter blocking instance, all its content including followers from this instance will be removed!", - "buttons": { - "confirm": "Confirm blocking", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "Share toot", - "button": "Share link to this toot" - }, - "account": { - "heading": "Share user", - "button": "Share link to this user" - } - }, - "status": { - "heading": "About toot", - "edit": { - "function": "Edit toot", - "button": "Edit this toot" - }, - "delete": { - "function": "Delete toot", - "button": "Delete this toot", - "alert": { - "title": "Confirm deleting toot?", - "message": "Are you sure to delete this toot? All boosts and favourites will be cleared, including all replies.", - "buttons": { - "confirm": "Confirm deleting", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "Delete toot", - "button": "Delete and re-draft", - "alert": { - "title": "Confirm deleting toot?", - "message": "Are you sure to delete and re-draft this toot? All boosts and favourites will be cleared, including all replies.", - "buttons": { - "confirm": "Confirm deleting", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "Mute toot", - "button": { - "positive": "Mute this toot and replies", - "negative": "Unmute this toot and replies" - } - }, - "pin": { - "function": "Pin", - "button": { - "positive": "Pin this toot", - "negative": "Unpin this toot" - } - } - } } }, "poll": { diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 5e951716..37ace006 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -60,8 +60,8 @@ i18n.use(initReactI18next).init({ returnEmptyString: false, saveMissing: true, - missingKeyHandler: (ns, key) => { - console.log('i18n missing: ' + ns + ' : ' + key) + missingKeyHandler: (_, ns, key) => { + console.log('i18n missing', ns, key) }, interpolation: { diff --git a/src/i18n/it/components/contextMenu.json b/src/i18n/it/components/contextMenu.json new file mode 100644 index 00000000..9a138494 --- /dev/null +++ b/src/i18n/it/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "Azioni per questo toot, per l'utente che l'ha pubblicato o per il toot stesso", + "account": { + "title": "", + "mute": { + "action": "Muta utente" + }, + "block": { + "action": "Blocca utente" + }, + "reports": { + "action": "Segnala utente" + } + }, + "instance": { + "title": "", + "block": { + "action": "Blocca istanza {{instance}}", + "alert": { + "title": "", + "message": "Sarebbe meglio mutare o bloccare singoli utenti.\n\nSe blocchi un'istanza, tutti i suoi contenuti a te relativi, inclusi tutti i tuoi seguaci da questa, saranno rimossi.", + "buttons": { + "confirm": "Ho capito" + } + } + } + }, + "share": { + "status": { + "action": "Condividi toot" + }, + "account": { + "action": "Condividi utente" + } + }, + "status": { + "title": "", + "edit": { + "action": "Modifica toot" + }, + "delete": { + "action": "Cancella toot", + "alert": { + "title": "Conferma?", + "message": "", + "buttons": { + "confirm": "Ho capito" + } + } + }, + "deleteEdit": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "Ho capito" + } + } + }, + "mute": { + "action-muted_false": "", + "action-muted_true": "" + }, + "pin": { + "action-pinned_false": "", + "action-pinned_true": "" + } + } +} \ No newline at end of file diff --git a/src/i18n/it/components/instance.json b/src/i18n/it/components/instance.json index 59ba3636..59fd2652 100644 --- a/src/i18n/it/components/instance.json +++ b/src/i18n/it/components/instance.json @@ -11,8 +11,10 @@ "domains": "Universi" }, "disclaimer": { - "base": "Per accedere, verrà aperta una pagina del browser di sistema. I dati di accesso del tuo account sono protetti. Leggi di più:", - "privacy": "politica sulla privacy" + "base": "Per accedere, verrà aperta una pagina del browser di sistema. I dati di accesso del tuo account sono protetti." + }, + "terms": { + "base": "" } }, "update": { diff --git a/src/i18n/it/components/mediaSelector.json b/src/i18n/it/components/mediaSelector.json index 5ab89dd9..57535ef6 100644 --- a/src/i18n/it/components/mediaSelector.json +++ b/src/i18n/it/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "Seleziona origine media", "options": { - "library": "Carica da libreria locale", - "photo": "Scatta una foto", - "cancel": "$t(common:buttons.cancel)" + "image": "", + "image_max": "", + "video": "", + "video_max": "" }, "library": { "alert": { "title": "Permesso non concesso", "message": "È richiesto l'accesso ai file del dispositivo per il caricamento dalla libreria", "buttons": { - "settings": "Correggi impostazioni", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "Permesso non concesso", - "message": "È richiesto l'accesso alla fotocamera per scattare foto", - "buttons": { - "settings": "Correggi impostazioni", - "cancel": "$t(common:buttons.cancel)" + "settings": "Correggi impostazioni" } } } diff --git a/src/i18n/it/components/timeline.json b/src/i18n/it/components/timeline.json index 9455ea6f..7efa9234 100644 --- a/src/i18n/it/components/timeline.json +++ b/src/i18n/it/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "Cancella messaggio" } - }, - "actions": { - "accessibilityHint": "Azioni per questo toot, per l'utente che l'ha pubblicato o per il toot stesso", - "account": { - "heading": "Riguardo quest'utente", - "mute": { - "function": "Muta utente", - "button": "Muta @{{acct}}" - }, - "block": { - "function": "Blocca utente", - "button": "Blocca @{{acct}}" - }, - "reports": { - "function": "Segnala utente", - "button": "Segnala @{{acct}}" - } - }, - "domain": { - "heading": "Riguardo questa istanza", - "block": { - "function": "Blocca istanza", - "button": "Blocca istanza {{domain}}" - }, - "alert": { - "title": "Conferma blocco di {{domain}} ?", - "message": "Sarebbe meglio mutare o bloccare singoli utenti.\n\nSe blocchi un'istanza, tutti i suoi contenuti a te relativi, inclusi tutti i tuoi seguaci da questa, saranno rimossi.", - "buttons": { - "confirm": "Conferma blocco", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "Condividi toot", - "button": "Condividi il link a questo toot" - }, - "account": { - "heading": "Condividi utente", - "button": "Share il link a questo utente" - } - }, - "status": { - "heading": "Riguardo questo toot", - "edit": { - "function": "Modifica toot", - "button": "Modifica questo toot" - }, - "delete": { - "function": "Cancella toot", - "button": "Cancella toot", - "alert": { - "title": "Cancellare il toot?", - "message": "Vuoi davvero cancellare questo toot? Tutti gli apprezzamenti, le ricondivisioni, e le risposte verranno persi.", - "buttons": { - "confirm": "Conferma", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "Cancella toot", - "button": "Cancella e riscrivi toot", - "alert": { - "title": "Cancellare e riscrivere il toot?", - "message": "Vuoi davvero cancellare e riscrivere questo toot? Tutti gli apprezzamenti, le ricondivisioni, e le risposte verranno persi.", - "buttons": { - "confirm": "Conferma", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "Muta toot", - "button": { - "positive": "Muta questo toot e le sue risposte", - "negative": "Smuta questo toot e le sue risposte" - } - }, - "pin": { - "function": "Fissa in cima", - "button": { - "positive": "Fissa questo toot in cima al profilo", - "negative": "Togli questo toot dalla cima del profilo" - } - } - } } }, "poll": { diff --git a/src/i18n/ko/components/contextMenu.json b/src/i18n/ko/components/contextMenu.json new file mode 100644 index 00000000..b1f3ba27 --- /dev/null +++ b/src/i18n/ko/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "이 툿에 할 동작, 툿 자체나 포스트한 사용자", + "account": { + "title": "", + "mute": { + "action": "사용자 음소거" + }, + "block": { + "action": "사용자 차단" + }, + "reports": { + "action": "사용자 신고" + } + }, + "instance": { + "title": "", + "block": { + "action": "인스턴스 {{instance}} 차단", + "alert": { + "title": "", + "message": "보통은 유저 음소거나 차단으로 충분해요.\n\n인스턴스를 차단하면, 팔로워를 포함하는 인스턴스의 모든 콘텐츠가 삭제됩니다!", + "buttons": { + "confirm": "확인" + } + } + } + }, + "share": { + "status": { + "action": "툿 공유" + }, + "account": { + "action": "사용자 공유" + } + }, + "status": { + "title": "", + "edit": { + "action": "" + }, + "delete": { + "action": "툿 삭제", + "alert": { + "title": "삭제 확인?", + "message": "", + "buttons": { + "confirm": "확인" + } + } + }, + "deleteEdit": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "확인" + } + } + }, + "mute": { + "action-muted_false": "", + "action-muted_true": "" + }, + "pin": { + "action-pinned_false": "", + "action-pinned_true": "" + } + } +} \ No newline at end of file diff --git a/src/i18n/ko/components/instance.json b/src/i18n/ko/components/instance.json index b1abaf03..47d7b29a 100644 --- a/src/i18n/ko/components/instance.json +++ b/src/i18n/ko/components/instance.json @@ -11,8 +11,10 @@ "domains": "연합" }, "disclaimer": { - "base": "로그인 과정에서는 시스템 브라우저를 사용해, tooot 앱이 당신의 계정 정보를 볼 수 없어요.\n더 알아보기 ", - "privacy": "개인정보 정책" + "base": "로그인 과정에서는 시스템 브라우저를 사용해, tooot 앱이 당신의 계정 정보를 볼 수 없어요." + }, + "terms": { + "base": "" } }, "update": { diff --git a/src/i18n/ko/components/mediaSelector.json b/src/i18n/ko/components/mediaSelector.json index 7d9fb7ce..2d15ced1 100644 --- a/src/i18n/ko/components/mediaSelector.json +++ b/src/i18n/ko/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "미디어 소스 선택", "options": { - "library": "라이브러리에서 업로드", - "photo": "사진 촬영", - "cancel": "$t(common:buttons.cancel)" + "image": "", + "image_max": "", + "video": "", + "video_max": "" }, "library": { "alert": { "title": "권한 없음", "message": "업로드를 위해 사진 라이브러리 권한이 필요해요", "buttons": { - "settings": "설정 업데이트", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "권한 없음", - "message": "업로드를 위해 카메라 사용 권한이 필요해요", - "buttons": { - "settings": "설정 업데이트", - "cancel": "$t(common:buttons.cancel)" + "settings": "설정 업데이트" } } } diff --git a/src/i18n/ko/components/timeline.json b/src/i18n/ko/components/timeline.json index 5b8f6e97..2cdda060 100644 --- a/src/i18n/ko/components/timeline.json +++ b/src/i18n/ko/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "개인 메시지 삭제" } - }, - "actions": { - "accessibilityHint": "이 툿에 할 동작, 툿 자체나 포스트한 사용자", - "account": { - "heading": "사용자 정보", - "mute": { - "function": "사용자 음소거", - "button": "@{{acct}} 음소거" - }, - "block": { - "function": "사용자 차단", - "button": "@{{acct}} 차단" - }, - "reports": { - "function": "사용자 신고", - "button": "@{{acct}} 신고" - } - }, - "domain": { - "heading": "인스턴스 정보", - "block": { - "function": "인스턴스 차단", - "button": "인스턴스 {{domain}} 차단" - }, - "alert": { - "title": "{{domain}}을 정말 차단할까요?", - "message": "보통은 유저 음소거나 차단으로 충분해요.\n\n인스턴스를 차단하면, 팔로워를 포함하는 인스턴스의 모든 콘텐츠가 삭제됩니다!", - "buttons": { - "confirm": "차단 확인", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "툿 공유", - "button": "이 툿의 링크 공유" - }, - "account": { - "heading": "사용자 공유", - "button": "이 사용자에게 링크 공유" - } - }, - "status": { - "heading": "툿 정보", - "edit": { - "function": "", - "button": "" - }, - "delete": { - "function": "툿 삭제", - "button": "이 툿 삭제", - "alert": { - "title": "툿을 정말 삭제할까요?", - "message": "", - "buttons": { - "confirm": "삭제 확인", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "툿 삭제", - "button": "삭제하고 다시 쓰기", - "alert": { - "title": "툿을 정말 삭제할까요?", - "message": "이 툿을 삭제하고 다시 초안을 작성하시겠어요? 모든 답장, 부스트와 즐겨찾기가 지워져요.", - "buttons": { - "confirm": "삭제 확인", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "툿 음소거", - "button": { - "positive": "이 툿과 답장 음소거", - "negative": "이 툿과 답장 음소거 해제" - } - }, - "pin": { - "function": "고정", - "button": { - "positive": "이 툿 고정", - "negative": "이 툿 고정 해제" - } - } - } } }, "poll": { diff --git a/src/i18n/pt_BR/components/contextMenu.json b/src/i18n/pt_BR/components/contextMenu.json new file mode 100644 index 00000000..f7ee3d83 --- /dev/null +++ b/src/i18n/pt_BR/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "Ações para este toot, como o seu usuário publicado", + "account": { + "title": "Ações do Usuário", + "mute": { + "action": "Silenciar usuário" + }, + "block": { + "action": "Bloquear usuário" + }, + "reports": { + "action": "Denunciar usuário" + } + }, + "instance": { + "title": "Ação da Instância", + "block": { + "action": "Bloquear a instância {{instance}}", + "alert": { + "title": "Confirmar o bloqueio da instância {{instance}}?", + "message": "Na maioria das vezes, você pode silenciar ou bloquear determinado usuário.\n\nDepois de bloquear a instância, todo seu conteúdo, incluindo seguidores, será removido!", + "buttons": { + "confirm": "Confirmar" + } + } + } + }, + "share": { + "status": { + "action": "Compartilhar toot" + }, + "account": { + "action": "Compartilhar Usuário" + } + }, + "status": { + "title": "Ações do toot", + "edit": { + "action": "Editar toot" + }, + "delete": { + "action": "Remover toot", + "alert": { + "title": "Confirme a exclusão?", + "message": "Todos os boosts e favoritos serão limpos, incluindo todas as respostas.", + "buttons": { + "confirm": "Confirmar" + } + } + }, + "deleteEdit": { + "action": "Excluir toot e repostar", + "alert": { + "title": "Confirmar exclusão e repostagem?", + "message": "Todos os boosts e favoritos serão limpos, incluindo todas as respostas.", + "buttons": { + "confirm": "Confirmar" + } + } + }, + "mute": { + "action-muted_false": "Silenciar este toot e respostas", + "action-muted_true": "Desbloquear este toot e respostas" + }, + "pin": { + "action-pinned_false": "Toot fixado", + "action-pinned_true": "Desafixar toot" + } + } +} \ No newline at end of file diff --git a/src/i18n/pt_BR/components/instance.json b/src/i18n/pt_BR/components/instance.json index 7d619638..8f237b89 100644 --- a/src/i18n/pt_BR/components/instance.json +++ b/src/i18n/pt_BR/components/instance.json @@ -11,8 +11,10 @@ "domains": "Universo" }, "disclaimer": { - "base": "O processo de login usa o navegador do sistema e as informações da sua conta não estarão visíveis para o aplicativo Tooot. Consulte Mais informação ", - "privacy": "política de privacidade" + "base": "O processo de login usa o navegador do sistema e as informações da sua conta não estarão visíveis para o aplicativo Tooot." + }, + "terms": { + "base": "Ao fazer o login, você concorda com a <0>política de privacidade e os <1>termos de serviço." } }, "update": { diff --git a/src/i18n/pt_BR/components/mediaSelector.json b/src/i18n/pt_BR/components/mediaSelector.json index bbb7c59a..f100048b 100644 --- a/src/i18n/pt_BR/components/mediaSelector.json +++ b/src/i18n/pt_BR/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "Selecionar fonte de mídia", "options": { - "library": "Carregar da biblioteca", - "photo": "Tirar foto", - "cancel": "$t(common:buttons.cancel)" + "image": "Enviar fotos", + "image_max": "Carregar fotos (máx. {{max}})", + "video": "Enviar vídeo", + "video_max": "Carregar vídeo (máx. {{max}})" }, "library": { "alert": { "title": "Sem permissão", "message": "Exigir permissão de leitura da biblioteca de fotos para fazer upload", "buttons": { - "settings": "Atualizar configurações", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "Sem permissão", - "message": "Requer permissão de uso da câmera para fazer upload", - "buttons": { - "settings": "Atualizar configurações", - "cancel": "$t(common:buttons.cancel)" + "settings": "Atualizar configurações" } } } diff --git a/src/i18n/pt_BR/components/timeline.json b/src/i18n/pt_BR/components/timeline.json index 36d0f77a..cc998cc9 100644 --- a/src/i18n/pt_BR/components/timeline.json +++ b/src/i18n/pt_BR/components/timeline.json @@ -30,7 +30,7 @@ "default": "{{name}} boostou", "notification": "{{name}} deu boost no teu toot" }, - "update": "" + "update": "Toot foi editado" }, "actions": { "reply": { @@ -46,7 +46,7 @@ }, "bookmarked": { "accessibilityLabel": "Adicionar este toot aos favoritos", - "function": "Favoritos" + "function": "Salvos" } }, "actionsUsers": { @@ -123,94 +123,6 @@ "delete": { "function": "Excluir mensagem direta" } - }, - "actions": { - "accessibilityHint": "Ações para este toot, como o seu usuário publicado", - "account": { - "heading": "Sobre o usuário", - "mute": { - "function": "Silenciar usuário", - "button": "Silenciar @{{acct}}" - }, - "block": { - "function": "Bloquear usuário", - "button": "Bloquear @{{acct}}" - }, - "reports": { - "function": "Denunciar usuário", - "button": "Reportar @{{acct}}" - } - }, - "domain": { - "heading": "Sobre a instância", - "block": { - "function": "Bloquear instância", - "button": "Bloquear a instância {{domain}}" - }, - "alert": { - "title": "Confirma bloquear {{domain}}?", - "message": "Na maioria das vezes, você pode silenciar ou bloquear determinado usuário.\n\nDepois de bloquear a instância, todo seu conteúdo, incluindo seguidores, será removido!", - "buttons": { - "confirm": "Confirmar bloqueio", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "Compartilhar toot", - "button": "Compartilhar o link para este mundo" - }, - "account": { - "heading": "Compartilhar Usuário", - "button": "Compartilhar link para este usuário" - } - }, - "status": { - "heading": "Sobre o toot", - "edit": { - "function": "Editar toot", - "button": "Editar este toot" - }, - "delete": { - "function": "Remover toot", - "button": "Deletar este toot", - "alert": { - "title": "Confirmar exclusão?", - "message": "Tem certeza que deseja excluir este toot? Todos os boosts e favoritos serão apagados, incluindo todas as respostas.", - "buttons": { - "confirm": "Confirme a exclusão", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "Remover toot", - "button": "Excluir e rascunhar", - "alert": { - "title": "Confirmar exclusão?", - "message": "Tem certeza que deseja excluir este toot? Todos os boosts e favoritos serão apagados, incluindo todas as respostas.", - "buttons": { - "confirm": "Confirme a exclusão", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "Silenciar toot", - "button": { - "positive": "Silenciar este toot e respostas", - "negative": "Desbloquear este toot e respostas" - } - }, - "pin": { - "function": "Fixar", - "button": { - "positive": "Fixar este toot", - "negative": "Desafixar este toot" - } - } - } } }, "poll": { diff --git a/src/i18n/pt_BR/screens/actions.json b/src/i18n/pt_BR/screens/actions.json index 19cec928..fa615e27 100644 --- a/src/i18n/pt_BR/screens/actions.json +++ b/src/i18n/pt_BR/screens/actions.json @@ -1,7 +1,7 @@ { "content": { "altText": { - "heading": "" + "heading": "Texto Alternativo" }, "notificationsFilter": { "heading": "Exibir notificações", @@ -12,8 +12,8 @@ "reblog": "$t(screenTabs:me.push.reblog.heading)", "mention": "$t(screenTabs:me.push.mention.heading)", "poll": "$t(screenTabs:me.push.poll.heading)", - "status": "", - "update": "" + "status": "Toot de usuários inscritos", + "update": "Toot foi editado" } } } diff --git a/src/i18n/pt_BR/screens/compose.json b/src/i18n/pt_BR/screens/compose.json index f226aa0f..aa3677e0 100644 --- a/src/i18n/pt_BR/screens/compose.json +++ b/src/i18n/pt_BR/screens/compose.json @@ -15,7 +15,7 @@ "button": { "default": "Toot", "conversation": "Toot DM", - "reply": "Resposta de toot", + "reply": "Responder", "deleteEdit": "Toot", "edit": "Toot", "share": "Toot" @@ -168,7 +168,7 @@ "header": { "title": "Rascunho" }, - "warning": "", + "warning": "Os rascunhos são armazenados localmente e podem ser perdidos em eventos infortúnios. Aconselhamos não usar rascunhos para armazenamento de longo prazo.", "content": { "accessibilityHint": "Toque para editar este rascunho", "textEmpty": "O conteúdo está vazio" diff --git a/src/i18n/pt_BR/screens/tabs.json b/src/i18n/pt_BR/screens/tabs.json index 0d52e55b..8581178c 100644 --- a/src/i18n/pt_BR/screens/tabs.json +++ b/src/i18n/pt_BR/screens/tabs.json @@ -32,7 +32,7 @@ "me": { "stacks": { "bookmarks": { - "name": "Favoritos" + "name": "Salvos" }, "conversations": { "name": "Mensagens diretas" @@ -82,7 +82,7 @@ "demo": "

Esta é uma demonstração também😊. Você pode escolher entre várias opções abaixo.

Esta configuração afeta apenas o conteúdo principal dos toots, mas não os tamanhos de outra fonte.

", "availableSizes": "Tamanhos disponíveis", "sizes": { - "S": "S", + "S": "P", "M": "M - Padrão", "L": "Grande", "XL": "Extra grande", @@ -170,7 +170,7 @@ "heading": "Novo seguidor" }, "follow_request": { - "heading": "" + "heading": "Solicitações de seguidores pendentes" }, "favourite": { "heading": "Favoritos" @@ -185,7 +185,7 @@ "heading": "Pesquisa atualizada" }, "status": { - "heading": "" + "heading": "Toot de usuários inscritos" }, "howitworks": "Saiba como funciona o roteamento" }, diff --git a/src/i18n/vi/components/contextMenu.json b/src/i18n/vi/components/contextMenu.json new file mode 100644 index 00000000..37be0217 --- /dev/null +++ b/src/i18n/vi/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "Hành động với tút này, bao gồm đăng thủ công hay đăng tự động", + "account": { + "title": "Hành động người dùng", + "mute": { + "action": "Ẩn người này" + }, + "block": { + "action": "Chặn người này" + }, + "reports": { + "action": "Báo cáo" + } + }, + "instance": { + "title": "Hành động máy chủ", + "block": { + "action": "Chặn {{instance}}", + "alert": { + "title": "Xác nhận chặn {{instance}} ?", + "message": "Bạn có thể ẩn hoặc chặn bất kỳ người nào.\n\nĐối với máy chủ, toàn bộ nội dung bao gồm người theo dõi bạn từ máy chủ đó cũng sẽ bị chặn!", + "buttons": { + "confirm": "Xác nhận" + } + } + } + }, + "share": { + "status": { + "action": "Đăng lại" + }, + "account": { + "action": "Chia sẻ" + } + }, + "status": { + "title": "Hành động tút", + "edit": { + "action": "Sửa tút" + }, + "delete": { + "action": "Xóa tút", + "alert": { + "title": "Tiếp tục xóa?", + "message": "Tất cả lượt thích và đăng lại sẽ bị mất, bao gồm cả những trả lời.", + "buttons": { + "confirm": "Xác nhận" + } + } + }, + "deleteEdit": { + "action": "Xóa và đăng lại", + "alert": { + "title": "Tiếp tục xóa và đăng lại?", + "message": "Tất cả lượt thích và đăng lại sẽ bị mất, bao gồm cả những trả lời.", + "buttons": { + "confirm": "Xác nhận" + } + } + }, + "mute": { + "action-muted_false": "Ẩn tút này", + "action-muted_true": "Bỏ ẩn tút này" + }, + "pin": { + "action-pinned_false": "Tút ghim", + "action-pinned_true": "Bỏ ghim tút" + } + } +} \ No newline at end of file diff --git a/src/i18n/vi/components/instance.json b/src/i18n/vi/components/instance.json index ee773025..901fbd3a 100644 --- a/src/i18n/vi/components/instance.json +++ b/src/i18n/vi/components/instance.json @@ -11,8 +11,10 @@ "domains": "Liên hợp" }, "disclaimer": { - "base": "tooot sẽ không thấy được thông tin tài khoản của bạn. Tìm hiểu thêm ", - "privacy": "Bảo mật" + "base": "tooot sẽ không thấy được thông tin tài khoản của bạn." + }, + "terms": { + "base": "Đăng nhập nghĩa là bạn đồng ý <0>chính sách bảo mật và <1>điều khoản dịch vụ." } }, "update": { diff --git a/src/i18n/vi/components/mediaSelector.json b/src/i18n/vi/components/mediaSelector.json index 022097e8..e06ffcbc 100644 --- a/src/i18n/vi/components/mediaSelector.json +++ b/src/i18n/vi/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "Chọn nguồn", "options": { - "library": "Từ thiết bị", - "photo": "Chụp ảnh", - "cancel": "$t(common:buttons.cancel)" + "image": "Tải ảnh lên", + "image_max": "Tải ảnh lên (tối đa {{max}})", + "video": "Tải video lên", + "video_max": "Tải video lên (tối đa {{max}})" }, "library": { "alert": { "title": "Chưa được cấp quyền", "message": "Bạn cần cấp quyền đọc thư viện ảnh trước", "buttons": { - "settings": "Cài đặt cập nhật", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "Chưa cấp quyền", - "message": "Cần cấp quyền sử dụng camera trước", - "buttons": { - "settings": "Cài đặt cập nhật", - "cancel": "$t(common:buttons.cancel)" + "settings": "Cài đặt cập nhật" } } } diff --git a/src/i18n/vi/components/timeline.json b/src/i18n/vi/components/timeline.json index 92224828..73db2dbe 100644 --- a/src/i18n/vi/components/timeline.json +++ b/src/i18n/vi/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "Xóa nhắn riêng" } - }, - "actions": { - "accessibilityHint": "Hành động cho tút này, bao gồm đăng thủ công hay đăng tự động", - "account": { - "heading": "Đối với người dùng", - "mute": { - "function": "Ẩn người dùng", - "button": "Ẩn @{{acct}}" - }, - "block": { - "function": "Chặn người dùng", - "button": "Chặn @{{acct}}" - }, - "reports": { - "function": "Báo cáo người dùng", - "button": "Báo cáo @{{acct}}" - } - }, - "domain": { - "heading": "Đối với máy chủ", - "block": { - "function": "Chặn máy chủ", - "button": "Chặn {{domain}}" - }, - "alert": { - "title": "Bạn có chắc muốn chặn {{domain}}?", - "message": "Bạn có thể ẩn hoặc chặn bất kỳ người nào.\n\nĐối với máy chủ, toàn bộ nội dung bao gồm người theo dõi bạn từ máy chủ đó cũng sẽ bị chặn!", - "buttons": { - "confirm": "Tiếp tục chặn", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "Đăng lại", - "button": "Đăng lại URL tút" - }, - "account": { - "heading": "Chia sẻ", - "button": "Chia sẻ URL người dùng này" - } - }, - "status": { - "heading": "Đối với tút", - "edit": { - "function": "Sửa tút", - "button": "Sửa tút này" - }, - "delete": { - "function": "Xóa tút", - "button": "Xóa tút này", - "alert": { - "title": "Vẫn xóa tút?", - "message": "Bạn có chắc muốn xóa tút này? Toàn bộ lượt thích, đăng lại và trả lời tút cũng sẽ bị xóa theo.", - "buttons": { - "confirm": "Xác nhận xóa", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "Xóa tút", - "button": "Xóa & viết lại", - "alert": { - "title": "Xác nhận xóa tút?", - "message": "Bạn có chắc muốn xóa và viết lại tút này? Toàn bộ lượt thích, đăng lại và trả lời tút cũng sẽ bị xóa theo.", - "buttons": { - "confirm": "Xác nhận xóa", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "Ẩn tút", - "button": { - "positive": "Ẩn tút này", - "negative": "Bỏ ẩn tút này" - } - }, - "pin": { - "function": "Ghim", - "button": { - "positive": "Ghim tút này", - "negative": "Bỏ ghim tút này" - } - } - } } }, "poll": { diff --git a/src/i18n/vi/screens/actions.json b/src/i18n/vi/screens/actions.json index 3a50892f..516c6077 100644 --- a/src/i18n/vi/screens/actions.json +++ b/src/i18n/vi/screens/actions.json @@ -1,7 +1,7 @@ { "content": { "altText": { - "heading": "" + "heading": "Văn bản thay thế" }, "notificationsFilter": { "heading": "Những kiểu thông báo cho phép", diff --git a/src/i18n/zh-Hans/components/contextMenu.json b/src/i18n/zh-Hans/components/contextMenu.json new file mode 100644 index 00000000..a167fe6f --- /dev/null +++ b/src/i18n/zh-Hans/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "更多关于此条嘟文,例如发布者等", + "account": { + "title": "用户操作", + "mute": { + "action": "静音用户" + }, + "block": { + "action": "屏蔽用户" + }, + "reports": { + "action": "举报用户" + } + }, + "instance": { + "title": "实例操作", + "block": { + "action": "屏蔽实例 {{instance}}", + "alert": { + "title": "确认屏蔽实例 {{instance}}?", + "message": "多数情况下,隐藏或屏蔽特定用户即可。\n\n屏蔽之后,来自此实例的所有内容将被移除。", + "buttons": { + "confirm": "确认" + } + } + } + }, + "share": { + "status": { + "action": "分享嘟文" + }, + "account": { + "action": "分享用户" + } + }, + "status": { + "title": "嘟文操作", + "edit": { + "action": "编辑嘟文" + }, + "delete": { + "action": "删除嘟文", + "alert": { + "title": "确认删除?", + "message": "所以转发及收藏将被清除,包括所有回复。", + "buttons": { + "confirm": "确认" + } + } + }, + "deleteEdit": { + "action": "删除嘟文并重新发布", + "alert": { + "title": "确认删除并重新发布?", + "message": "所以转发及收藏将被清除,包括所有回复。", + "buttons": { + "confirm": "确认" + } + } + }, + "mute": { + "action-muted_false": "静音嘟文及回复", + "action-muted_true": "取消静音嘟文及回复" + }, + "pin": { + "action-pinned_false": "置顶嘟文", + "action-pinned_true": "取消置顶嘟文" + } + } +} \ No newline at end of file diff --git a/src/i18n/zh-Hans/components/instance.json b/src/i18n/zh-Hans/components/instance.json index 1ac79f8f..bdda2741 100644 --- a/src/i18n/zh-Hans/components/instance.json +++ b/src/i18n/zh-Hans/components/instance.json @@ -11,8 +11,10 @@ "domains": "连结总数" }, "disclaimer": { - "base": "登录过程将使用系统浏览器,你的账号登录信息tooot应用无法读取。详见 ", - "privacy": "隐私条款" + "base": "登录过程将使用系统浏览器,你的账号登录信息tooot应用无法读取。" + }, + "terms": { + "base": "登录即表示您同意<0>隐私政策和<1>服务条款。" } }, "update": { diff --git a/src/i18n/zh-Hans/components/mediaSelector.json b/src/i18n/zh-Hans/components/mediaSelector.json index 1b4cc25e..039007b7 100644 --- a/src/i18n/zh-Hans/components/mediaSelector.json +++ b/src/i18n/zh-Hans/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "选择媒体", "options": { - "library": "从相册上传", - "photo": "拍摄照片", - "cancel": "$t(common:buttons.cancel)" + "image": "上传图片", + "image_max": "上传照片(上限 {{max}})", + "video": "上传视频", + "video_max": "上传视频(上限 {{max}})" }, "library": { "alert": { "title": "无权限", "message": "需要读取相册权限才能上传附件", "buttons": { - "settings": "去更新设置", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "无权限", - "message": "需要使用相机权限才能上传附件", - "buttons": { - "settings": "去更新设置", - "cancel": "$t(common:buttons.cancel)" + "settings": "去更新设置" } } } diff --git a/src/i18n/zh-Hans/components/timeline.json b/src/i18n/zh-Hans/components/timeline.json index 6347abaa..3fcbbf1d 100644 --- a/src/i18n/zh-Hans/components/timeline.json +++ b/src/i18n/zh-Hans/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "删除私信" } - }, - "actions": { - "accessibilityHint": "更多关于此条嘟文,例如发布者等", - "account": { - "heading": "关于用户", - "mute": { - "function": "隐藏 @{{acct}} 的嘟文", - "button": "隐藏 @{{acct}} 的嘟文" - }, - "block": { - "function": "屏蔽 @{{acct}}", - "button": "屏蔽 @{{acct}}" - }, - "reports": { - "function": "举报 @{{acct}}", - "button": "举报 @{{acct}}" - } - }, - "domain": { - "heading": "关于社区", - "block": { - "function": "屏蔽社区", - "button": "屏蔽社区 {{domain}}" - }, - "alert": { - "title": "确定要屏蔽 {{domain}} 吗?", - "message": "多数情况下,隐藏或屏蔽特定用户即可。\n\n屏蔽之后,来自此社区的所有内容将不再出现在你的时间轴里。同时,来自该社区的关注者将被移除。请谨慎使用。", - "buttons": { - "confirm": "确定屏蔽整个社区", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "share": { - "status": { - "heading": "分享嘟文", - "button": "分享此条嘟文的链接" - }, - "account": { - "heading": "分享用户", - "button": "分享此用户的链接" - } - }, - "status": { - "heading": "关于嘟文", - "edit": { - "function": "编辑嘟文", - "button": "编辑此条嘟文" - }, - "delete": { - "function": "删除", - "button": "删除此条嘟文", - "alert": { - "title": "确认删除嘟文?", - "message": "确定要删除这条嘟文吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。", - "buttons": { - "confirm": "确认删除", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "deleteEdit": { - "function": "删除并重新编辑", - "button": "删除并重新编辑此条嘟文", - "alert": { - "title": "确认删除嘟文?", - "message": "确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。", - "buttons": { - "confirm": "删除并重新编辑", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "mute": { - "function": "静音", - "button": { - "positive": "静音此条嘟文及对话", - "negative": "取消静音此条嘟文及对话" - } - }, - "pin": { - "function": "置顶", - "button": { - "positive": "置顶此条嘟文", - "negative": "取消置顶此条嘟文" - } - } - } } }, "poll": { diff --git a/src/i18n/zh-Hant/components/contextMenu.json b/src/i18n/zh-Hant/components/contextMenu.json new file mode 100644 index 00000000..2715b111 --- /dev/null +++ b/src/i18n/zh-Hant/components/contextMenu.json @@ -0,0 +1,70 @@ +{ + "accessibilityHint": "", + "account": { + "title": "", + "mute": { + "action": "" + }, + "block": { + "action": "" + }, + "reports": { + "action": "" + } + }, + "instance": { + "title": "", + "block": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "" + } + } + } + }, + "share": { + "status": { + "action": "" + }, + "account": { + "action": "" + } + }, + "status": { + "title": "", + "edit": { + "action": "" + }, + "delete": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "" + } + } + }, + "deleteEdit": { + "action": "", + "alert": { + "title": "", + "message": "", + "buttons": { + "confirm": "" + } + } + }, + "mute": { + "action-muted_false": "", + "action-muted_true": "" + }, + "pin": { + "action-pinned_false": "", + "action-pinned_true": "" + } + } +} \ No newline at end of file diff --git a/src/i18n/zh-Hant/components/instance.json b/src/i18n/zh-Hant/components/instance.json index 71c2445e..0df8632d 100644 --- a/src/i18n/zh-Hant/components/instance.json +++ b/src/i18n/zh-Hant/components/instance.json @@ -11,8 +11,10 @@ "domains": "串聯宇宙" }, "disclaimer": { - "base": "將使用系統內建的瀏覽器來登入,tooot app 無法讀取您的帳號資訊;詳見 ", - "privacy": "隱私權政策" + "base": "" + }, + "terms": { + "base": "" } }, "update": { diff --git a/src/i18n/zh-Hant/components/mediaSelector.json b/src/i18n/zh-Hant/components/mediaSelector.json index a2738c7b..1f0d0b96 100644 --- a/src/i18n/zh-Hant/components/mediaSelector.json +++ b/src/i18n/zh-Hant/components/mediaSelector.json @@ -1,27 +1,17 @@ { "title": "選擇媒體來源", "options": { - "library": "上傳", - "photo": "拍照", - "cancel": "$t(common:buttons.cancel)" + "image": "", + "image_max": "", + "video": "", + "video_max": "" }, "library": { "alert": { "title": "權限不足", "message": "上傳照片需要讀取的權限", "buttons": { - "settings": "更新設定", - "cancel": "$t(common:buttons.cancel)" - } - } - }, - "photo": { - "alert": { - "title": "權限不足", - "message": "需要使用相機的權限來上傳", - "buttons": { - "settings": "更新設定", - "cancel": "$t(common:buttons.cancel)" + "settings": "更新設定" } } } diff --git a/src/i18n/zh-Hant/components/timeline.json b/src/i18n/zh-Hant/components/timeline.json index 620ecc99..5623526d 100644 --- a/src/i18n/zh-Hant/components/timeline.json +++ b/src/i18n/zh-Hant/components/timeline.json @@ -123,94 +123,6 @@ "delete": { "function": "" } - }, - "actions": { - "accessibilityHint": "", - "account": { - "heading": "", - "mute": { - "function": "", - "button": "" - }, - "block": { - "function": "", - "button": "" - }, - "reports": { - "function": "", - "button": "" - } - }, - "domain": { - "heading": "", - "block": { - "function": "", - "button": "" - }, - "alert": { - "title": "", - "message": "", - "buttons": { - "confirm": "", - "cancel": "" - } - } - }, - "share": { - "status": { - "heading": "", - "button": "" - }, - "account": { - "heading": "", - "button": "" - } - }, - "status": { - "heading": "", - "edit": { - "function": "", - "button": "" - }, - "delete": { - "function": "", - "button": "", - "alert": { - "title": "", - "message": "", - "buttons": { - "confirm": "", - "cancel": "" - } - } - }, - "deleteEdit": { - "function": "", - "button": "", - "alert": { - "title": "", - "message": "", - "buttons": { - "confirm": "", - "cancel": "" - } - } - }, - "mute": { - "function": "", - "button": { - "positive": "", - "negative": "" - } - }, - "pin": { - "function": "", - "button": { - "positive": "", - "negative": "" - } - } - } } }, "poll": { diff --git a/src/screens/Actions.tsx b/src/screens/Actions.tsx index a633cda1..c6db5ffa 100644 --- a/src/screens/Actions.tsx +++ b/src/screens/Actions.tsx @@ -1,14 +1,7 @@ -import analytics from '@components/analytics' -import Button from '@components/Button' import { RootStackScreenProps } from '@utils/navigation/navigators' -import { - getInstanceAccount, - getInstanceUrl -} from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useEffect } from 'react' -import { useTranslation } from 'react-i18next' import { Dimensions, StyleSheet, View } from 'react-native' import { PanGestureHandler, @@ -28,47 +21,13 @@ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context' -import { useSelector } from 'react-redux' -import ActionsAccount from './Actions/Account' import ActionsAltText from './Actions/AltText' -import ActionsDomain from './Actions/Domain' import ActionsNotificationsFilter from './Actions/NotificationsFilter' -import ActionsShare from './Actions/Share' -import ActionsStatus from './Actions/Status' const ScreenActions = ({ route: { params }, navigation }: RootStackScreenProps<'Screen-Actions'>) => { - const { t } = useTranslation() - - const instanceAccount = useSelector( - getInstanceAccount, - (prev, next) => prev?.id === next?.id - ) - let sameAccount = false - switch (params.type) { - case 'status': - console.log('media length', params.status.media_attachments.length) - sameAccount = instanceAccount?.id === params.status.account.id - break - case 'account': - sameAccount = instanceAccount?.id === params.account.id - break - } - - const instanceDomain = useSelector(getInstanceUrl) - let sameDomain = true - let statusDomain: string - switch (params.type) { - case 'status': - statusDomain = params.status.uri - ? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1] - : '' - sameDomain = instanceDomain === statusDomain - break - } - const { colors } = useTheme() const insets = useSafeAreaInsets() @@ -106,72 +65,6 @@ const ScreenActions = ({ const actions = () => { switch (params.type) { - case 'status': - return ( - <> - {!sameAccount ? ( - - ) : null} - {sameAccount && params.status ? ( - - ) : null} - {!sameDomain && statusDomain ? ( - - ) : null} - {params.status.visibility !== 'direct' ? ( - - ) : null} -