mirror of https://github.com/tooot-app/app
commit
5a4af08751
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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']
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
|
@ -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,
|
||||
|
|
197
ios/Podfile.lock
197
ios/Podfile.lock
|
@ -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
|
||||
|
|
|
@ -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";
|
|
@ -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";
|
|
@ -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>";
|
||||
|
|
|
@ -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>
|
59
package.json
59
package.json
|
@ -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",
|
||||
|
|
|
@ -274,6 +274,7 @@ declare namespace Mastodon {
|
|||
type List = {
|
||||
id: string
|
||||
title: string
|
||||
replies_policy: 'none' | 'list' | 'followed'
|
||||
}
|
||||
|
||||
type Instance = {
|
||||
|
|
26
src/App.tsx
26
src/App.tsx
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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={{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
type ContextMenu = {
|
||||
key: string
|
||||
item: { onSelect: () => void; disabled: boolean; destructive: boolean; hidden: boolean }
|
||||
title: string
|
||||
icon: string
|
||||
}
|
|
@ -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':
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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?"
|
||||
}
|
||||
}
|
|
@ -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ó"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"frequentUsed": "D'ús freqüent"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}})"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 />"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"heading": "Comparteix amb...",
|
||||
"content": {
|
||||
"select_account": "Selecciona el compte"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"heading": "Avisos",
|
||||
"content": {
|
||||
"published": "S'ha publicat <0 />",
|
||||
"button": {
|
||||
"read": "Llegit",
|
||||
"unread": "Marca com a llegit"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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..."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"server": {
|
||||
"textInput": {
|
||||
"placeholder": ""
|
||||
},
|
||||
"button": "",
|
||||
"information": {
|
||||
"name": "",
|
||||
"accounts": "",
|
||||
"statuses": "",
|
||||
"domains": ""
|
||||
},
|
||||
"disclaimer": {
|
||||
"base": ""
|
||||
},
|
||||
"terms": {
|
||||
"base": ""
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"alert": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"title": "",
|
||||
"message": "",
|
||||
"options": {
|
||||
"image": "",
|
||||
"image_max": "",
|
||||
"video": "",
|
||||
"video_max": ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"HTML": {
|
||||
"accessibilityHint": "",
|
||||
"expanded": "",
|
||||
"moreLines": "",
|
||||
"defaultHint": ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"follow": {
|
||||
"function": ""
|
||||
},
|
||||
"block": {
|
||||
"function": ""
|
||||
},
|
||||
"button": {
|
||||
"error": "",
|
||||
"blocked_by": "",
|
||||
"blocking": "",
|
||||
"following": "",
|
||||
"requested": "",
|
||||
"default": ""
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"screenshot": {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"button": ""
|
||||
},
|
||||
"localCorrupt": {
|
||||
"message": ""
|
||||
},
|
||||
"pushError": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"shareError": {
|
||||
"imageNotSupported": "",
|
||||
"videoNotSupported": ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"heading": "",
|
||||
"content": {
|
||||
"select_account": ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"content": {
|
||||
"altText": {
|
||||
"heading": ""
|
||||
},
|
||||
"notificationsFilter": {
|
||||
"heading": "",
|
||||
"content": {
|
||||
"follow": "",
|
||||
"follow_request": "",
|
||||
"favourite": "",
|
||||
"reblog": "",
|
||||
"mention": "",
|
||||
"poll": "",
|
||||
"status": "",
|
||||
"update": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue