From 1072d88191fa24fb748962defbd147a18816b2e1 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 31 Jan 2021 03:09:35 +0100 Subject: [PATCH] Added fastlane and other updates --- .envrc | 5 - .../{testing.yml => development.yml} | 6 +- .gitignore | 2 + Gemfile | 3 + Gemfile.lock | 201 ++++++++++++++++++ android/app/src/main/AndroidManifest.xml | 1 + fastlane/Fastfile | 82 +++++++ fastlane/Matchfile | 10 + fastlane/README.md | 39 ++++ fastlane/report.xml | 43 ++++ ios/Podfile | 11 + ios/Podfile.lock | 8 +- ios/tooot/Info.plist | 4 +- ios/tooot/Supporting/Expo.plist | 28 +-- package.json | 4 +- src/@types/react-navigation.d.ts | 26 ++- src/Screens.tsx | 2 +- src/i18n/en/_all.ts | 4 +- src/i18n/en/screens/meRoot.ts | 3 +- src/i18n/en/screens/meSettings.ts | 3 + src/i18n/en/screens/screenImageViewer.ts | 13 ++ src/i18n/zh-Hans/_all.ts | 4 +- src/i18n/zh-Hans/screens/meRoot.ts | 3 +- src/i18n/zh-Hans/screens/meSettings.ts | 3 + src/i18n/zh-Hans/screens/screenImageViewer.ts | 13 ++ src/screens/Actions.tsx | 112 ++++++---- src/screens/Compose.tsx | 11 +- src/screens/Compose/EditAttachment.tsx | 15 +- src/screens/ImagesViewer.tsx | 89 +++++++- src/screens/Tabs.tsx | 2 +- src/screens/Tabs/Me/Root/Collections.tsx | 20 +- src/screens/Tabs/Me/Settings.tsx | 5 +- src/screens/Tabs/Me/Settings/App.tsx | 8 +- src/screens/Tabs/Me/Settings/Tooot.tsx | 36 ++-- src/screens/Tabs/Me/Switch/Root.tsx | 25 ++- src/screens/Tabs/Shared/Account.tsx | 38 +--- .../Tabs/Shared/Account/Attachments.tsx | 23 +- .../Tabs/Shared/Account/Information/Name.tsx | 2 +- src/startup/sentry.ts | 6 +- src/store.ts | 2 +- src/utils/slices/settingsSlice.ts | 18 +- src/utils/styles/constants.ts | 2 +- yarn.lock | 5 + 43 files changed, 755 insertions(+), 185 deletions(-) delete mode 100644 .envrc rename .github/workflows/{testing.yml => development.yml} (91%) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Fastfile create mode 100644 fastlane/Matchfile create mode 100644 fastlane/README.md create mode 100644 fastlane/report.xml create mode 100644 src/i18n/en/screens/screenImageViewer.ts create mode 100644 src/i18n/zh-Hans/screens/screenImageViewer.ts diff --git a/.envrc b/.envrc deleted file mode 100644 index 35af134c..00000000 --- a/.envrc +++ /dev/null @@ -1,5 +0,0 @@ -export SENTRY_ORGANIZATION="xmflsct" -export SENTRY_PROJECT="tooot-app" -export SENTRY_DEPLOY_ENV="expo" -export SENTRY_AUTH_TOKEN="dbccffb69144454784f2171ee7e39211b54392e5b535439aa5b77f2681578f4c" -export SENTRY_DSN="https://835b42fb2b25463284edb5a7e18c377d@o389581.ingest.sentry.io/5571975" \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/development.yml similarity index 91% rename from .github/workflows/testing.yml rename to .github/workflows/development.yml index 32191216..b20868ea 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/development.yml @@ -1,8 +1,8 @@ -name: Publish testing +name: Publish development on: push: branches: - - '*-testing' + - '*-development' jobs: publish: runs-on: ubuntu-latest @@ -27,5 +27,5 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - SENTRY_DEPLOY_ENV: testing + SENTRY_DEPLOY_ENV: development run: expo publish --release-channel=${GITHUB_REF#refs/heads/} diff --git a/.gitignore b/.gitignore index fb87d9ef..01994883 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ web-build/ coverage/ builds/ +fastlane/id_rsa + # @generated expo-cli sync-28e2ab0e9ece60556eaf932abe52d017ec33db50 # The following patterns were generated by expo-cli diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..adc90d98 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..d611c9d3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,201 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.3) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.422.0) + aws-sdk-core (3.111.2) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.41.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.87.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.20) + declarative-option (0.1.0) + digest-crc (0.6.3) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.1) + excon (0.78.1) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-net_http (1.0.1) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.2.1) + fastlane (2.172.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-apis-core (0.2.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.14) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + rexml + signet (~> 0.14) + webrick + google-apis-iamcredentials_v1 (0.1.0) + google-apis-core (~> 0.1) + google-apis-storage_v1 (0.1.0) + google-apis-core (~> 0.1) + google-cloud-core (1.5.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.4.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.30.0) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.15.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.5.1) + jwt (2.2.2) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.0.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + os (1.1.1) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.3) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.4) + rouge (2.0.7) + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + security (0.1.3) + signet (0.14.1) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.19.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 1.17.2 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 25f1422e..eca08281 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,4 +43,5 @@ + \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..2bd74629 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,82 @@ +$ExpoSDK = '40.0.0' +$ExpoRelease = '40' + +fastlane_version '2.172.0' + +platform :ios do + desc 'Build and deploy' + private_lane :build do |options| + branch = 'RELEASE-TYPE'.gsub('RELEASE', $ExpoRelease).gsub('TYPE', options[:type]) + + case options[:type] + when 'staging', 'production' + ensure_git_branch( + branch: branch + ) + ensure_git_status_clean + build_number = Time.new.strftime('%y%m%d%k') + increment_build_number build_number: build_number + end + + match( + type: options[:type], + readonly: true + ) + + set_info_plist_value( + path: './ios/tooot/Supporting/Expo.plist', + key: 'EXUpdatesSDKVersion', + value: $ExpoSDK + ) + set_info_plist_value( + path: './ios/tooot/Supporting/Expo.plist', + key: 'EXUpdatesReleaseChannel', + value: branch + ) + + case options[:type] + when 'development' + build_ios_app( + scheme: 'tooot', + silent: true, + include_bitcode: true, + workspace: './ios/tooot.xcworkspace', + output_directory: './build/ios', + output_name: 'development', + export_method: 'development' + ) + install_on_device( + skip_wifi: true, + ipa: './build/ios/development.ipa' + ) + when 'staging' + build_ios_app( + scheme: 'tooot', + workspace: './ios/tooot.xcworkspace' + ) + upload_to_testflight( + api_key: '{"key_id": "KEY_ID", "issuer_id": "ISSUER_ID", "key_filepath": "appstore.p8"}'.gsub('KEY_ID', ENV['APP_STORE_KEY_ID']).gsub('ISSUER_ID', ENV['APP_STORE_ISSUER_ID']), + skip_submission: true + ) + end + end + + desc 'Build development to phone' + lane :development do + build(type: 'development') + end + + desc 'Build staging to TestFlight' + lane :staging do + build(type: 'staging') + end + + desc 'Build product to App Store' + lane :production do + build(type: 'production') + end +end + +platform :android do + # Android Lanes +end \ No newline at end of file diff --git a/fastlane/Matchfile b/fastlane/Matchfile new file mode 100644 index 00000000..37097a2e --- /dev/null +++ b/fastlane/Matchfile @@ -0,0 +1,10 @@ +git_url(ENV('MATCH_GIT_REPO')) +git_user_email("me@xmflsct.com") +git_private_key("./id_rsa") + +storage_mode("git") + +type("development") + +app_identifier(["com.xmflsct.app.tooot"]) +username(ENV['APP_STORE_EMAIL']) diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 00000000..7476eb8e --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,39 @@ +fastlane documentation +================ +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +``` +xcode-select --install +``` + +Install _fastlane_ using +``` +[sudo] gem install fastlane -NV +``` +or alternatively using `brew install fastlane` + +# Available Actions +## iOS +### ios development +``` +fastlane ios development +``` +Build development to phone +### ios staging +``` +fastlane ios staging +``` +Build staging to TestFlight +### ios production +``` +fastlane ios production +``` +Build product to App Store + +---- + +This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). +The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 00000000..595519a7 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Podfile b/ios/Podfile index 6e73eeab..956c992c 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -22,3 +22,14 @@ target 'tooot' do # flipper_post_install(installer) # end end + +# https://github.com/CocoaPods/CocoaPods/issues/9884 +post_install do |pi| + pi.pods_project.targets.each do |t| + t.build_configurations.each do |bc| + if bc.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] == '8.0' + bc.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2e4d6e59..0cae0003 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -334,6 +334,8 @@ PODS: - React - react-native-blurhash (1.0.29): - React + - react-native-cameraroll (4.0.2): + - React-Core - react-native-netinfo (5.9.10): - React-Core - react-native-safe-area-context (3.1.9): @@ -524,6 +526,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - "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-netinfo (from `../node_modules/@react-native-community/netinfo`)" - 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`)" @@ -669,6 +672,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/blur" react-native-blurhash: :path: "../node_modules/react-native-blurhash" + react-native-cameraroll: + :path: "../node_modules/@react-native-community/cameraroll" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-safe-area-context: @@ -802,6 +807,7 @@ SPEC CHECKSUMS: React-jsinspector: 58aef7155bc9a9683f5b60b35eccea8722a4f53a react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c react-native-blurhash: 90886ae897cafbbdf2773cb3654656bcb34e8f43 + react-native-cameraroll: 1965db75c851b15e77a22ca0ac78e32af6b571ae react-native-netinfo: 30fb89fa913c342be82a887b56e96be6d71201dd react-native-safe-area-context: b6e0e284002381d2ff29fa4fff42b4d8282e3c94 react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 @@ -843,6 +849,6 @@ SPEC CHECKSUMS: UMTaskManagerInterface: 4c60b43eaf3cb05a164bc9113258a171c18b7bf7 Yoga: 4bd86afe9883422a7c4028c00e34790f560923d6 -PODFILE CHECKSUM: cda6b7a1593395b311286b33b0036167ce6f0a15 +PODFILE CHECKSUM: 45a588d6415c6afb7aa7c5ef73a2ca1e38011f49 COCOAPODS: 1.10.1 diff --git a/ios/tooot/Info.plist b/ios/tooot/Info.plist index fee24a50..73d0c046 100644 --- a/ios/tooot/Info.plist +++ b/ios/tooot/Info.plist @@ -19,7 +19,7 @@ CFBundleSignature ???? CFBundleVersion - 4 + 0 LSRequiresIPhoneOS NSAppTransportSecurity @@ -74,6 +74,8 @@ Allow $(PRODUCT_NAME) to access your microphone NSPhotoLibraryUsageDescription Give $(PRODUCT_NAME) permission to save photos + NSPhotoLibraryAddUsageDescription + Give $(PRODUCT_NAME) permission to save photos NSCameraUsageDescription Give $(PRODUCT_NAME) permission to access your camera NSMicrophoneUsageDescription diff --git a/ios/tooot/Supporting/Expo.plist b/ios/tooot/Supporting/Expo.plist index 2644cb3b..b54f4423 100644 --- a/ios/tooot/Supporting/Expo.plist +++ b/ios/tooot/Supporting/Expo.plist @@ -1,16 +1,18 @@ - - EXUpdatesSDKVersion - 40.0.0 - EXUpdatesURL - https://exp.host/@xmflsct/tooot - EXUpdatesEnabled - - EXUpdatesCheckOnLaunch - ALWAYS - EXUpdatesLaunchWaitMs - 0 - - \ No newline at end of file + + EXUpdatesCheckOnLaunch + WIFI_ONLY + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + EXUpdatesReleaseChannel + 40-development + EXUpdatesSDKVersion + 40.0.0 + EXUpdatesURL + https://exp.host/@xmflsct/tooot + + diff --git a/package.json b/package.json index 7e4a0dd7..db36db3c 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "start": "react-native start", "android": "react-native run-android", "ios": "react-native run-ios", - "web": "expo start --web", - "eject": "expo eject", + "ios:development": "bundle exec fastlane ios development", "test": "jest --watchAll", "release": "scripts/release.sh" }, @@ -13,6 +12,7 @@ "@neverdull-agency/expo-unlimited-secure-store": "^1.0.10", "@react-native-async-storage/async-storage": "^1.13.3", "@react-native-community/blur": "^3.6.0", + "@react-native-community/cameraroll": "^4.0.2", "@react-native-community/masked-view": "0.1.10", "@react-native-community/netinfo": "^5.9.10", "@react-native-community/segmented-control": "2.2.2", diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts index 9c0a9187..05f43109 100644 --- a/src/@types/react-navigation.d.ts +++ b/src/@types/react-navigation.d.ts @@ -9,12 +9,16 @@ interface IImageInfo { declare namespace Nav { type RootStackParamList = { 'Screen-Tabs': undefined - 'Screen-Actions': { - queryKey: QueryKeyTimeline - status: Mastodon.Status - url?: string - type?: 'status' | 'account' - } + 'Screen-Actions': + | { + type: 'status' + queryKey: QueryKeyTimeline + status: Mastodon.Status + } + | { + type: 'account' + account: Mastodon.Account + } 'Screen-Announcements': { showAll: boolean } 'Screen-Compose': | { @@ -61,6 +65,11 @@ declare namespace Nav { } } + type ScreenComposeStackParamList = { + 'Screen-Compose-Root': undefined + 'Screen-Compose-EditAttachment': { index: number } + } + type ScreenTabsStackParamList = { 'Tab-Local': undefined 'Tab-Public': undefined @@ -95,11 +104,6 @@ declare namespace Nav { 'Tab-Public-Root': undefined } & TabSharedStackParamList - type TabComposeStackParamList = { - 'Tab-Compose-Root': undefined - 'Tab-Compose-EditAttachment': unknown - } - type TabNotificationsStackParamList = { 'Tab-Notifications-Root': undefined } & TabSharedStackParamList diff --git a/src/Screens.tsx b/src/Screens.tsx index 322e8580..095b6ad2 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -200,7 +200,7 @@ const Index: React.FC = ({ localCorrupt }) => { }} sharedElements={route => { const { imageIndex, imageUrls } = route.params - return [{ id: `image.${imageUrls[imageIndex].url}`, debug: true }] + return [{ id: `image.${imageUrls[imageIndex].url}` }] }} /> diff --git a/src/i18n/en/_all.ts b/src/i18n/en/_all.ts index 0a26f7f1..22271cdb 100644 --- a/src/i18n/en/_all.ts +++ b/src/i18n/en/_all.ts @@ -28,5 +28,7 @@ export default { componentParse: require('./components/parse').default, componentRelationship: require('./components/relationship').default, componentRelativeTime: require('./components/relativeTime').default, - componentTimeline: require('./components/timeline').default + componentTimeline: require('./components/timeline').default, + + screenImageViewer: require('./screens/screenImageViewer').default } diff --git a/src/i18n/en/screens/meRoot.ts b/src/i18n/en/screens/meRoot.ts index 4f4aee22..f8bf0405 100644 --- a/src/i18n/en/screens/meRoot.ts +++ b/src/i18n/en/screens/meRoot.ts @@ -9,7 +9,8 @@ export default { heading: '$t(sharedAnnouncements:heading)', content: { unread: '{{amount}} unread', - read: 'All read' + read: 'All read', + empty: 'None' } } }, diff --git a/src/i18n/en/screens/meSettings.ts b/src/i18n/en/screens/meSettings.ts index 130b121f..19412ec0 100644 --- a/src/i18n/en/screens/meSettings.ts +++ b/src/i18n/en/screens/meSettings.ts @@ -40,6 +40,9 @@ export default { review: { heading: 'Review tooot' }, + contact: { + heading: 'Contact tooot' + }, analytics: { heading: 'Help us improve', description: 'Collecting only non-user relative usage' diff --git a/src/i18n/en/screens/screenImageViewer.ts b/src/i18n/en/screens/screenImageViewer.ts new file mode 100644 index 00000000..dc37a9e3 --- /dev/null +++ b/src/i18n/en/screens/screenImageViewer.ts @@ -0,0 +1,13 @@ +export default { + content: { + options: { + save: 'Save image', + share: 'Share iamge', + cancel: '$t(common:buttons.cancel)' + }, + save: { + function: 'Saving image', + success: 'Image saved' + } + } +} diff --git a/src/i18n/zh-Hans/_all.ts b/src/i18n/zh-Hans/_all.ts index 0a26f7f1..22271cdb 100644 --- a/src/i18n/zh-Hans/_all.ts +++ b/src/i18n/zh-Hans/_all.ts @@ -28,5 +28,7 @@ export default { componentParse: require('./components/parse').default, componentRelationship: require('./components/relationship').default, componentRelativeTime: require('./components/relativeTime').default, - componentTimeline: require('./components/timeline').default + componentTimeline: require('./components/timeline').default, + + screenImageViewer: require('./screens/screenImageViewer').default } diff --git a/src/i18n/zh-Hans/screens/meRoot.ts b/src/i18n/zh-Hans/screens/meRoot.ts index 5a7ebdcc..6aaafc04 100644 --- a/src/i18n/zh-Hans/screens/meRoot.ts +++ b/src/i18n/zh-Hans/screens/meRoot.ts @@ -9,7 +9,8 @@ export default { heading: '$t(sharedAnnouncements:heading)', content: { unread: '{{amount}} 条未读公告', - read: '无未读公告' + read: '无未读公告', + empty: '无公告' } } }, diff --git a/src/i18n/zh-Hans/screens/meSettings.ts b/src/i18n/zh-Hans/screens/meSettings.ts index 28917b9d..4cc479e0 100644 --- a/src/i18n/zh-Hans/screens/meSettings.ts +++ b/src/i18n/zh-Hans/screens/meSettings.ts @@ -40,6 +40,9 @@ export default { review: { heading: '给 tooot 打分' }, + contact: { + heading: '联系 tooot' + }, analytics: { heading: '帮助我们改进', description: '收集不与用户相关联的使用信息' diff --git a/src/i18n/zh-Hans/screens/screenImageViewer.ts b/src/i18n/zh-Hans/screens/screenImageViewer.ts new file mode 100644 index 00000000..6fa1d247 --- /dev/null +++ b/src/i18n/zh-Hans/screens/screenImageViewer.ts @@ -0,0 +1,13 @@ +export default { + content: { + options: { + save: '保存图片', + share: '分享图片', + cancel: '$t(common:buttons.cancel)' + }, + save: { + function: '保存图片', + success: '图片保存成功' + } + } +} diff --git a/src/screens/Actions.tsx b/src/screens/Actions.tsx index 5a36e46a..1cbec5c9 100644 --- a/src/screens/Actions.tsx +++ b/src/screens/Actions.tsx @@ -2,7 +2,7 @@ import { StackScreenProps } from '@react-navigation/stack' import { getLocalAccount, getLocalUrl } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' import { Dimensions, StyleSheet, View } from 'react-native' import { PanGestureHandler, @@ -31,20 +31,29 @@ export type ScreenAccountProp = StackScreenProps< > const ScreenActions = React.memo( - ({ - route: { - params: { queryKey, status, url, type } - }, - navigation - }: ScreenAccountProp) => { + ({ route: { params }, navigation }: ScreenAccountProp) => { const localAccount = useSelector(getLocalAccount) - const sameAccount = localAccount?.id === status.account.id + let sameAccount = false + switch (params.type) { + case 'status': + sameAccount = localAccount?.id === params.status.account.id + break + case 'account': + sameAccount = localAccount?.id === params.account.id + break + } const localDomain = useSelector(getLocalUrl) - const statusDomain = status.uri - ? status.uri.split(new RegExp(/\/\/(.*?)\//))[1] - : '' - const sameDomain = localDomain === statusDomain + let sameDomain = true + let statusDomain: string + switch (params.type) { + case 'status': + statusDomain = params.status.uri + ? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1] + : '' + sameDomain = localDomain === statusDomain + break + } const { theme } = useTheme() const insets = useSafeAreaInsets() @@ -82,6 +91,56 @@ const ScreenActions = React.memo( } }) + const actions = useMemo(() => { + switch (params.type) { + case 'status': + return ( + <> + {!sameAccount && ( + + )} + {sameAccount && params.status && ( + + )} + {!sameDomain && statusDomain && ( + + )} + + + ) + case 'account': + return ( + <> + {!sameAccount && ( + + )} + + + ) + } + }, []) + return ( - {!sameAccount && ( - - )} - - {sameAccount && status && ( - - )} - - {!sameDomain && ( - - )} - - {url && type ? ( - - ) : null} + {actions} diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index cb35829b..56aaed40 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -229,7 +229,7 @@ const ScreenCompose: React.FC = ({ return ( = ({ edges={hasKeyboard ? ['top'] : ['top', 'bottom']} > - + diff --git a/src/screens/Compose/EditAttachment.tsx b/src/screens/Compose/EditAttachment.tsx index 44103178..c2d16344 100644 --- a/src/screens/Compose/EditAttachment.tsx +++ b/src/screens/Compose/EditAttachment.tsx @@ -2,6 +2,7 @@ import client from '@api/client' import analytics from '@components/analytics' import haptics from '@components/haptics' import { HeaderLeft, HeaderRight } from '@components/Header' +import { StackScreenProps } from '@react-navigation/stack' import React, { useCallback, useContext, @@ -18,16 +19,12 @@ import ComposeContext from './utils/createContext' const Stack = createNativeStackNavigator() -export interface Props { - route: { - params: { - index: number - } - } - navigation: any -} +export type ScreenComposeEditAttachmentProp = StackScreenProps< + Nav.ScreenComposeStackParamList, + 'Screen-Compose-EditAttachment' +> -const ComposeEditAttachment: React.FC = ({ +const ComposeEditAttachment: React.FC = ({ route: { params: { index } }, diff --git a/src/screens/ImagesViewer.tsx b/src/screens/ImagesViewer.tsx index 36ccd544..ece79f12 100644 --- a/src/screens/ImagesViewer.tsx +++ b/src/screens/ImagesViewer.tsx @@ -1,14 +1,24 @@ import analytics from '@components/analytics' import { HeaderRight } from '@components/Header' +import { useActionSheet } from '@expo/react-native-action-sheet' import { StackScreenProps } from '@react-navigation/stack' +import CameraRoll from '@react-native-community/cameraroll' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { findIndex } from 'lodash' import React, { useCallback, useLayoutEffect, useState } from 'react' -import { Platform, Share, StyleSheet, Text } from 'react-native' +import { useTranslation } from 'react-i18next' +import { + PermissionsAndroid, + Platform, + Share, + StyleSheet, + Text +} from 'react-native' import FastImage from 'react-native-fast-image' import ImageViewer from 'react-native-image-zoom-viewer' import { SharedElement } from 'react-navigation-shared-element' +import { toast } from '@components/toast' export type ScreenImagesViewerProp = StackScreenProps< Nav.RootStackParamList, @@ -27,14 +37,70 @@ const ScreenImagesViewer = React.memo( findIndex(imageUrls, ['imageIndex', imageIndex]) ) - const onPress = useCallback(() => { - analytics('imageviewer_share_press') - switch (Platform.OS) { - case 'ios': - return Share.share({ url: imageUrls[currentIndex].url }) - case 'android': - return Share.share({ message: imageUrls[currentIndex].url }) + const hasAndroidPermission = async () => { + const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE + + const hasPermission = await PermissionsAndroid.check(permission) + if (hasPermission) { + return true } + + const status = await PermissionsAndroid.request(permission) + return status === 'granted' + } + const saveImage = async () => { + if (Platform.OS === 'android' && !(await hasAndroidPermission())) { + return + } + CameraRoll.save( + imageUrls[imageIndex].originUrl || + imageUrls[imageIndex].remote_url || + imageUrls[imageIndex].preview_url + ) + .then(() => + toast({ type: 'success', message: t('content.save.success') }) + ) + .catch(() => + toast({ + type: 'error', + message: t('common:toastMessage.error.message', { + function: t('content.save.function') + }) + }) + ) + } + + const { t } = useTranslation('screenImageViewer') + const { showActionSheetWithOptions } = useActionSheet() + const onPress = useCallback(() => { + analytics('imageviewer_more_press') + showActionSheetWithOptions( + { + options: [ + t('content.options.save'), + t('content.options.share'), + t('content.options.cancel') + ], + cancelButtonIndex: 2 + }, + async buttonIndex => { + switch (buttonIndex) { + case 0: + analytics('imageviewer_more_save_press') + saveImage() + break + case 1: + analytics('imageviewer_more_share_press') + switch (Platform.OS) { + case 'ios': + return Share.share({ url: imageUrls[currentIndex].url }) + case 'android': + return Share.share({ message: imageUrls[currentIndex].url }) + } + break + } + } + ) }, [currentIndex]) useLayoutEffect( @@ -48,7 +114,11 @@ const ScreenImagesViewer = React.memo( ), headerRight: () => ( - + ) }), [currentIndex] @@ -77,6 +147,7 @@ const ScreenImagesViewer = React.memo( style={{ flex: 1 }} onChange={index => index !== undefined && setCurrentIndex(index)} renderImage={renderImage} + onLongPress={saveImage} /> ) }, diff --git a/src/screens/Tabs.tsx b/src/screens/Tabs.tsx index cdc7fb40..1a705c6a 100644 --- a/src/screens/Tabs.tsx +++ b/src/screens/Tabs.tsx @@ -26,7 +26,7 @@ import TabPublic from './Tabs/Public' export type ScreenTabsParamList = { 'Tab-Local': NavigatorScreenParams 'Tab-Public': NavigatorScreenParams - 'Tab-Compose': NavigatorScreenParams + 'Tab-Compose': NavigatorScreenParams 'Tab-Notifications': NavigatorScreenParams 'Tab-Me': NavigatorScreenParams } diff --git a/src/screens/Tabs/Me/Root/Collections.tsx b/src/screens/Tabs/Me/Root/Collections.tsx index 2bfad115..89059dfa 100644 --- a/src/screens/Tabs/Me/Root/Collections.tsx +++ b/src/screens/Tabs/Me/Root/Collections.tsx @@ -8,15 +8,23 @@ const Collections: React.FC = () => { const { t, i18n } = useTranslation('meRoot') const navigation = useNavigation() - const { data, isFetching } = useAnnouncementQuery({ showAll: true }) + const { data, isFetching } = useAnnouncementQuery({ + showAll: true + }) const announcementContent = useMemo(() => { if (data) { - const amount = data.filter(announcement => !announcement.read).length - if (amount) { - return t('content.collections.announcements.content.unread', { amount }) + if (data.length === 0) { + return t('content.collections.announcements.content.empty') } else { - return t('content.collections.announcements.content.read') + const amount = data.filter(announcement => !announcement.read).length + if (amount) { + return t('content.collections.announcements.content.unread', { + amount + }) + } else { + return t('content.collections.announcements.content.read') + } } } }, [data, i18n.language]) @@ -49,7 +57,7 @@ const Collections: React.FC = () => { /> { - {__DEV__ || Constants.manifest.releaseChannel?.includes('testing') ? ( + {__DEV__ || + ['development'].some(channel => + Constants.manifest.releaseChannel?.includes(channel) + ) ? ( ) : null} diff --git a/src/screens/Tabs/Me/Settings/App.tsx b/src/screens/Tabs/Me/Settings/App.tsx index 29518f17..a85cff47 100644 --- a/src/screens/Tabs/Me/Settings/App.tsx +++ b/src/screens/Tabs/Me/Settings/App.tsx @@ -25,7 +25,7 @@ const SettingsApp: React.FC = () => { const settingsLanguage = useSelector(getSettingsLanguage) const settingsTheme = useSelector(getSettingsTheme) const settingsBrowser = useSelector(getSettingsBrowser) - + console.log(settingsLanguage) return ( { i18n.services.resourceStore.data ) const options = availableLanguages - .map(language => t(`content.language.options.${language}`)) + .map(language => { + return t(`content.language.options.${language}`) + }) .concat(t('content.language.options.cancel')) showActionSheetWithOptions( @@ -47,7 +49,7 @@ const SettingsApp: React.FC = () => { cancelButtonIndex: options.length - 1 }, buttonIndex => { - if (buttonIndex < options.length) { + if (buttonIndex < options.length - 1) { analytics('settings_language_press', { current: i18n.language, new: availableLanguages[buttonIndex] diff --git a/src/screens/Tabs/Me/Settings/Tooot.tsx b/src/screens/Tabs/Me/Settings/Tooot.tsx index 9e87138b..5fafe8a8 100644 --- a/src/screens/Tabs/Me/Settings/Tooot.tsx +++ b/src/screens/Tabs/Me/Settings/Tooot.tsx @@ -6,6 +6,7 @@ import { useSearchQuery } from '@utils/queryHooks/search' import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' +import Constants from 'expo-constants' import * as Linking from 'expo-linking' import * as StoreReview from 'expo-store-review' import * as WebBrowser from 'expo-web-browser' @@ -41,19 +42,30 @@ const SettingsTooot: React.FC = () => { Linking.openURL('https://www.patreon.com/xmflsct') }} /> + {__DEV__ || + ['production', 'development'].some(channel => + Constants.manifest.releaseChannel?.includes(channel) + ) ? ( + + } + iconBack='ChevronRight' + onPress={() => { + analytics('settings_review_press') + StoreReview.isAvailableAsync().then(() => + StoreReview.requestReview() + ) + }} + /> + ) : null} - } - iconBack='ChevronRight' - onPress={() => { - analytics('settings_review_press') - StoreReview.isAvailableAsync().then(() => StoreReview.requestReview()) - }} - /> - = ({ disabled ? ' ✓' : '' }`} onPress={() => { + haptics('Light') analytics('switch_existing_press') dispatch(localUpdateActiveIndex(index)) queryClient.clear() @@ -77,14 +79,21 @@ const ScreenMeSwitchRoot: React.FC = () => { {localInstances.length - ? localInstances.map((instance, index) => ( - - )) + ? localInstances + .slice() + .sort((a, b) => + `${a.uri}${a.account.acct}`.localeCompare( + `${b.uri}${b.account.acct}` + ) + ) + .map((instance, index) => ( + + )) : null} diff --git a/src/screens/Tabs/Shared/Account.tsx b/src/screens/Tabs/Shared/Account.tsx index 7b226c5b..9a6ba534 100644 --- a/src/screens/Tabs/Shared/Account.tsx +++ b/src/screens/Tabs/Shared/Account.tsx @@ -1,21 +1,11 @@ import analytics from '@components/analytics' import { HeaderRight } from '@components/Header' import Timeline from '@components/Timelines/Timeline' -import HeaderActionsAccount from '@components/Timelines/Timeline/Shared/HeaderActions/Account' -import HeaderActionsShare from '@components/Timelines/Timeline/Shared/HeaderActions/Share' import { useAccountQuery } from '@utils/queryHooks/account' -import { getLocalAccount } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' -import React, { - useCallback, - useEffect, - useMemo, - useReducer, - useState -} from 'react' +import React, { useCallback, useEffect, useMemo, useReducer } from 'react' import { StyleSheet, View } from 'react-native' import { useSharedValue } from 'react-native-reanimated' -import { useSelector } from 'react-redux' import AccountAttachments from './Account/Attachments' import AccountHeader from './Account/Header' import AccountInformation from './Account/Information' @@ -33,7 +23,6 @@ const TabSharedAccount: React.FC = ({ }) => { const { theme } = useTheme() - const localAccount = useSelector(getLocalAccount) const { data } = useAccountQuery({ id: account.id }) const scrollY = useSharedValue(0) @@ -42,7 +31,6 @@ const TabSharedAccount: React.FC = ({ accountInitialState ) - const [modalVisible, setBottomSheetVisible] = useState(false) useEffect(() => { const updateHeaderRight = () => navigation.setOptions({ @@ -53,7 +41,10 @@ const TabSharedAccount: React.FC = ({ analytics('bottomsheet_open_press', { page: 'account' }) - setBottomSheetVisible(true) + navigation.navigate('Screen-Actions', { + type: 'account', + account + }) }} /> ) @@ -89,25 +80,6 @@ const TabSharedAccount: React.FC = ({ ListHeaderComponent }} /> - - {/* setBottomSheetVisible(false)} - > - 添加到列表 - {localAccount?.id !== account.id && ( - - )} - - - */} ) } diff --git a/src/screens/Tabs/Shared/Account/Attachments.tsx b/src/screens/Tabs/Shared/Account/Attachments.tsx index a4b72947..6bfbd24e 100644 --- a/src/screens/Tabs/Shared/Account/Attachments.tsx +++ b/src/screens/Tabs/Shared/Account/Attachments.tsx @@ -5,7 +5,6 @@ import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { useTimelineQuery } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' -import layoutAnimation from '@utils/styles/layoutAnimation' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useEffect } from 'react' import { @@ -29,6 +28,8 @@ const AccountAttachments = React.memo( >() const { theme } = useTheme() + const DISPLAY_AMOUNT = 6 + const width = (Dimensions.get('screen').width - StyleConstants.Spacing.Global.PagePadding * 2) / @@ -38,7 +39,7 @@ const AccountAttachments = React.memo( page: 'Account_Attachments' as 'Account_Attachments', account: account?.id } - const { data, refetch } = useTimelineQuery({ + const { data, refetch } = useTimelineQuery({ ...queryKeyParams, options: { enabled: false } }) @@ -48,18 +49,16 @@ const AccountAttachments = React.memo( } }, [account]) - const flattenData = (data?.pages - ? data.pages.flatMap(d => [...d]) - : []) as Mastodon.Status[] - useEffect(() => { - if (flattenData.length) { - layoutAnimation() - } - }, [flattenData.length]) + const flattenData = data?.pages + ? data.pages + .flatMap(d => [...d]) + .filter(status => !status.sensitive) + .splice(0, DISPLAY_AMOUNT) + : [] const renderItem = useCallback>( ({ item, index }) => { - if (index === 3) { + if (index === DISPLAY_AMOUNT - 1) { return ( { @@ -128,7 +127,7 @@ const AccountAttachments = React.memo( !status.sensitive).splice(0, 4)} + data={flattenData} renderItem={renderItem} showsHorizontalScrollIndicator={false} /> diff --git a/src/screens/Tabs/Shared/Account/Information/Name.tsx b/src/screens/Tabs/Shared/Account/Information/Name.tsx index 132a3651..b651230b 100644 --- a/src/screens/Tabs/Shared/Account/Information/Name.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Name.tsx @@ -66,7 +66,7 @@ const AccountInformationName: React.FC = ({ account }) => { const styles = StyleSheet.create({ base: { borderRadius: 0, - marginTop: StyleConstants.Spacing.M, + marginTop: StyleConstants.Spacing.S, marginBottom: StyleConstants.Spacing.XS }, moved: { diff --git a/src/startup/sentry.ts b/src/startup/sentry.ts index 9384c8de..ff4f2430 100644 --- a/src/startup/sentry.ts +++ b/src/startup/sentry.ts @@ -8,7 +8,11 @@ const sentry = () => { environment: Constants.manifest.extra.sentryEnv, dsn: Constants.manifest.extra.sentryDSN, enableInExpoDevelopment: false, - debug: __DEV__ + debug: + __DEV__ || + ['development'].some(channel => + Constants.manifest.releaseChannel?.includes(channel) + ) }) } diff --git a/src/store.ts b/src/store.ts index ec4bd409..81841408 100644 --- a/src/store.ts +++ b/src/store.ts @@ -12,7 +12,7 @@ import { persistReducer, persistStore } from 'redux-persist' const secureStorage = createSecureStore() -const prefix = 'ajieorjaiojwoirjwe' +const prefix = 'tooot' const contextsPersistConfig = { key: 'contexts', diff --git a/src/utils/slices/settingsSlice.ts b/src/utils/slices/settingsSlice.ts index 828e5e14..86c35456 100644 --- a/src/utils/slices/settingsSlice.ts +++ b/src/utils/slices/settingsSlice.ts @@ -2,16 +2,30 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '@root/store' import * as Analytics from 'expo-firebase-analytics' import * as Localization from 'expo-localization' +import { pickBy } from 'lodash' + +enum availableLanguages { + 'zh-Hans', + 'en' +} export type SettingsState = { - language: 'zh-Hans' | 'en' + language: keyof availableLanguages theme: 'light' | 'dark' | 'auto' browser: 'internal' | 'external' analytics: boolean } export const settingsInitialState = { - language: Localization.locale, + language: Object.keys( + pickBy(availableLanguages, (_, key) => Localization.locale.includes(key)) + ) + ? Object.keys( + pickBy(availableLanguages, (_, key) => + Localization.locale.includes(key) + ) + )[0] + : 'en', theme: 'auto', browser: 'internal', analytics: true diff --git a/src/utils/styles/constants.ts b/src/utils/styles/constants.ts index 3aaf55f0..755d3e78 100644 --- a/src/utils/styles/constants.ts +++ b/src/utils/styles/constants.ts @@ -21,5 +21,5 @@ export const StyleConstants = { Global: { PagePadding: Base * 4 } }, - Avatar: { S: 40, M: 52, L: 104 } + Avatar: { S: 40, M: 52, L: 96 } } diff --git a/yarn.lock b/yarn.lock index 684afa31..1ce7e416 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1934,6 +1934,11 @@ dependencies: prop-types "^15.5.10" +"@react-native-community/cameraroll@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cameraroll/-/cameraroll-4.0.2.tgz#5baac97f4a56f50b7307b08efcc1fe37acdec462" + integrity sha512-GtSZO6pqUzyZvaYidB5zH90o6Yb9YatapgiMQ+JVdbK4bDD74GdrNGDwyinDTzE5LkAQ90HDoAhVgV/uWt5OrQ== + "@react-native-community/cli-debugger-ui@^4.13.1": version "4.13.1" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.13.1.tgz#07de6d4dab80ec49231de1f1fbf658b4ad39b32c"