Merge pull request #502 from tooot-app/main

Test v4.6.5
This commit is contained in:
xmflsct 2022-12-04 01:25:17 +01:00 committed by GitHub
commit 5a4af08751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
267 changed files with 8456 additions and 5112 deletions

View File

@ -11,18 +11,20 @@ Please **do not** create a pull request to update translation. tooot's translati
## Special thanks
[@amrtf](https://crowdin.com/profile/amrtf) for Spanish translation
[@pat](https://piaille.fr/@pat) for French translation
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
[@forenta](https://github.com/forenta) for German translation
[@pat](https://piaille.fr/@pat) for French translation
[@andrigamerita](https://github.com/andrigamerita) for Italian translation
[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
[@hellojaccc](https://github.com/hellojaccc) for Korean translation
[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
[@janlindblom](https://github.com/janlindblom) for Swedish

View File

@ -337,5 +337,3 @@ def isNewArchitectureEnabled() {
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
apply plugin: 'com.google.gms.google-services'

View File

@ -1,46 +0,0 @@
{
"project_info": {
"project_number": "661638997772",
"project_id": "xmflsct-mastodon-app",
"storage_bucket": "xmflsct-mastodon-app.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:661638997772:android:4fd02851f757f8fa9f8b29",
"android_client_info": {
"package_name": "com.xmflsct.app.tooot"
}
},
"oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDUw4s-mhQsHvs4hdIsldsi68ZIygM5MC4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "661638997772-sqa4raeghhrieqt9guljhcul9b51dvna.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.xmflsct.app.mastodon"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -22,7 +22,6 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.google.gms:google-services:4.3.3'
classpath("com.android.tools.build:gradle:7.2.1")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:5.0.1")

View File

@ -15,7 +15,6 @@ export default (): ExpoConfig => ({
},
android: {
package: 'com.xmflsct.app.tooot',
googleServicesFile: './configs/google-services.json',
permissions: ['CAMERA', 'VIBRATE'],
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
},

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>661638997772-65g8ce369ugck3ii4ulk6jhb3ijg51kl.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.661638997772-65g8ce369ugck3ii4ulk6jhb3ijg51kl</string>
<key>API_KEY</key>
<string>AIzaSyAOS1Yq_uNVctG89LB6Dl1PVhb_FAQRbRg</string>
<key>GCM_SENDER_ID</key>
<string>661638997772</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.xmflsct.app.tooot</string>
<key>PROJECT_ID</key>
<string>xmflsct-mastodon-app</string>
<key>STORAGE_BUCKET</key>
<string>xmflsct-mastodon-app.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:661638997772:ios:c8d2e09264a344b09f8b29</string>
</dict>
</plist>

View File

@ -1,46 +0,0 @@
{
"project_info": {
"project_number": "661638997772",
"project_id": "xmflsct-mastodon-app",
"storage_bucket": "xmflsct-mastodon-app.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:661638997772:android:4fd02851f757f8fa9f8b29",
"android_client_info": {
"package_name": "com.xmflsct.app.tooot"
}
},
"oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDUw4s-mhQsHvs4hdIsldsi68ZIygM5MC4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "661638997772-sqa4raeghhrieqt9guljhcul9b51dvna.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.xmflsct.app.mastodon"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -15,12 +15,6 @@ target 'tooot' do
# Flags change depending on the env values.
flags = get_default_flags()
# https://stackoverflow.com/questions/72289521/swift-pods-cannot-yet-be-integrated-as-static-libraries-firebasecoreinternal-lib/72969220#72969220
pod 'Firebase', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
$RNFirebaseAsStaticFramework = true
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true,

View File

@ -3,7 +3,7 @@ PODS:
- DoubleConversion (1.1.6)
- EXApplication (5.0.1):
- ExpoModulesCore
- EXAV (13.0.1):
- EXAV (13.0.2):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (14.0.2):
@ -12,18 +12,11 @@ PODS:
- ExpoModulesCore
- EXFileSystem (15.1.1):
- ExpoModulesCore
- EXFirebaseAnalytics (8.0.0):
- EXFirebaseCore
- ExpoModulesCore
- Firebase/Core (= 9.5.0)
- EXFirebaseCore (6.0.0):
- ExpoModulesCore
- Firebase/Core (= 9.5.0)
- EXFont (11.0.1):
- ExpoModulesCore
- EXNotifications (0.17.0):
- ExpoModulesCore
- Expo (47.0.7):
- Expo (47.0.8):
- ExpoModulesCore
- ExpoCrypto (12.0.0):
- ExpoModulesCore
@ -59,107 +52,8 @@ PODS:
- React-Core (= 0.70.6)
- React-jsi (= 0.70.6)
- ReactCommon/turbomodule/core (= 0.70.6)
- Firebase (9.5.0):
- Firebase/Core (= 9.5.0)
- Firebase/Core (9.5.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 9.5.0)
- Firebase/CoreOnly (9.5.0):
- FirebaseCore (= 9.5.0)
- FirebaseAnalytics (9.5.0):
- FirebaseAnalytics/AdIdSupport (= 9.5.0)
- FirebaseCore (~> 9.0)
- FirebaseInstallations (~> 9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
- GoogleUtilities/MethodSwizzler (~> 7.7)
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseAnalytics/AdIdSupport (9.5.0):
- FirebaseCore (~> 9.0)
- FirebaseInstallations (~> 9.0)
- GoogleAppMeasurement (= 9.5.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
- GoogleUtilities/MethodSwizzler (~> 7.7)
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseCore (9.5.0):
- FirebaseCoreDiagnostics (~> 9.0)
- FirebaseCoreInternal (~> 9.0)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/Logger (~> 7.7)
- FirebaseCoreDiagnostics (9.6.0):
- GoogleDataTransport (< 10.0.0, >= 9.1.4)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/Logger (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseCoreInternal (9.6.0):
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- FirebaseInstallations (9.6.0):
- FirebaseCore (~> 9.0)
- GoogleUtilities/Environment (~> 7.7)
- GoogleUtilities/UserDefaults (~> 7.7)
- PromisesObjC (~> 2.1)
- fmt (6.2.1)
- glog (0.3.5)
- GoogleAppMeasurement (9.5.0):
- GoogleAppMeasurement/AdIdSupport (= 9.5.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
- GoogleUtilities/MethodSwizzler (~> 7.7)
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (< 2.30910.0, >= 2.30908.0)
- GoogleAppMeasurement/AdIdSupport (9.5.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 9.5.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
- GoogleUtilities/MethodSwizzler (~> 7.7)
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (< 2.30910.0, >= 2.30908.0)
- GoogleAppMeasurement/WithoutAdIdSupport (9.5.0):
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
- GoogleUtilities/MethodSwizzler (~> 7.7)
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (< 2.30910.0, >= 2.30908.0)
- GoogleDataTransport (9.2.0):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities (7.10.0):
- GoogleUtilities/AppDelegateSwizzler (= 7.10.0)
- GoogleUtilities/Environment (= 7.10.0)
- GoogleUtilities/ISASwizzler (= 7.10.0)
- GoogleUtilities/Logger (= 7.10.0)
- GoogleUtilities/MethodSwizzler (= 7.10.0)
- GoogleUtilities/Network (= 7.10.0)
- "GoogleUtilities/NSData+zlib (= 7.10.0)"
- GoogleUtilities/Reachability (= 7.10.0)
- GoogleUtilities/SwizzlerTestHelpers (= 7.10.0)
- GoogleUtilities/UserDefaults (= 7.10.0)
- GoogleUtilities/AppDelegateSwizzler (7.10.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Environment (7.10.0):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/ISASwizzler (7.10.0)
- GoogleUtilities/Logger (7.10.0):
- GoogleUtilities/Environment
- GoogleUtilities/MethodSwizzler (7.10.0):
- GoogleUtilities/Logger
- GoogleUtilities/Network (7.10.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.10.0)"
- GoogleUtilities/Reachability (7.10.0):
- GoogleUtilities/Logger
- GoogleUtilities/SwizzlerTestHelpers (7.10.0):
- GoogleUtilities/MethodSwizzler
- GoogleUtilities/UserDefaults (7.10.0):
- GoogleUtilities/Logger
- hermes-engine (0.70.6)
- libevent (2.1.12)
- libwebp (1.2.4):
@ -171,12 +65,6 @@ PODS:
- libwebp/mux (1.2.4):
- libwebp/demux
- libwebp/webp (1.2.4)
- nanopb (2.30909.0):
- nanopb/decode (= 2.30909.0)
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- PromisesObjC (2.1.1)
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
@ -409,17 +297,19 @@ PODS:
- React-Core
- react-native-cameraroll (5.1.0):
- React-Core
- react-native-context-menu-view (1.5.4):
- React
- react-native-image-picker (4.10.1):
- react-native-image-picker (4.10.2):
- React-Core
- react-native-ios-context-menu (1.15.1):
- React-Core
- react-native-language-detection (0.1.0):
- React
- react-native-live-text-image-view (0.4.0):
- React-Core
- react-native-netinfo (9.3.6):
- react-native-menu (0.7.2):
- React
- react-native-netinfo (9.3.7):
- React-Core
- react-native-pager-view (6.1.1):
- react-native-pager-view (6.1.2):
- React-Core
- react-native-paste-input (0.5.1):
- React-Core
@ -538,9 +428,9 @@ PODS:
- RNScreens (3.18.2):
- React-Core
- React-RCTImage
- RNSentry (4.8.0):
- RNSentry (4.10.1):
- React-Core
- Sentry (= 7.29.0)
- Sentry/HybridSDK (= 7.31.2)
- RNShareMenu (6.0.0):
- React
- RNSVG (13.6.0):
@ -551,9 +441,7 @@ PODS:
- SDWebImageWebPCoder (0.9.1):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.13)
- Sentry (7.29.0):
- Sentry/Core (= 7.29.0)
- Sentry/Core (7.29.0)
- Sentry/HybridSDK (7.31.2)
- Swime (3.0.6)
- Yoga (1.14.0)
@ -565,8 +453,6 @@ DEPENDENCIES:
- EXConstants (from `../node_modules/expo-constants/ios`)
- EXErrorRecovery (from `../node_modules/expo-error-recovery/ios`)
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
- EXFirebaseAnalytics (from `../node_modules/expo-firebase-analytics/ios`)
- EXFirebaseCore (from `../node_modules/expo-firebase-core/ios`)
- EXFont (from `../node_modules/expo-font/ios`)
- EXNotifications (from `../node_modules/expo-notifications/ios`)
- Expo (from `../node_modules/expo`)
@ -584,10 +470,7 @@ DEPENDENCIES:
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- Firebase
- FirebaseCore
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- GoogleUtilities
- hermes-engine (from `../node_modules/react-native/sdks/hermes/hermes-engine.podspec`)
- libevent (~> 2.1.12)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
@ -609,10 +492,11 @@ DEPENDENCIES:
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-blurhash (from `../node_modules/react-native-blurhash`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-context-menu-view (from `../node_modules/react-native-context-menu-view`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
- react-native-language-detection (from `../node_modules/react-native-language-detection`)
- react-native-live-text-image-view (from `../node_modules/react-native-live-text-image-view`)
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
@ -643,20 +527,9 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseCoreInternal
- FirebaseInstallations
- fmt
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- libevent
- libwebp
- nanopb
- PromisesObjC
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@ -677,10 +550,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-error-recovery/ios"
EXFileSystem:
:path: "../node_modules/expo-file-system/ios"
EXFirebaseAnalytics:
:path: "../node_modules/expo-firebase-analytics/ios"
EXFirebaseCore:
:path: "../node_modules/expo-firebase-core/ios"
EXFont:
:path: "../node_modules/expo-font/ios"
EXNotifications:
@ -755,14 +624,16 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-blurhash"
react-native-cameraroll:
:path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-context-menu-view:
:path: "../node_modules/react-native-context-menu-view"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-ios-context-menu:
:path: "../node_modules/react-native-ios-context-menu"
react-native-language-detection:
:path: "../node_modules/react-native-language-detection"
react-native-live-text-image-view:
:path: "../node_modules/react-native-live-text-image-view"
react-native-menu:
:path: "../node_modules/@react-native-menu/menu"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pager-view:
@ -822,15 +693,13 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
EXAV: 766516466675fc5fdd7c500acced5934e8b00de2
EXAV: 9a45d37772c5329294c054a041dcc39931fc5032
EXConstants: 3c86653c422dd77e40d10cbbabb3025003977415
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
EXFirebaseAnalytics: 58d70e698859b070b2450ad8664d7b5bc6c6e3e1
EXFirebaseCore: d0d88cb904e893af07f809ab08c0892489bc6956
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
Expo: a37d568e9ae87645b74ed597dd0f0fd89e2daf2d
Expo: 36b5f625d36728adbdd1934d4d57182f319ab832
ExpoCrypto: 51e7662c7f5bfeab25b7909b8a5d545ec15d4877
ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
@ -845,22 +714,11 @@ SPEC CHECKSUMS:
EXVideoThumbnails: 8b3e48f3716679dd0cbf949217a31eab5c555799
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
Firebase: 800f16f07af493d98d017446a315c27af0552f41
FirebaseAnalytics: 1b60984a408320dda637306f3f733699ef8473d7
FirebaseCore: 25c0400b670fd1e2f2104349cd3b5dcce8d9418f
FirebaseCoreDiagnostics: 99a495094b10a57eeb3ae8efa1665700ad0bdaa6
FirebaseCoreInternal: bca76517fe1ed381e989f5e7d8abb0da8d85bed3
FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
GoogleAppMeasurement: 6ee231473fbd75c11221dfce489894334024eead
GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f
GoogleUtilities: bad72cb363809015b1f7f19beb1f1cd23c589f95
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
@ -879,12 +737,13 @@ SPEC CHECKSUMS:
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
react-native-context-menu-view: b0beca02aad4bd9f9d7d932bf437e0a03baa69ef
react-native-image-picker: f2ab1215d17bcfe27b0eb6417cc236fd1f4775e7
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d
react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c
react-native-netinfo: f80db8cac2151405633324cb645c60af098ee461
react-native-pager-view: 3c66c4e2f3ab423643d07b2c7041f8ac48395f72
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
react-native-paste-input: 183ad7dc224e192719616f4258dde5b548627d08
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097
@ -906,15 +765,15 @@ SPEC CHECKSUMS:
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
RNSentry: db7fd7b66efda28885e4e904a8b5e7349aec61c1
RNSentry: 3c27f3c57f16bab9835d9555add298571077e0c1
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
Sentry: 4272663eb0eda312024d795ca3f5a562a8ce5e18
Sentry: b15765d11769852fe78c9add942f7df60ed5dbf5
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
PODFILE CHECKSUM: e4191b63c8f15031b2365226730770e7978dca41
PODFILE CHECKSUM: 05bf71d31ba782dfda5a6b47d38e98a6f6bc079a
COCOAPODS: 1.11.3

View File

@ -0,0 +1,2 @@
"NSPhotoLibraryAddUsageDescription" = "Permet que tooot desi imatges al carret de la càmera";
"NSPhotoLibraryUsageDescription" = "Permet que tooot desi imatges al carret de la càmera";

View File

@ -0,0 +1,2 @@
"NSPhotoLibraryAddUsageDescription" = "Sta tooot toe om afbeeldingen op te slaan in je filmrol";
"NSPhotoLibraryUsageDescription" = "Sta tooot toe om afbeeldingen op te slaan in je filmrol";

View File

@ -17,7 +17,6 @@
5EE088C926297820007E5FEC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5EE088CB26297820007E5FEC /* InfoPlist.strings */; };
5EE44DD62600124E00A9BCED /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE44DD52600124E00A9BCED /* File.swift */; };
96905EF65AED1B983A6B3ABC /* libPods-tooot.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-tooot.a */; };
DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */; };
E3BC22F5F8ABE515E14CF199 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D878F932AF7A9974E06E461 /* ExpoModulesProvider.swift */; };
E613A80B28282A01003C97D6 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E613A80A28282A01003C97D6 /* AppDelegate.mm */; };
E633A42B281EAEAB000E540F /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E633A420281EAEAB000E540F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -69,9 +68,9 @@
7A4D352CD337FB3A3BF06240 /* Pods-tooot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-tooot.release.xcconfig"; path = "Target Support Files/Pods-tooot/Pods-tooot.release.xcconfig"; sourceTree = "<group>"; };
9D878F932AF7A9974E06E461 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-tooot/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = tooot/SplashScreen.storyboard; sourceTree = "<group>"; };
B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "tooot/GoogleService-Info.plist"; sourceTree = "<group>"; };
DF8133F098604A10B0D94952 /* boop.mp3 */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = boop.mp3; path = tooot/boop.mp3; sourceTree = "<group>"; };
E613A80A28282A01003C97D6 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = tooot/AppDelegate.mm; sourceTree = "<group>"; };
E6217B7E293C1EBF00B1755E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E633A420281EAEAB000E540F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E633A42F281EAF38000E540F /* ShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ShareViewController.swift"; sourceTree = "<group>"; };
@ -85,6 +84,7 @@
E69EBACC28DF28420057EDEC /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E69EBACD28DF284D0057EDEC /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
E69EBACE28DF28560057EDEC /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E6A4895D293C1F740047951A /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E6C8B26628F5F9FC0062CF2E /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
@ -122,7 +122,6 @@
13B07FB71A68108700A75B9A /* main.m */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */,
B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */,
5EE088CB26297820007E5FEC /* InfoPlist.strings */,
DF8133F098604A10B0D94952 /* boop.mp3 */,
);
@ -297,6 +296,8 @@
fr,
es,
sv,
nl,
ca,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@ -319,7 +320,6 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */,
4986628FD0DD4630BFE5F388 /* boop.mp3 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -528,6 +528,8 @@
E66C0842291F095800DFFF60 /* fr */,
E690AF692926B737002C38A8 /* es */,
E63E7FF0292A828100C76FD4 /* sv */,
E6217B7E293C1EBF00B1755E /* nl */,
E6A4895D293C1F740047951A /* ca */,
);
name = InfoPlist.strings;
sourceTree = "<group>";

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>661638997772-65g8ce369ugck3ii4ulk6jhb3ijg51kl.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.661638997772-65g8ce369ugck3ii4ulk6jhb3ijg51kl</string>
<key>API_KEY</key>
<string>AIzaSyAOS1Yq_uNVctG89LB6Dl1PVhb_FAQRbRg</string>
<key>GCM_SENDER_ID</key>
<string>661638997772</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.xmflsct.app.tooot</string>
<key>PROJECT_ID</key>
<string>xmflsct-mastodon-app</string>
<key>STORAGE_BUCKET</key>
<string>xmflsct-mastodon-app.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:661638997772:ios:c8d2e09264a344b09f8b29</string>
</dict>
</plist>

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.6.4",
"version": "4.6.5",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later",
@ -19,35 +19,35 @@
},
"dependencies": {
"@expo/react-native-action-sheet": "^4.0.1",
"@formatjs/intl-datetimeformat": "^6.3.1",
"@formatjs/intl-getcanonicallocales": "^2.0.4",
"@formatjs/intl-locale": "^3.0.7",
"@formatjs/intl-numberformat": "^8.2.0",
"@formatjs/intl-pluralrules": "^5.1.4",
"@formatjs/intl-relativetimeformat": "^11.1.4",
"@formatjs/intl-datetimeformat": "^6.4.3",
"@formatjs/intl-getcanonicallocales": "^2.0.5",
"@formatjs/intl-locale": "^3.0.11",
"@formatjs/intl-numberformat": "^8.3.3",
"@formatjs/intl-pluralrules": "^5.1.8",
"@formatjs/intl-relativetimeformat": "^11.1.8",
"@mattermost/react-native-paste-input": "^0.5.1",
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
"@react-native-async-storage/async-storage": "~1.17.11",
"@react-native-camera-roll/camera-roll": "^5.1.0",
"@react-native-clipboard/clipboard": "^1.11.1",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/netinfo": "9.3.6",
"@react-native-community/netinfo": "9.3.7",
"@react-native-community/segmented-control": "^2.2.2",
"@react-navigation/bottom-tabs": "^6.4.1",
"@react-navigation/native": "^6.0.14",
"@react-navigation/native-stack": "^6.9.2",
"@react-navigation/stack": "^6.3.5",
"@reduxjs/toolkit": "^1.9.0",
"@sentry/react-native": "4.8.0",
"@sharcoux/slider": "^6.0.3",
"@react-native-menu/menu": "^0.7.2",
"@react-navigation/bottom-tabs": "^6.4.3",
"@react-navigation/native": "^6.0.16",
"@react-navigation/native-stack": "^6.9.4",
"@react-navigation/stack": "^6.3.7",
"@reduxjs/toolkit": "^1.9.1",
"@sentry/react-native": "4.10.1",
"@sharcoux/slider": "^6.1.1",
"axios": "^0.27.2",
"expo": "^47.0.7",
"expo-auth-session": "^3.7.2",
"expo-av": "^13.0.1",
"expo": "^47.0.8",
"expo-auth-session": "^3.7.3",
"expo-av": "^13.0.2",
"expo-constants": "^14.0.2",
"expo-crypto": "^12.0.0",
"expo-file-system": "^15.1.1",
"expo-firebase-analytics": "^8.0.0",
"expo-haptics": "^12.0.1",
"expo-linking": "^3.2.3",
"expo-localization": "^14.0.0",
@ -66,21 +66,21 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.0.0",
"react-intl": "^6.2.1",
"react-intl": "^6.2.5",
"react-native": "0.70.6",
"react-native-animated-spinkit": "^1.5.2",
"react-native-base64": "^0.2.1",
"react-native-blurhash": "^1.1.10",
"react-native-context-menu-view": "xmflsct/react-native-context-menu-view",
"react-native-fast-image": "^8.6.3",
"react-native-feather": "^1.1.2",
"react-native-flash-message": "^0.3.1",
"react-native-gesture-handler": "~2.8.0",
"react-native-htmlview": "^0.16.0",
"react-native-image-picker": "^4.10.1",
"react-native-image-picker": "^4.10.2",
"react-native-ios-context-menu": "^1.15.1",
"react-native-language-detection": "^0.1.0",
"react-native-live-text-image-view": "^0.4.0",
"react-native-pager-view": "^6.1.1",
"react-native-pager-view": "^6.1.2",
"react-native-reanimated": "^2.13.0",
"react-native-reanimated-zoom": "^0.3.3",
"react-native-safe-area-context": "^4.4.1",
@ -88,24 +88,25 @@
"react-native-share-menu": "^6.0.0",
"react-native-svg": "^13.6.0",
"react-native-swipe-list-view": "^3.2.9",
"react-native-tab-view": "^3.3.0",
"react-native-tab-view": "^3.3.2",
"react-query": "^3.39.2",
"react-redux": "^8.0.5",
"redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3",
"valid-url": "^1.0.9"
"valid-url": "^1.0.9",
"zeego": "^0.5.0"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/core": "^7.20.5",
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@expo/config": "^7.0.3",
"@types/linkify-it": "^3.0.2",
"@types/lodash": "^4.14.189",
"@types/react": "~18.0.25",
"@types/lodash": "^4.14.191",
"@types/react": "~18.0.26",
"@types/react-dom": "~18.0.9",
"@types/react-native": "~0.70.6",
"@types/react-native": "~0.70.7",
"@types/react-native-base64": "^0.2.0",
"@types/react-native-share-menu": "^5.0.2",
"@types/react-timeago": "^4.1.3",

View File

@ -274,6 +274,7 @@ declare namespace Mastodon {
type List = {
id: string
title: string
replies_policy: 'none' | 'list' | 'followed'
}
type Instance = {

View File

@ -1,5 +1,4 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import getLanguage from '@helpers/getLanguage'
import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens'
@ -13,11 +12,12 @@ import timezone from '@root/startup/timezone'
import { persistor, store } from '@root/store'
import * as Sentry from '@sentry/react-native'
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
import { changeLanguage } from '@utils/slices/settingsSlice'
import { changeLanguage, getSettingsLanguage } from '@utils/slices/settingsSlice'
import ThemeManager from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
import { IntlProvider } from 'react-intl'
import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context'
@ -85,7 +85,7 @@ const App: React.FC = () => {
if (bootstrapped) {
log('log', 'App', 'loading actual app :)')
log('log', 'App', `Locale: ${Localization.locale}`)
const language = getLanguage()
const language = getSettingsLanguage(store.getState())
if (!language) {
if (Platform.OS !== 'ios') {
store.dispatch(changeLanguage('en'))
@ -96,15 +96,17 @@ const App: React.FC = () => {
}
return (
<SafeAreaProvider>
<ActionSheetProvider>
<AccessibilityManager>
<ThemeManager>
<Screens localCorrupt={localCorrupt} />
</ThemeManager>
</AccessibilityManager>
</ActionSheetProvider>
</SafeAreaProvider>
<IntlProvider locale={language}>
<SafeAreaProvider>
<ActionSheetProvider>
<AccessibilityManager>
<ThemeManager>
<Screens localCorrupt={localCorrupt} />
</ThemeManager>
</AccessibilityManager>
</ActionSheetProvider>
</SafeAreaProvider>
</IntlProvider>
)
} else {
return null

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import { HeaderLeft } from '@components/Header'
import { displayMessage, Message } from '@components/Message'
import navigationRef from '@helpers/navigationRef'
@ -28,7 +27,6 @@ import * as Linking from 'expo-linking'
import { addScreenshotListener } from 'expo-screen-capture'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IntlProvider } from 'react-intl'
import { Alert, Platform, StatusBar } from 'react-native'
import ShareMenu from 'react-native-share-menu'
import { useSelector } from 'react-redux'
@ -113,7 +111,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
}
if (previousRoute?.name !== currentRoute?.name) {
analytics('screen_view', { screen_name: currentRoute?.name })
Sentry.setContext('page', {
previous: previousRoute,
current: currentRoute
@ -273,7 +270,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
}, [])
return (
<IntlProvider locale={i18n.language}>
<>
<StatusBar
backgroundColor={colors.backgroundDefault}
{...(Platform.OS === 'android' && {
@ -350,7 +347,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
<Message />
</NavigationContainer>
</IntlProvider>
</>
)
}

View File

@ -7,30 +7,23 @@ const handleError = (error: any) => {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(
ctx.bold(' API instance '),
ctx.bold(' API '),
ctx.bold('response'),
error.response.status,
error?.response.data?.error || error?.response.message || 'Unknown error'
)
return Promise.reject({
status: error?.response.status,
message:
error?.response.data?.error ||
error?.response.message ||
'Unknown error'
message: error?.response.data?.error || error?.response.message || 'Unknown error'
})
} else if (error?.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
console.error(ctx.bold(' API '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(
ctx.bold(' API instance '),
ctx.bold('internal'),
error?.message
)
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
return Promise.reject()
}
}

View File

@ -13,10 +13,7 @@ export type Params = {
}
headers?: { [key: string]: string }
body?: FormData
extras?: Omit<
AxiosRequestConfig,
'method' | 'url' | 'params' | 'headers' | 'data'
>
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
}
export type InstanceResponse<T = unknown> = {
@ -35,9 +32,7 @@ const apiInstance = async <T = unknown>({
}: Params): Promise<InstanceResponse<T>> => {
const { store } = require('@root/store')
const state = store.getState() as RootState
const instanceActive = state.instances.instances.findIndex(
instance => instance.active
)
const instanceActive = state.instances.instances.findIndex(instance => instance.active)
let domain
let token
@ -45,21 +40,19 @@ const apiInstance = async <T = unknown>({
domain = state.instances.instances[instanceActive].url
token = state.instances.instances[instanceActive].token
} else {
console.warn(
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
)
console.warn(ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided')
return Promise.reject()
}
console.log(
ctx.bgGreen.bold(' API instance ') +
' ' +
domain +
' ' +
method +
ctx.green(' -> ') +
`/${url}` +
(params ? ctx.green(' -> ') : ''),
' ' +
domain +
' ' +
method +
ctx.green(' -> ') +
`/${url}` +
(params ? ctx.green(' -> ') : ''),
params ? params : ''
)
@ -70,10 +63,7 @@ const apiInstance = async <T = unknown>({
url,
params,
headers: {
'Content-Type':
body && body instanceof FormData
? 'multipart/form-data'
: 'application/json',
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
Accept: '*/*',
...userAgent,
...headers,
@ -87,10 +77,10 @@ const apiInstance = async <T = unknown>({
.then(response => {
let prev
let next
if (response.headers.link) {
const headersLinks = li.parse(response.headers.link)
prev = headersLinks.prev?.match(/_id=([0-9]*)/)[1]
next = headersLinks.next?.match(/_id=([0-9]*)/)[1]
if (response.headers?.link) {
const headersLinks = li.parse(response.headers?.link)
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1]
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1]
}
return Promise.resolve({
body: response.data,

View File

@ -4,49 +4,47 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react'
import React, { PropsWithChildren } from 'react'
import { Pressable, View } from 'react-native'
import analytics from './analytics'
import GracefullyImage from './GracefullyImage'
import CustomText from './Text'
export interface Props {
account: Mastodon.Account
onPress?: () => void
origin?: string
Component?: typeof View | typeof Pressable
props?: {}
}
const ComponentAccount: React.FC<Props> = ({
const ComponentAccount: React.FC<PropsWithChildren & Props> = ({
account,
onPress: customOnPress,
origin
Component,
props,
children
}) => {
const { colors } = useTheme()
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
analytics('search_account_press', { page: origin })
navigation.push('Tab-Shared-Account', { account })
}, [])
if (!props) {
props = { onPress: () => navigation.push('Tab-Shared-Account', { account }) }
}
return (
<Pressable
accessibilityRole='button'
style={{
return React.createElement(
Component || Pressable,
{
...props,
style: {
flex: 1,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M,
flexDirection: 'row',
alignSelf: 'flex-start',
justifyContent: 'space-between',
alignItems: 'center'
}}
onPress={customOnPress || onPress}
>
}
},
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }}
style={{
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
@ -72,7 +70,8 @@ const ComponentAccount: React.FC<Props> = ({
@{account.acct}
</CustomText>
</View>
</Pressable>
</View>,
children
)
}

View File

@ -1,185 +0,0 @@
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import {
MutationVarsTimelineUpdateAccountProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
actions: ContextMenuAction[]
type: 'status' | 'account' // Do not need to fetch relationship in timeline
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
id: Mastodon.Account['id']
}
const contextMenuAccount = ({ actions, type, queryKey, rootQueryKey, id: accountId }: Props) => {
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
onSuccess: (_, params) => {
queryClient.refetchQueries(['Relationship', { id: accountId }])
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`account.${theParams.payload.property}.action`, {
...(theParams.payload.property !== 'reports' && {
context: (theParams.payload.currentValue || false).toString()
})
})
})
})
},
onError: (err: any, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(`account.${theParams.payload.property}.action`, {
...(theParams.payload.property !== 'reports' && {
context: (theParams.payload.currentValue || false).toString()
})
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
},
onSettled: () => {
queryKey && queryClient.invalidateQueries(queryKey)
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
}
})
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id)
const ownAccount = instanceAccount?.id === accountId
const { data: relationship } = useRelationshipQuery({
id: accountId,
options: { enabled: type === 'account' }
})
if (!ownAccount) {
actions.push({
id: 'account-mute',
title: t('account.mute.action', {
context: (relationship?.muting || false).toString()
}),
systemIcon: 'eye.slash'
})
switch (Platform.OS) {
case 'ios':
actions.push({
id: 'account',
title: t('account.title'),
actions: [
{
id: 'account-block',
title: t('account.block.action', {
context: (relationship?.blocking || false).toString()
}),
systemIcon: 'xmark.circle',
destructive: true
},
{
id: 'account-reports',
title: t('account.reports.action'),
systemIcon: 'flag',
destructive: true
}
]
})
break
default:
actions.push(
{
id: 'account-block',
title: t('account.block.action', {
context: (relationship?.blocking || false).toString()
}),
systemIcon: 'xmark.circle',
destructive: true
},
{
id: 'account-reports',
title: t('account.reports.action'),
systemIcon: 'flag',
destructive: true
}
)
break
}
}
return (index: number) => {
if (typeof index !== 'number' || !actions[index]) {
return // For Android
}
if (actions[index].id === 'account-mute') {
analytics('timeline_shared_headeractions_account_mute_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'mute', currentValue: relationship?.muting }
})
}
if (
actions[index].id === 'account-block' ||
(actions[index].id === 'account' && actions[index].actions?.[0].id === 'account-block')
) {
analytics('timeline_shared_headeractions_account_block_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'block', currentValue: relationship?.blocking }
})
}
if (
actions[index].id === 'account-reports' ||
(actions[index].id === 'account' && actions[index].actions?.[0].id === 'account-reports')
) {
analytics('timeline_shared_headeractions_account_reports_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'reports' }
})
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'block', currentValue: false }
})
}
}
}
export default contextMenuAccount

View File

@ -1,27 +1,25 @@
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Alert, Platform } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
actions: ContextMenuAction[]
status: Mastodon.Status
queryKey: QueryKeyTimeline
const menuInstance = ({
status,
queryKey,
rootQueryKey
}: {
status?: Mastodon.Status
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
}
}): ContextMenu[][] => {
if (!status || !queryKey) return []
const contextMenuInstance = ({ actions, status, queryKey, rootQueryKey }: Props) => {
const { t } = useTranslation('componentContextMenu')
const { theme } = useTheme()
const currentInstance = useSelector(getInstanceUrl)
const instance = status?.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
const { t } = useTranslation('componentContextMenu')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
@ -38,67 +36,48 @@ const contextMenuInstance = ({ actions, status, queryKey, rootQueryKey }: Props)
}
})
const menus: ContextMenu[][] = []
const currentInstance = useSelector(getInstanceUrl)
const instance = status.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
if (currentInstance !== instance && instance) {
switch (Platform.OS) {
case 'ios':
actions.push({
id: 'instance',
title: t('instance.title'),
actions: [
{
id: 'instance-block',
title: t('instance.block.action', { instance }),
destructive: true
}
]
})
break
default:
actions.push({
id: 'instance-block',
title: t('instance.block.action', { instance }),
destructive: true
})
break
}
menus.push([
{
key: 'instance-block',
item: {
onSelect: () =>
Alert.alert(
t('instance.block.alert.title', { instance }),
t('instance.block.alert.message'),
[
{
text: t('instance.block.alert.buttons.confirm'),
style: 'destructive',
onPress: () => {
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: instance
})
}
},
{
text: t('common:buttons.cancel')
}
]
),
disabled: false,
destructive: true,
hidden: false
},
title: t('instance.block.action', { instance }),
icon: ''
}
])
}
return (index: number) => {
if (typeof index !== 'number' || !actions[index]) {
return // For Android
}
if (
actions[index].id === 'instance-block' ||
(actions[index].id === 'instance' && actions[index].actions?.[0].id === 'instance-block')
) {
analytics('timeline_shared_headeractions_domain_block_press', {
page: queryKey[1].page
})
Alert.alert(
t('instance.block.alert.title', { instance }),
t('instance.block.alert.message'),
[
{
text: t('instance.block.alert.buttons.confirm'),
style: 'destructive',
onPress: () => {
analytics('timeline_shared_headeractions_domain_block_confirm', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: instance
})
}
},
{
text: t('common:buttons.cancel')
}
]
)
}
}
return menus
}
export default contextMenuInstance
export default menuInstance

View File

@ -1,64 +1,76 @@
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import Clipboard from '@react-native-clipboard/clipboard'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Platform, Share } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
export interface Props {
copiableContent?: React.MutableRefObject<{
content?: string | undefined
complete: boolean
}>
actions: ContextMenuAction[]
type: 'status' | 'account'
url: string
}
const menuShare = (
params:
| {
visibility?: Mastodon.Status['visibility']
copiableContent?: React.MutableRefObject<{
content?: string | undefined
complete: boolean
}>
type: 'status'
url?: string
}
| {
type: 'account'
url: string
}
): ContextMenu[][] => {
if (params.type === 'status' && params.visibility === 'direct') return []
const contextMenuShare = ({ copiableContent, actions, type, url }: Props) => {
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
actions.push({
id: 'share',
title: t(`share.${type}.action`),
systemIcon: 'square.and.arrow.up'
})
Platform.OS !== 'android' &&
type === 'status' &&
actions.push({
id: 'copy',
title: t(`copy.action`),
systemIcon: 'doc.on.doc',
disabled: !copiableContent?.current.content?.length
const menus: ContextMenu[][] = [[]]
if (params.url) {
const url = params.url
menus[0].push({
key: 'share',
item: {
onSelect: () => {
switch (Platform.OS) {
case 'ios':
Share.share({ url })
break
case 'android':
Share.share({ message: url })
break
}
},
disabled: false,
destructive: false,
hidden: false
},
title: t(`share.${params.type}.action`),
icon: 'square.and.arrow.up'
})
}
if (params.type === 'status' && Platform.OS === 'ios')
menus[0].push({
key: 'copy',
item: {
onSelect: () => {
Clipboard.setString(params.copiableContent?.current.content || '')
displayMessage({
theme,
type: 'success',
message: t(`copy.succeed`)
})
},
disabled: false,
destructive: false,
hidden: !params.copiableContent?.current.content?.length
},
title: t('copy.action'),
icon: 'doc.on.doc'
})
return (index: number) => {
if (typeof index !== 'number' || !actions[index]) {
return // For Android
}
if (actions[index].id === 'copy') {
analytics('timeline_shared_headeractions_copy_press')
Clipboard.setString(copiableContent?.current.content || '')
displayMessage({
theme,
type: 'success',
message: t(`copy.succeed`)
})
}
if (actions[index].id === 'share') {
analytics('timeline_shared_headeractions_share_press')
switch (Platform.OS) {
case 'ios':
Share.share({ url })
break
case 'android':
Share.share({ message: url })
break
}
}
}
return menus
}
export default contextMenuShare
export default menuShare

View File

@ -1,5 +1,4 @@
import apiInstance from '@api/instance'
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@ -13,18 +12,20 @@ import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instance
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
actions: ContextMenuAction[]
status: Mastodon.Status
queryKey: QueryKeyTimeline
const menuStatus = ({
status,
queryKey,
rootQueryKey
}: {
status?: Mastodon.Status
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
}
}): ContextMenu[][] => {
if (!status || !queryKey) return []
const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) => {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Screen-Tabs'>>()
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
@ -54,96 +55,19 @@ const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) =
}
})
const menus: ContextMenu[][] = []
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id)
const ownAccount = instanceAccount?.id === status?.account?.id
const ownAccount = instanceAccount?.id === status.account?.id
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
if (ownAccount) {
const accountMenuItems: ContextMenuAction[] = [
menus.push([
{
id: 'status-delete',
title: t('status.delete.action'),
systemIcon: 'trash',
destructive: true
},
{
id: 'status-delete-edit',
title: t('status.deleteEdit.action'),
systemIcon: 'pencil.and.outline',
destructive: true
},
{
id: 'status-mute',
title: t('status.mute.action', {
context: (status.muted || false).toString()
}),
systemIcon: status.muted ? 'speaker' : 'speaker.slash'
}
]
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
if (canEditPost) {
accountMenuItems.unshift({
id: 'status-edit',
title: t('status.edit.action'),
systemIcon: 'square.and.pencil'
})
}
if (status.visibility === 'public' || status.visibility === 'unlisted') {
accountMenuItems.push({
id: 'status-pin',
title: t('status.pin.action', {
context: (status.pinned || false).toString()
}),
systemIcon: status.pinned ? 'pin.slash' : 'pin'
})
}
actions.push(...accountMenuItems)
}
return async (index: number) => {
if (typeof index !== 'number' || !actions[index]) {
return // For Android
}
if (actions[index].id === 'status-delete') {
analytics('timeline_shared_headeractions_status_delete_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(t('status.delete.alert.title'), t('status.delete.alert.message'), [
{
text: t('status.delete.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics('timeline_shared_headeractions_status_delete_confirm', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'deleteItem',
source: 'statuses',
queryKey,
rootQueryKey,
id: status.id
})
}
},
{
text: t('common:buttons.cancel')
}
])
}
if (actions[index].id === 'status-delete-edit') {
analytics('timeline_shared_headeractions_status_deleteedit_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(t('status.deleteEdit.alert.title'), t('status.deleteEdit.alert.message'), [
{
text: t('status.deleteEdit.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics('timeline_shared_headeractions_status_deleteedit_confirm', {
page: queryKey && queryKey[1].page
})
key: 'status-edit',
item: {
onSelect: async () => {
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
@ -151,96 +75,166 @@ const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) =
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
mutation
.mutateAsync({
type: 'deleteItem',
source: 'statuses',
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
spoiler_text: Mastodon.Status['spoiler_text']
}>({
method: 'get',
url: `statuses/${status.id}/source`
}).then(res => {
navigation.navigate('Screen-Compose', {
type: 'edit',
incomingStatus: {
...status,
text: res.body.text,
spoiler_text: res.body.spoiler_text
},
...(replyToStatus && { replyToStatus }),
queryKey,
id: status.id
rootQueryKey
})
.then(res => {
navigation.navigate('Screen-Compose', {
type: 'deleteEdit',
incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }),
queryKey
})
})
}
},
{
text: t('common:buttons.cancel')
}
])
}
if (actions[index].id === 'status-mute') {
analytics('timeline_shared_headeractions_status_mute_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
}
})
}
if (actions[index].id === 'status-edit') {
analytics('timeline_shared_headeractions_status_edit_press', {
page: queryKey && queryKey[1].page
})
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
spoiler_text: Mastodon.Status['spoiler_text']
}>({
method: 'get',
url: `statuses/${status.id}/source`
}).then(res => {
navigation.navigate('Screen-Compose', {
type: 'edit',
incomingStatus: {
...status,
text: res.body.text,
spoiler_text: res.body.spoiler_text
})
},
...(replyToStatus && { replyToStatus }),
queryKey,
rootQueryKey
})
})
}
if (actions[index].id === 'status-pin') {
// Also note that reblogs cannot be pinned.
analytics('timeline_shared_headeractions_status_pin_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
})
}
disabled: false,
destructive: false,
hidden: !canEditPost
},
title: t('status.edit.action'),
icon: 'square.and.pencil'
},
{
key: 'status-delete-edit',
item: {
onSelect: () =>
Alert.alert(t('status.deleteEdit.alert.title'), t('status.deleteEdit.alert.message'), [
{
text: t('status.deleteEdit.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
mutation
.mutateAsync({
type: 'deleteItem',
source: 'statuses',
queryKey,
id: status.id
})
.then(res => {
navigation.navigate('Screen-Compose', {
type: 'deleteEdit',
incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }),
queryKey
})
})
}
},
{
text: t('common:buttons.cancel')
}
]),
disabled: false,
destructive: true,
hidden: false
},
title: t('status.deleteEdit.action'),
icon: 'pencil.and.outline'
},
{
key: 'status-delete',
item: {
onSelect: () =>
Alert.alert(t('status.delete.alert.title'), t('status.delete.alert.message'), [
{
text: t('status.delete.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
mutation.mutate({
type: 'deleteItem',
source: 'statuses',
queryKey,
rootQueryKey,
id: status.id
})
}
},
{
text: t('common:buttons.cancel'),
style: 'default'
}
]),
disabled: false,
destructive: true,
hidden: false
},
title: t('status.delete.action'),
icon: 'trash'
}
])
menus.push([
{
key: 'status-mute',
item: {
onSelect: () =>
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
}
}),
disabled: false,
destructive: false,
hidden: false
},
title: t('status.mute.action', {
context: (status.muted || false).toString()
}),
icon: status.muted ? 'speaker' : 'speaker.slash'
},
{
key: 'status-pin',
item: {
onSelect: () =>
// Also note that reblogs cannot be pinned.
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
}),
disabled: false,
destructive: false,
hidden: status.visibility !== 'public' && status.visibility !== 'unlisted'
},
title: t('status.pin.action', {
context: (status.pinned || false).toString()
}),
icon: status.pinned ? 'pin.slash' : 'pin'
}
])
}
return menus
}
export default contextMenuStatus
export default menuStatus

View File

@ -3,40 +3,60 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react'
import { Pressable } from 'react-native'
import analytics from './analytics'
import React, { useCallback, useState } from 'react'
import { Dimensions, Pressable } from 'react-native'
import Sparkline from './Sparkline'
import CustomText from './Text'
export interface Props {
hashtag: Mastodon.Tag
onPress?: () => void
origin?: string
}
const ComponentHashtag: React.FC<Props> = ({
hashtag,
onPress: customOnPress,
origin
}) => {
const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress }) => {
const { colors } = useTheme()
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
analytics('search_account_press', { page: origin })
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
}, [])
const padding = StyleConstants.Spacing.Global.PagePadding
const width = Dimensions.get('window').width / 4
const [height, setHeight] = useState<number>(0)
return (
<Pressable
accessibilityRole='button'
style={{ padding: StyleConstants.Spacing.S * 1.5 }}
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
padding
}}
onPress={customOnPress || onPress}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height - padding * 2 - 1)}
>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }}>
<CustomText
fontStyle='M'
style={{
flexShrink: 1,
color: colors.primaryDefault,
paddingRight: StyleConstants.Spacing.M
}}
numberOfLines={1}
>
#{hashtag.name}
</CustomText>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
/>
</Pressable>
)
}

View File

@ -1,5 +1,6 @@
import Button from '@components/Button'
import Icon from '@components/Icon'
import browserPackage from '@helpers/browserPackage'
import { useAppsQuery } from '@utils/queryHooks/apps'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { getInstances } from '@utils/slices/instancesSlice'
@ -9,18 +10,10 @@ import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash'
import React, { RefObject, useCallback, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import {
Alert,
Image,
KeyboardAvoidingView,
Platform,
TextInput,
View
} from 'react-native'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder'
import analytics from './analytics'
import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info'
import CustomText from './Text'
@ -65,18 +58,14 @@ const ComponentInstance: React.FC<Props> = ({
const processUpdate = useCallback(() => {
if (domain) {
analytics('instance_login')
if (
instances &&
instances.filter(instance => instance.url === domain).length
) {
if (instances && instances.filter(instance => instance.url === domain).length) {
Alert.alert(t('update.alert.title'), t('update.alert.message'), [
{
text: t('update.alert.buttons.cancel'),
text: t('common:buttons.cancel'),
style: 'cancel'
},
{
text: t('update.alert.buttons.continue'),
text: t('common:buttons.continue'),
onPress: () => {
appsQuery.refetch()
}
@ -142,9 +131,7 @@ const ComponentInstance: React.FC<Props> = ({
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
}}
editable={false}
defaultValue='https://'
@ -156,9 +143,7 @@ const ComponentInstance: React.FC<Props> = ({
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
}}
onChangeText={onChangeText}
autoCapitalize='none'
@ -166,7 +151,6 @@ const ComponentInstance: React.FC<Props> = ({
keyboardType='url'
textContentType='URL'
onSubmitEditing={({ nativeEvent: { text } }) => {
analytics('instance_textinput_submit', { match: text === domain })
if (
text === domain &&
instanceQuery.isSuccess &&
@ -182,11 +166,7 @@ const ComponentInstance: React.FC<Props> = ({
keyboardAppearance={mode}
{...(scrollViewRef && {
onFocus: () =>
setTimeout(
() =>
scrollViewRef.current?.scrollTo({ y: 0, animated: true }),
150
)
setTimeout(() => scrollViewRef.current?.scrollTo({ y: 0, animated: true }), 150)
})}
autoCorrect={false}
spellCheck={false}
@ -211,27 +191,19 @@ const ComponentInstance: React.FC<Props> = ({
<InstanceInfo
style={{ alignItems: 'flex-start' }}
header={t('server.information.accounts')}
content={
instanceQuery.data?.stats?.user_count?.toString() || undefined
}
content={instanceQuery.data?.stats?.user_count?.toString() || undefined}
potentialWidth={4}
/>
<InstanceInfo
style={{ alignItems: 'center' }}
header={t('server.information.statuses')}
content={
instanceQuery.data?.stats?.status_count?.toString() ||
undefined
}
content={instanceQuery.data?.stats?.status_count?.toString() || undefined}
potentialWidth={4}
/>
<InstanceInfo
style={{ alignItems: 'flex-end' }}
header={t('server.information.domains')}
content={
instanceQuery.data?.stats?.domain_count?.toString() ||
undefined
}
content={instanceQuery.data?.stats?.domain_count?.toString() || undefined}
potentialWidth={4}
/>
</View>
@ -248,17 +220,11 @@ const ComponentInstance: React.FC<Props> = ({
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={{
marginTop:
(StyleConstants.Font.LineHeight.S -
StyleConstants.Font.Size.S) /
2,
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
marginRight: StyleConstants.Spacing.XS
}}
/>
<CustomText
fontStyle='S'
style={{ flex: 1, color: colors.secondary }}
>
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
{t('server.disclaimer.base')}
</CustomText>
</View>
@ -274,10 +240,7 @@ const ComponentInstance: React.FC<Props> = ({
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={{
marginTop:
(StyleConstants.Font.LineHeight.S -
StyleConstants.Font.Size.S) /
2,
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
marginRight: StyleConstants.Spacing.XS
}}
/>
@ -292,22 +255,20 @@ const ComponentInstance: React.FC<Props> = ({
<CustomText
accessible
style={{ color: colors.blue }}
onPress={() => {
analytics('view_privacy')
WebBrowser.openBrowserAsync(
'https://tooot.app/privacy-policy'
)
}}
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/privacy-policy', {
browserPackage: await browserPackage()
})
}
/>,
<CustomText
accessible
style={{ color: colors.blue }}
onPress={() => {
analytics('view_tos')
WebBrowser.openBrowserAsync(
'https://tooot.app/terms-of-service'
)
}}
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/terms-of-service', {
browserPackage: await browserPackage()
})
}
/>
]}
/>

View File

@ -1,3 +1,4 @@
import browserPackage from '@helpers/browserPackage'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { InstanceLatest } from '@utils/migrations/instances/migration'
@ -24,14 +25,11 @@ const InstanceAuth = React.memo(
useProxy: false
})
const navigation =
useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const queryClient = useQueryClient()
const dispatch = useAppDispatch()
const deprecateAuthFollow = useSelector(
checkInstanceFeature('deprecate_auth_follow')
)
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: appData.clientId,
@ -48,7 +46,7 @@ const InstanceAuth = React.memo(
useEffect(() => {
;(async () => {
if (request?.clientId) {
await promptAsync()
await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e))
}
})()
}, [request])

View File

@ -1,5 +1,5 @@
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { View } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
@ -7,14 +7,16 @@ export interface Props {
}
const MenuContainer: React.FC<Props> = ({ children }) => {
return <View style={styles.base}>{children}</View>
return (
<View
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
>
{children}
</View>
)
}
const styles = StyleSheet.create({
base: {
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}
})
export default MenuContainer

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react'
import { View } from 'react-native'
import { Text, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
@ -65,6 +65,7 @@ const MenuRow: React.FC<Props> = ({
>
<TapGestureHandler
onHandlerStateChange={async ({ nativeEvent }) => {
if (typeof iconBack !== 'string') return // Let icon back handles the gesture
if (nativeEvent.state === State.ACTIVE && !loading) {
if (screenReaderEnabled && switchOnValueChange) {
switchOnValueChange()
@ -79,12 +80,13 @@ const MenuRow: React.FC<Props> = ({
style={{
flex: 1,
flexDirection: 'row',
paddingTop: StyleConstants.Spacing.S
justifyContent: 'space-between',
marginTop: StyleConstants.Spacing.S
}}
>
<View
style={{
flexGrow: 3,
flex: 3,
flexDirection: 'row',
alignItems: 'center'
}}

View File

@ -125,7 +125,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
shadowRadius: 4,
paddingRight: StyleConstants.Spacing.M * 2,
marginTop: insets.top
marginTop: ref ? undefined : insets.top
}}
titleStyle={{
color: colors.primaryDefault,

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import openLink from '@components/openLink'
import ParseEmojis from '@components/Parse/Emojis'
@ -63,7 +62,6 @@ const renderNode = ({
lineHeight: adaptedLineheight
}}
onPress={() => {
analytics('status_hashtag_press')
!disableDetails &&
differentTag &&
navigation.push('Tab-Shared-Hashtag', {
@ -89,7 +87,6 @@ const renderNode = ({
lineHeight: adaptedLineheight
}}
onPress={() => {
analytics('status_mention_press')
accountIndex !== -1 &&
!disableDetails &&
differentAccount &&
@ -118,7 +115,6 @@ const renderNode = ({
lineHeight: adaptedLineheight
}}
onPress={async () => {
analytics('status_link_press')
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
@ -172,6 +168,7 @@ export interface Props {
highlighted?: boolean
disableDetails?: boolean
selectable?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
}
const ParseHTML = React.memo(
@ -187,7 +184,8 @@ const ParseHTML = React.memo(
expandHint,
highlighted = false,
disableDetails = false,
selectable = false
selectable = false,
setSpoilerExpanded
}: Props) => {
const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale(
@ -255,9 +253,11 @@ const ParseHTML = React.memo(
<Pressable
accessibilityLabel={t('HTML.accessibilityHint')}
onPress={() => {
analytics('status_readmore', { totalLines, expanded })
layoutAnimation()
setExpanded(!expanded)
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
}}
style={{
flexDirection: 'row',

View File

@ -1,11 +1,7 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import {
QueryKeyRelationship,
useRelationshipMutation
} from '@utils/queryHooks/relationship'
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -23,17 +19,12 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const queryKeyNotification: QueryKeyTimeline = [
'Timeline',
{ page: 'Notifications' }
]
const queryKeyNotification: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
const queryClient = useQueryClient()
const mutation = useRelationshipMutation({
onSuccess: res => {
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [
res
])
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
queryClient.refetchQueries(queryKeyNotification)
},
onError: (err: any, { type }) => {
@ -62,28 +53,26 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
type='icon'
content='X'
loading={mutation.isLoading}
onPress={() => {
analytics('relationship_incoming_press_reject')
onPress={() =>
mutation.mutate({
id,
type: 'incoming',
payload: { action: 'reject' }
})
}}
}
/>
<Button
round
type='icon'
content='Check'
loading={mutation.isLoading}
onPress={() => {
analytics('relationship_incoming_press_authorize')
onPress={() =>
mutation.mutate({
id,
type: 'incoming',
payload: { action: 'authorize' }
})
}}
}
style={styles.approve}
/>
</View>

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
@ -29,10 +28,7 @@ const RelationshipOutgoing = React.memo(
const mutation = useRelationshipMutation({
onSuccess: (res, { payload: { action } }) => {
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(
queryKeyRelationship,
[res]
)
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey)
@ -64,17 +60,12 @@ const RelationshipOutgoing = React.memo(
onPress = () => {}
} else {
if (query.data?.blocked_by) {
analytics('relationship_outgoing_blocked_by')
content = t('button.blocked_by')
onPress = () => {
analytics('relationship_outgoing_blocked_by_press')
}
onPress = () => {}
} else {
if (query.data?.blocking) {
analytics('relationship_outgoing_blocking')
content = t('button.blocking')
onPress = () => {
analytics('relationship_outgoing_blocking_press')
mutation.mutate({
id,
type: 'outgoing',
@ -86,10 +77,8 @@ const RelationshipOutgoing = React.memo(
}
} else {
if (query.data?.following) {
analytics('relationship_outgoing_following')
content = t('button.following')
onPress = () => {
analytics('relationship_outgoing_following_press')
mutation.mutate({
id,
type: 'outgoing',
@ -101,10 +90,8 @@ const RelationshipOutgoing = React.memo(
}
} else {
if (query.data?.requested) {
analytics('relationship_outgoing_requested')
content = t('button.requested')
onPress = () => {
analytics('relationship_outgoing_requested_press')
mutation.mutate({
id,
type: 'outgoing',
@ -115,10 +102,8 @@ const RelationshipOutgoing = React.memo(
})
}
} else {
analytics('relationship_outgoing_default')
content = t('button.default')
onPress = () => {
analytics('relationship_outgoing_default_press')
mutation.mutate({
id,
type: 'outgoing',

View File

@ -9,7 +9,7 @@ export interface Props {
const RelativeTime: React.FC<Props> = ({ time }) => {
const [now, setNow] = useState(new Date().getTime())
useEffect(() => {
const appStateListener = AppState.addEventListener('change', state => {
const appStateListener = AppState.addEventListener('change', () => {
setNow(new Date().getTime())
})

View File

@ -0,0 +1,71 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Pressable, View } from 'react-native'
import haptics from './haptics'
import Icon from './Icon'
import { ParseEmojis } from './Parse'
import CustomText from './Text'
export interface Props {
multiple?: boolean
options: { selected: boolean; content: string }[]
setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>>
}
const Selections: React.FC<Props> = ({ multiple = false, options, setOptions }) => {
const { colors } = useTheme()
const isSelected = (index: number): string =>
options[index].selected
? `Check${multiple ? 'Square' : 'Circle'}`
: `${multiple ? 'Square' : 'Circle'}`
return (
<>
{options.map((option, index) => (
<Pressable
key={index}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
onPress={() => {
if (multiple) {
haptics('Light')
setOptions(options.map((o, i) => (i === index ? { ...o, selected: !o.selected } : o)))
} else {
if (!option.selected) {
haptics('Light')
setOptions(
options.map((o, i) => {
if (i === index) {
return { ...o, selected: true }
} else {
return { ...o, selected: false }
}
})
)
}
}
}}
>
<View style={{ flex: 1, flexDirection: 'row' }}>
<Icon
style={{
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
}}
name={isSelected(index)}
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.content} />
</CustomText>
</View>
</Pressable>
))}
</>
)
}
export default Selections

View File

@ -1,32 +1,35 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
export interface Props {
extraMarginLeft?: number
extraMarginRight?: number
style?: StyleProp<ViewStyle>
}
const ComponentSeparator = React.memo(
({ extraMarginLeft = 0, extraMarginRight = 0 }: Props) => {
const { colors } = useTheme()
const ComponentSeparator: React.FC<Props> = ({
extraMarginLeft = 0,
extraMarginRight = 0,
style
}) => {
const { colors } = useTheme()
return (
<View
style={{
return (
<View
style={[
style,
{
backgroundColor: colors.backgroundDefault,
borderTopColor: colors.border,
borderTopWidth: StyleSheet.hairlineWidth,
marginLeft:
StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
marginRight:
StyleConstants.Spacing.Global.PagePadding + extraMarginRight
}}
/>
)
},
() => true
)
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
}
]}
/>
)
}
export default ComponentSeparator

View File

@ -0,0 +1,87 @@
import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy, minBy } from 'lodash'
import React from 'react'
import { Platform } from 'react-native'
import Svg, { G, Path } from 'react-native-svg'
export interface Props {
data: number[]
width: number
height: number
margin?: number
}
const Sparkline: React.FC<Props> = ({ data, width, height, margin = 0 }) => {
const { colors } = useTheme()
const dataToPoints = ({
data,
width,
height
}: {
data: number[]
width: number
height: number
}): { x: number; y: number }[] => {
const max = maxBy(data) || 0
const min = minBy(data) || 0
const len = data.length
const vfactor = (height - margin * 2) / (max - min || 2)
const hfactor = (width - margin * 2) / (len - (len > 1 ? 1 : 0))
return data.map((d, i) => ({
x: i * hfactor + margin,
y: (max === min ? 1 : max - d) * vfactor + margin
}))
}
const points = dataToPoints({ data, width, height })
const divisor = 0.25
let prev: { x: number; y: number }
const curve = (p: { x: number; y: number }) => {
let res
if (!prev) {
res = [p.x, p.y]
} else {
const len = (p.x - prev.x) * divisor
res = [
'C',
prev.x + len, // x1
prev.y, // y1
p.x - len, // x2
p.y, // y2
p.x, // x
p.y // y
]
}
prev = p
return res
}
const linePoints = points.map(p => curve(p)).reduce((a, b) => a.concat(b))
const closePolyPoints = [
'L' + points[points.length - 1].x,
height - margin,
margin,
height - margin,
margin,
points[0].y
]
const fillPoints = linePoints.concat(closePolyPoints)
return (
<Svg height={Platform.OS !== 'android' ? 'auto' : 24} width={width}>
<G>
<Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} />
<Path
d={'M' + linePoints.join(' ')}
stroke={colors.blue}
strokeWidth={1}
strokeLinejoin='round'
strokeLinecap='round'
/>
</G>
</Svg>
)
}
export default Sparkline

View File

@ -1,95 +1,55 @@
import apiInstance from '@api/instance'
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual } from 'lodash'
import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineActions from './Shared/Actions'
import TimelineContent from './Shared/Content'
import StatusContext from './Shared/Context'
import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelinePoll from './Shared/Poll'
const Avatars: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => {
return (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M,
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{accounts.slice(0, 4).map(account => (
<GracefullyImage
key={account.id}
uri={{ original: account.avatar, static: account.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height:
accounts.length > 2
? StyleConstants.Avatar.M / 2
: StyleConstants.Avatar.M
}}
style={{ flex: 1, flexBasis: '50%' }}
/>
))}
</View>
)
}
export interface Props {
conversation: Mastodon.Conversation
queryKey: QueryKeyTimeline
highlighted?: boolean
}
const TimelineConversation = React.memo(
({ conversation, queryKey, highlighted = false }: Props) => {
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const { colors } = useTheme()
const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlighted = false }) => {
const { colors } = useTheme()
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return apiInstance<Mastodon.Conversation>({
method: 'post',
url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return apiInstance<Mastodon.Conversation>({
method: 'post',
url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
})
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
analytics('timeline_conversation_press')
if (conversation.last_status) {
conversation.unread && mutate()
navigation.push('Tab-Shared-Toot', {
toot: conversation.last_status,
rootQueryKey: queryKey
})
}
}, [])
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
if (conversation.last_status) {
conversation.unread && mutate()
navigation.push('Tab-Shared-Toot', {
toot: conversation.last_status,
rootQueryKey: queryKey
})
}
}, [])
return (
return (
<StatusContext.Provider value={{ queryKey, status: conversation.last_status }}>
<Pressable
style={[
{
@ -102,19 +62,39 @@ const TimelineConversation = React.memo(
conversation.unread && {
borderLeftWidth: StyleConstants.Spacing.XS,
borderLeftColor: colors.blue,
paddingLeft:
StyleConstants.Spacing.Global.PagePadding -
StyleConstants.Spacing.XS
paddingLeft: StyleConstants.Spacing.Global.PagePadding - StyleConstants.Spacing.XS
}
]}
onPress={onPress}
>
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<Avatars accounts={conversation.accounts} />
<TimelineHeaderConversation
queryKey={queryKey}
conversation={conversation}
/>
<View
style={{
borderRadius: 4,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M,
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{conversation.accounts.slice(0, 4).map(account => (
<GracefullyImage
key={account.id}
uri={{ original: account.avatar, static: account.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height:
conversation.accounts.length > 2
? StyleConstants.Avatar.M / 2
: StyleConstants.Avatar.M
}}
style={{ flex: 1, flexBasis: '50%' }}
/>
))}
</View>
<TimelineHeaderConversation conversation={conversation} />
</View>
{conversation.last_status ? (
@ -122,40 +102,19 @@ const TimelineConversation = React.memo(
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineContent
status={conversation.last_status}
highlighted={highlighted}
/>
{conversation.last_status.poll ? (
<TimelinePoll
queryKey={queryKey}
statusId={conversation.last_status.id}
poll={conversation.last_status.poll}
reblog={false}
sameAccount={
conversation.last_status.id === instanceAccount?.id
}
/>
) : null}
<TimelineContent />
<TimelinePoll />
</View>
<TimelineActions
queryKey={queryKey}
status={conversation.last_status}
highlighted={highlighted}
accts={conversation.accounts.map(account => account.acct)}
reblog={false}
/>
<TimelineActions />
</>
) : null}
</Pressable>
)
},
(prev, next) => isEqual(prev.conversation, next.conversation)
)
</StatusContext.Provider>
)
}
export default TimelineConversation

View File

@ -1,11 +1,12 @@
import analytics from '@components/analytics'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import TimelineActioned from '@components/Timeline/Shared/Actioned'
import TimelineActions from '@components/Timeline/Shared/Actions'
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
import TimelineCard from '@components/Timeline/Shared/Card'
import TimelineContent from '@components/Timeline/Shared/Content'
// @ts-ignore
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
@ -15,21 +16,21 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
import React, { useRef } from 'react'
import { Platform, Pressable, StyleProp, View, ViewStyle } from 'react-native'
import React, { useRef, useState } from 'react'
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
import { useSelector } from 'react-redux'
import TimelineContextMenu from './Shared/ContextMenu'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFeedback from './Shared/Feedback'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
import TimelineTranslate from './Shared/Translate'
export interface Props {
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
origin?: string
highlighted?: boolean
disableDetails?: boolean
disableOnPress?: boolean
@ -40,40 +41,33 @@ const TimelineDefault: React.FC<Props> = ({
item,
queryKey,
rootQueryKey,
origin,
highlighted = false,
disableDetails = false,
disableOnPress = false
}) => {
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualStatus = item.reblog ? item.reblog : item
const ownAccount = actualStatus.account?.id === instanceAccount?.id
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = item.reblog ? item.reblog : item
const ownAccount = status.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false
)
const spoilerHidden = status.spoiler_text?.length
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const filtered = queryKey && shouldFilter({ copiableContent, status: actualStatus, queryKey })
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
if (queryKey && filtered && !highlighted) {
return <TimelineFiltered phrase={filtered} />
}
const onPress = () => {
if (highlighted) return
analytics('timeline_default_press', {
page: queryKey ? queryKey[1].page : origin
})
navigation.push('Tab-Shared-Toot', {
toot: actualStatus,
rootQueryKey: queryKey
})
}
const mainStyle: StyleProp<ViewStyle> = {
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
@ -82,22 +76,14 @@ const TimelineDefault: React.FC<Props> = ({
const main = () => (
<>
{item.reblog ? (
<TimelineActioned action='reblog' account={item.account} />
<TimelineActioned action='reblog' />
) : item._pinned ? (
<TimelineActioned action='pinned' account={item.account} />
<TimelineActioned action='pinned' />
) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar
queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account}
highlighted={highlighted}
/>
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
status={actualStatus}
highlighted={highlighted}
/>
<TimelineAvatar />
<TimelineHeaderDefault />
</View>
<View
@ -106,73 +92,103 @@ const TimelineDefault: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? (
<TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
) : null}
{queryKey && actualStatus.poll ? (
<TimelinePoll
queryKey={queryKey}
rootQueryKey={rootQueryKey}
statusId={actualStatus.id}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={ownAccount}
/>
) : null}
{!disableDetails &&
Array.isArray(actualStatus.media_attachments) &&
actualStatus.media_attachments.length ? (
<TimelineAttachment status={actualStatus} />
) : null}
{!disableDetails && actualStatus.card ? <TimelineCard card={actualStatus.card} /> : null}
{!disableDetails ? (
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineFeedback status={actualStatus} highlighted={highlighted} />
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelinePoll />
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
<TimelineTranslate />
<TimelineFeedback />
</View>
{queryKey && !disableDetails ? (
<TimelineActions
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={highlighted}
status={actualStatus}
ownAccount={ownAccount}
accts={uniqBy(
([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(actualStatus.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={item.reblog ? true : false}
/>
) : null}
<TimelineActions />
</>
)
return disableOnPress ? (
<View style={mainStyle}>{main()}</View>
) : (
<TimelineContextMenu
copiableContent={copiableContent}
status={actualStatus}
queryKey={queryKey}
rootQueryKey={rootQueryKey}
const mShare = menuShare({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<StatusContext.Provider
value={{
queryKey,
rootQueryKey,
status,
isReblog: !!item.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted,
disableDetails,
disableOnPress
}}
>
<Pressable
accessible={highlighted ? false : true}
style={mainStyle}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
</TimelineContextMenu>
{disableOnPress ? (
<View style={mainStyle}>{main()}</View>
) : (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable
accessible={highlighted ? false : true}
style={mainStyle}
disabled={highlighted}
onPress={() =>
navigation.push('Tab-Shared-Toot', {
toot: status,
rootQueryKey: queryKey
})
}
onLongPress={() => {}}
children={main()}
/>
</ContextMenu.Trigger>
<ContextMenu.Content>
{mShare.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
</ContextMenu.Content>
</ContextMenu.Root>
<TimelineHeaderAndroid />
</>
)}
</StatusContext.Provider>
)
}

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
@ -27,20 +26,11 @@ const TimelineEmpty = React.memo(
const children = () => {
switch (status) {
case 'loading':
return (
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
)
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
case 'error':
return (
<>
<Icon
name='Frown'
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
/>
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
<CustomText
fontStyle='M'
style={{
@ -51,14 +41,7 @@ const TimelineEmpty = React.memo(
>
{t('empty.error.message')}
</CustomText>
<Button
type='text'
content={t('empty.error.button')}
onPress={() => {
analytics('timeline_error_press_refetch')
refetch()
}}
/>
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
</>
)
case 'success':
@ -74,7 +57,7 @@ const TimelineEmpty = React.memo(
style={{
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L,
color: colors.primaryDefault
color: colors.secondary
}}
>
{t('empty.success.message')}

View File

@ -1,11 +1,12 @@
import analytics from '@components/analytics'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import TimelineActioned from '@components/Timeline/Shared/Actioned'
import TimelineActions from '@components/Timeline/Shared/Actions'
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
import TimelineCard from '@components/Timeline/Shared/Card'
import TimelineContent from '@components/Timeline/Shared/Content'
// @ts-ignore
import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotification'
import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
@ -15,13 +16,14 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual, uniqBy } from 'lodash'
import React, { useCallback, useRef } from 'react'
import { Platform, Pressable, View } from 'react-native'
import React, { useCallback, useRef, useState } from 'react'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import TimelineContextMenu from './Shared/ContextMenu'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
export interface Props {
notification: Mastodon.Notification
@ -34,6 +36,17 @@ const TimelineNotifications: React.FC<Props> = ({
queryKey,
highlighted = false
}) => {
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = notification.status
const account = notification.status ? notification.status.account : notification.account
const ownAccount = notification.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false
)
const spoilerHidden = notification.status?.spoiler_text?.length
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
@ -51,13 +64,9 @@ const TimelineNotifications: React.FC<Props> = ({
}
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualAccount = notification.status ? notification.status.account : notification.account
const onPress = useCallback(() => {
analytics('timeline_notification_press')
notification.status &&
navigation.push('Tab-Shared-Toot', {
toot: notification.status,
@ -69,11 +78,7 @@ const TimelineNotifications: React.FC<Props> = ({
return (
<>
{notification.type !== 'mention' ? (
<TimelineActioned
action={notification.type}
account={notification.account}
notification
/>
<TimelineActioned action={notification.type} isNotification account={account} />
) : null}
<View
@ -88,8 +93,8 @@ const TimelineNotifications: React.FC<Props> = ({
}}
>
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
<TimelineAvatar account={account} />
<TimelineHeaderNotification notification={notification} />
</View>
{notification.status ? (
@ -99,75 +104,92 @@ const TimelineNotifications: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{notification.status.content.length > 0 ? (
<TimelineContent status={notification.status} highlighted={highlighted} />
) : null}
{notification.status.poll ? (
<TimelinePoll
queryKey={queryKey}
statusId={notification.status.id}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === instanceAccount?.id}
/>
) : null}
{notification.status.media_attachments.length > 0 ? (
<TimelineAttachment status={notification.status} />
) : null}
{notification.status.card ? <TimelineCard card={notification.status.card} /> : null}
<TimelineFullConversation queryKey={queryKey} status={notification.status} />
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelinePoll />
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
</View>
) : null}
</View>
{notification.status ? (
<TimelineActions
queryKey={queryKey}
status={notification.status}
highlighted={highlighted}
accts={uniqBy(
([notification.status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(notification.status.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={false}
/>
) : null}
<TimelineActions />
</>
)
}
return Platform.OS === 'android' ? (
<Pressable
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
const mShare = menuShare({
visibility: notification.status?.visibility,
type: 'status',
url: notification.status?.url || notification.status?.uri,
copiableContent
})
const mStatus = menuStatus({ status: notification.status, queryKey })
const mInstance = menuInstance({ status: notification.status, queryKey })
return (
<StatusContext.Provider
value={{
queryKey,
status,
isReblog: !!status?.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted
}}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
) : (
<TimelineContextMenu
copiableContent={copiableContent}
status={notification.status}
queryKey={queryKey}
>
<Pressable
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
}}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
</TimelineContextMenu>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
}}
onPress={onPress}
onLongPress={() => {}}
children={main()}
/>
</ContextMenu.Trigger>
<ContextMenu.Content>
{mShare.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
</ContextMenu.Content>
</ContextMenu.Root>
<TimelineHeaderAndroid />
</StatusContext.Provider>
)
}

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { ParseEmojis } from '@components/Parse'
import { useNavigation } from '@react-navigation/native'
@ -6,167 +5,164 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props {
account: Mastodon.Account
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
notification?: boolean
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
isNotification?: boolean
account?: Mastodon.Account
}
const TimelineActioned = React.memo(
({ account, action, notification = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const name = account?.display_name || account?.username
const iconColor = colors.primaryDefault
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
const { status } = useContext(StatusContext)
const account = isNotification ? rest.account : status?.account
if (!status || !account) return null
const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
)
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const name = account?.display_name || account?.username
const iconColor = colors.primaryDefault
const onPress = useCallback(() => {
analytics('timeline_shared_actioned_press', { action })
navigation.push('Tab-Shared-Account', { account })
}, [])
const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
)
const children = () => {
switch (action) {
case 'pinned':
return (
<>
<Icon
name='Anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.pinned'))}
</>
)
case 'favourite':
return (
<>
<Icon
name='Heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.favourite', { name }))}
</Pressable>
</>
)
case 'follow':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow', { name }))}
</Pressable>
</>
)
case 'follow_request':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow_request', { name }))}
</Pressable>
</>
)
case 'poll':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.poll'))}
</>
)
case 'reblog':
return (
<>
<Icon
name='Repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(
notification
? t('shared.actioned.reblog.notification', { name })
: t('shared.actioned.reblog.default', { name })
)}
</Pressable>
</>
)
case 'status':
return (
<>
<Icon
name='Activity'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.status', { name }))}
</Pressable>
</>
)
case 'update':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.update'))}
</>
)
default:
return <></>
}
const onPress = () => navigation.push('Tab-Shared-Account', { account })
const children = () => {
switch (action) {
case 'pinned':
return (
<>
<Icon
name='Anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.pinned'))}
</>
)
case 'favourite':
return (
<>
<Icon
name='Heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.favourite', { name }))}
</Pressable>
</>
)
case 'follow':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow', { name }))}
</Pressable>
</>
)
case 'follow_request':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow_request', { name }))}
</Pressable>
</>
)
case 'poll':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.poll'))}
</>
)
case 'reblog':
return (
<>
<Icon
name='Repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(
isNotification
? t('shared.actioned.reblog.notification', { name })
: t('shared.actioned.reblog.default', { name })
)}
</Pressable>
</>
)
case 'status':
return (
<>
<Icon
name='Activity'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.status', { name }))}
</Pressable>
</>
)
case 'update':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.update'))}
</>
)
default:
return <></>
}
}
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}}
>
{children()}
</View>
)
},
() => true
)
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}}
children={children()}
/>
)
}
const styles = StyleSheet.create({
icon: {

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
@ -11,32 +10,22 @@ import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { uniqBy } from 'lodash'
import React, { useCallback, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props {
queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
highlighted: boolean
status: Mastodon.Status
ownAccount?: boolean
accts: Mastodon.Account['acct'][] // When replying to conversations
reblog: boolean
}
const TimelineActions: React.FC = () => {
const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } =
useContext(StatusContext)
if (!queryKey || !status || disableDetails) return null
const TimelineActions: React.FC<Props> = ({
queryKey,
rootQueryKey,
highlighted,
status,
ownAccount = false,
accts,
reblog
}) => {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const { t } = useTranslation('componentTimeline')
const { colors, theme } = useTheme()
@ -84,11 +73,14 @@ const TimelineActions: React.FC<Props> = ({
}
})
const instanceAccount = useSelector(getInstanceAccount, () => true)
const onPressReply = useCallback(() => {
analytics('timeline_shared_actions_reply_press', {
page: queryKey[1].page,
count: status.replies_count
})
const accts = uniqBy(
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(status.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)
navigation.navigate('Screen-Compose', {
type: 'reply',
incomingStatus: status,
@ -112,17 +104,12 @@ const TimelineActions: React.FC<Props> = ({
(selectedIndex: number) => {
switch (selectedIndex) {
case 0:
analytics('timeline_shared_actions_reblog_public_press', {
page: queryKey[1].page,
count: status.reblogs_count,
current: status.reblogged
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@ -133,17 +120,12 @@ const TimelineActions: React.FC<Props> = ({
})
break
case 1:
analytics('timeline_shared_actions_reblog_unlisted_press', {
page: queryKey[1].page,
count: status.reblogs_count,
current: status.reblogged
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@ -157,17 +139,12 @@ const TimelineActions: React.FC<Props> = ({
}
)
} else {
analytics('timeline_shared_actions_reblog_press', {
page: queryKey[1].page,
count: status.reblogs_count,
current: status.reblogged
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
isReblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged,
@ -179,17 +156,12 @@ const TimelineActions: React.FC<Props> = ({
}
}, [status.reblogged, status.reblogs_count])
const onPressFavourite = useCallback(() => {
analytics('timeline_shared_actions_favourite_press', {
page: queryKey[1].page,
count: status.favourites_count,
current: status.favourited
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
isReblog,
payload: {
property: 'favourited',
currentValue: status.favourited,
@ -199,16 +171,12 @@ const TimelineActions: React.FC<Props> = ({
})
}, [status.favourited, status.favourites_count])
const onPressBookmark = useCallback(() => {
analytics('timeline_shared_actions_bookmark_press', {
page: queryKey[1].page,
current: status.bookmarked
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
reblog,
isReblog,
payload: {
property: 'bookmarked',
currentValue: status.bookmarked,

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import AttachmentAudio from '@components/Timeline/Shared/Attachment/Audio'
@ -11,51 +10,140 @@ import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useState } from 'react'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
}
const TimelineAttachment = () => {
const { status, disableDetails } = useContext(StatusContext)
if (
!status ||
disableDetails ||
!Array.isArray(status.media_attachments) ||
!status.media_attachments.length
)
return null
const TimelineAttachment = React.memo(
({ status }: Props) => {
const { t } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline')
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
// @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] =
status.media_attachments
.map(attachment => {
// @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
.map(attachment => {
switch (attachment.type) {
case 'image':
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
}
}
})
.filter(i => i)
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigateToImagesViewer = (id: string) => {
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
}
return (
<View>
<View
style={{
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'stretch'
}}
>
{status.media_attachments.map((attachment, index) => {
switch (attachment.type) {
case 'image':
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
case 'video':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
/>
)
case 'gifv':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
@ -67,178 +155,74 @@ const TimelineAttachment = React.memo(
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
}
}
})
.filter(i => i)
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigateToImagesViewer = (id: string) => {
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
}
return (
<View>
<View
style={{
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'stretch'
}}
>
{status.media_attachments.map((attachment, index) => {
switch (attachment.type) {
case 'image':
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
case 'video':
} else {
return (
<AttachmentVideo
<AttachmentUnsupported
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
attachment={attachment}
/>
)
case 'gifv':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
} else {
return (
<AttachmentUnsupported
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
attachment={attachment}
/>
)
}
}
})}
</View>
}
}
})}
</View>
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button
type='text'
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
analytics('timeline_shared_attachment_blurview_press_show')
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
/>
</Pressable>
) : (
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button
type='icon'
content='EyeOff'
round
type='text'
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
analytics('timeline_shared_attachment_blurview_press_hide')
setSensitiveShown(true)
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/>
))}
</View>
)
},
(prev, next) => {
let isEqual = true
if (prev.status.media_attachments.length !== next.status.media_attachments.length) {
isEqual = false
return isEqual
}
prev.status.media_attachments.forEach((attachment, index) => {
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) {
isEqual = false
}
})
return isEqual
}
)
</Pressable>
) : (
<Button
type='icon'
content='EyeOff'
round
overlay
onPress={() => {
setSensitiveShown(true)
haptics('Light')
}}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/>
))}
</View>
)
}
export default TimelineAttachment

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import { Slider } from '@sharcoux/slider'
@ -25,7 +24,6 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
const [audioPlaying, setAudioPlaying] = useState(false)
const [audioPosition, setAudioPosition] = useState(0)
const playAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_play_press', { id: audio.id })
if (!audioPlayer) {
const { sound } = await Audio.Sound.createAsync(
{ uri: audio.url },
@ -41,7 +39,6 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
}
}, [audioPlayer, audioPosition])
const pauseAudio = useCallback(async () => {
analytics('timeline_shared_attachment_audio_pause_press', { id: audio.id })
audioPlayer!.pauseAsync()
setAudioPlaying(false)
}, [audioPlayer])

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
@ -34,27 +33,17 @@ const AttachmentImage = ({
hidden={sensitiveShown}
uri={{ original: image.preview_url, remote: image.remote_url }}
blurhash={image.blurhash}
onPress={() => {
analytics('timeline_shared_attachment_image_press', {
id: image.id
})
navigateToImagesViewer(image.id)
}}
onPress={() => navigateToImagesViewer(image.id)}
style={{
aspectRatio:
total > 1 ||
!image.meta?.original?.width ||
!image.meta?.original?.height
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height
? attachmentAspectRatio({ total, index })
: image.meta.original.height / image.meta.original.width > 1
? 1
: image.meta.original.width / image.meta.original.height
}}
/>
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={image.description}
/>
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
</View>
)
}

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
@ -18,12 +17,7 @@ export interface Props {
attachment: Mastodon.AttachmentUnknown
}
const AttachmentUnsupported: React.FC<Props> = ({
total,
index,
sensitiveShown,
attachment
}) => {
const AttachmentUnsupported: React.FC<Props> = ({ total, index, sensitiveShown, attachment }) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
@ -55,9 +49,7 @@ const AttachmentUnsupported: React.FC<Props> = ({
style={{
textAlign: 'center',
marginBottom: StyleConstants.Spacing.S,
color: attachment.blurhash
? colors.backgroundDefault
: colors.primaryDefault
color: attachment.blurhash ? colors.backgroundDefault : colors.primaryDefault
}}
>
{t('shared.attachment.unsupported.text')}
@ -69,17 +61,13 @@ const AttachmentUnsupported: React.FC<Props> = ({
size='S'
overlay
onPress={() => {
analytics('timeline_shared_attachment_unsupported_press')
attachment.remote_url && openLink(attachment.remote_url)
}}
/>
) : null}
</>
) : null}
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={attachment.description}
/>
<AttachmentAltText sensitiveShown={sensitiveShown} text={attachment.description} />
</View>
)
}

View File

@ -5,7 +5,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import { AppState, AppStateStatus, Pressable, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
import analytics from '@components/analytics'
import AttachmentAltText from './AltText'
import { Platform } from 'expo-modules-core'
@ -30,13 +29,6 @@ const AttachmentVideo: React.FC<Props> = ({
const [videoPosition, setVideoPosition] = useState<number>(0)
const [videoResizeMode, setVideoResizeMode] = useState<ResizeMode>(ResizeMode.COVER)
const playOnPress = useCallback(async () => {
analytics('timeline_shared_attachment_video_length', {
length: video.meta?.length
})
analytics('timeline_shared_attachment_vide_play_press', {
id: video.id,
timestamp: Date.now()
})
setVideoLoading(true)
if (!videoLoaded) {
await videoPlayer.current?.loadAsync({ uri: video.url })

View File

@ -1,56 +1,49 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import StatusContext from './Context'
export interface Props {
queryKey?: QueryKeyTimeline
account: Mastodon.Account
highlighted: boolean
account?: Mastodon.Account
}
const TimelineAvatar = React.memo(
({ queryKey, account, highlighted }: Props) => {
const { t } = useTranslation('componentTimeline')
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
// Need to fix go back root
const onPress = useCallback(() => {
analytics('timeline_shared_avatar_press', {
page: queryKey && queryKey[1].page
})
queryKey && navigation.push('Tab-Shared-Account', { account })
}, [])
const TimelineAvatar: React.FC<Props> = ({ account }) => {
const { status, highlighted, disableOnPress } = useContext(StatusContext)
const actualAccount = account || status?.account
if (!actualAccount) return null
return (
<GracefullyImage
{...(highlighted && {
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
name: account.display_name
}),
accessibilityHint: t('shared.avatar.accessibilityHint', {
name: account.display_name
})
})}
onPress={onPress}
uri={{ original: account?.avatar, static: account?.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M
}}
style={{
borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
}}
/>
)
}
)
const { t } = useTranslation('componentTimeline')
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
return (
<GracefullyImage
{...(highlighted && {
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
name: actualAccount.display_name
}),
accessibilityHint: t('shared.avatar.accessibilityHint', {
name: actualAccount.display_name
})
})}
onPress={() =>
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
}
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M
}}
style={{
borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
}}
/>
)
}
export default TimelineAvatar

View File

@ -1,5 +1,4 @@
import ComponentAccount from '@components/Account'
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
@ -10,23 +9,23 @@ import { useSearchQuery } from '@utils/queryHooks/search'
import { useStatusQuery } from '@utils/queryHooks/status'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import React, { useContext, useEffect, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import TimelineDefault from '../Default'
import StatusContext from './Context'
export interface Props {
card: Pick<Mastodon.Card, 'url' | 'image' | 'blurhash' | 'title' | 'description'>
}
const TimelineCard: React.FC = () => {
const { status, spoilerHidden, disableDetails } = useContext(StatusContext)
if (!status || !status.card) return null
const TimelineCard = React.memo(({ card }: Props) => {
const { colors } = useTheme()
const navigation = useNavigation()
const [loading, setLoading] = useState(false)
const isStatus = matchStatus(card.url)
const isStatus = matchStatus(status.card.url)
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
const isAccount = matchAccount(card.url)
const isAccount = matchAccount(status.card.url)
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
const searchQuery = useSearchQuery({
@ -39,7 +38,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
if (isStatus.sameInstance) {
return
} else {
return card.url
return status.card.url
}
}
if (isAccount) {
@ -50,7 +49,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
return isAccount.username
}
} else {
return card.url
return status.card.url
}
}
})(),
@ -130,17 +129,17 @@ const TimelineCard = React.memo(({ card }: Props) => {
)
}
if (isStatus && foundStatus) {
return <TimelineDefault item={foundStatus} disableDetails disableOnPress origin='card' />
return <TimelineDefault item={foundStatus} disableDetails disableOnPress />
}
if (isAccount && foundAccount) {
return <ComponentAccount account={foundAccount} origin='card' />
return <ComponentAccount account={foundAccount} />
}
return (
<>
{card.image ? (
{status.card?.image ? (
<GracefullyImage
uri={{ original: card.image }}
blurhash={card.blurhash}
uri={{ original: status.card.image }}
blurhash={status.card.blurhash}
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
/>
@ -156,9 +155,9 @@ const TimelineCard = React.memo(({ card }: Props) => {
fontWeight='Bold'
testID='title'
>
{card.title}
{status.card?.title}
</CustomText>
{card.description ? (
{status.card?.description ? (
<CustomText
fontStyle='S'
numberOfLines={1}
@ -168,17 +167,19 @@ const TimelineCard = React.memo(({ card }: Props) => {
}}
testID='description'
>
{card.description}
{status.card.description}
</CustomText>
) : null}
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
{card.url}
{status.card?.url}
</CustomText>
</View>
</>
)
}
if (spoilerHidden || disableDetails) return null
return (
<Pressable
accessible
@ -193,13 +194,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
overflow: 'hidden',
borderColor: colors.border
}}
onPress={async () => {
analytics('timeline_shared_card_press')
await openLink(card.url, navigation)
}}
children={cardContent}
onPress={async () => status.card && (await openLink(status.card.url, navigation))}
children={cardContent()}
/>
)
})
}
export default TimelineCard

View File

@ -1,52 +1,36 @@
import { ParseHTML } from '@components/Parse'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props {
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & {
mentions?: Mastodon.Status['mentions']
tags?: Mastodon.Status['tags']
}
highlighted?: boolean
disableDetails?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
}
const TimelineContent = React.memo(
({ status, highlighted = false, disableDetails = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true)
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
const { status, highlighted, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
return (
<>
{status.spoiler_text ? (
<>
<ParseHTML
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
/>
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
expandHint={t('shared.content.expandHint')}
highlighted={highlighted}
disableDetails={disableDetails}
/>
</>
) : (
const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true)
return (
<>
{status.spoiler_text?.length ? (
<>
<ParseHTML
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
/>
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
@ -54,16 +38,27 @@ const TimelineContent = React.memo(
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={highlighted ? 999 : undefined}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
expandHint={t('shared.content.expandHint')}
setSpoilerExpanded={setSpoilerExpanded}
highlighted={highlighted}
disableDetails={disableDetails}
/>
)}
</>
)
},
(prev, next) =>
prev.status.content === next.status.content &&
prev.status.spoiler_text === next.status.spoiler_text
)
</>
) : (
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={highlighted ? 999 : undefined}
disableDetails={disableDetails}
/>
)}
</>
)
}
export default TimelineContent

View File

@ -0,0 +1,24 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { createContext } from 'react'
type ContextType = {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
status?: Mastodon.Status
isReblog?: boolean
ownAccount?: boolean
spoilerHidden?: boolean
copiableContent?: React.MutableRefObject<{
content: string
complete: boolean
}>
highlighted?: boolean
disableDetails?: boolean
disableOnPress?: boolean
}
const StatusContext = createContext<ContextType>({} as ContextType)
export default StatusContext

View File

@ -1,84 +0,0 @@
import contextMenuAccount from '@components/ContextMenu/account'
import contextMenuInstance from '@components/ContextMenu/instance'
import contextMenuShare from '@components/ContextMenu/share'
import contextMenuStatus from '@components/ContextMenu/status'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
import { createContext } from 'react'
import { Platform } from 'react-native'
import ContextMenu, { ContextMenuAction, ContextMenuProps } from 'react-native-context-menu-view'
export interface Props {
copiableContent: React.MutableRefObject<{
content: string
complete: boolean
}>
status?: Mastodon.Status
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
}
export const ContextMenuContext = createContext<ContextMenuAction[]>([])
const TimelineContextMenu: React.FC<Props & ContextMenuProps> = ({
children,
copiableContent,
status,
queryKey,
rootQueryKey,
...props
}) => {
if (!status || !queryKey || Platform.OS === 'android') {
return <>{children}</>
}
const actions: ContextMenuAction[] = []
const shareOnPress =
status.visibility !== 'direct'
? contextMenuShare({
copiableContent,
actions,
type: 'status',
url: status.url || status.uri
})
: null
const statusOnPress = contextMenuStatus({
actions,
status,
queryKey,
rootQueryKey
})
const accountOnPress = status?.account?.id
? contextMenuAccount({
actions,
type: 'status',
queryKey,
rootQueryKey,
id: status.account.id
})
: null
const instanceOnPress = contextMenuInstance({
actions,
status,
queryKey,
rootQueryKey
})
return (
<ContextMenuContext.Provider value={actions}>
<ContextMenu
actions={actions}
onPress={({ nativeEvent: { index } }) => {
for (const on of [shareOnPress, statusOnPress, accountOnPress, instanceOnPress]) {
on && on(index)
}
}}
children={children}
{...props}
/>
</ContextMenuContext.Provider>
)
}
export default TimelineContextMenu

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
@ -6,133 +5,92 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props {
status: Pick<
Mastodon.Status,
'id' | 'edited_at' | 'reblogs_count' | 'favourites_count'
>
highlighted: boolean
}
const TimelineFeedback = () => {
const { status, highlighted } = useContext(StatusContext)
if (!status || !highlighted) return null
const TimelineFeedback = React.memo(
({ status, highlighted }: Props) => {
if (!highlighted) {
return null
}
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { data } = useStatusHistory({
id: status.id,
options: { enabled: status.edited_at !== undefined }
})
const { data } = useStatusHistory({
id: status.id,
options: { enabled: status.edited_at !== undefined }
})
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row' }}>
{status.reblogs_count > 0 ? (
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.reblogged_by.accessibilityLabel',
{
count: status.reblogs_count
}
)}
accessibilityHint={t(
'shared.actionsUsers.reblogged_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() => {
analytics('timeline_shared_feedback_press_reblog', {
count: status.reblogs_count
})
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'reblogged_by',
count: status.reblogs_count
})
}}
>
{t('shared.actionsUsers.reblogged_by.text', {
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row' }}>
{status.reblogs_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
count: status.reblogs_count
})}
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'reblogged_by',
count: status.reblogs_count
})}
</CustomText>
) : null}
{status.favourites_count > 0 ? (
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.favourited_by.accessibilityLabel',
{
count: status.reblogs_count
}
)}
accessibilityHint={t(
'shared.actionsUsers.favourited_by.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() => {
analytics('timeline_shared_feedback_press_favourite', {
count: status.favourites_count
})
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'favourited_by',
count: status.favourites_count
})
}}
>
{t('shared.actionsUsers.favourited_by.text', {
})
}
>
{t('shared.actionsUsers.reblogged_by.text', {
count: status.reblogs_count
})}
</CustomText>
) : null}
{status.favourites_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
count: status.reblogs_count
})}
accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'favourited_by',
count: status.favourites_count
})}
</CustomText>
) : null}
</View>
<View>
{data && data.length > 1 ? (
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.history.accessibilityLabel',
{
count: data.length - 1
}
)}
accessibilityHint={t(
'shared.actionsUsers.history.accessibilityHint'
)}
accessibilityRole='button'
style={[styles.text, { marginRight: 0, color: colors.blue }]}
onPress={() => {
analytics('timeline_shared_feedback_press_history', {
count: data.length - 1
})
navigation.push('Tab-Shared-History', { id: status.id })
}}
>
{t('shared.actionsUsers.history.text', {
count: data.length - 1
})}
</CustomText>
) : null}
</View>
})
}
>
{t('shared.actionsUsers.favourited_by.text', {
count: status.favourites_count
})}
</CustomText>
) : null}
</View>
)
},
(prev, next) =>
prev.status.edited_at === next.status.edited_at &&
prev.status.reblogs_count === next.status.reblogs_count &&
prev.status.favourites_count === next.status.favourites_count
)
<View>
{data && data.length > 1 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
count: data.length - 1
})}
accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { marginRight: 0, color: colors.blue }]}
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
>
{t('shared.actionsUsers.history.text', {
count: data.length - 1
})}
</CustomText>
) : null}
</View>
</View>
)
}
const styles = StyleSheet.create({
text: {

View File

@ -1,39 +1,32 @@
import CustomText from '@components/Text'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import StatusContext from './Context'
export interface Props {
queryKey?: QueryKeyTimeline
status: Mastodon.Status
const TimelineFullConversation = () => {
const { queryKey, status, disableDetails } = useContext(StatusContext)
if (!status || disableDetails) return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return queryKey &&
queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id &&
(status.mentions.length === 0 ||
status.mentions.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
<CustomText
fontStyle='S'
style={{
color: colors.blue,
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}
</CustomText>
) : null
}
const TimelineFullConversation = React.memo(
({ queryKey, status }: Props) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return queryKey &&
queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id &&
(status.mentions.length === 0 ||
status.mentions.filter(
mention => mention.id !== status.in_reply_to_account_id
).length) ? (
<CustomText
fontStyle='S'
style={{
color: colors.blue,
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}
</CustomText>
) : null
},
() => true
)
export default TimelineFullConversation

View File

@ -0,0 +1,101 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useState } from 'react'
import { Platform, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
const TimelineHeaderAndroid: React.FC = () => {
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } =
useContext(StatusContext)
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
const { colors } = useTheme()
const [openChange, setOpenChange] = useState(false)
const mShare = menuShare({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri
})
const mAccount = menuAccount({
type: 'status',
openChange,
account: status.account,
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ position: 'absolute', top: 0, right: 0 }}>
{queryKey ? (
<DropdownMenu.Root onOpenChange={setOpenChange}>
<DropdownMenu.Trigger>
<View style={{ padding: StyleConstants.Spacing.L }}>
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{mShare.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mAccount.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
) : null}
</View>
)
}
export default TimelineHeaderAndroid

View File

@ -1,51 +1,26 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import { ParseEmojis } from '@components/Parse'
import CustomText from '@components/Text'
import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { useTimelineMutation } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import StatusContext from './Context'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
const Names = ({ accounts }: { accounts: Mastodon.Account[] }) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return (
<CustomText
numberOfLines={1}
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
>
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
{accounts.map((account, index) => (
<CustomText key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</CustomText>
))}
</CustomText>
)
}
export interface Props {
queryKey: QueryKeyTimeline
conversation: Mastodon.Conversation
}
const HeaderConversation = ({ queryKey, conversation }: Props) => {
const HeaderConversation = ({ conversation }: Props) => {
const { queryKey } = useContext(StatusContext)
if (!queryKey) return null
const { colors, theme } = useTheme()
const { t } = useTranslation('componentTimeline')
@ -71,31 +46,25 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
}
})
const actionOnPress = useCallback(() => {
analytics('timeline_conversation_delete_press')
mutation.mutate({
type: 'deleteItem',
source: 'conversations',
queryKey,
id: conversation.id
})
}, [])
const actionChildren = useMemo(
() => (
<Icon
name='Trash'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
),
[]
)
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 3 }}>
<Names accounts={conversation.accounts} />
<CustomText
numberOfLines={1}
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
>
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
{conversation.accounts.map((account, index) => (
<CustomText key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</CustomText>
))}
</CustomText>
<View
style={{
flexDirection: 'row',
@ -116,8 +85,15 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
<Pressable
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center' }}
onPress={actionOnPress}
children={actionChildren}
onPress={() =>
mutation.mutate({
type: 'deleteItem',
source: 'conversations',
queryKey,
id: conversation.id
})
}
children={<Icon name='Trash' color={colors.secondary} size={StyleConstants.Font.Size.L} />}
/>
</View>
)

View File

@ -1,114 +0,0 @@
import contextMenuAccount from '@components/ContextMenu/account'
import contextMenuInstance from '@components/ContextMenu/instance'
import contextMenuShare from '@components/ContextMenu/share'
import contextMenuStatus from '@components/ContextMenu/status'
import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props {
queryKey?: QueryKeyTimeline
status: Mastodon.Status
highlighted: boolean
}
const TimelineHeaderDefault = ({ queryKey, status, highlighted }: Props) => {
if (!queryKey) return null
const { t } = useTranslation('componentContextMenu')
const { colors } = useTheme()
const actions: ContextMenuAction[] = []
const shareOnPress =
status.visibility !== 'direct'
? contextMenuShare({
actions,
type: 'status',
url: status.url || status.uri
})
: null
const statusOnPress = contextMenuStatus({
actions,
status,
queryKey
})
const accountOnPress = contextMenuAccount({
actions,
type: 'status',
queryKey,
id: status.account.id
})
const instanceOnPress = contextMenuInstance({
actions,
status,
queryKey
})
const { showActionSheetWithOptions } = useActionSheet()
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 7 }}>
<HeaderSharedAccount account={status.account} />
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={status.created_at}
edited_at={status.edited_at}
highlighted={highlighted}
/>
<HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />
<HeaderSharedApplication application={status.application} />
</View>
</View>
{queryKey ? (
<Pressable
accessibilityHint={t('accessibilityHint')}
style={{ flex: 1, flexBasis: StyleConstants.Font.Size.L }}
onPress={() =>
showActionSheetWithOptions(
{
options: actions.map(action => action.title),
cancelButtonIndex: 999,
destructiveButtonIndex: actions
.map((action, index) => (action.destructive ? index : 999))
.filter(num => num !== 999)
},
index => {
if (index !== undefined) {
for (const on of [shareOnPress, statusOnPress, accountOnPress, instanceOnPress]) {
on && on(index)
}
}
}
)
}
>
<Icon name='MoreHorizontal' color={colors.secondary} size={StyleConstants.Font.Size.L} />
</Pressable>
) : null}
</View>
)
}
export default TimelineHeaderDefault

View File

@ -1,75 +0,0 @@
import Icon from '@components/Icon'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import { ContextMenuContext } from './ContextMenu'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props {
queryKey?: QueryKeyTimeline
status: Mastodon.Status
highlighted: boolean
}
const TimelineHeaderDefault = ({ queryKey, status, highlighted }: Props) => {
const { t } = useTranslation('componentContextMenu')
const { colors } = useTheme()
const contextMenuContext = useContext(ContextMenuContext)
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 7 }}>
<HeaderSharedAccount account={status.account} />
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={status.created_at}
edited_at={status.edited_at}
highlighted={highlighted}
/>
<HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />
<HeaderSharedApplication application={status.application} />
</View>
</View>
{queryKey ? (
<Pressable
accessibilityHint={t('accessibilityHint')}
style={{ flex: 1, flexBasis: StyleConstants.Font.Size.L }}
>
<ContextMenu
style={{ flex: 1, alignItems: 'center' }}
dropdownMenuMode
actions={contextMenuContext}
onPress={() => {}}
children={
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
}
/>
</Pressable>
) : null}
</View>
)
}
export default TimelineHeaderDefault

View File

@ -0,0 +1,132 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, Pressable, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
const TimelineHeaderDefault: React.FC = () => {
const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
useContext(StatusContext)
if (!status) return null
const { colors } = useTheme()
const { t } = useTranslation('componentContextMenu')
const [openChange, setOpenChange] = useState(false)
const mShare = menuShare({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
})
const mAccount = menuAccount({
type: 'status',
openChange,
account: status.account,
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 7 }}>
<HeaderSharedAccount account={status.account} />
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={status.created_at}
edited_at={status.edited_at}
highlighted={highlighted}
/>
<HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />
<HeaderSharedApplication application={status.application} />
</View>
</View>
{Platform.OS !== 'android' && !disableDetails ? (
<Pressable
accessibilityHint={t('accessibilityHint')}
style={{ flex: 1, alignItems: 'center' }}
>
<DropdownMenu.Root onOpenChange={setOpenChange}>
<DropdownMenu.Trigger>
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{mShare.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mAccount.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</Pressable>
) : null}
</View>
)
}
export default TimelineHeaderDefault

View File

@ -1,157 +0,0 @@
import contextMenuAccount from '@components/ContextMenu/account'
import contextMenuInstance from '@components/ContextMenu/instance'
import contextMenuShare from '@components/ContextMenu/share'
import contextMenuStatus from '@components/ContextMenu/status'
import Icon from '@components/Icon'
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { Pressable, View } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props {
queryKey: QueryKeyTimeline
notification: Mastodon.Notification
}
const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
const { colors } = useTheme()
const contextMenuActions: ContextMenuAction[] = []
const status = notification.status
const shareOnPress =
status && status?.visibility !== 'direct'
? contextMenuShare({
actions: contextMenuActions,
type: 'status',
url: status.url || status.uri
})
: null
const statusOnPress =
status &&
contextMenuStatus({
actions: contextMenuActions,
status: status,
queryKey
})
const accountOnPress =
status &&
contextMenuAccount({
actions: contextMenuActions,
type: 'status',
queryKey,
id: status.account.id
})
const instanceOnPress =
status &&
contextMenuInstance({
actions: contextMenuActions,
status: status,
queryKey
})
const { showActionSheetWithOptions } = useActionSheet()
const actions = useMemo(() => {
switch (notification.type) {
case 'follow':
return <RelationshipOutgoing id={notification.account.id} />
case 'follow_request':
return <RelationshipIncoming id={notification.account.id} />
default:
if (notification.status) {
return (
<Pressable
style={{ flex: 1, flexBasis: StyleConstants.Font.Size.L }}
onPress={() =>
showActionSheetWithOptions(
{
options: contextMenuActions.map(action => action.title),
cancelButtonIndex: 999,
destructiveButtonIndex: contextMenuActions
.map((action, index) => (action.destructive ? index : 999))
.filter(num => num !== 999)
},
index => {
if (index !== undefined) {
for (const on of [
shareOnPress,
statusOnPress,
accountOnPress,
instanceOnPress
]) {
on && on(index)
}
}
}
)
}
children={
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
}
/>
)
}
}
}, [notification.type])
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View
style={{
flex: notification.type === 'follow' || notification.type === 'follow_request' ? 1 : 4
}}
>
<HeaderSharedAccount
account={notification.status ? notification.status.account : notification.account}
{...((notification.type === 'follow' || notification.type === 'follow_request') && {
withoutName: true
})}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={notification.status?.created_at || notification.created_at}
edited_at={notification.status?.edited_at}
/>
{notification.status?.visibility ? (
<HeaderSharedVisibility visibility={notification.status.visibility} />
) : null}
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication application={notification.status?.application} />
</View>
</View>
<View
style={[
{ marginLeft: StyleConstants.Spacing.M },
notification.type === 'follow' || notification.type === 'follow_request'
? { flexShrink: 1 }
: { flex: 1 }
]}
>
{actions}
</View>
</View>
)
}
export default TimelineHeaderNotification

View File

@ -1,105 +0,0 @@
import Icon from '@components/Icon'
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useMemo } from 'react'
import { Pressable, View } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import { ContextMenuContext } from './ContextMenu'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props {
queryKey: QueryKeyTimeline
notification: Mastodon.Notification
}
const TimelineHeaderNotification = ({ notification }: Props) => {
const { colors } = useTheme()
const contextMenuContext = useContext(ContextMenuContext)
const actions = useMemo(() => {
switch (notification.type) {
case 'follow':
return <RelationshipOutgoing id={notification.account.id} />
case 'follow_request':
return <RelationshipIncoming id={notification.account.id} />
default:
if (notification.status) {
return (
<Pressable
style={{ flex: 1, flexBasis: StyleConstants.Font.Size.L }}
children={
<ContextMenu
style={{ flex: 1, alignItems: 'center' }}
dropdownMenuMode
actions={contextMenuContext}
onPress={() => {}}
children={
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
}
/>
}
/>
)
}
}
}, [notification.type])
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View
style={{
flex: notification.type === 'follow' || notification.type === 'follow_request' ? 1 : 4
}}
>
<HeaderSharedAccount
account={notification.status ? notification.status.account : notification.account}
{...((notification.type === 'follow' || notification.type === 'follow_request') && {
withoutName: true
})}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={notification.status?.created_at || notification.created_at}
edited_at={notification.status?.edited_at}
/>
{notification.status?.visibility ? (
<HeaderSharedVisibility visibility={notification.status.visibility} />
) : null}
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication application={notification.status?.application} />
</View>
</View>
<View
style={[
{ marginLeft: StyleConstants.Spacing.M },
notification.type === 'follow' || notification.type === 'follow_request'
? { flexShrink: 1 }
: { flex: 1 }
]}
>
{actions}
</View>
</View>
)
}
export default TimelineHeaderNotification

View File

@ -0,0 +1,165 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useState } from 'react'
import { Platform, Pressable, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
export type Props = {
notification: Mastodon.Notification
}
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const { queryKey, status } = useContext(StatusContext)
const { colors } = useTheme()
const [openChange, setOpenChange] = useState(false)
const mShare = menuShare({
visibility: status?.visibility,
type: 'status',
url: status?.url || status?.uri
})
const mAccount = menuAccount({
type: 'status',
openChange,
account: status?.account,
queryKey
})
const mStatus = menuStatus({ status, queryKey })
const mInstance = menuInstance({ status, queryKey })
const actions = () => {
switch (notification.type) {
case 'follow':
return <RelationshipOutgoing id={notification.account.id} />
case 'follow_request':
return <RelationshipIncoming id={notification.account.id} />
default:
if (status) {
return (
<Pressable
style={{ flex: 1, alignItems: 'center' }}
children={
<DropdownMenu.Root onOpenChange={setOpenChange}>
<DropdownMenu.Trigger>
<Icon
name='MoreHorizontal'
color={colors.secondary}
size={StyleConstants.Font.Size.L}
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{mShare.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mAccount.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
}
/>
)
}
}
}
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View
style={{
flex: notification.type === 'follow' || notification.type === 'follow_request' ? 1 : 4
}}
>
<HeaderSharedAccount
account={notification.status ? notification.status.account : notification.account}
{...((notification.type === 'follow' || notification.type === 'follow_request') && {
withoutName: true
})}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
}}
>
<HeaderSharedCreated
created_at={notification.status?.created_at || notification.created_at}
edited_at={notification.status?.edited_at}
/>
{notification.status?.visibility ? (
<HeaderSharedVisibility visibility={notification.status.visibility} />
) : null}
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication application={notification.status?.application} />
</View>
</View>
{Platform.OS !== 'android' ? (
<View
style={[
{ marginLeft: StyleConstants.Spacing.M },
notification.type === 'follow' || notification.type === 'follow_request'
? { flexShrink: 1 }
: { flex: 1 }
]}
children={actions()}
/>
) : null}
</View>
)
}
export default TimelineHeaderNotification

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
@ -20,9 +19,6 @@ const HeaderSharedApplication = React.memo(
fontStyle='S'
accessibilityRole='link'
onPress={async () => {
analytics('timeline_shared_header_application_press', {
application
})
application.website && (await openLink(application.website))
}}
style={{

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
@ -8,41 +7,28 @@ import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import StatusContext from './Context'
export interface Props {
queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
statusId: Mastodon.Status['id']
poll: NonNullable<Mastodon.Status['poll']>
reblog: boolean
sameAccount: boolean
}
const TimelinePoll: React.FC = () => {
const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } =
useContext(StatusContext)
if (!queryKey || !status || !status.poll) return null
const poll = status.poll
const TimelinePoll: React.FC<Props> = ({
queryKey,
rootQueryKey,
statusId,
poll,
reblog,
sameAccount
}) => {
const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentTimeline')
const [allOptions, setAllOptions] = useState(
new Array(poll.options.length).fill(false)
)
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
@ -82,18 +68,17 @@ const TimelinePoll: React.FC<Props> = ({
const pollButton = useMemo(() => {
if (!poll.expired) {
if (!sameAccount && !poll.voted) {
if (!ownAccount && !poll.voted) {
return (
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button
onPress={() => {
analytics('timeline_shared_vote_vote_press')
onPress={() =>
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: statusId,
reblog,
id: status.id,
isReblog,
payload: {
property: 'poll',
id: poll.id,
@ -101,7 +86,7 @@ const TimelinePoll: React.FC<Props> = ({
options: allOptions
}
})
}}
}
type='text'
content={t('shared.poll.meta.button.vote')}
loading={mutation.isLoading}
@ -113,21 +98,20 @@ const TimelinePoll: React.FC<Props> = ({
return (
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button
onPress={() => {
analytics('timeline_shared_vote_refresh_press')
onPress={() =>
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: statusId,
reblog,
id: status.id,
isReblog,
payload: {
property: 'poll',
id: poll.id,
type: 'refresh'
}
})
}}
}
type='text'
content={t('shared.poll.meta.button.refresh')}
loading={mutation.isLoading}
@ -136,14 +120,7 @@ const TimelinePoll: React.FC<Props> = ({
)
}
}
}, [
theme,
i18n.language,
poll.expired,
poll.voted,
allOptions,
mutation.isLoading
])
}, [theme, i18n.language, poll.expired, poll.voted, allOptions, mutation.isLoading])
const isSelected = useCallback(
(index: number): string =>
@ -154,20 +131,13 @@ const TimelinePoll: React.FC<Props> = ({
)
const pollBodyDisallow = useMemo(() => {
const maxValue = maxBy(
poll.options,
option => option.votes_count
)?.votes_count
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
return poll.options.map((option, index) => (
<View
key={index}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
>
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
<View style={{ flex: 1, flexDirection: 'row' }}>
<Icon
style={{
paddingTop:
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
}}
name={
@ -176,9 +146,7 @@ const TimelinePoll: React.FC<Props> = ({
}` as any
}
size={StyleConstants.Font.Size.M}
color={
poll.own_votes?.includes(index) ? colors.blue : colors.disabled
}
color={poll.own_votes?.includes(index) ? colors.blue : colors.disabled}
/>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.title} emojis={poll.emojis} />
@ -194,11 +162,7 @@ const TimelinePoll: React.FC<Props> = ({
}}
>
{poll.votes_count
? Math.round(
(option.votes_count /
(poll.voters_count || poll.votes_count)) *
100
)
? Math.round((option.votes_count / (poll.voters_count || poll.votes_count)) * 100)
: 0}
%
</CustomText>
@ -213,11 +177,9 @@ const TimelinePoll: React.FC<Props> = ({
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S,
width: `${Math.round(
(option.votes_count / (poll.voters_count || poll.votes_count)) *
100
(option.votes_count / (poll.voters_count || poll.votes_count)) * 100
)}%`,
backgroundColor:
option.votes_count === maxValue ? colors.blue : colors.disabled
backgroundColor: option.votes_count === maxValue ? colors.blue : colors.disabled
}}
/>
</View>
@ -229,21 +191,15 @@ const TimelinePoll: React.FC<Props> = ({
key={index}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
onPress={() => {
analytics('timeline_shared_vote_option_press')
!allOptions[index] && haptics('Light')
if (poll.multiple) {
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
} else {
{
const otherOptions =
allOptions[index] === false ? false : undefined
const otherOptions = allOptions[index] === false ? false : undefined
setAllOptions(
allOptions.map((o, i) =>
i === index
? !o
: otherOptions !== undefined
? otherOptions
: o
i === index ? !o : otherOptions !== undefined ? otherOptions : o
)
)
}
@ -253,8 +209,7 @@ const TimelinePoll: React.FC<Props> = ({
<View style={{ flex: 1, flexDirection: 'row' }}>
<Icon
style={{
paddingTop:
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
}}
name={isSelected(index)}
@ -271,13 +226,9 @@ const TimelinePoll: React.FC<Props> = ({
const pollVoteCounts = () => {
if (poll.voters_count !== null) {
return (
t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
)
return t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
} else if (poll.votes_count !== null) {
return (
t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
)
return t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
}
}
@ -296,6 +247,8 @@ const TimelinePoll: React.FC<Props> = ({
}
}
if (spoilerHidden || disableDetails) return null
return (
<View style={{ marginTop: StyleConstants.Spacing.M }}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
@ -308,10 +261,7 @@ const TimelinePoll: React.FC<Props> = ({
}}
>
{pollButton}
<CustomText
fontStyle='S'
style={{ flexShrink: 1, color: colors.secondary }}
>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{pollVoteCounts()}
{pollExpiration()}
</CustomText>

View File

@ -1,4 +1,3 @@
import analytics from '@components/analytics'
import { ParseHTML } from '@components/Parse'
import CustomText from '@components/Text'
import getLanguage from '@helpers/getLanguage'
@ -6,137 +5,119 @@ import { useTranslateQuery } from '@utils/queryHooks/translate'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React, { useEffect, useState } from 'react'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import detectLanguage from 'react-native-language-detection'
import StatusContext from './Context'
export interface Props {
highlighted: boolean
status: Pick<Mastodon.Status, 'language' | 'spoiler_text' | 'content' | 'emojis'>
}
const TimelineTranslate = () => {
const { status, highlighted } = useContext(StatusContext)
if (!status || !highlighted) return null
const TimelineTranslate = React.memo(
({ highlighted, status }: Props) => {
if (!highlighted) {
return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
}
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
}
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
}
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
}
detect()
}, [])
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
}
detect()
}, [])
const settingsLanguage = getLanguage()
const targetLanguage = settingsLanguage?.startsWith('en')
? Localization.locale || settingsLanguage || 'en'
: settingsLanguage || Localization.locale || 'en'
const settingsLanguage = getLanguage()
const targetLanguage = settingsLanguage?.startsWith('en')
? Localization.locale || settingsLanguage || 'en'
: settingsLanguage || Localization.locale || 'en'
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage,
target: targetLanguage,
text,
options: { enabled }
})
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage,
target: targetLanguage,
text,
options: { enabled }
})
if (!detectedLanguage) {
return null
}
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (!detectedLanguage) {
return null
}
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
}
return (
<>
<Pressable
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingBottom: isSuccess ? 0 : undefined
}}
onPress={() => {
if (enabled) {
if (!isSuccess) {
analytics('timeline_shared_translate_retry', {
language: detectedLanguage
})
refetch()
}
} else {
analytics('timeline_shared_translate', {
language: detectedLanguage
})
setEnabled(true)
return (
<>
<Pressable
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingBottom: isSuccess ? 0 : undefined
}}
onPress={() => {
if (enabled) {
if (!isSuccess) {
refetch()
}
} else {
setEnabled(true)
}
}}
>
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}}
>
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}}
>
{isError
? t('shared.translate.failed')
: isSuccess
? typeof data?.error === 'string'
? t(`shared.translate.${data.error}`)
: t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
</CustomText>
<CustomText>
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
</CustomText>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
{data && data.error === undefined
? data.text.map((d, i) => (
<ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />
))
: null}
</>
)
},
(prev, next) =>
prev.status.content === next.status.content &&
prev.status.spoiler_text === next.status.spoiler_text
)
{isError
? t('shared.translate.failed')
: isSuccess
? typeof data?.error === 'string'
? t(`shared.translate.${data.error}`)
: t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
</CustomText>
<CustomText>
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
</CustomText>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
{data && data.error === undefined
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
: null}
</>
)
}
export default TimelineTranslate

View File

@ -1,7 +0,0 @@
import * as Analytics from 'expo-firebase-analytics'
const analytics = (event: string, params?: { [key: string]: any }) => {
Analytics.logEvent(event, params).catch(() => {})
}
export default analytics

View File

@ -0,0 +1,238 @@
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import {
QueryKeyRelationship,
useRelationshipMutation,
useRelationshipQuery
} from '@utils/queryHooks/relationship'
import {
MutationVarsTimelineUpdateAccountProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
const menuAccount = ({
type,
openChange,
account,
queryKey,
rootQueryKey
}: {
type: 'status' | 'account' // Where the action is coming from
openChange: boolean
account?: Pick<Mastodon.Account, 'id' | 'username'>
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
}): ContextMenu[][] => {
if (!account) return []
const navigation =
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
const menus: ContextMenu[][] = [[]]
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id)
const ownAccount = instanceAccount?.id === account.id
const [enabled, setEnabled] = useState(openChange)
useEffect(() => {
if (!ownAccount && enabled === false && openChange === true) {
setEnabled(true)
}
}, [openChange, enabled])
const { data, isFetching } = useRelationshipQuery({ id: account.id, options: { enabled } })
const queryClient = useQueryClient()
const timelineMutation = useTimelineMutation({
onSuccess: (_, params) => {
queryClient.refetchQueries(['Relationship', { id: account.id }])
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`account.${theParams.payload.property}.action`, {
...(theParams.payload.property !== 'reports' && {
context: (theParams.payload.currentValue || false).toString()
})
})
})
})
},
onError: (err: any, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(`account.${theParams.payload.property}.action`, {
...(theParams.payload.property !== 'reports' && {
context: (theParams.payload.currentValue || false).toString()
})
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
},
onSettled: () => {
queryKey && queryClient.invalidateQueries(queryKey)
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
}
})
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }]
const relationshipMutation = useRelationshipMutation({
onSuccess: (res, { payload: { action } }) => {
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey)
}
},
onError: (err: any, { payload: { action } }) => {
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(`${action}.function`)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
menus[0].push({
key: 'account-following',
item: {
onSelect: () =>
data &&
relationshipMutation.mutate({
id: account.id,
type: 'outgoing',
payload: { action: 'follow', state: !data?.requested ? data.following : true }
}),
disabled: !data || isFetching,
destructive: false,
hidden: false
},
title: !data?.requested
? t('account.following.action', {
context: (data?.following || false).toString()
})
: t('componentRelationship:button.requested'),
icon: !data?.requested
? data?.following
? 'person.badge.minus'
: 'person.badge.plus'
: 'person.badge.minus'
})
}
if (!ownAccount) {
menus[0].push({
key: 'account-list',
item: {
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
destructive: false,
hidden: isFetching ? false : !data?.following
},
title: t('account.inLists'),
icon: 'checklist'
})
menus[0].push({
key: 'account-mute',
item: {
onSelect: () =>
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'mute', currentValue: data?.muting }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
destructive: false,
hidden: false
},
title: t('account.mute.action', {
context: (data?.muting || false).toString()
}),
icon: data?.muting ? 'eye' : 'eye.slash'
})
}
!ownAccount &&
menus.push([
{
key: 'account-block',
item: {
onSelect: () =>
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block', currentValue: data?.blocking }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
destructive: !data?.blocking,
hidden: false
},
title: t('account.block.action', {
context: (data?.blocking || false).toString()
}),
icon: data?.blocking ? 'checkmark.circle' : 'xmark.circle'
},
{
key: 'account-reports',
item: {
onSelect: () => {
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'reports' }
})
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block', currentValue: false }
})
},
disabled: false,
destructive: true,
hidden: false
},
title: t('account.reports.action'),
icon: 'flag'
}
])
return menus
}
export default menuAccount

6
src/components/contextMenu/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
type ContextMenu = {
key: string
item: { onSelect: () => void; disabled: boolean; destructive: boolean; hidden: boolean }
title: string
icon: string
}

View File

@ -1,4 +1,5 @@
import apiInstance from '@api/instance'
import browserPackage from '@helpers/browserPackage'
import navigationRef from '@helpers/navigationRef'
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
import { store } from '@root/store'
@ -91,7 +92,8 @@ const openLink = async (url: string, navigation?: any) => {
case 'internal':
await WebBrowser.openBrowserAsync(encodeURI(url), {
dismissButtonStyle: 'close',
enableBarCollapsing: true
enableBarCollapsing: true,
browserPackage: await browserPackage()
})
break
case 'external':

View File

@ -0,0 +1,16 @@
import * as WebBrowser from 'expo-web-browser'
import { Platform } from 'react-native'
const browserPackage = async () => {
let browserPackage: string | undefined
if (Platform.OS === 'android') {
const tabsSupportingBrowsers = await WebBrowser.getCustomTabsSupportingBrowsersAsync()
browserPackage =
tabsSupportingBrowsers?.preferredBrowserPackage ||
tabsSupportingBrowsers.browserPackages[0] ||
tabsSupportingBrowsers.servicePackages[0]
}
return browserPackage
}
export default browserPackage

View File

@ -24,6 +24,11 @@
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "trends_new_path",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "follow_tags",
"version": 4.0,

30
src/i18n/ca/common.json Normal file
View File

@ -0,0 +1,30 @@
{
"buttons": {
"OK": "D'acord",
"apply": "Aplica",
"cancel": "Cancel·la",
"discard": "Descarta",
"continue": "Continua",
"delete": "Esborra",
"done": "Fet"
},
"customEmoji": {
"accessibilityLabel": "Emoji personalitzat {{emoji}}"
},
"message": {
"success": {
"message": "{{function}} amb èxit"
},
"warning": {
"message": ""
},
"error": {
"message": "{{function}} ha fallat, torna-ho a intentar"
}
},
"separator": ", ",
"discard": {
"title": "Canvis no desats",
"message": "Els canvis no han sigut desats. Vols descartar-los?"
}
}

View File

@ -0,0 +1,81 @@
{
"accessibilityHint": "Accions per aquesta publicació, com el seu usuari o la mateixa publicació",
"account": {
"title": "Accions d'usuari",
"following": {
"action_false": "Segueix l'usuari",
"action_true": "Deixa de seguir l'usuari"
},
"inLists": "Gestionar usuari de llistes",
"mute": {
"action_false": "Silencia l'usuari",
"action_true": "Deixa de silenciar l'usuari"
},
"block": {
"action_false": "Bloqueja l'usuari",
"action_true": "Deixa de bloquejar l'usuari"
},
"reports": {
"action": "Denuncia i bloqueja l'usuari"
}
},
"copy": {
"action": "Copia la publicació",
"succeed": "Copiat"
},
"instance": {
"title": "Acció de la instància",
"block": {
"action": "Bloquejar la instància {{instance}}",
"alert": {
"title": "Confirma el bloqueig de la instància {{instance}}?",
"message": "Pots silenciar o bloquejar a un usuari.\n\nDesprés de bloquejar una instància, tot el seu contingut, amb els seus seguidors, seran esborrats!",
"buttons": {
"confirm": "Confirma"
}
}
}
},
"share": {
"status": {
"action": "Comparteix la publicació"
},
"account": {
"action": "Comparteix l'usuari"
}
},
"status": {
"title": "Accions de la publicació",
"edit": {
"action": "Edita la publicació"
},
"delete": {
"action": "Elimina la publicació",
"alert": {
"title": "Confirma l'eliminació?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes.",
"buttons": {
"confirm": "Confirma"
}
}
},
"deleteEdit": {
"action": "Elimina la publicació i torna a publicar",
"alert": {
"title": "Confirma l'eliminació i tornar a publicar?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes.",
"buttons": {
"confirm": "Confirma"
}
}
},
"mute": {
"action_false": "Silencia la publicació i les respostes",
"action_true": "Deixa de silenciar la publicació i les respostes"
},
"pin": {
"action_false": "Fixa la publicació",
"action_true": "Deixa de fixar la publicació"
}
}
}

View File

@ -0,0 +1,3 @@
{
"frequentUsed": "D'ús freqüent"
}

View File

@ -0,0 +1,26 @@
{
"server": {
"textInput": {
"placeholder": "Domini de la instància"
},
"button": "Inicia la sessió",
"information": {
"name": "Nom",
"accounts": "Usuaris",
"statuses": "Publicacions",
"domains": "Universos"
},
"disclaimer": {
"base": "L'inici de sessió fa servir el navegador del sistema. Per tant, el tooot no accedirà a la informació del compte."
},
"terms": {
"base": "En iniciar la sessió, acceptes la <0>política de privacitat</0> i les <1>condicions del servei</1>."
}
},
"update": {
"alert": {
"title": "Iniciada la sessió a aquesta instància",
"message": "Pots iniciar la sessió a un altre compte, mantenint els existents connectats"
}
}
}

View File

@ -0,0 +1,10 @@
{
"title": "Selecciona origen multimèdia",
"message": "Les dades multimèdia EXIF no s'han penjat",
"options": {
"image": "Penja fotos",
"image_max": "Penja fotos (màx. {{max}})",
"video": "Penja vídeo",
"video_max": "Penja vídeo (màx. {{max}})"
}
}

View File

@ -0,0 +1,8 @@
{
"HTML": {
"accessibilityHint": "Prem per expandir o contraure el contingut",
"expanded": "{{hint}}{{moreLines}}",
"moreLines": " ({{count}} línies més)",
"defaultHint": "Publicació llarga"
}
}

View File

@ -0,0 +1,16 @@
{
"follow": {
"function": "Segueix l'usuari"
},
"block": {
"function": "Bloqueja l'usuari"
},
"button": {
"error": "Error en carregar",
"blocked_by": "Bloquejat per l'usuari",
"blocking": "Desbloquejar",
"following": "Deixa de seguir",
"requested": "Retirar la sol·licitud",
"default": "Segueix"
}
}

View File

@ -0,0 +1,152 @@
{
"empty": {
"error": {
"message": "Error en carregar",
"button": "Torna-ho a provar"
},
"success": {
"message": "Cronologia buida"
}
},
"end": {
"message": "Ja estàs, vols una tassa de <0 />?"
},
"lookback": {
"message": "Llegit a"
},
"refresh": {
"fetchPreviousPage": "Més recent des d'aquí",
"refetch": "A l'últim"
},
"shared": {
"actioned": {
"pinned": "Fixat",
"favourite": "{{name}} ha marcat la teva publicació com a favorita",
"status": "{{name}} ha publicat",
"follow": "{{name}} et segueix",
"follow_request": "{{name}} ha sol·licitat seguir-te",
"poll": "S'ha acabat una enquesta en què havies participat",
"reblog": {
"default": "{{name}} ha impulsat",
"notification": "{{name}} ha impulsat la teva publicació"
},
"update": "L'impuls ha sigut editat"
},
"actions": {
"reply": {
"accessibilityLabel": "Respon a aquesta publicació"
},
"reblogged": {
"accessibilityLabel": "Impulsa aquesta publicació",
"function": "Impulsa la publicació",
"options": {
"title": "Escull la visibilitat de l'impuls",
"public": "Impuls públic",
"unlisted": "Impuls no llistat"
}
},
"favourited": {
"accessibilityLabel": "Afegeix aquesta publicació a favorits",
"function": "Marca la publicació com a favorita"
},
"bookmarked": {
"accessibilityLabel": "Afegeix aquesta publicació a marcadors",
"function": "Afegeix la publicació a marcadors"
}
},
"actionsUsers": {
"reblogged_by": {
"accessibilityLabel": "{{count}} usuaris han impulsat aquesta publicació",
"accessibilityHint": "Prem per conèixer els usuaris",
"text": "$t(screenTabs:shared.users.statuses.reblogged_by)"
},
"favourited_by": {
"accessibilityLabel": "{{count}} usuaris han marcat com a favorits aquesta publicació",
"accessibilityHint": "Prem per conèixer els usuaris",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
},
"history": {
"accessibilityLabel": "Aquesta publicació ha sigut editada unes {{count}} vegades",
"accessibilityHint": "Prem per veure tot l'historial d'edicions",
"text_one": "{{count}} edició",
"text_other": "{{count}} edicions"
}
},
"attachment": {
"sensitive": {
"button": "Mostra contingut sensible"
},
"unsupported": {
"text": "Error en carregar",
"button": "Prova amb l'enllaç remot"
}
},
"avatar": {
"accessibilityLabel": "Avatar de {{name}}",
"accessibilityHint": "Prem per anar a la pàgina de {{name}}"
},
"content": {
"expandHint": "Contingut ocult"
},
"filtered": "Filtrat: {{phrase}}.",
"fullConversation": "Llegeix conversacions",
"translate": {
"default": "Tradueix",
"succeed": "Traduït per {{provider}} amb {{source}}",
"failed": "Error al traduir",
"source_not_supported": "L'idioma de la publicació no està suportada",
"target_not_supported": "Aquest idioma no està suportat"
},
"header": {
"shared": {
"account": {
"name": {
"accessibilityHint": "Nom d'usuari"
},
"account": {
"accessibilityHint": "Compte d'usuari"
}
},
"application": "Amb {{application}}",
"edited": {
"accessibilityLabel": "Publicació editada"
},
"muted": {
"accessibilityLabel": "Publicació silenciada"
},
"visibility": {
"direct": {
"accessibilityLabel": "La publicació és un missatge directe"
},
"private": {
"accessibilityLabel": "La publicació és visible només per als seguidors"
}
}
},
"conversation": {
"withAccounts": "Amb",
"delete": {
"function": "Esborra el missatge directe"
}
}
},
"poll": {
"meta": {
"button": {
"vote": "Vota",
"refresh": "Actualitza"
},
"count": {
"voters_one": "{{count}} usuari ha votat",
"voters_other": "{{count}} usuaris han votat",
"votes_one": "{{count}} vot",
"votes_other": "{{count}} vots"
},
"expiration": {
"expired": "Votació finalitzada",
"until": "Finalitza <0 />"
}
}
}
}
}

18
src/i18n/ca/index.ts Normal file
View File

@ -0,0 +1,18 @@
export default {
common: require('./common'),
screens: require('./screens'),
screenActions: require('./screens/actions'),
screenAnnouncements: require('./screens/announcements'),
screenCompose: require('./screens/compose'),
screenImageViewer: require('./screens/imageViewer'),
screenTabs: require('./screens/tabs'),
componentContextMenu: require('./components/contextMenu'),
componentEmojis: require('./components/emojis'),
componentInstance: require('./components/instance'),
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentTimeline: require('./components/timeline')
}

18
src/i18n/ca/screens.json Normal file
View File

@ -0,0 +1,18 @@
{
"screenshot": {
"title": "Protecció de la privacitat",
"message": "Si us plau, no revelis la identitat d'altres usuaris, així com el nom d'usuari, avatar, etc. Gràcies!",
"button": "Confirma"
},
"localCorrupt": {
"message": "La sessió ha sigut expirada. Si us plau, torna a iniciar la sessió"
},
"pushError": {
"message": "Error del servei push",
"description": "Si us plau, torna a habilitar les notificacions push a la configuració"
},
"shareError": {
"imageNotSupported": "Format d'imatge {{type}} no suportat",
"videoNotSupported": "Format de vídeo {{type}} no suportat"
}
}

View File

@ -0,0 +1,6 @@
{
"heading": "Comparteix amb...",
"content": {
"select_account": "Selecciona el compte"
}
}

View File

@ -0,0 +1,20 @@
{
"content": {
"altText": {
"heading": "Text alternatiu"
},
"notificationsFilter": {
"heading": "Mostra els tipus de notificació",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Sol·licitud de seguiment",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Publicació d'usuaris subscrits",
"update": "L'impuls ha sigut editat"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"heading": "Avisos",
"content": {
"published": "S'ha publicat <0 />",
"button": {
"read": "Llegit",
"unread": "Marca com a llegit"
}
}
}

View File

@ -0,0 +1,175 @@
{
"heading": {
"left": {
"button": "Cancel·la",
"alert": {
"title": "Voleu cancel·lar l'edició?",
"buttons": {
"save": "Desa l'esborrany",
"delete": "Esborra l'esborrany",
"cancel": "Cancel·la"
}
}
},
"right": {
"button": {
"default": "Publicació",
"conversation": "Envia un missatge directe",
"reply": "Resposta de la publicació",
"deleteEdit": "Publicació",
"edit": "Publicació",
"share": "Publicació"
},
"alert": {
"default": {
"title": "Error en publicar",
"button": "Torna a provar"
},
"removeReply": {
"title": "No s'ha pogut trobar la publicació resposta",
"description": "La publicació resposta és possible que hagi sigut esborrada. Vols eliminar-ho de la teva referència?",
"confirm": "Elimina la referència"
}
}
}
},
"content": {
"root": {
"header": {
"postingAs": "Publicant la publicació com a @{{acct}}@{{domain}}",
"spoilerInput": {
"placeholder": "Missatge d'alerta d'espòiler"
},
"textInput": {
"placeholder": "Què et passa pel cap?",
"keyboardImage": {
"exceedMaximum": {
"title": "S'ha arribat al nombre màxim d'adjunts",
"OK": "$t(common:buttons.OK)"
}
}
}
},
"footer": {
"attachments": {
"sensitive": "Marca els adjunts com a contingut sensible",
"remove": {
"accessibilityLabel": "Esborra l'adjunt afegit, número {{attachment}}"
},
"edit": {
"accessibilityLabel": "Edita l'adjunt afegit, número {{attachment}}"
},
"upload": {
"accessibilityLabel": "Afegeix més adjunts"
}
},
"emojis": {
"accessibilityHint": "Toca per a afegir emojis a la publicació"
},
"poll": {
"option": {
"placeholder": {
"accessibilityLabel": "Resposta {{index}}",
"single": "Resposta única",
"multiple": "Resposta múltiple"
}
},
"quantity": {
"reduce": {
"accessibilityLabel": "Rebaixa el nombre de respostes a {{amount}}",
"accessibilityHint": "S'ha arribat al nombre mínim de respostes, ara mateix n'hi ha {{amount}}"
},
"increase": {
"accessibilityLabel": "Augmentar el nombre de respostes a {{amount}}",
"accessibilityHint": "S'ha arribat al nombre màxim de respostes, ara mateix n'hi ha {{amount}}"
}
},
"multiple": {
"heading": "Tipus d'enquesta",
"options": {
"single": "Elecció única",
"multiple": "Elecció múltiple"
}
},
"expiration": {
"heading": "Caducitat",
"options": {
"300": "5 minuts",
"1800": "30 minuts",
"3600": "1 hora",
"21600": "6 hores",
"86400": "1 dia",
"259200": "3 dies",
"604800": "7 dies"
}
}
}
},
"actions": {
"attachment": {
"accessibilityLabel": "Puja fitxer",
"accessibilityHint": "La funció d'enquesta serà desactivada si hi ha algun fitxer adjunt",
"failed": {
"alert": {
"title": "Ha fallat la pujada",
"button": "Torna-ho a provar"
}
}
},
"poll": {
"accessibilityLabel": "Afegeix una enquesta",
"accessibilityHint": "La funció d'adjuntar fitxers serà desactivada si hi ha una enquesta"
},
"visibility": {
"accessibilityLabel": "La visibilitat de la publicació és {{visibility}}",
"title": "Visibilitat de la publicació",
"options": {
"public": "Públic",
"unlisted": "Sense llistar",
"private": "Només seguidors",
"direct": "Missatge directe"
}
},
"spoiler": {
"accessibilityLabel": "Espòiler"
},
"emoji": {
"accessibilityLabel": "Afegeix un emoji",
"accessibilityHint": "Obre el panell d'emojis, llisca horitzontalment per canviar de pàgina"
}
},
"drafts_one": "Esborrany ({{count}})",
"drafts_other": "Esborranys ({{count}})"
},
"editAttachment": {
"header": {
"title": "Edita el fitxer adjunt",
"right": {
"accessibilityLabel": "Desa canvis",
"failed": {
"title": "Error a l'editar",
"button": "Torna-ho a provar"
}
}
},
"content": {
"altText": {
"heading": "Descriu el contingut per a persones amb discapacitat visual",
"placeholder": "Pots afegir una descripció, també conegut com a text alternatiu, als teus fitxers perquè siguin accessibles a més persones, també aquelles amb discapacitat visual.\n\nLes bones descripcions han de ser concises, però que s'expressin tot el que surt als fitxers amb exactitud per poder entendre el seu context."
},
"imageFocus": "Arrossega el cercle per a canviar el seu punt d'atenció"
}
},
"draftsList": {
"header": {
"title": "Esborrany"
},
"warning": "Els esborranys només estan desats en aquest dispositiu i es poden perdre. No es recomana desar-los durant molt de temps.",
"content": {
"accessibilityHint": "S'ha desat l'esborrany, prem per a editar",
"textEmpty": "Sense contingut"
},
"checkAttachment": "Comprovant els fitxers adjunts al servidor..."
}
}
}

View File

@ -0,0 +1,16 @@
{
"content": {
"actions": {
"accessibilityLabel": "Més accions per aquesta imatge",
"accessibilityHint": "Pots desar o compartir aquesta imatge"
},
"options": {
"save": "Desa la imatge",
"share": "Comparteix la imatge"
},
"save": {
"succeed": "Imatge desada",
"failed": "Error al desar la imatge"
}
}
}

View File

@ -0,0 +1,379 @@
{
"tabs": {
"local": {
"name": "Seguint"
},
"public": {
"name": "",
"segments": {
"left": "Federat",
"right": "Local"
}
},
"notifications": {
"name": "Notificacions"
},
"me": {
"name": "Sobre mi"
}
},
"common": {
"search": {
"accessibilityLabel": "Cerca",
"accessibilityHint": "Cerca per a etiquetes, usuaris o publicacions"
}
},
"notifications": {
"filter": {
"accessibilityLabel": "Filtra",
"accessibilityHint": "Filtra els tipus de notificacions"
}
},
"me": {
"stacks": {
"bookmarks": {
"name": "Marcadors"
},
"conversations": {
"name": "Missatges directes"
},
"favourites": {
"name": "Favorits"
},
"fontSize": {
"name": "Mida de la font de la publicació"
},
"language": {
"name": "Idioma"
},
"list": {
"name": "Llista: {{list}}"
},
"listAccounts": {
"name": "Usuaris a la llista: {{list}}"
},
"listAdd": {
"name": "Afegeix a la llista"
},
"listEdit": {
"name": "Edita els detalls de la llista"
},
"lists": {
"name": "Llistes"
},
"push": {
"name": "Notificacions push"
},
"profile": {
"name": "Edita el teu perfil"
},
"profileName": {
"name": "Edita el nom"
},
"profileNote": {
"name": "Edita la descripció"
},
"profileFields": {
"name": "Editar les metadades"
},
"settings": {
"name": "Configuració de l'aplicació"
},
"webSettings": {
"name": "Més configuracions del compte"
},
"switch": {
"name": "Canvia de compte"
}
},
"fontSize": {
"demo": "<p>Això és una publicació de prova😊. Pots escollir entre moltes opcions<br /><br />Aquesta configuració només afecta el contingut principal de les publicacions, però altres mides de la font.</p>",
"sizes": {
"S": "S",
"M": "M - Per defecte",
"L": "L",
"XL": "XL",
"XXL": "XXL"
}
},
"listAccounts": {
"heading": "Gestiona els usuaris",
"error": "Esborra l'usuari de la llista",
"empty": "Cap usuari afegit en aquesta llista"
},
"listEdit": {
"heading": "Edita els detalls de la llista",
"title": "Títol",
"repliesPolicy": {
"heading": "Mostra respostes a:",
"options": {
"none": "Ningú",
"list": "Membres de la llista",
"followed": "Qualsevol usuari seguit"
}
}
},
"listDelete": {
"heading": "Esborra la llista",
"confirm": {
"title": "Vol esborrar la llista \"{{list}}\"?",
"message": "Aquesta acció no es pot desfer."
}
},
"profile": {
"feedback": {
"succeed": "{{type}} actualitzat",
"failed": "Error a l'actualitzar {{type}}. Si us plau, torna-ho a provar."
},
"root": {
"name": {
"title": "Nom"
},
"avatar": {
"title": "Avatar",
"description": "Es reduirà a 400x400px"
},
"header": {
"title": "Capçalera",
"description": "Es reduirà a 1500x500px"
},
"note": {
"title": "Descripció"
},
"fields": {
"title": "Metadades",
"total_one": "{{count}} camp",
"total_other": "{{count}} camps"
},
"visibility": {
"title": "Visibilitat de la publicació",
"options": {
"public": "Públic",
"unlisted": "Sense llistar",
"private": "Només als seguidors"
}
},
"sensitive": {
"title": "Publica contingut multimèdia sensible"
},
"lock": {
"title": "Fes el compte privat",
"description": "Caldrà l'aprovació manual de seguidors nous"
},
"bot": {
"title": "Compte bot",
"description": "Aquest compte executa principalment accions automatitzades i no podrà ser monitorat"
}
},
"fields": {
"group": "Grup {{index}}",
"label": "Etiqueta",
"content": "Contingut"
},
"mediaSelectionFailed": "Ha fallat el processament d'imatge. Si us plau, torneu-ho a provar."
},
"push": {
"notAvailable": "El seu telèfon no suporta les notificacions push de tooot",
"enable": {
"direct": "Habilita les notificacions push",
"settings": "Activa'ls a la configuració"
},
"global": {
"heading": "Activa per {{acct}}",
"description": "Els missatges s'envien a través del servidor del tooot"
},
"decode": {
"heading": "Mostra els detalls del missatge",
"description": "Els missatges que s'envien a través del servidor del tooot estan encriptades, però pots escollir per desencriptar-los en el servidor. El nostre servidor és de codi obert i tenim una política de zero registres."
},
"default": {
"heading": "Per defecte"
},
"follow": {
"heading": "Nou seguidor"
},
"follow_request": {
"heading": "Sol·licitud de seguiment"
},
"favourite": {
"heading": "Favorits"
},
"reblog": {
"heading": "Impulsat"
},
"mention": {
"heading": "T'ha mencionat"
},
"poll": {
"heading": "Actualització d'una votació"
},
"status": {
"heading": "Publicació d'usuaris subscrits"
},
"howitworks": "Aprèn com funciona"
},
"root": {
"announcements": {
"content": {
"unread": "{{amount}} sense llegir",
"read": "Tots llegits",
"empty": "Cap"
}
},
"push": {
"content": {
"enabled": "Habilitat",
"disabled": "Deshabilitat"
}
},
"update": {
"title": "Actualitza a la última versió"
},
"logout": {
"button": "Tanca la sessió",
"alert": {
"title": "Vol tancar la sessió?",
"message": "Després de tancar la sessió, hauràs de tornar a iniciar la sessió",
"buttons": {
"logout": "Tanca la sessió"
}
}
}
},
"settings": {
"fontsize": {
"heading": "$t(me.stacks.fontSize.name)",
"content": {
"S": "$t(me.fontSize.sizes.S)",
"M": "$t(me.fontSize.sizes.M)",
"L": "$t(me.fontSize.sizes.L)",
"XL": "$t(me.fontSize.sizes.XL)",
"XXL": "$t(me.fontSize.sizes.XXL)"
}
},
"language": {
"heading": "$t(me.stacks.language.name)"
},
"theme": {
"heading": "Aspecte",
"options": {
"auto": "Com el sistema",
"light": "Mode clar",
"dark": "Mode fosc"
}
},
"darkTheme": {
"heading": "Tema fosc",
"options": {
"lighter": "Més clar",
"darker": "Més fosc"
}
},
"browser": {
"heading": "Obertura d'enllaços",
"options": {
"internal": "Dins de l'aplicació",
"external": "Utilitza el navegador del sistema"
}
},
"staticEmoji": {
"heading": "Utilitza emojis estàtics",
"description": "Si l'aplicació falla freqüentment en visualitzar la llista d'emojis, pots intentar fer servir els emojis estàtics."
},
"feedback": {
"heading": "Peticions de característiques"
},
"support": {
"heading": "Dona suport al tooot"
},
"review": {
"heading": "Valora al tooot"
},
"contact": {
"heading": "Contacta al tooot"
},
"version": "Versió v{{version}}",
"instanceVersion": "Versió del Mastodon v{{version}}"
},
"switch": {
"existing": "Escull la sessió",
"new": "Inicia la sessió a la instància"
}
},
"shared": {
"account": {
"actions": {
"accessibilityLabel": "Accions per l'usuari {{user}}",
"accessibilityHint": "Pots silenciar, bloquejar, reportar o compartir aquest usuari"
},
"followed_by": " et segueix",
"moved": "S'ha traslladat",
"created_at": "Es va unir el dia {{date}}",
"summary": {
"statuses_count": "{{count}} publicacions",
"following_count": "$t(shared.users.accounts.following)",
"followers_count": "$t(shared.users.accounts.followers)"
},
"toots": {
"default": "Publicacions",
"all": "Publicacions i respostes"
},
"suspended": "Compte suspès pels moderadors del teu servidor"
},
"accountInLists": {
"name": "Llistes de @{{username}}",
"inLists": "En les llistes",
"notInLists": "Altres llistes"
},
"attachments": {
"name": "Multimèdia de <0 /><1></1>"
},
"hashtag": {
"follow": "Segueix",
"unfollow": "Deixa de seguir"
},
"history": {
"name": "Edita l'historial"
},
"search": {
"header": {
"prefix": "Cercant",
"placeholder": "alguna cosa..."
},
"empty": {
"general": "Escriu per cercar <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold> o <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
"advanced": {
"header": "Cerca avançada",
"example": {
"account": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)",
"hashtag": "$t(shared.search.header.prefix) $t(shared.search.sections.hashtags)",
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
}
},
"trending": {
"tags": ""
}
},
"sections": {
"accounts": "Usuari",
"hashtags": "Etiqueta",
"statuses": "Publicació"
},
"notFound": "No s'ha trobat <bold>{{searchTerm}}</bold> relacionat {{type}}"
},
"toot": {
"name": "Discussions"
},
"users": {
"accounts": {
"following": "{{count}} seguits",
"followers": "{{count}} seguidors"
},
"statuses": {
"reblogged_by": "{{count}} impulsats",
"favourited_by": "{{count}} favorits"
}
}
}
}

30
src/i18n/cs/common.json Normal file
View File

@ -0,0 +1,30 @@
{
"buttons": {
"OK": "",
"apply": "",
"cancel": "",
"discard": "",
"continue": "",
"delete": "",
"done": ""
},
"customEmoji": {
"accessibilityLabel": ""
},
"message": {
"success": {
"message": ""
},
"warning": {
"message": ""
},
"error": {
"message": ""
}
},
"separator": "",
"discard": {
"title": "",
"message": ""
}
}

View File

@ -0,0 +1,81 @@
{
"accessibilityHint": "",
"account": {
"title": "",
"following": {
"action_false": "",
"action_true": ""
},
"inLists": "",
"mute": {
"action_false": "",
"action_true": ""
},
"block": {
"action_false": "",
"action_true": ""
},
"reports": {
"action": ""
}
},
"copy": {
"action": "",
"succeed": ""
},
"instance": {
"title": "",
"block": {
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
}
}
},
"share": {
"status": {
"action": ""
},
"account": {
"action": ""
}
},
"status": {
"title": "",
"edit": {
"action": ""
},
"delete": {
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
}
},
"deleteEdit": {
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
}
},
"mute": {
"action_false": "",
"action_true": ""
},
"pin": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,26 @@
{
"server": {
"textInput": {
"placeholder": ""
},
"button": "",
"information": {
"name": "",
"accounts": "",
"statuses": "",
"domains": ""
},
"disclaimer": {
"base": ""
},
"terms": {
"base": ""
}
},
"update": {
"alert": {
"title": "",
"message": ""
}
}
}

View File

@ -0,0 +1,10 @@
{
"title": "",
"message": "",
"options": {
"image": "",
"image_max": "",
"video": "",
"video_max": ""
}
}

View File

@ -0,0 +1,8 @@
{
"HTML": {
"accessibilityHint": "",
"expanded": "",
"moreLines": "",
"defaultHint": ""
}
}

View File

@ -0,0 +1,16 @@
{
"follow": {
"function": ""
},
"block": {
"function": ""
},
"button": {
"error": "",
"blocked_by": "",
"blocking": "",
"following": "",
"requested": "",
"default": ""
}
}

View File

@ -0,0 +1,152 @@
{
"empty": {
"error": {
"message": "",
"button": ""
},
"success": {
"message": ""
}
},
"end": {
"message": ""
},
"lookback": {
"message": ""
},
"refresh": {
"fetchPreviousPage": "",
"refetch": ""
},
"shared": {
"actioned": {
"pinned": "",
"favourite": "",
"status": "",
"follow": "",
"follow_request": "",
"poll": "",
"reblog": {
"default": "",
"notification": ""
},
"update": ""
},
"actions": {
"reply": {
"accessibilityLabel": ""
},
"reblogged": {
"accessibilityLabel": "",
"function": "",
"options": {
"title": "",
"public": "",
"unlisted": ""
}
},
"favourited": {
"accessibilityLabel": "",
"function": ""
},
"bookmarked": {
"accessibilityLabel": "",
"function": ""
}
},
"actionsUsers": {
"reblogged_by": {
"accessibilityLabel": "",
"accessibilityHint": "",
"text": ""
},
"favourited_by": {
"accessibilityLabel": "",
"accessibilityHint": "",
"text": ""
},
"history": {
"accessibilityLabel": "",
"accessibilityHint": "",
"text_one": "",
"text_other": ""
}
},
"attachment": {
"sensitive": {
"button": ""
},
"unsupported": {
"text": "",
"button": ""
}
},
"avatar": {
"accessibilityLabel": "",
"accessibilityHint": ""
},
"content": {
"expandHint": ""
},
"filtered": "",
"fullConversation": "",
"translate": {
"default": "",
"succeed": "",
"failed": "",
"source_not_supported": "",
"target_not_supported": ""
},
"header": {
"shared": {
"account": {
"name": {
"accessibilityHint": ""
},
"account": {
"accessibilityHint": ""
}
},
"application": "",
"edited": {
"accessibilityLabel": ""
},
"muted": {
"accessibilityLabel": ""
},
"visibility": {
"direct": {
"accessibilityLabel": ""
},
"private": {
"accessibilityLabel": ""
}
}
},
"conversation": {
"withAccounts": "",
"delete": {
"function": ""
}
}
},
"poll": {
"meta": {
"button": {
"vote": "",
"refresh": ""
},
"count": {
"voters_one": "",
"voters_other": "",
"votes_one": "",
"votes_other": ""
},
"expiration": {
"expired": "",
"until": ""
}
}
}
}
}

18
src/i18n/cs/screens.json Normal file
View File

@ -0,0 +1,18 @@
{
"screenshot": {
"title": "",
"message": "",
"button": ""
},
"localCorrupt": {
"message": ""
},
"pushError": {
"message": "",
"description": ""
},
"shareError": {
"imageNotSupported": "",
"videoNotSupported": ""
}
}

View File

@ -0,0 +1,6 @@
{
"heading": "",
"content": {
"select_account": ""
}
}

View File

@ -0,0 +1,20 @@
{
"content": {
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "",
"content": {
"follow": "",
"follow_request": "",
"favourite": "",
"reblog": "",
"mention": "",
"poll": "",
"status": "",
"update": ""
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"heading": "",
"content": {
"published": "",
"button": {
"read": "",
"unread": ""
}
}
}

Some files were not shown because too many files have changed in this diff Show More