Merge pull request #134 from tooot-app/main

Release v2.0
This commit is contained in:
xmflsct 2021-05-27 12:15:31 +02:00 committed by GitHub
commit 8fa458a74d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3113 additions and 2843 deletions

View File

@ -5,6 +5,8 @@ export SENTRY_PROJECT=""
export SENTRY_AUTH_TOKEN="" export SENTRY_AUTH_TOKEN=""
export SENTRY_DSN="" export SENTRY_DSN=""
export TRANSLATE_KEY=""
# Fastlane start # Fastlane start
export LC_ALL="" export LC_ALL=""
export LANG="" export LANG=""

View File

@ -40,6 +40,7 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TRANSLATE_KEY: ${{ secrets.TRANSLATE_KEY }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }} FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}

View File

@ -15,7 +15,7 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.1.1) aws-eventstream (1.1.1)
aws-partitions (1.445.0) aws-partitions (1.455.0)
aws-sdk-core (3.114.0) aws-sdk-core (3.114.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
@ -24,7 +24,7 @@ GEM
aws-sdk-kms (1.43.0) aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.93.1) aws-sdk-s3 (1.94.1)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
@ -71,10 +71,9 @@ GEM
cocoapods-try (1.2.0) cocoapods-try (1.2.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander-fastlane (4.4.6) commander (4.6.0)
highline (~> 1.7.2) highline (~> 2.0.0)
concurrent-ruby (1.1.8) concurrent-ruby (1.1.8)
connection_pool (2.2.5)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.3) digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
@ -85,25 +84,23 @@ GEM
escape (0.0.4) escape (0.0.4)
ethon (0.12.0) ethon (0.12.0)
ffi (>= 1.3.0) ffi (>= 1.3.0)
excon (0.80.1) excon (0.81.0)
faraday (1.4.0) faraday (1.4.1)
faraday-excon (~> 1.0) faraday-excon (~> 1.1)
faraday-net_http (~> 1.0) faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0) faraday-net_http_persistent (~> 1.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7) faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0) faraday (>= 0.8.0)
http-cookie (~> 1.0.0) http-cookie (~> 1.0.0)
faraday-excon (1.0.0) faraday-excon (1.1.0)
excon (>= 0.27.4)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.0.3) faraday-net_http_persistent (1.1.0)
net-http-persistent (>= 3.1)
faraday_middleware (1.0.0) faraday_middleware (1.0.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.3) fastimage (2.2.3)
fastlane (2.180.1) fastlane (2.182.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -111,7 +108,7 @@ GEM
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
colored colored
commander-fastlane (>= 4.4.6, < 5.0.0) commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0) excon (>= 0.71.0, < 1.0.0)
@ -122,7 +119,7 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0) google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0) google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0) highline (~> 2.0)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
@ -132,7 +129,6 @@ GEM
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3) security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0) terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
@ -185,14 +181,14 @@ GEM
google-cloud-core (~> 1.2) google-cloud-core (~> 1.2)
googleauth (~> 0.9) googleauth (~> 0.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.16.1) googleauth (0.16.2)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.14) signet (~> 0.14)
highline (1.7.10) highline (2.0.3)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
@ -200,7 +196,7 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jmespath (1.4.0) jmespath (1.4.0)
json (2.5.1) json (2.5.1)
jwt (2.2.2) jwt (2.2.3)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.0) mini_mime (1.1.0)
@ -211,8 +207,6 @@ GEM
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0) nap (1.1.0)
naturally (2.2.1) naturally (2.2.1)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
netrc (0.11.0) netrc (0.11.0)
os (1.1.1) os (1.1.1)
plist (3.6.0) plist (3.6.0)
@ -237,7 +231,6 @@ GEM
simctl (1.6.8) simctl (1.6.8)
CFPropertyList CFPropertyList
naturally naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)

View File

@ -1,5 +1,5 @@
# [tooot](https://tooot.app/) app for Mastodon # [tooot](https://tooot.app/) app for Mastodon
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push?style=flat-square)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app?style=flat-square) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases&style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app?style=flat-square) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot) [![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build?style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate&style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release&style=flat-square) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release)

View File

@ -8,8 +8,8 @@ public class BasePackageList {
public List<Package> getPackageList() { public List<Package> getPackageList() {
return Arrays.<Package>asList( return Arrays.<Package>asList(
new expo.modules.application.ApplicationPackage(), new expo.modules.application.ApplicationPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.av.AVPackage(), new expo.modules.av.AVPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.crypto.CryptoPackage(), new expo.modules.crypto.CryptoPackage(),
new expo.modules.device.DevicePackage(), new expo.modules.device.DevicePackage(),
new expo.modules.errorrecovery.ErrorRecoveryPackage(), new expo.modules.errorrecovery.ErrorRecoveryPackage(),
@ -19,17 +19,15 @@ public class BasePackageList {
new expo.modules.font.FontLoaderPackage(), new expo.modules.font.FontLoaderPackage(),
new expo.modules.haptics.HapticsPackage(), new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(), new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.permissions.PermissionsPackage(), new expo.modules.imagemanipulator.ImageManipulatorPackage(),
new expo.modules.imagepicker.ImagePickerPackage(), new expo.modules.imagepicker.ImagePickerPackage(),
new expo.modules.keepawake.KeepAwakePackage(), new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.localization.LocalizationPackage(), new expo.modules.localization.LocalizationPackage(),
new expo.modules.location.LocationPackage(),
new expo.modules.notifications.NotificationsPackage(), new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.screencapture.ScreenCapturePackage(), new expo.modules.screencapture.ScreenCapturePackage(),
new expo.modules.securestore.SecureStorePackage(), new expo.modules.securestore.SecureStorePackage(),
new expo.modules.splashscreen.SplashScreenPackage(), new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.storereview.StoreReviewPackage(), new expo.modules.storereview.StoreReviewPackage(),
new expo.modules.updates.UpdatesPackage(), new expo.modules.updates.UpdatesPackage(),
new expo.modules.videothumbnails.VideoThumbnailsPackage(), new expo.modules.videothumbnails.VideoThumbnailsPackage(),

View File

@ -4,8 +4,8 @@ buildscript {
ext { ext {
buildToolsVersion = "29.0.3" buildToolsVersion = "29.0.3"
minSdkVersion = 21 minSdkVersion = 21
compileSdkVersion = 29 compileSdkVersion = 30
targetSdkVersion = 29 targetSdkVersion = 30
ndkVersion = "20.1.5948944" ndkVersion = "20.1.5948944"
} }
repositories { repositories {

View File

@ -13,7 +13,8 @@ export default (): ExpoConfig => ({
privacy: 'hidden', privacy: 'hidden',
assetBundlePatterns: ['assets/*'], assetBundlePatterns: ['assets/*'],
extra: { extra: {
sentryDSN: process.env.SENTRY_DSN sentryDSN: process.env.SENTRY_DSN,
translateKey: process.env.TRANSLATE_KEY
}, },
hooks: { hooks: {
postPublish: [ postPublish: [

View File

@ -11,6 +11,7 @@ module.exports = function (api) {
'@assets': './assets', '@assets': './assets',
'@root': './src', '@root': './src',
'@api': './src/api', '@api': './src/api',
'@helpers': './src/helpers',
'@components': './src/components', '@components': './src/components',
'@screens': './src/screens', '@screens': './src/screens',
'@utils': './src/utils' '@utils': './src/utils'

View File

@ -28,8 +28,3 @@ submission_information({
add_id_info_tracks_install: false, add_id_info_tracks_install: false,
add_id_info_uses_idfa: true add_id_info_uses_idfa: true
}) })
release_notes({
'zh-Hans' => "添加支持修改账户信息",
'en-US' => "Added the possibility to update account information"
})

View File

@ -109,8 +109,8 @@ private_lane :build_ios do
upload_to_app_store( ipa: IPA_FILE, app_version: VERSION ) upload_to_app_store( ipa: IPA_FILE, app_version: VERSION )
else else
if !is_ci if !is_ci
match( type: "development", readonly: true ) match( type: "adhoc", readonly: true )
build_ios_app( export_method: "development", output_directory: BUILD_DIRECTORY, silent: true ) build_ios_app( export_method: "ad-hoc", output_directory: BUILD_DIRECTORY, silent: true )
install_on_device( skip_wifi: true ) install_on_device( skip_wifi: true )
end end
end end

View File

@ -1 +1,2 @@
Added translation option, translation service is provided by various providers
When updating profile, now avatar and banner can be uploaded

View File

@ -1 +1,2 @@
加入翻译嘟文支持,翻译服务由多个服务商提供
修改个人信息里可以上传头像及横幅

File diff suppressed because it is too large Load Diff

View File

@ -346,7 +346,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements; CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2102022230; CURRENT_PROJECT_VERSION = 2102022230;
DEVELOPMENT_TEAM = 8EGBLQ2MA6; DEVELOPMENT_TEAM = 8EGBLQ2MA6;
@ -366,7 +366,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot; PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
PRODUCT_NAME = tooot; PRODUCT_NAME = tooot;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.xmflsct.app.tooot"; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot";
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -1,11 +1,11 @@
{ {
"name": "tooot", "name": "tooot",
"versions": { "versions": {
"native": "210317", "native": "210511",
"major": 1, "major": 2,
"minor": 1, "minor": 0,
"patch": 0, "patch": 1,
"expo": "40.0.0" "expo": "41.0.0"
}, },
"description": "tooot app for Mastodon", "description": "tooot app for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
@ -26,43 +26,45 @@
"dependencies": { "dependencies": {
"@expo/react-native-action-sheet": "^3.9.0", "@expo/react-native-action-sheet": "^3.9.0",
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10", "@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
"@react-native-async-storage/async-storage": "^1.14.1", "@react-native-async-storage/async-storage": "^1.15.4",
"@react-native-community/blur": "^3.6.0", "@react-native-community/blur": "^3.6.0",
"@react-native-community/cameraroll": "^4.0.2", "@react-native-community/cameraroll": "^4.0.4",
"@react-native-community/masked-view": "0.1.10", "@react-native-community/masked-view": "0.1.11",
"@react-native-community/netinfo": "^6.0.0", "@react-native-community/netinfo": "6.0.0",
"@react-native-community/segmented-control": "2.2.2", "@react-native-community/segmented-control": "2.2.2",
"@react-navigation/bottom-tabs": "^5.11.8", "@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/native": "^5.9.3", "@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.3", "@react-navigation/stack": "^5.14.5",
"@reduxjs/toolkit": "^1.5.0", "@reduxjs/toolkit": "^1.5.1",
"@sentry/react-native": "^2.3.0", "@sentry/react-native": "^2.4.3",
"@sharcoux/slider": "^5.1.3", "@sharcoux/slider": "^5.3.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"expo": "^40.0.1", "expo": "^41.0.1",
"expo-auth-session": "~3.1.0", "expo-auth-session": "~3.2.3",
"expo-av": "~9.0.0", "expo-av": "~9.1.2",
"expo-crypto": "~9.0.0", "expo-crypto": "~9.1.0",
"expo-firebase-analytics": "~3.0.0", "expo-firebase-analytics": "~4.0.2",
"expo-haptics": "~9.0.0", "expo-haptics": "~10.0.0",
"expo-image-picker": "~10.0.0", "expo-image-manipulator": "~9.1.0",
"expo-linking": "~2.1.1", "expo-image-picker": "~10.1.4",
"expo-localization": "~10.0.0", "expo-linking": "~2.2.3",
"expo-notifications": "~0.9.0", "expo-localization": "~10.1.0",
"expo-random": "~11.0.0", "expo-notifications": "~0.11.6",
"expo-screen-capture": "^3.0.0", "expo-random": "~11.1.2",
"expo-splash-screen": "~0.9.0", "expo-screen-capture": "^3.1.0",
"expo-status-bar": "~1.0.3", "expo-secure-store": "~10.1.0",
"expo-store-review": "~3.0.0", "expo-splash-screen": "~0.10.2",
"expo-video-thumbnails": "~5.0.0", "expo-status-bar": "~1.0.4",
"expo-web-browser": "~9.0.0", "expo-store-review": "~4.0.2",
"i18next": "^19.9.2", "expo-video-thumbnails": "~5.1.0",
"expo-web-browser": "~9.1.0",
"i18next": "^20.3.0",
"li": "^1.3.0", "li": "^1.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "17.0.1", "react": "17.0.2",
"react-dom": "17.0.1", "react-dom": "17.0.2",
"react-i18next": "^11.8.10", "react-i18next": "^11.9.0",
"react-native": "~0.64.0", "react-native": "~0.64.1",
"react-native-animated-spinkit": "^1.5.2", "react-native-animated-spinkit": "^1.5.2",
"react-native-blurhash": "^1.1.4", "react-native-blurhash": "^1.1.4",
"react-native-fast-image": "^8.3.4", "react-native-fast-image": "^8.3.4",
@ -70,56 +72,55 @@
"react-native-flash-message": "^0.1.23", "react-native-flash-message": "^0.1.23",
"react-native-gesture-handler": "~1.10.3", "react-native-gesture-handler": "~1.10.3",
"react-native-htmlview": "^0.16.0", "react-native-htmlview": "^0.16.0",
"react-native-pager-view": "^5.1.2", "react-native-pager-view": "5.1.9",
"react-native-reanimated": "^2.0.0", "react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0", "react-native-safe-area-context": "3.2.0",
"react-native-screens": "~2.17.1", "react-native-screens": "~3.3.0",
"react-native-svg": "12.1.0", "react-native-svg": "12.1.1",
"react-native-swipe-list-view": "^3.2.6", "react-native-swipe-list-view": "^3.2.7",
"react-native-tab-view": "^3.0.0", "react-native-tab-view": "^3.0.1",
"react-native-unimodules": "~0.12.0", "react-native-unimodules": "~0.13.3",
"react-query": "^3.12.2", "react-query": "^3.16.0",
"react-redux": "^7.2.2", "react-redux": "^7.2.4",
"react-timeago": "^5.2.0", "react-timeago": "^5.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3", "rn-placeholder": "^3.0.3",
"sentry-expo": "^3.0.5", "sentry-expo": "^3.1.3",
"tslib": "^2.1.0", "tslib": "^2.2.0",
"valid-url": "^1.0.9" "valid-url": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "~7.13.10", "@babel/core": "~7.14.3",
"@babel/plugin-proposal-optional-chaining": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.14.2",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@expo/config": "^3.3.31", "@expo/config": "^3.3.43",
"@jest/types": "^26.6.2", "@jest/types": "^26.6.2",
"@testing-library/jest-native": "^4.0.1", "@testing-library/jest-native": "^4.0.1",
"@testing-library/react-hooks": "^5.1.0", "@testing-library/react-hooks": "^5.1.2",
"@testing-library/react-native": "^7.2.0", "@testing-library/react-native": "^7.2.0",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.23",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.170",
"@types/react": "~17.0.3", "@types/react": "~17.0.8",
"@types/react-dom": "~17.0.2", "@types/react-dom": "~17.0.5",
"@types/react-native": "~0.63.52", "@types/react-native": "~0.64.6",
"@types/react-navigation": "^3.4.0", "@types/react-navigation": "^3.4.0",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.16",
"@types/react-test-renderer": "^17.0.1", "@types/react-test-renderer": "^17.0.1",
"@types/react-timeago": "^4.1.2", "@types/react-timeago": "^4.1.2",
"@types/valid-url": "^1.0.3", "@types/valid-url": "^1.0.3",
"@welldone-software/why-did-you-render": "^6.1.1", "@welldone-software/why-did-you-render": "^6.1.4",
"babel-jest": "~26.6.3", "babel-jest": "~26.6.3",
"babel-plugin-module-resolver": "^4.1.0", "babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-console": "^6.9.4",
"chalk": "^4.1.0", "chalk": "^4.1.1",
"dotenv": "^8.2.0", "dotenv": "^10.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-expo": "^40.0.2", "jest-expo": "^41.0.0",
"nock": "^13.0.11", "nock": "^13.0.11",
"react-native-clean-project": "^3.6.3", "react-native-clean-project": "^3.6.4",
"react-navigation": "^4.4.4", "react-navigation": "^4.4.4",
"react-navigation-stack": "^2.10.4", "react-navigation-stack": "^2.10.4",
"react-test-renderer": "^17.0.1", "react-test-renderer": "^17.0.2",
"typescript": "~4.2.3", "typescript": "~4.2.4"
"uri-scheme": "^1.0.68"
} }
} }

View File

@ -1,6 +1,4 @@
declare namespace Nav { declare namespace Nav {
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
type RootStackParamList = { type RootStackParamList = {
'Screen-Tabs': undefined 'Screen-Tabs': undefined
'Screen-Actions': 'Screen-Actions':
@ -151,8 +149,4 @@ declare namespace Nav {
fields?: Mastodon.Source['fields'] fields?: Mastodon.Source['fields']
} }
} }
type TabMePushStackParamList = {
'Tab-Me-Push-Root': undefined
}
} }

View File

@ -1,4 +1,5 @@
declare module 'gl-react-blurhash' declare module 'gl-react-blurhash'
declare module 'htmlparser2-without-node-native'
declare module 'li' declare module 'li'
declare module 'react-native-feather' declare module 'react-native-feather'
declare module 'react-native-htmlview' declare module 'react-native-htmlview'

View File

@ -1,4 +1,5 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet' import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n' import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens' import Screens from '@root/Screens'
import audio from '@root/startup/audio' import audio from '@root/startup/audio'
@ -14,8 +15,7 @@ import * as Notifications from 'expo-notifications'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { AppState, LogBox, Platform } from 'react-native' import { AppState, LogBox, Platform } from 'react-native'
import { enableScreens } from 'react-native-screens' import { QueryClientProvider } from 'react-query'
import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import push from './startup/push' import push from './startup/push'
@ -29,12 +29,6 @@ sentry()
audio() audio()
push() push()
log('log', 'react-query', 'initializing')
export const queryClient = new QueryClient()
log('log', 'react-native-screens', 'initializing')
enableScreens()
const App: React.FC = () => { const App: React.FC = () => {
log('log', 'App', 'rendering App') log('log', 'App', 'rendering App')
const [localCorrupt, setLocalCorrupt] = useState<string>() const [localCorrupt, setLocalCorrupt] = useState<string>()

View File

@ -1,9 +1,8 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { displayMessage, Message, removeMessage } from '@components/Message' import { displayMessage, Message, removeMessage } from '@components/Message'
import navigationRef from '@helpers/navigationRef'
import { useNetInfo } from '@react-native-community/netinfo' import { useNetInfo } from '@react-native-community/netinfo'
import { import { NavigationContainer } from '@react-navigation/native'
NavigationContainer,
NavigationContainerRef
} from '@react-navigation/native'
import ScreenActions from '@screens/Actions' import ScreenActions from '@screens/Actions'
import ScreenAnnouncements from '@screens/Announcements' import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose' import ScreenCompose from '@screens/Compose'
@ -19,7 +18,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes' import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics' import * as Analytics from 'expo-firebase-analytics'
import { addScreenshotListener } from 'expo-screen-capture' import { addScreenshotListener } from 'expo-screen-capture'
import React, { createRef, useCallback, useEffect, useRef } from 'react' import React, { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Platform, StatusBar } from 'react-native' import { Alert, Platform, StatusBar } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
@ -28,7 +27,6 @@ import { useDispatch, useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo' import * as Sentry from 'sentry-expo'
const Stack = createNativeStackNavigator<Nav.RootStackParamList>() const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
export const navigationRef = createRef<NavigationContainerRef>()
export interface Props { export interface Props {
localCorrupt?: string localCorrupt?: string
@ -174,18 +172,30 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
<Stack.Screen <Stack.Screen
name='Screen-Announcements' name='Screen-Announcements'
component={ScreenAnnouncements} component={ScreenAnnouncements}
options={{ options={({ navigation }) => ({
stackPresentation: 'transparentModal', stackPresentation: 'transparentModal',
stackAnimation: 'fade', stackAnimation: 'fade',
headerShown: false headerShown: true,
}} headerHideShadow: true,
headerTopInsetEnabled: false,
headerStyle: { backgroundColor: 'transparent' },
headerLeft: () => (
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
),
headerTitle: t('screenAnnouncements:heading'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('screenAnnouncements:heading')} />
)
})
})}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Compose' name='Screen-Compose'
component={ScreenCompose} component={ScreenCompose}
options={{ options={{
stackPresentation: 'fullScreenModal', stackPresentation: 'fullScreenModal',
headerShown: false ...(Platform.OS === 'android' && { headerShown: false })
}} }}
/> />
<Stack.Screen <Stack.Screen
@ -194,7 +204,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
options={{ options={{
stackPresentation: 'fullScreenModal', stackPresentation: 'fullScreenModal',
stackAnimation: 'fade', stackAnimation: 'fade',
headerShown: false ...(Platform.OS === 'android' && { headerShown: false })
}} }}
/> />
</Stack.Navigator> </Stack.Navigator>

View File

@ -69,12 +69,16 @@ const apiGeneral = async <T = unknown>({
error.response.status, error.response.status,
error.response.data.error error.response.data.error
) )
return Promise.reject(error.response) return Promise.reject(error.response.data.error)
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(ctx.bold(' API general '), ctx.bold('request'), error) console.error(
ctx.bold(' API general '),
ctx.bold('request'),
error.request
)
return Promise.reject() return Promise.reject()
} else { } else {
console.error( console.error(

View File

@ -98,7 +98,7 @@ const apiInstance = async <T = unknown>({
error.response.status, error.response.status,
error.response.data.error error.response.data.error
) )
return Promise.reject(error.response) return Promise.reject(error.response.data.error)
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of

View File

@ -4,7 +4,6 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useEmojisQuery } from '@utils/queryHooks/emojis' import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { chunk, forEach, groupBy, sortBy } from 'lodash' import { chunk, forEach, groupBy, sortBy } from 'lodash'
import React, { import React, {
createContext,
Dispatch, Dispatch,
MutableRefObject, MutableRefObject,
SetStateAction, SetStateAction,
@ -13,44 +12,7 @@ import React, {
useReducer useReducer
} from 'react' } from 'react'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext'
type EmojisState = {
enabled: boolean
active: boolean
emojis: { title: string; data: Mastodon.Emoji[][] }[]
shortcode: Mastodon.Emoji['shortcode'] | null
}
type EmojisAction =
| {
type: 'load'
payload: NonNullable<EmojisState['emojis']>
}
| {
type: 'activate'
payload: EmojisState['active']
}
| {
type: 'shortcode'
payload: EmojisState['shortcode']
}
const emojisReducer = (state: EmojisState, action: EmojisAction) => {
switch (action.type) {
case 'activate':
return { ...state, active: action.payload }
case 'load':
return { ...state, emojis: action.payload }
case 'shortcode':
return { ...state, shortcode: action.payload }
}
}
type ContextType = {
emojisState: EmojisState
emojisDispatch: Dispatch<EmojisAction>
}
const EmojisContext = createContext<ContextType>({} as ContextType)
const prefetchEmojis = ( const prefetchEmojis = (
sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[], sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
@ -163,4 +125,4 @@ const ComponentEmojis: React.FC<Props> = ({
) )
} }
export { ComponentEmojis, EmojisContext, EmojisButton, EmojisList } export { ComponentEmojis, EmojisButton, EmojisList }

View File

@ -1,9 +1,9 @@
import { EmojisContext } from '@components/Emojis'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Pressable, StyleSheet } from 'react-native' import { Pressable, StyleSheet } from 'react-native'
import EmojisContext from './helpers/EmojisContext'
const EmojisButton = React.memo( const EmojisButton = React.memo(
() => { () => {

View File

@ -1,4 +1,3 @@
import { EmojisContext } from '@components/Emojis'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
@ -16,6 +15,7 @@ import {
} from 'react-native' } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import validUrl from 'valid-url' import validUrl from 'valid-url'
import EmojisContext from './helpers/EmojisContext'
const EmojisList = React.memo( const EmojisList = React.memo(
() => { () => {

View File

@ -0,0 +1,41 @@
import { createContext, Dispatch } from 'react'
export type EmojisState = {
enabled: boolean
active: boolean
emojis: { title: string; data: Mastodon.Emoji[][] }[]
shortcode: Mastodon.Emoji['shortcode'] | null
}
export type EmojisAction =
| {
type: 'load'
payload: NonNullable<EmojisState['emojis']>
}
| {
type: 'activate'
payload: EmojisState['active']
}
| {
type: 'shortcode'
payload: EmojisState['shortcode']
}
type ContextType = {
emojisState: EmojisState
emojisDispatch: Dispatch<EmojisAction>
}
const EmojisContext = createContext<ContextType>({} as ContextType)
export const emojisReducer = (state: EmojisState, action: EmojisAction) => {
switch (action.type) {
case 'activate':
return { ...state, active: action.payload }
case 'load':
return { ...state, emojis: action.payload }
case 'shortcode':
return { ...state, shortcode: action.payload }
}
}
export default EmojisContext

View File

@ -18,12 +18,8 @@ import {
View View
} from 'react-native' } from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis'
ComponentEmojis, import EmojisContext from './Emojis/helpers/EmojisContext'
EmojisButton,
EmojisContext,
EmojisList
} from './Emojis'
export interface Props { export interface Props {
autoFocus?: boolean autoFocus?: boolean
@ -114,7 +110,8 @@ const Input: React.FC<Props> = ({
styles.base, styles.base,
{ {
borderColor: theme.border, borderColor: theme.border,
flexDirection: multiline ? 'column' : 'row' flexDirection: multiline ? 'column' : 'row',
alignItems: 'stretch'
} }
]} ]}
> >
@ -157,7 +154,7 @@ const Input: React.FC<Props> = ({
{title} {title}
</Animated.Text> </Animated.Text>
) : null} ) : null}
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
{options?.maxLength && value?.length ? ( {options?.maxLength && value?.length ? (
<Text style={[styles.maxLength, { color: theme.secondary }]}> <Text style={[styles.maxLength, { color: theme.secondary }]}>
{value?.length} / {options.maxLength} {value?.length} / {options.maxLength}

View File

@ -76,6 +76,7 @@ const MenuRow: React.FC<Props> = ({
} }
}} }}
> >
<View style={{ flex: 1 }}>
<View style={styles.core}> <View style={styles.core}>
<View style={styles.front}> <View style={styles.front}>
{iconFront && ( {iconFront && (
@ -148,24 +149,25 @@ const MenuRow: React.FC<Props> = ({
</View> </View>
) : null} ) : null}
</View> </View>
</TapGestureHandler>
{description ? ( {description ? (
<Text style={[styles.description, { color: theme.secondary }]}> <Text style={[styles.description, { color: theme.secondary }]}>
{description} {description}
</Text> </Text>
) : null} ) : null}
</View> </View>
</TapGestureHandler>
</View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
minHeight: 46, minHeight: 50
paddingVertical: StyleConstants.Spacing.S
}, },
core: { core: {
flex: 1, flex: 1,
flexDirection: 'row' flexDirection: 'row',
paddingVertical: StyleConstants.Spacing.S
}, },
front: { front: {
flex: 2, flex: 2,

View File

@ -80,7 +80,7 @@ const displayMessage = ({
}) })
} else { } else {
showMessage({ showMessage({
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000, duration: type === 'error' ? 3500 : duration === 'short' ? 1500 : 2500,
autoHide, autoHide,
message, message,
description, description,

View File

@ -4,7 +4,6 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling' import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -28,7 +27,6 @@ const ParseEmojis = React.memo(
adaptiveSize = false, adaptiveSize = false,
fontBold = false fontBold = false
}: Props) => { }: Props) => {
const { t } = useTranslation('componentParse')
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptiveFontsize = useSelector(getSettingsFontsize)

View File

@ -162,7 +162,9 @@ export interface Props {
showFullLink?: boolean showFullLink?: boolean
numberOfLines?: number numberOfLines?: number
expandHint?: string expandHint?: string
highlighted?: boolean
disableDetails?: boolean disableDetails?: boolean
selectable?: boolean
} }
const ParseHTML = React.memo( const ParseHTML = React.memo(
@ -176,7 +178,9 @@ const ParseHTML = React.memo(
showFullLink = false, showFullLink = false,
numberOfLines = 10, numberOfLines = 10,
expandHint, expandHint,
disableDetails = false highlighted = false,
disableDetails = false,
selectable = false
}: Props) => { }: Props) => {
const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale( const adaptedFontsize = adaptiveScale(
@ -234,7 +238,7 @@ const ParseHTML = React.memo(
const { t } = useTranslation('componentParse') const { t } = useTranslation('componentParse')
const [expandAllow, setExpandAllow] = useState(false) const [expandAllow, setExpandAllow] = useState(false)
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(highlighted)
const onTextLayout = useCallback(({ nativeEvent }) => { const onTextLayout = useCallback(({ nativeEvent }) => {
if ( if (
@ -253,6 +257,7 @@ const ParseHTML = React.memo(
numberOfLines={ numberOfLines={
expandAllow ? (expanded ? 999 : numberOfLines) : undefined expandAllow ? (expanded ? 999 : numberOfLines) : undefined
} }
selectable={selectable}
/> />
{expandAllow ? ( {expandAllow ? (
<Pressable <Pressable

View File

@ -19,6 +19,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineActionsUsers from './Shared/ActionsUsers' import TimelineActionsUsers from './Shared/ActionsUsers'
import TimelineFullConversation from './Shared/FullConversation' import TimelineFullConversation from './Shared/FullConversation'
import TimelineTranslate from './Shared/Translate'
export interface Props { export interface Props {
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
@ -128,10 +129,12 @@ const TimelineDefault: React.FC<Props> = ({
{!disableDetails && actualStatus.card && ( {!disableDetails && actualStatus.card && (
<TimelineCard card={actualStatus.card} /> <TimelineCard card={actualStatus.card} />
)} )}
{!disableDetails ? (
<TimelineFullConversation queryKey={queryKey} status={actualStatus} /> <TimelineFullConversation queryKey={queryKey} status={actualStatus} />
</View> ) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} /> <TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
</View>
{queryKey && !disableDetails && ( {queryKey && !disableDetails && (
<TimelineActions <TimelineActions

View File

@ -340,7 +340,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4, minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
marginHorizontal: StyleConstants.Spacing.S marginHorizontal: StyleConstants.Spacing.S
} }
}) })

View File

@ -38,7 +38,7 @@ const TimelineActionsUsers = React.memo(
'shared.actionsUsers.reblogged_by.accessibilityHint' 'shared.actionsUsers.reblogged_by.accessibilityHint'
)} )}
accessibilityRole='button' accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]} style={[styles.text, { color: theme.blue }]}
onPress={() => { onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', { analytics('timeline_shared_actionsusers_press_boosted', {
count: status.reblogs_count count: status.reblogs_count
@ -68,7 +68,7 @@ const TimelineActionsUsers = React.memo(
'shared.actionsUsers.favourited_by.accessibilityHint' 'shared.actionsUsers.favourited_by.accessibilityHint'
)} )}
accessibilityRole='button' accessibilityRole='button'
style={[styles.text, { color: theme.secondary }]} style={[styles.text, { color: theme.blue }]}
onPress={() => { onPress={() => {
analytics('timeline_shared_actionsusers_press_boosted', { analytics('timeline_shared_actionsusers_press_boosted', {
count: status.favourites_count count: status.favourites_count
@ -98,10 +98,9 @@ const styles = StyleSheet.create({
base: { base: {
flexDirection: 'row' flexDirection: 'row'
}, },
pressable: { margin: StyleConstants.Spacing.M },
text: { text: {
...StyleConstants.FontStyle.S, ...StyleConstants.FontStyle.M,
padding: StyleConstants.Spacing.S * 1.5, padding: StyleConstants.Spacing.S,
paddingLeft: 0, paddingLeft: 0,
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
} }

View File

@ -30,7 +30,9 @@ const TimelineContent = React.memo(
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={999} numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails} disableDetails={disableDetails}
selectable={highlighted}
/> />
<ParseHTML <ParseHTML
content={status.content} content={status.content}
@ -41,7 +43,9 @@ const TimelineContent = React.memo(
tags={status.tags} tags={status.tags}
numberOfLines={1} numberOfLines={1}
expandHint={t('shared.content.expandHint')} expandHint={t('shared.content.expandHint')}
highlighted={highlighted}
disableDetails={disableDetails} disableDetails={disableDetails}
selectable={highlighted}
/> />
</> </>
) : ( ) : (
@ -54,6 +58,7 @@ const TimelineContent = React.memo(
tags={status.tags} tags={status.tags}
numberOfLines={highlighted ? 999 : numberOfLines} numberOfLines={highlighted ? 999 : numberOfLines}
disableDetails={disableDetails} disableDetails={disableDetails}
selectable={highlighted}
/> />
)} )}
</> </>

View File

@ -26,7 +26,7 @@ const TimelineFullConversation = React.memo(
style={{ style={{
...StyleConstants.FontStyle.S, ...StyleConstants.FontStyle.S,
color: theme.blue, color: theme.blue,
marginTop: StyleConstants.Font.Size.S marginTop: StyleConstants.Spacing.S
}} }}
> >
{t('shared.fullConversation')} {t('shared.fullConversation')}

View File

@ -0,0 +1,131 @@
import analytics from '@components/analytics'
import { ParseHTML } from '@components/Parse'
import { useTranslateQuery } from '@utils/queryHooks/translate'
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import { useSelector } from 'react-redux'
export interface Props {
highlighted: boolean
status: Mastodon.Status
}
const TimelineTranslate = React.memo(
({ highlighted, status }: Props) => {
if (!highlighted) {
return null
}
if (!status.language) {
return null
}
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme()
const tootLanguage = status.language.slice(0, 2)
const settingsLanguage = useSelector(getSettingsLanguage)
if (settingsLanguage.includes(tootLanguage)) {
return null
}
let text = status.spoiler_text
? [status.spoiler_text, status.content]
: [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, '')
}
}
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
uri: status.uri,
source: status.language,
target: settingsLanguage,
text,
options: { enabled }
})
return (
<>
<Pressable
style={[styles.button, { paddingBottom: isSuccess ? 0 : undefined }]}
onPress={() => {
if (enabled) {
if (!isSuccess) {
analytics('timeline_shared_translate_retry', {
language: status.language
})
refetch()
}
} else {
analytics('timeline_shared_translate', {
language: status.language
})
setEnabled(true)
}
}}
>
<Text
style={{
...StyleConstants.FontStyle.M,
color:
isLoading || isSuccess
? theme.secondary
: isError
? theme.red
: theme.blue
}}
>
{isError
? t('shared.translate.failed')
: isSuccess
? t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
{__DEV__ ? ` Source: ${status.language}` : undefined}
</Text>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={theme.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
{data
? data.text.map((d, i) => (
<ParseHTML
key={i}
content={d}
size={'M'}
numberOfLines={999}
selectable
/>
))
: null}
</>
)
},
() => true
)
const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S
}
})
export default TimelineTranslate

View File

@ -1,13 +1,14 @@
import * as ImagePicker from 'expo-image-picker'
import { Alert, Linking } from 'react-native'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next'
import analytics from '@components/analytics' import analytics from '@components/analytics'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import * as ImageManipulator from 'expo-image-manipulator'
import * as ImagePicker from 'expo-image-picker'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import i18next from 'i18next'
import { Alert, Linking } from 'react-native'
export interface Props { export interface Props {
mediaTypes?: ImagePicker.MediaTypeOptions mediaTypes?: ImagePicker.MediaTypeOptions
uploader: (imageInfo: ImageInfo) => void resize?: { width?: number; height?: number } // Resize mode contain
showActionSheetWithOptions: ( showActionSheetWithOptions: (
options: ActionSheetOptions, options: ActionSheetOptions,
callback: (i: number) => void callback: (i: number) => void
@ -16,9 +17,34 @@ export interface Props {
const mediaSelector = async ({ const mediaSelector = async ({
mediaTypes = ImagePicker.MediaTypeOptions.All, mediaTypes = ImagePicker.MediaTypeOptions.All,
uploader, resize,
showActionSheetWithOptions showActionSheetWithOptions
}: Props): Promise<any> => { }: Props): Promise<ImageInfo> => {
return new Promise((resolve, reject) => {
const resolveResult = async (result: ImageInfo) => {
if (resize && result.type === 'image') {
let newResult: ImageManipulator.ImageResult
if (resize.width && resize.height) {
if (resize.width / resize.height > result.width / result.height) {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { width: resize.width } }
])
} else {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize: { height: resize.height } }
])
}
} else {
newResult = await ImageManipulator.manipulateAsync(result.uri, [
{ resize }
])
}
resolve(newResult)
} else {
resolve(result)
}
}
showActionSheetWithOptions( showActionSheetWithOptions(
{ {
title: i18next.t('componentMediaSelector:title'), title: i18next.t('componentMediaSelector:title'),
@ -45,7 +71,9 @@ const mediaSelector = async ({
), ),
style: 'cancel', style: 'cancel',
onPress: () => onPress: () =>
analytics('mediaSelector_nopermission', { action: 'cancel' }) analytics('mediaSelector_nopermission', {
action: 'cancel'
})
}, },
{ {
text: i18next.t( text: i18next.t(
@ -68,13 +96,7 @@ const mediaSelector = async ({
}) })
if (!result.cancelled) { if (!result.cancelled) {
// https://github.com/expo/expo/issues/11214 await resolveResult(result)
const fixResult = {
...result,
uri: result.uri.replace('file:/data', 'file:///data')
}
uploader(fixResult)
return
} }
} }
} else if (buttonIndex === 1) { } else if (buttonIndex === 1) {
@ -116,18 +138,13 @@ const mediaSelector = async ({
}) })
if (!result.cancelled) { if (!result.cancelled) {
// https://github.com/expo/expo/issues/11214 await resolveResult(result)
const fixResult = {
...result,
uri: result.uri.replace('file:/data', 'file:///data')
}
uploader(fixResult)
return
} }
} }
} }
} }
) )
})
} }
export default mediaSelector export default mediaSelector

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import navigationRef from '@helpers/navigationRef'
import { NavigationProp, ParamListBase } from '@react-navigation/native' import { NavigationProp, ParamListBase } from '@react-navigation/native'
import { navigationRef } from '@root/Screens'
import { store } from '@root/store' import { store } from '@root/store'
import { SearchResult } from '@utils/queryHooks/search' import { SearchResult } from '@utils/queryHooks/search'
import { getInstanceUrl } from '@utils/slices/instancesSlice' import { getInstanceUrl } from '@utils/slices/instancesSlice'

View File

@ -0,0 +1,6 @@
import { NavigationContainerRef } from '@react-navigation/native'
import { createRef } from 'react'
const navigationRef = createRef<NavigationContainerRef>()
export default navigationRef

View File

@ -0,0 +1,5 @@
import { QueryClient } from 'react-query'
const queryClient = new QueryClient()
export default queryClient

View File

@ -74,6 +74,11 @@
"expandHint": "hidden content" "expandHint": "hidden content"
}, },
"fullConversation": "Read conversations", "fullConversation": "Read conversations",
"translate": {
"default": "Translate",
"succeed": "Translated by {{provider}} from {{source}}",
"failed": "Translation failed"
},
"header": { "header": {
"shared": { "shared": {
"account": { "account": {

View File

@ -102,11 +102,11 @@
}, },
"avatar": { "avatar": {
"title": "Avatar", "title": "Avatar",
"description": "Available in next version" "description": "Will be downscaled to 400x400px"
}, },
"banner": { "header": {
"title": "Banner", "title": "Banner",
"description": "Available in next version" "description": "Will be downscaled to 1500x500px"
}, },
"note": { "note": {
"title": "Description" "title": "Description"

View File

@ -74,6 +74,11 @@
"expandHint": "隐藏内容" "expandHint": "隐藏内容"
}, },
"fullConversation": "阅读全部对话", "fullConversation": "阅读全部对话",
"translate": {
"default": "翻译",
"succeed": "由{{provider}}翻译自{{source}}",
"failed": "翻译失败"
},
"header": { "header": {
"shared": { "shared": {
"account": { "account": {

View File

@ -102,11 +102,11 @@
}, },
"avatar": { "avatar": {
"title": "头像", "title": "头像",
"description": "将在下一版中启用" "description": "将缩小到400x400px"
}, },
"banner": { "header": {
"title": "横幅", "title": "横幅",
"description": "将在下一版中启用" "description": "将缩小到1500x500px"
}, },
"note": { "note": {
"title": "简介" "title": "简介"

View File

@ -1,7 +1,6 @@
import analytics from '@components/analytics' import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import RelativeTime from '@components/RelativeTime' import RelativeTime from '@components/RelativeTime'
import { BlurView } from '@react-native-community/blur' import { BlurView } from '@react-native-community/blur'
@ -88,6 +87,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
emojis={item.emojis} emojis={item.emojis}
mentions={item.mentions} mentions={item.mentions}
numberOfLines={999} numberOfLines={999}
selectable
/> />
</ScrollView> </ScrollView>
{item.reactions?.length ? ( {item.reactions?.length ? (
@ -210,28 +210,6 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
reducedTransparencyFallbackColor={theme.backgroundDefault} reducedTransparencyFallbackColor={theme.backgroundDefault}
> >
<SafeAreaView style={styles.base}> <SafeAreaView style={styles.base}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
flexBasis: 44
}}
>
<HeaderLeft
content='X'
native={false}
onPress={() => navigation.goBack()}
/>
<HeaderCenter content={t('screenAnnouncements:heading')} />
<View style={{ opacity: 0 }} accessible={false}>
<HeaderRight
content='MoreHorizontal'
native={false}
onPress={() => {}}
/>
</View>
</View>
<FlatList <FlatList
horizontal horizontal
data={query.data} data={query.data}

View File

@ -88,6 +88,14 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
return { return {
...composeInitialState, ...composeInitialState,
timestamp: Date.now(), timestamp: Date.now(),
attachments: {
...composeInitialState.attachments,
sensitive:
localAccount?.preferences &&
localAccount?.preferences['posting:default:sensitive']
? localAccount?.preferences['posting:default:sensitive']
: false
},
visibility: visibility:
localAccount?.preferences && localAccount?.preferences &&
localAccount.preferences['posting:default:visibility'] localAccount.preferences['posting:default:visibility']
@ -397,12 +405,18 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
<Stack.Screen <Stack.Screen
name='Screen-Compose-DraftsList' name='Screen-Compose-DraftsList'
component={ComposeDraftsList} component={ComposeDraftsList}
options={{ stackPresentation: 'modal', headerShown: false }} options={{
stackPresentation: 'modal',
...(Platform.OS === 'android' && { headerShown: false })
}}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Compose-EditAttachment' name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment} component={ComposeEditAttachment}
options={{ stackPresentation: 'modal', headerShown: false }} options={{
stackPresentation: 'modal',
...(Platform.OS === 'android' && { headerShown: false })
}}
/> />
</Stack.Navigator> </Stack.Navigator>
</ComposeContext.Provider> </ComposeContext.Provider>

View File

@ -3,7 +3,7 @@ import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { useSearchQuery } from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { forEach, groupBy, sortBy } from 'lodash' import { chunk, forEach, groupBy, sortBy } from 'lodash'
import React, { import React, {
useCallback, useCallback,
useContext, useContext,
@ -28,15 +28,17 @@ import ComposeContext from './utils/createContext'
import ComposeDrafts from './Root/Drafts' import ComposeDrafts from './Root/Drafts'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { ComposeState } from './utils/types'
const prefetchEmojis = ( const prefetchEmojis = (
sortedEmojis: { title: string; data: Mastodon.Emoji[] }[], sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
reduceMotionEnabled: boolean reduceMotionEnabled: boolean
) => { ) => {
const prefetches: { uri: string }[] = [] const prefetches: { uri: string }[] = []
let requestedIndex = 0 let requestedIndex = 0
sortedEmojis.forEach(sorted => { sortedEmojis.forEach(sorted => {
sorted.data.forEach(emoji => { sorted.data.forEach(emojis =>
emojis.forEach(emoji => {
if (requestedIndex > 40) { if (requestedIndex > 40) {
return return
} }
@ -45,6 +47,7 @@ const prefetchEmojis = (
}) })
requestedIndex++ requestedIndex++
}) })
)
}) })
try { try {
FastImage.preload(prefetches) FastImage.preload(prefetches)
@ -90,10 +93,11 @@ const ComposeRoot = React.memo(
const { data: emojisData } = useEmojisQuery({}) const { data: emojisData } = useEmojisQuery({})
useEffect(() => { useEffect(() => {
if (emojisData && emojisData.length) { if (emojisData && emojisData.length) {
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = [] let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
forEach( forEach(
groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'), groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
(value, key) => sortedEmojis.push({ title: key, data: value }) (value, key) =>
sortedEmojis.push({ title: key, data: chunk(value, 5) })
) )
composeDispatch({ composeDispatch({
type: 'emoji', type: 'emoji',

View File

@ -1,15 +1,8 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { import React, { RefObject, useCallback, useContext, useEffect } from 'react'
RefObject,
useCallback,
useContext,
useEffect,
useMemo
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
AccessibilityInfo, AccessibilityInfo,
@ -25,52 +18,15 @@ import validUrl from 'valid-url'
import updateText from '../../updateText' import updateText from '../../updateText'
import ComposeContext from '../../utils/createContext' import ComposeContext from '../../utils/createContext'
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
const { t } = useTranslation()
const { reduceMotionEnabled } = useAccessibility()
const { composeState, composeDispatch } = useContext(ComposeContext)
const onPress = useCallback(() => {
analytics('compose_emoji_add')
updateText({
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
haptics('Light')
}, [composeState])
const children = useMemo(() => {
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
if (validUrl.isHttpsUri(uri)) {
return (
<FastImage
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
emoji: emoji.shortcode
})}
accessibilityHint={t(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }}
style={styles.emoji}
/>
)
} else {
return null
}
}, [])
return (
<Pressable key={emoji.shortcode} onPress={onPress} children={children} />
)
}
export interface Props { export interface Props {
accessibleRefEmojis: RefObject<SectionList> accessibleRefEmojis: RefObject<SectionList>
} }
const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => { const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
const { composeState } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
const tagEmojis = findNodeHandle(accessibleRefEmojis.current) const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
@ -86,21 +42,49 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
[] []
) )
const emojiList = useCallback(
section =>
section.data.map((emoji: Mastodon.Emoji) => (
<SingleEmoji key={emoji.shortcode} emoji={emoji} />
)),
[]
)
const listItem = useCallback( const listItem = useCallback(
({ section, index }) => ({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
index === 0 ? ( return (
<View key={section.title} style={styles.emojis}> <View key={index} style={styles.emojis}>
{emojiList(section)} {item.map(emoji => {
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
if (validUrl.isHttpsUri(uri)) {
return (
<Pressable
key={emoji.shortcode}
onPress={() => {
updateText({
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
haptics('Light')
}}
>
<FastImage
accessibilityLabel={t(
'common:customEmoji.accessibilityLabel',
{
emoji: emoji.shortcode
}
)}
accessibilityHint={t(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri }}
style={styles.emoji}
/>
</Pressable>
)
} else {
return null
}
})}
</View> </View>
) : null, )
[] },
[composeState]
) )
return ( return (
@ -111,7 +95,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
horizontal horizontal
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
sections={composeState.emoji.emojis || []} sections={composeState.emoji.emojis || []}
keyExtractor={item => item.shortcode} keyExtractor={item => item[0].shortcode}
renderSectionHeader={listHeader} renderSectionHeader={listHeader}
renderItem={listItem} renderItem={listItem}
windowSize={2} windowSize={2}

View File

@ -123,7 +123,8 @@ const addAttachment = async ({
}) })
} }
mediaSelector({ uploader, showActionSheetWithOptions }) const result = await mediaSelector({ showActionSheetWithOptions })
await uploader(result)
} }
export default addAttachment export default addAttachment

View File

@ -31,7 +31,10 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
multiple: false, multiple: false,
expire: '86400' expire: '86400'
}, },
attachments: { sensitive: false, uploads: [] }, attachments: {
sensitive: false,
uploads: []
},
visibility: 'public', visibility: 'public',
visibilityLock: false, visibilityLock: false,
replyToStatus: undefined, replyToStatus: undefined,

View File

@ -40,7 +40,7 @@ export type ComposeState = {
} }
emoji: { emoji: {
active: boolean active: boolean
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined emojis: { title: string; data: Mastodon.Emoji[][] }[] | undefined
} }
poll: { poll: {
active: boolean active: boolean

View File

@ -109,16 +109,28 @@ const TabMe = React.memo(
component={TabMeProfile} component={TabMeProfile}
options={{ options={{
stackPresentation: 'modal', stackPresentation: 'modal',
headerShown: false ...(Platform.OS === 'android' && { headerShown: false })
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Push' name='Tab-Me-Push'
component={TabMePush} component={TabMePush}
options={{ options={({ navigation }) => ({
stackPresentation: 'modal', stackPresentation: 'modal',
headerShown: false headerShown: true,
}} headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
})}
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Settings' name='Tab-Me-Settings'
@ -149,10 +161,22 @@ const TabMe = React.memo(
<Stack.Screen <Stack.Screen
name='Tab-Me-Switch' name='Tab-Me-Switch'
component={TabMeSwitch} component={TabMeSwitch}
options={{ options={({ navigation }) => ({
stackPresentation: 'modal', stackPresentation: 'modal',
headerShown: false headerShown: true,
}} headerTitle: t('me.stacks.switch.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.switch.name')} />
)
}),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
)
})}
/> />
{sharedScreens(Stack as any)} {sharedScreens(Stack as any)}

View File

@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
> >
<Stack.Screen <Stack.Screen
name='Tab-Me-Profile-Root' name='Tab-Me-Profile-Root'
component={TabMeProfileRoot}
options={{ options={{
headerTitle: t('me.stacks.profile.name'), headerTitle: t('me.stacks.profile.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -45,7 +44,15 @@ const TabMeProfile: React.FC<StackScreenProps<
/> />
) )
}} }}
>
{({ route, navigation }) => (
<TabMeProfileRoot
messageRef={messageRef}
route={route}
navigation={navigation}
/> />
)}
</Stack.Screen>
<Stack.Screen <Stack.Screen
name='Tab-Me-Profile-Name' name='Tab-Me-Profile-Name'
options={{ options={{

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input' import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile' import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -79,31 +78,19 @@ const TabMeProfileFields: React.FC<StackScreenProps<
content='Save' content='Save'
onPress={async () => { onPress={async () => {
mutateAsync({ mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.note.title',
succeed: true,
failed: true
},
type: 'fields_attributes', type: 'fields_attributes',
data: newFields data: newFields
.filter(field => field.name.length && field.value.length) .filter(field => field.name.length && field.value.length)
.map(field => ({ name: field.name, value: field.value })) .map(field => ({ name: field.name, value: field.value }))
}) }).then(() => {
.then(() => {
navigation.navigate('Tab-Me-Profile-Root') navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'error'
})
}) })
}} }}
/> />

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input' import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile' import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -65,27 +64,18 @@ const TabMeProfileName: React.FC<StackScreenProps<
loading={status === 'loading'} loading={status === 'loading'}
content='Save' content='Save'
onPress={async () => { onPress={async () => {
mutateAsync({ type: 'display_name', data: displayName }) mutateAsync({
.then(() => { mode,
messageRef,
message: {
text: 'me.profile.root.name.title',
succeed: true,
failed: true
},
type: 'display_name',
data: displayName
}).then(() => {
navigation.navigate('Tab-Me-Profile-Root') navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.name.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.name.title')
}),
mode,
type: 'error'
})
}) })
}} }}
/> />

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import Input from '@components/Input' import Input from '@components/Input'
import { displayMessage } from '@components/Message'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation } from '@utils/queryHooks/profile' import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -65,27 +64,18 @@ const TabMeProfileNote: React.FC<StackScreenProps<
loading={status === 'loading'} loading={status === 'loading'}
content='Save' content='Save'
onPress={async () => { onPress={async () => {
mutateAsync({ type: 'note', data: newNote }) mutateAsync({
.then(() => { mode,
messageRef,
message: {
text: 'me.profile.root.note.title',
succeed: true,
failed: true
},
type: 'note',
data: newNote
}).then(() => {
navigation.navigate('Tab-Me-Profile-Root') navigation.navigate('Tab-Me-Profile-Root')
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'success'
})
})
.catch(() => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title')
}),
mode,
type: 'error'
})
}) })
}} }}
/> />

View File

@ -1,21 +1,29 @@
import analytics from '@components/analytics'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile' import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import React, { useCallback } from 'react' import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch } from 'react-redux'
import ProfileAvatarHeader from './Root/AvatarHeader'
const TabMeProfileRoot: React.FC<StackScreenProps< const TabMeProfileRoot: React.FC<StackScreenProps<
Nav.TabMeProfileStackParamList, Nav.TabMeProfileStackParamList,
'Tab-Me-Profile-Root' 'Tab-Me-Profile-Root'
>> = ({ navigation }) => { > & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { data, isLoading } = useProfileQuery({}) const { data, isLoading } = useProfileQuery({})
const { mutate } = useProfileMutation() const { mutateAsync } = useProfileMutation()
const dispatch = useDispatch()
const onPressVisibility = useCallback(() => { const onPressVisibility = useCallback(() => {
showActionSheetWithOptions( showActionSheetWithOptions(
@ -32,41 +40,90 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
async buttonIndex => { async buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
mutate({ type: 'source[privacy]', data: 'public' })
break
case 1: case 1:
mutate({ type: 'source[privacy]', data: 'unlisted' })
break
case 2: case 2:
mutate({ type: 'source[privacy]', data: 'private' }) const indexVisibilityMapping = [
'public',
'unlisted',
'private'
] as ['public', 'unlisted', 'private']
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
analytics('me_profile_visibility', {
current: t(
`me.profile.root.visibility.options.${data?.source.privacy}`
),
new: indexVisibilityMapping[buttonIndex]
})
mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.visibility.title',
succeed: false,
failed: true
},
type: 'source[privacy]',
data: indexVisibilityMapping[buttonIndex]
}).then(() => dispatch(updateAccountPreferences()))
}
break break
} }
} }
) )
}, []) }, [data?.source.privacy])
const onPressSensitive = useCallback(() => { const onPressSensitive = useCallback(() => {
if (data?.source.sensitive === undefined) { analytics('me_profile_sensitive', {
mutate({ type: 'source[sensitive]', data: true }) current: data?.source.sensitive,
} else { new: data?.source.sensitive === undefined ? true : !data.source.sensitive
mutate({ type: 'source[sensitive]', data: !data.source.sensitive }) })
} mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.sensitive.title',
succeed: false,
failed: true
},
type: 'source[sensitive]',
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences()))
}, [data?.source.sensitive]) }, [data?.source.sensitive])
const onPressLock = useCallback(() => { const onPressLock = useCallback(() => {
if (data?.locked === undefined) { analytics('me_profile_lock', {
mutate({ type: 'locked', data: true }) current: data?.locked,
} else { new: data?.locked === undefined ? true : !data.locked
mutate({ type: 'locked', data: !data.locked }) })
} mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.lock.title',
succeed: false,
failed: true
},
type: 'locked',
data: data?.locked === undefined ? true : !data.locked
})
}, [data?.locked]) }, [data?.locked])
const onPressBot = useCallback(() => { const onPressBot = useCallback(() => {
if (data?.bot === undefined) { analytics('me_profile_bot', {
mutate({ type: 'bot', data: true }) current: data?.bot,
} else { new: data?.bot === undefined ? true : !data.bot
mutate({ type: 'bot', data: !data?.bot }) })
} mutateAsync({
mode,
messageRef,
message: {
text: 'me.profile.root.bot.title',
succeed: false,
failed: true
},
type: 'bot',
data: data?.bot === undefined ? true : !data.bot
})
}, [data?.bot]) }, [data?.bot])
return ( return (
@ -84,42 +141,17 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
}) })
}} }}
/> />
<MenuRow <ProfileAvatarHeader type='avatar' messageRef={messageRef} />
title={t('me.profile.root.avatar.title')} <ProfileAvatarHeader type='header' messageRef={messageRef} />
description={t('me.profile.root.avatar.description')}
// content={
// <GracefullyImage
// style={{ flex: 1 }}
// uri={{
// original: data?.avatar_static
// }}
// />
// }
// loading={isLoading}
// iconBack='ChevronRight'
/>
<MenuRow
title={t('me.profile.root.banner.title')}
description={t('me.profile.root.banner.description')}
// content={
// <GracefullyImage
// style={{ flex: 1 }}
// uri={{
// original: data?.header_static
// }}
// />
// }
// loading={isLoading}
// iconBack='ChevronRight'
/>
<MenuRow <MenuRow
title={t('me.profile.root.note.title')} title={t('me.profile.root.note.title')}
content={data?.source.note} content={data?.source.note}
loading={isLoading} loading={isLoading}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => { onPress={() => {
data &&
navigation.navigate('Tab-Me-Profile-Note', { navigation.navigate('Tab-Me-Profile-Note', {
note: data?.source?.note || '' note: data.source?.note
}) })
}} }}
/> />

View File

@ -0,0 +1,53 @@
import mediaSelector from '@components/mediaSelector'
import { MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { useTheme } from '@utils/styles/ThemeManager'
import * as ImagePicker from 'expo-image-picker'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
export interface Props {
type: 'avatar' | 'header'
messageRef: RefObject<FlashMessage>
}
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
const query = useProfileQuery({})
const mutation = useProfileMutation()
return (
<MenuRow
title={t(`me.profile.root.${type}.title`)}
description={t(`me.profile.root.${type}.description`)}
loading={query.isLoading || mutation.isLoading}
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 400, height: 400 }
})
mutation.mutate({
mode,
messageRef,
message: {
text: `me.profile.root.${type}.title`,
succeed: true,
failed: true
},
type,
data: image.uri
})
}}
/>
)
}
export default ProfileAvatarHeader

View File

@ -1,42 +1,173 @@
import { HeaderCenter, HeaderLeft } from '@components/Header' import analytics from '@components/analytics'
import { StackScreenProps } from '@react-navigation/stack' import Button from '@components/Button'
import React from 'react' import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import {
clearPushLoading,
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import * as Notifications from 'expo-notifications'
import * as WebBrowser from 'expo-web-browser'
import React, { useState, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { AppState, Linking, ScrollView } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { useDispatch, useSelector } from 'react-redux'
import TabMePushRoot from './Push/Root'
const Stack = createNativeStackNavigator<Nav.TabMePushStackParamList>() const TabMePush: React.FC = () => {
const TabMePush: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Push'
>> = ({ navigation }) => {
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const instanceAccount = useSelector(
return ( getInstanceAccount,
<Stack.Navigator (prev, next) => prev?.acct === next?.acct
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
>
<Stack.Screen
name='Tab-Me-Push-Root'
component={TabMePushRoot}
options={{
headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
) )
}), const instanceUri = useSelector(getInstanceUri)
headerLeft: () => (
<HeaderLeft const dispatch = useDispatch()
content='ChevronDown' const instancePush = useSelector(getInstancePush)
onPress={() => navigation.goBack()}
/> const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
useEffect(() => {
checkPush()
AppState.addEventListener('change', checkPush)
return () => {
AppState.removeEventListener('change', checkPush)
}
}, [])
useEffect(() => {
dispatch(clearPushLoading())
}, [])
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
const alerts = useMemo(() => {
return instancePush?.alerts
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
'follow',
'favourite',
'reblog',
'mention',
'poll'
]).map(alert => (
<MenuRow
key={alert}
title={t(`me.push.${alert}.heading`)}
switchDisabled={
!pushEnabled || !instancePush.global.value || isLoading
}
switchValue={instancePush?.alerts[alert].value}
switchOnValueChange={() => {
analytics(`me_push_${alert}`, {
current: instancePush?.alerts[alert].value,
new: !instancePush?.alerts[alert].value
})
dispatch(
updateInstancePushAlert({
changed: alert,
alerts: {
...instancePush?.alerts,
[alert]: {
...instancePush?.alerts[alert],
value: !instancePush?.alerts[alert].value
}
}
})
) )
}} }}
/> />
</Stack.Navigator> ))
: null
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
return (
<ScrollView>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain
? t('me.push.enable.direct')
: t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
analytics('me_push_enabled_dialogue')
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
analytics('me_push_enabled_setting')
Linking.openSettings()
}
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instanceAccount?.acct}@${instanceUri}`
})}
description={t('me.push.global.description')}
loading={instancePush?.global.loading}
switchDisabled={!pushEnabled || isLoading}
switchValue={
pushEnabled === false ? false : instancePush?.global.value
}
switchOnValueChange={() => {
analytics('me_push_global', {
current: instancePush?.global.value,
new: !instancePush?.global.value
})
dispatch(updateInstancePush(!instancePush?.global.value))
}}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode.loading}
switchDisabled={
!pushEnabled || !instancePush?.global.value || isLoading
}
switchValue={instancePush?.decode.value}
switchOnValueChange={() => {
analytics('me_push_decode', {
current: instancePush?.decode.value,
new: !instancePush?.decode.value
})
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
}}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={() => {
analytics('me_push_howitworks')
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
}}
/>
</MenuContainer>
<MenuContainer>{alerts}</MenuContainer>
</ScrollView>
) )
} }

View File

@ -1,163 +0,0 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import {
clearPushLoading,
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import * as WebBrowser from 'expo-web-browser'
import * as Notifications from 'expo-notifications'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch, useSelector } from 'react-redux'
import layoutAnimation from '@utils/styles/layoutAnimation'
import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants'
import { AppState, Linking } from 'react-native'
import { StackScreenProps } from '@react-navigation/stack'
const TabMePushRoot: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Push'
>> = () => {
const { t } = useTranslation('screenTabs')
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const instanceUri = useSelector(getInstanceUri)
const dispatch = useDispatch()
const instancePush = useSelector(getInstancePush)
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
useEffect(() => {
checkPush()
AppState.addEventListener('change', checkPush)
return () => {
AppState.removeEventListener('change', checkPush)
}
}, [])
useEffect(() => {
dispatch(clearPushLoading())
}, [])
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
const alerts = useMemo(() => {
return instancePush?.alerts
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
'follow',
'favourite',
'reblog',
'mention',
'poll'
]).map(alert => (
<MenuRow
key={alert}
title={t(`me.push.${alert}.heading`)}
switchDisabled={
!pushEnabled || !instancePush.global.value || isLoading
}
switchValue={instancePush?.alerts[alert].value}
switchOnValueChange={() =>
dispatch(
updateInstancePushAlert({
changed: alert,
alerts: {
...instancePush?.alerts,
[alert]: {
...instancePush?.alerts[alert],
value: !instancePush?.alerts[alert].value
}
}
})
)
}
/>
))
: null
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
return (
<ScrollView>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain
? t('me.push.enable.direct')
: t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
Linking.openSettings()
}
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instanceAccount?.acct}@${instanceUri}`
})}
description={t('me.push.global.description')}
loading={instancePush?.global.loading}
switchDisabled={!pushEnabled || isLoading}
switchValue={
pushEnabled === false ? false : instancePush?.global.value
}
switchOnValueChange={() =>
dispatch(updateInstancePush(!instancePush?.global.value))
}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode.loading}
switchDisabled={
!pushEnabled || !instancePush?.global.value || isLoading
}
switchValue={instancePush?.decode.value}
switchOnValueChange={() =>
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={() =>
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
}
/>
</MenuContainer>
<MenuContainer>{alerts}</MenuContainer>
</ScrollView>
)
}
export default TabMePushRoot

View File

@ -2,65 +2,25 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement' import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const Collections: React.FC = () => { const Collections: React.FC = () => {
const { t, i18n } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const navigation = useNavigation() const navigation = useNavigation()
const listsQuery = useListsQuery({ const listsQuery = useListsQuery({
options: { options: {
notifyOnChangeProps: [] notifyOnChangeProps: ['data']
} }
}) })
const rowLists = useMemo(() => {
if (listsQuery.isSuccess && listsQuery.data?.length) {
return (
<MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('me.stacks.lists.name')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
)
} else {
return null
}
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
const announcementsQuery = useAnnouncementQuery({ const announcementsQuery = useAnnouncementQuery({
showAll: true, showAll: true,
options: { options: {
notifyOnChangeProps: [] notifyOnChangeProps: ['data']
} }
}) })
const rowAnnouncements = useMemo(() => {
if (announcementsQuery.isSuccess && announcementsQuery.data?.length) {
const amount = announcementsQuery.data.filter(
announcement => !announcement.read
).length
return (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('screenAnnouncements:heading')}
content={
amount
? t('me.root.announcements.content.unread', {
amount
})
: t('me.root.announcements.content.read')
}
onPress={() =>
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
)
} else {
return null
}
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])
return ( return (
<MenuContainer> <MenuContainer>
@ -82,8 +42,34 @@ const Collections: React.FC = () => {
title={t('me.stacks.favourites.name')} title={t('me.stacks.favourites.name')}
onPress={() => navigation.navigate('Tab-Me-Favourites')} onPress={() => navigation.navigate('Tab-Me-Favourites')}
/> />
{rowLists} {listsQuery.data?.length ? (
{rowAnnouncements} <MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('me.stacks.lists.name')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
) : null}
{announcementsQuery.data?.length ? (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('screenAnnouncements:heading')}
content={
announcementsQuery.data.filter(announcement => !announcement.read)
.length
? t('me.root.announcements.content.unread', {
amount: announcementsQuery.data.filter(
announcement => !announcement.read
).length
})
: t('me.root.announcements.content.read')
}
onPress={() =>
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
) : null}
</MenuContainer> </MenuContainer>
) )
} }

View File

@ -1,47 +1,152 @@
import { HeaderCenter, HeaderLeft } from '@components/Header' import analytics from '@components/analytics'
import { StackScreenProps } from '@react-navigation/stack' import Button from '@components/Button'
import React from 'react' import haptics from '@components/haptics'
import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native'
import {
getInstanceActive,
getInstances,
Instance,
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native' import {
import { createNativeStackNavigator } from 'react-native-screens/native-stack' KeyboardAvoidingView,
import TabMeSwitchRoot from './Switch/Root' Platform,
StyleSheet,
Text,
View
} from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
const Stack = createNativeStackNavigator() interface Props {
instance: Instance
selected?: boolean
}
const TabMeSwitch: React.FC<StackScreenProps< const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
Nav.TabMeStackParamList, const queryClient = useQueryClient()
'Tab-Me-Switch' const navigation = useNavigation()
>> = ({ navigation }) => { const dispatch = useDispatch()
return (
<Button
type='text'
selected={selected}
style={styles.button}
content={`@${instance.account.acct}@${instance.uri}${
selected ? ' ✓' : ''
}`}
onPress={() => {
haptics('Light')
analytics('switch_existing_press')
dispatch(updateInstanceActive(instance))
queryClient.clear()
navigation.goBack()
}}
/>
)
}
const TabMeSwitch: React.FC = () => {
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { theme } = useTheme()
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive, () => true)
const scrollViewRef = useRef<ScrollView>(null)
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<Stack.Navigator <ScrollView
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }} ref={scrollViewRef}
style={styles.base}
keyboardShouldPersistTaps='always'
> >
<Stack.Screen <View
name='Screen-Me-Switch-Root' style={[styles.firstSection, { borderBottomColor: theme.border }]}
component={TabMeSwitchRoot} >
options={{ <Text style={[styles.header, { color: theme.primaryDefault }]}>
headerTitle: t('me.stacks.switch.name'), {t('me.switch.existing')}
...(Platform.OS === 'android' && { </Text>
headerCenter: () => ( <View style={styles.accountButtons}>
<HeaderCenter content={t('me.stacks.switch.name')} /> {instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
) )
}), )
headerLeft: () => ( .map((instance, index) => {
<HeaderLeft const localAccount = instances[instanceActive!]
content='ChevronDown' return (
onPress={() => navigation.goBack()} <AccountButton
key={index}
instance={instance}
selected={
instance.url === localAccount.url &&
instance.token === localAccount.token &&
instance.account.id === localAccount.account.id
}
/> />
) )
}} })
: null}
</View>
</View>
<View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.new')}
</Text>
<ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/> />
</Stack.Navigator> </View>
</ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }
const styles = StyleSheet.create({
base: {
marginBottom: StyleConstants.Spacing.L * 2
},
header: {
...StyleConstants.FontStyle.M,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S
},
firstSection: {
marginTop: StyleConstants.Spacing.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S,
borderBottomWidth: StyleSheet.hairlineWidth
},
secondSection: {
paddingTop: StyleConstants.Spacing.M
},
accountButtons: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
},
button: {
marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M
}
})
export default TabMeSwitch export default TabMeSwitch

View File

@ -1,139 +0,0 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native'
import {
getInstanceActive,
getInstances,
Instance,
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
interface Props {
instance: Instance
selected?: boolean
}
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
const queryClient = useQueryClient()
const navigation = useNavigation()
const dispatch = useDispatch()
return (
<Button
type='text'
selected={selected}
style={styles.button}
content={`@${instance.account.acct}@${instance.uri}${
selected ? ' ✓' : ''
}`}
onPress={() => {
haptics('Light')
analytics('switch_existing_press')
dispatch(updateInstanceActive(instance))
queryClient.clear()
navigation.goBack()
}}
/>
)
}
const TabMeSwitchRoot: React.FC = () => {
const { t } = useTranslation('screenTabs')
const { theme } = useTheme()
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive, () => true)
const scrollViewRef = useRef<ScrollView>(null)
return (
<ScrollView
ref={scrollViewRef}
style={styles.base}
keyboardShouldPersistTaps='always'
>
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.existing')}
</Text>
<View style={styles.accountButtons}>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
)
.map((instance, index) => {
const localAccount = instances[instanceActive!]
return (
<AccountButton
key={index}
instance={instance}
selected={
instance.url === localAccount.url &&
instance.token === localAccount.token &&
instance.account.id === localAccount.account.id
}
/>
)
})
: null}
</View>
</View>
<View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('me.switch.new')}
</Text>
<ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
base: {
marginBottom: StyleConstants.Spacing.L
},
header: {
...StyleConstants.FontStyle.M,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S
},
firstSection: {
marginTop: StyleConstants.Spacing.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S,
borderBottomWidth: StyleSheet.hairlineWidth
},
secondSection: {
paddingTop: StyleConstants.Spacing.M
},
accountButtons: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
},
button: {
marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M
}
})
export default TabMeSwitchRoot

View File

@ -34,6 +34,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis} emojis={account.emojis}
showFullLink showFullLink
numberOfLines={5} numberOfLines={5}
selectable
/> />
{field.verified_at ? ( {field.verified_at ? (
<Icon <Icon
@ -51,6 +52,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis} emojis={account.emojis}
showFullLink showFullLink
numberOfLines={5} numberOfLines={5}
selectable
/> />
</View> </View>
</View> </View>
@ -58,7 +60,7 @@ const AccountInformationFields = React.memo(
</View> </View>
) )
}, },
() => true (_, next) => next.account === undefined
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -28,11 +28,11 @@ const AccountInformationNote = React.memo(
return ( return (
<View style={styles.note}> <View style={styles.note}>
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} /> <ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
</View> </View>
) )
}, },
() => true (_, next) => next.account === undefined
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -14,11 +14,11 @@ import { debounce } from 'lodash'
import React from 'react' import React from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native' import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript' import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript/native-stack'
import { import {
NativeStackNavigationEventMap, NativeStackNavigationEventMap,
NativeStackNavigatorProps NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/types' } from 'react-native-screens/lib/typescript/native-stack/types'
export type BaseScreens = export type BaseScreens =
| Nav.TabLocalStackParamList | Nav.TabLocalStackParamList
@ -150,17 +150,13 @@ const sharedScreens = (
<View style={styles.searchBar}> <View style={styles.searchBar}>
<TextInput <TextInput
editable={false} editable={false}
children={
<Text
style={[ style={[
styles.textInput, styles.textInput,
{ {
color: theme.primaryDefault color: theme.primaryDefault
} }
]} ]}
children={t('shared.search.header.prefix')} defaultValue={t('shared.search.header.prefix')}
/>
}
/> />
<TextInput <TextInput
accessibilityRole='search' accessibilityRole='search'

View File

@ -1,12 +1,12 @@
import Constants from 'expo-constants'
import * as Updates from 'expo-updates' import * as Updates from 'expo-updates'
import { Constants } from 'react-native-unimodules'
import * as Sentry from 'sentry-expo' import * as Sentry from 'sentry-expo'
import log from './log' import log from './log'
const sentry = () => { const sentry = () => {
log('log', 'Sentry', 'initializing') log('log', 'Sentry', 'initializing')
Sentry.init({ Sentry.init({
dsn: Constants.manifest.extra.sentryDSN, dsn: Constants.manifest?.extra?.sentryDSN,
enableInExpoDevelopment: false, enableInExpoDevelopment: false,
debug: debug:
__DEV__ || __DEV__ ||

View File

@ -1,9 +1,12 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import { queryClient } from '@root/App' import queryClient from '@helpers/queryClient'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import i18next from 'i18next'
import { RefObject } from 'react'
import FlashMessage from 'react-native-flash-message'
import { useMutation, useQuery, UseQueryOptions } from 'react-query' import { useMutation, useQuery, UseQueryOptions } from 'react-query'
import { QueryKeyAccount } from './account'
type AccountWithSource = Mastodon.Account & type AccountWithSource = Mastodon.Account &
Required<Pick<Mastodon.Account, 'source'>> Required<Pick<Mastodon.Account, 'source'>>
@ -26,7 +29,7 @@ const useProfileQuery = <TData = AccountWithSource>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
type MutationVarsProfile = type MutationVarsProfileBase =
| { type: 'display_name'; data: string } | { type: 'display_name'; data: string }
| { type: 'note'; data: string } | { type: 'note'; data: string }
| { type: 'avatar'; data: string } | { type: 'avatar'; data: string }
@ -46,6 +49,16 @@ type MutationVarsProfile =
data: { name: string; value: string }[] data: { name: string; value: string }[]
} }
type MutationVarsProfile = MutationVarsProfileBase & {
mode: 'light' | 'dark'
messageRef: RefObject<FlashMessage>
message: {
text: string
succeed: boolean
failed: boolean
}
}
const mutationFunction = async ({ type, data }: MutationVarsProfile) => { const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
const formData = new FormData() const formData = new FormData()
if (type === 'fields_attributes') { if (type === 'fields_attributes') {
@ -109,8 +122,33 @@ const useProfileMutation = () => {
return oldData return oldData
}, },
onError: (_, variables, context) => { onError: (err, variables, context) => {
queryClient.setQueryData(queryKey, context) queryClient.setQueryData(queryKey, context)
haptics('Error')
if (variables.message.failed) {
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.failed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
...(err && { description: err.message }),
mode: variables.mode,
type: 'error'
})
}
},
onSuccess: (_, variables) => {
if (variables.message.succeed) {
haptics('Success')
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
mode: variables.mode,
type: 'success'
})
}
}, },
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey)

View File

@ -1,6 +1,6 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { queryClient } from '@root/App' import queryClient from '@helpers/queryClient'
import { store } from '@root/store' import { store } from '@root/store'
import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice' import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'

View File

@ -1,4 +1,4 @@
import { queryClient } from '@root/App' import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query' import { InfiniteData } from 'react-query'
import { MutationVarsTimelineDeleteItem } from '../timeline' import { MutationVarsTimelineDeleteItem } from '../timeline'

View File

@ -1,4 +1,4 @@
import { queryClient } from '@root/App' import queryClient from '@helpers/queryClient'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import { InfiniteData } from 'react-query' import { InfiniteData } from 'react-query'
import { import {

View File

@ -0,0 +1,65 @@
import apiGeneral from '@api/general'
import haptics from '@components/haptics'
import { AxiosError } from 'axios'
import { Buffer } from 'buffer'
import Constants from 'expo-constants'
import { useQuery, UseQueryOptions } from 'react-query'
type Translations = {
provider: string
sourceLanguage: string
text: string[]
}
export type QueryKeyTranslate = [
'Translate',
{
uri: string
source: string
target: string
text: string[]
}
]
export const TRANSLATE_SERVER = __DEV__
? 'testtranslate.tooot.app'
: 'translate.tooot.app'
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
const key = Constants.manifest.extra?.translateKey
if (!key) {
return Promise.reject()
}
const { uri, source, target, text } = queryKey[1]
const uriEncoded = Buffer.from(uri.replace(/https?:\/\//, ''))
.toString('base64')
.replace('+', '-')
.replace('/', '_')
.replace(/=+$/, '')
const original = Buffer.from(JSON.stringify({ source, text })).toString(
'base64'
)
const res = await apiGeneral<Translations>({
domain: TRANSLATE_SERVER,
method: 'get',
url: `v1/translate/${uriEncoded}/${target}`,
headers: { key, original }
})
haptics('Light')
return res.body
}
const useTranslateQuery = ({
options,
...queryKeyParams
}: QueryKeyTranslate[1] & {
options?: UseQueryOptions<Translations, AxiosError, Translations>
}) => {
const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
}
export { useTranslateQuery }

View File

@ -1,6 +1,6 @@
import apiGeneral from '@api/general'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import apiGeneral from '@api/general'
import { Constants } from 'react-native-unimodules' import { Constants } from 'react-native-unimodules'
export const retriveVersionLatest = createAsyncThunk( export const retriveVersionLatest = createAsyncThunk(

View File

@ -9,10 +9,12 @@
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"strict": true, "strict": true,
"strictFunctionTypes": false,
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@api/*": ["./src/api/*"], "@api/*": ["./src/api/*"],
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@helpers/*": ["./src/helpers/*"],
"@screens/*": ["./src/screens/*"], "@screens/*": ["./src/screens/*"],
"@utils/*": ["./src/utils/*"], "@utils/*": ["./src/utils/*"],
"@root/*": ["./src/*"] "@root/*": ["./src/*"]

2761
yarn.lock

File diff suppressed because it is too large Load Diff