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
|
## Special thanks
|
||||||
|
|
||||||
[@amrtf](https://crowdin.com/profile/amrtf) for Spanish translation
|
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
||||||
|
|
||||||
[@pat](https://piaille.fr/@pat) for French translation
|
|
||||||
|
|
||||||
[@forenta](https://github.com/forenta) for German 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
|
[@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
|
[@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
|
[@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
|
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
||||||
|
|
||||||
[@janlindblom](https://github.com/janlindblom) for Swedish
|
[@janlindblom](https://github.com/janlindblom) for Swedish
|
||||||
|
|
|
@ -337,5 +337,3 @@ def isNewArchitectureEnabled() {
|
||||||
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
||||||
return project.hasProperty("newArchEnabled") && 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()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.gms:google-services:4.3.3'
|
|
||||||
classpath("com.android.tools.build:gradle:7.2.1")
|
classpath("com.android.tools.build:gradle:7.2.1")
|
||||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||||
|
|
|
@ -15,7 +15,6 @@ export default (): ExpoConfig => ({
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
package: 'com.xmflsct.app.tooot',
|
package: 'com.xmflsct.app.tooot',
|
||||||
googleServicesFile: './configs/google-services.json',
|
|
||||||
permissions: ['CAMERA', 'VIBRATE'],
|
permissions: ['CAMERA', 'VIBRATE'],
|
||||||
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
|
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 change depending on the env values.
|
||||||
flags = get_default_flags()
|
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!(
|
use_react_native!(
|
||||||
:path => config[:reactNativePath],
|
:path => config[:reactNativePath],
|
||||||
:hermes_enabled => true,
|
:hermes_enabled => true,
|
||||||
|
|
197
ios/Podfile.lock
197
ios/Podfile.lock
|
@ -3,7 +3,7 @@ PODS:
|
||||||
- DoubleConversion (1.1.6)
|
- DoubleConversion (1.1.6)
|
||||||
- EXApplication (5.0.1):
|
- EXApplication (5.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXAV (13.0.1):
|
- EXAV (13.0.2):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- EXConstants (14.0.2):
|
- EXConstants (14.0.2):
|
||||||
|
@ -12,18 +12,11 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXFileSystem (15.1.1):
|
- EXFileSystem (15.1.1):
|
||||||
- ExpoModulesCore
|
- 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):
|
- EXFont (11.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXNotifications (0.17.0):
|
- EXNotifications (0.17.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- Expo (47.0.7):
|
- Expo (47.0.8):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoCrypto (12.0.0):
|
- ExpoCrypto (12.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
@ -59,107 +52,8 @@ PODS:
|
||||||
- React-Core (= 0.70.6)
|
- React-Core (= 0.70.6)
|
||||||
- React-jsi (= 0.70.6)
|
- React-jsi (= 0.70.6)
|
||||||
- ReactCommon/turbomodule/core (= 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)
|
- fmt (6.2.1)
|
||||||
- glog (0.3.5)
|
- 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)
|
- hermes-engine (0.70.6)
|
||||||
- libevent (2.1.12)
|
- libevent (2.1.12)
|
||||||
- libwebp (1.2.4):
|
- libwebp (1.2.4):
|
||||||
|
@ -171,12 +65,6 @@ PODS:
|
||||||
- libwebp/mux (1.2.4):
|
- libwebp/mux (1.2.4):
|
||||||
- libwebp/demux
|
- libwebp/demux
|
||||||
- libwebp/webp (1.2.4)
|
- 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):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
|
@ -409,17 +297,19 @@ PODS:
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-cameraroll (5.1.0):
|
- react-native-cameraroll (5.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-context-menu-view (1.5.4):
|
- react-native-image-picker (4.10.2):
|
||||||
- React
|
- React-Core
|
||||||
- react-native-image-picker (4.10.1):
|
- react-native-ios-context-menu (1.15.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-language-detection (0.1.0):
|
- react-native-language-detection (0.1.0):
|
||||||
- React
|
- React
|
||||||
- react-native-live-text-image-view (0.4.0):
|
- react-native-live-text-image-view (0.4.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-netinfo (9.3.6):
|
- react-native-menu (0.7.2):
|
||||||
|
- React
|
||||||
|
- react-native-netinfo (9.3.7):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-pager-view (6.1.1):
|
- react-native-pager-view (6.1.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-paste-input (0.5.1):
|
- react-native-paste-input (0.5.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -538,9 +428,9 @@ PODS:
|
||||||
- RNScreens (3.18.2):
|
- RNScreens (3.18.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTImage
|
- React-RCTImage
|
||||||
- RNSentry (4.8.0):
|
- RNSentry (4.10.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- Sentry (= 7.29.0)
|
- Sentry/HybridSDK (= 7.31.2)
|
||||||
- RNShareMenu (6.0.0):
|
- RNShareMenu (6.0.0):
|
||||||
- React
|
- React
|
||||||
- RNSVG (13.6.0):
|
- RNSVG (13.6.0):
|
||||||
|
@ -551,9 +441,7 @@ PODS:
|
||||||
- SDWebImageWebPCoder (0.9.1):
|
- SDWebImageWebPCoder (0.9.1):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.13)
|
- SDWebImage/Core (~> 5.13)
|
||||||
- Sentry (7.29.0):
|
- Sentry/HybridSDK (7.31.2)
|
||||||
- Sentry/Core (= 7.29.0)
|
|
||||||
- Sentry/Core (7.29.0)
|
|
||||||
- Swime (3.0.6)
|
- Swime (3.0.6)
|
||||||
- Yoga (1.14.0)
|
- Yoga (1.14.0)
|
||||||
|
|
||||||
|
@ -565,8 +453,6 @@ DEPENDENCIES:
|
||||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||||
- EXErrorRecovery (from `../node_modules/expo-error-recovery/ios`)
|
- EXErrorRecovery (from `../node_modules/expo-error-recovery/ios`)
|
||||||
- EXFileSystem (from `../node_modules/expo-file-system/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`)
|
- EXFont (from `../node_modules/expo-font/ios`)
|
||||||
- EXNotifications (from `../node_modules/expo-notifications/ios`)
|
- EXNotifications (from `../node_modules/expo-notifications/ios`)
|
||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
|
@ -584,10 +470,7 @@ DEPENDENCIES:
|
||||||
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
|
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
|
||||||
- Firebase
|
|
||||||
- FirebaseCore
|
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
- GoogleUtilities
|
|
||||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes/hermes-engine.podspec`)
|
- hermes-engine (from `../node_modules/react-native/sdks/hermes/hermes-engine.podspec`)
|
||||||
- libevent (~> 2.1.12)
|
- libevent (~> 2.1.12)
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- 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-blur (from `../node_modules/@react-native-community/blur`)"
|
||||||
- react-native-blurhash (from `../node_modules/react-native-blurhash`)
|
- react-native-blurhash (from `../node_modules/react-native-blurhash`)
|
||||||
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
|
- "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-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-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-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-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||||
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||||
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
|
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
|
||||||
|
@ -643,20 +527,9 @@ DEPENDENCIES:
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- Firebase
|
|
||||||
- FirebaseAnalytics
|
|
||||||
- FirebaseCore
|
|
||||||
- FirebaseCoreDiagnostics
|
|
||||||
- FirebaseCoreInternal
|
|
||||||
- FirebaseInstallations
|
|
||||||
- fmt
|
- fmt
|
||||||
- GoogleAppMeasurement
|
|
||||||
- GoogleDataTransport
|
|
||||||
- GoogleUtilities
|
|
||||||
- libevent
|
- libevent
|
||||||
- libwebp
|
- libwebp
|
||||||
- nanopb
|
|
||||||
- PromisesObjC
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
- Sentry
|
- Sentry
|
||||||
|
@ -677,10 +550,6 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-error-recovery/ios"
|
:path: "../node_modules/expo-error-recovery/ios"
|
||||||
EXFileSystem:
|
EXFileSystem:
|
||||||
:path: "../node_modules/expo-file-system/ios"
|
:path: "../node_modules/expo-file-system/ios"
|
||||||
EXFirebaseAnalytics:
|
|
||||||
:path: "../node_modules/expo-firebase-analytics/ios"
|
|
||||||
EXFirebaseCore:
|
|
||||||
:path: "../node_modules/expo-firebase-core/ios"
|
|
||||||
EXFont:
|
EXFont:
|
||||||
:path: "../node_modules/expo-font/ios"
|
:path: "../node_modules/expo-font/ios"
|
||||||
EXNotifications:
|
EXNotifications:
|
||||||
|
@ -755,14 +624,16 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native-blurhash"
|
:path: "../node_modules/react-native-blurhash"
|
||||||
react-native-cameraroll:
|
react-native-cameraroll:
|
||||||
:path: "../node_modules/@react-native-camera-roll/camera-roll"
|
: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:
|
react-native-image-picker:
|
||||||
:path: "../node_modules/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:
|
react-native-language-detection:
|
||||||
:path: "../node_modules/react-native-language-detection"
|
:path: "../node_modules/react-native-language-detection"
|
||||||
react-native-live-text-image-view:
|
react-native-live-text-image-view:
|
||||||
:path: "../node_modules/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:
|
react-native-netinfo:
|
||||||
:path: "../node_modules/@react-native-community/netinfo"
|
:path: "../node_modules/@react-native-community/netinfo"
|
||||||
react-native-pager-view:
|
react-native-pager-view:
|
||||||
|
@ -822,15 +693,13 @@ SPEC CHECKSUMS:
|
||||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||||
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
|
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
|
||||||
EXAV: 766516466675fc5fdd7c500acced5934e8b00de2
|
EXAV: 9a45d37772c5329294c054a041dcc39931fc5032
|
||||||
EXConstants: 3c86653c422dd77e40d10cbbabb3025003977415
|
EXConstants: 3c86653c422dd77e40d10cbbabb3025003977415
|
||||||
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
|
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
|
||||||
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
||||||
EXFirebaseAnalytics: 58d70e698859b070b2450ad8664d7b5bc6c6e3e1
|
|
||||||
EXFirebaseCore: d0d88cb904e893af07f809ab08c0892489bc6956
|
|
||||||
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
||||||
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
|
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
|
||||||
Expo: a37d568e9ae87645b74ed597dd0f0fd89e2daf2d
|
Expo: 36b5f625d36728adbdd1934d4d57182f319ab832
|
||||||
ExpoCrypto: 51e7662c7f5bfeab25b7909b8a5d545ec15d4877
|
ExpoCrypto: 51e7662c7f5bfeab25b7909b8a5d545ec15d4877
|
||||||
ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f
|
ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f
|
||||||
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
||||||
|
@ -845,22 +714,11 @@ SPEC CHECKSUMS:
|
||||||
EXVideoThumbnails: 8b3e48f3716679dd0cbf949217a31eab5c555799
|
EXVideoThumbnails: 8b3e48f3716679dd0cbf949217a31eab5c555799
|
||||||
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
|
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
|
||||||
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
|
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
|
||||||
Firebase: 800f16f07af493d98d017446a315c27af0552f41
|
|
||||||
FirebaseAnalytics: 1b60984a408320dda637306f3f733699ef8473d7
|
|
||||||
FirebaseCore: 25c0400b670fd1e2f2104349cd3b5dcce8d9418f
|
|
||||||
FirebaseCoreDiagnostics: 99a495094b10a57eeb3ae8efa1665700ad0bdaa6
|
|
||||||
FirebaseCoreInternal: bca76517fe1ed381e989f5e7d8abb0da8d85bed3
|
|
||||||
FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
|
|
||||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||||
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
||||||
GoogleAppMeasurement: 6ee231473fbd75c11221dfce489894334024eead
|
|
||||||
GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f
|
|
||||||
GoogleUtilities: bad72cb363809015b1f7f19beb1f1cd23c589f95
|
|
||||||
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
|
||||||
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
|
|
||||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||||
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
||||||
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
||||||
|
@ -879,12 +737,13 @@ SPEC CHECKSUMS:
|
||||||
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
||||||
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
|
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
|
||||||
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
|
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
|
||||||
react-native-context-menu-view: b0beca02aad4bd9f9d7d932bf437e0a03baa69ef
|
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
|
||||||
react-native-image-picker: f2ab1215d17bcfe27b0eb6417cc236fd1f4775e7
|
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
||||||
react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d
|
react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d
|
||||||
react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c
|
react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c
|
||||||
react-native-netinfo: f80db8cac2151405633324cb645c60af098ee461
|
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
||||||
react-native-pager-view: 3c66c4e2f3ab423643d07b2c7041f8ac48395f72
|
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||||
|
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
||||||
react-native-paste-input: 183ad7dc224e192719616f4258dde5b548627d08
|
react-native-paste-input: 183ad7dc224e192719616f4258dde5b548627d08
|
||||||
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
|
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
|
||||||
react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097
|
react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097
|
||||||
|
@ -906,15 +765,15 @@ SPEC CHECKSUMS:
|
||||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||||
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
|
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
|
||||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||||
RNSentry: db7fd7b66efda28885e4e904a8b5e7349aec61c1
|
RNSentry: 3c27f3c57f16bab9835d9555add298571077e0c1
|
||||||
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
||||||
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
|
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
|
||||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||||
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
|
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
|
||||||
Sentry: 4272663eb0eda312024d795ca3f5a562a8ce5e18
|
Sentry: b15765d11769852fe78c9add942f7df60ed5dbf5
|
||||||
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
||||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||||
|
|
||||||
PODFILE CHECKSUM: e4191b63c8f15031b2365226730770e7978dca41
|
PODFILE CHECKSUM: 05bf71d31ba782dfda5a6b47d38e98a6f6bc079a
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
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 */; };
|
5EE088C926297820007E5FEC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5EE088CB26297820007E5FEC /* InfoPlist.strings */; };
|
||||||
5EE44DD62600124E00A9BCED /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE44DD52600124E00A9BCED /* File.swift */; };
|
5EE44DD62600124E00A9BCED /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE44DD52600124E00A9BCED /* File.swift */; };
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-tooot.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-tooot.a */; };
|
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 */; };
|
E3BC22F5F8ABE515E14CF199 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D878F932AF7A9974E06E461 /* ExpoModulesProvider.swift */; };
|
||||||
E613A80B28282A01003C97D6 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E613A80A28282A01003C97D6 /* AppDelegate.mm */; };
|
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, ); }; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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 */,
|
13B07FB71A68108700A75B9A /* main.m */,
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */,
|
5E36538225C9B8BD009F93EE /* RootViewColor.xcassets */,
|
||||||
B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */,
|
|
||||||
5EE088CB26297820007E5FEC /* InfoPlist.strings */,
|
5EE088CB26297820007E5FEC /* InfoPlist.strings */,
|
||||||
DF8133F098604A10B0D94952 /* boop.mp3 */,
|
DF8133F098604A10B0D94952 /* boop.mp3 */,
|
||||||
);
|
);
|
||||||
|
@ -297,6 +296,8 @@
|
||||||
fr,
|
fr,
|
||||||
es,
|
es,
|
||||||
sv,
|
sv,
|
||||||
|
nl,
|
||||||
|
ca,
|
||||||
);
|
);
|
||||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||||
|
@ -319,7 +320,6 @@
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */,
|
|
||||||
4986628FD0DD4630BFE5F388 /* boop.mp3 in Resources */,
|
4986628FD0DD4630BFE5F388 /* boop.mp3 in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -528,6 +528,8 @@
|
||||||
E66C0842291F095800DFFF60 /* fr */,
|
E66C0842291F095800DFFF60 /* fr */,
|
||||||
E690AF692926B737002C38A8 /* es */,
|
E690AF692926B737002C38A8 /* es */,
|
||||||
E63E7FF0292A828100C76FD4 /* sv */,
|
E63E7FF0292A828100C76FD4 /* sv */,
|
||||||
|
E6217B7E293C1EBF00B1755E /* nl */,
|
||||||
|
E6A4895D293C1F740047951A /* ca */,
|
||||||
);
|
);
|
||||||
name = InfoPlist.strings;
|
name = InfoPlist.strings;
|
||||||
sourceTree = "<group>";
|
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",
|
"name": "tooot",
|
||||||
"version": "4.6.4",
|
"version": "4.6.5",
|
||||||
"description": "tooot for Mastodon",
|
"description": "tooot for Mastodon",
|
||||||
"author": "xmflsct <me@xmflsct.com>",
|
"author": "xmflsct <me@xmflsct.com>",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
|
@ -19,35 +19,35 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/react-native-action-sheet": "^4.0.1",
|
"@expo/react-native-action-sheet": "^4.0.1",
|
||||||
"@formatjs/intl-datetimeformat": "^6.3.1",
|
"@formatjs/intl-datetimeformat": "^6.4.3",
|
||||||
"@formatjs/intl-getcanonicallocales": "^2.0.4",
|
"@formatjs/intl-getcanonicallocales": "^2.0.5",
|
||||||
"@formatjs/intl-locale": "^3.0.7",
|
"@formatjs/intl-locale": "^3.0.11",
|
||||||
"@formatjs/intl-numberformat": "^8.2.0",
|
"@formatjs/intl-numberformat": "^8.3.3",
|
||||||
"@formatjs/intl-pluralrules": "^5.1.4",
|
"@formatjs/intl-pluralrules": "^5.1.8",
|
||||||
"@formatjs/intl-relativetimeformat": "^11.1.4",
|
"@formatjs/intl-relativetimeformat": "^11.1.8",
|
||||||
"@mattermost/react-native-paste-input": "^0.5.1",
|
"@mattermost/react-native-paste-input": "^0.5.1",
|
||||||
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
|
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
|
||||||
"@react-native-async-storage/async-storage": "~1.17.11",
|
"@react-native-async-storage/async-storage": "~1.17.11",
|
||||||
"@react-native-camera-roll/camera-roll": "^5.1.0",
|
"@react-native-camera-roll/camera-roll": "^5.1.0",
|
||||||
"@react-native-clipboard/clipboard": "^1.11.1",
|
"@react-native-clipboard/clipboard": "^1.11.1",
|
||||||
"@react-native-community/blur": "^4.3.0",
|
"@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-native-community/segmented-control": "^2.2.2",
|
||||||
"@react-navigation/bottom-tabs": "^6.4.1",
|
"@react-native-menu/menu": "^0.7.2",
|
||||||
"@react-navigation/native": "^6.0.14",
|
"@react-navigation/bottom-tabs": "^6.4.3",
|
||||||
"@react-navigation/native-stack": "^6.9.2",
|
"@react-navigation/native": "^6.0.16",
|
||||||
"@react-navigation/stack": "^6.3.5",
|
"@react-navigation/native-stack": "^6.9.4",
|
||||||
"@reduxjs/toolkit": "^1.9.0",
|
"@react-navigation/stack": "^6.3.7",
|
||||||
"@sentry/react-native": "4.8.0",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"@sharcoux/slider": "^6.0.3",
|
"@sentry/react-native": "4.10.1",
|
||||||
|
"@sharcoux/slider": "^6.1.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"expo": "^47.0.7",
|
"expo": "^47.0.8",
|
||||||
"expo-auth-session": "^3.7.2",
|
"expo-auth-session": "^3.7.3",
|
||||||
"expo-av": "^13.0.1",
|
"expo-av": "^13.0.2",
|
||||||
"expo-constants": "^14.0.2",
|
"expo-constants": "^14.0.2",
|
||||||
"expo-crypto": "^12.0.0",
|
"expo-crypto": "^12.0.0",
|
||||||
"expo-file-system": "^15.1.1",
|
"expo-file-system": "^15.1.1",
|
||||||
"expo-firebase-analytics": "^8.0.0",
|
|
||||||
"expo-haptics": "^12.0.1",
|
"expo-haptics": "^12.0.1",
|
||||||
"expo-linking": "^3.2.3",
|
"expo-linking": "^3.2.3",
|
||||||
"expo-localization": "^14.0.0",
|
"expo-localization": "^14.0.0",
|
||||||
|
@ -66,21 +66,21 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^12.0.0",
|
||||||
"react-intl": "^6.2.1",
|
"react-intl": "^6.2.5",
|
||||||
"react-native": "0.70.6",
|
"react-native": "0.70.6",
|
||||||
"react-native-animated-spinkit": "^1.5.2",
|
"react-native-animated-spinkit": "^1.5.2",
|
||||||
"react-native-base64": "^0.2.1",
|
"react-native-base64": "^0.2.1",
|
||||||
"react-native-blurhash": "^1.1.10",
|
"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-fast-image": "^8.6.3",
|
||||||
"react-native-feather": "^1.1.2",
|
"react-native-feather": "^1.1.2",
|
||||||
"react-native-flash-message": "^0.3.1",
|
"react-native-flash-message": "^0.3.1",
|
||||||
"react-native-gesture-handler": "~2.8.0",
|
"react-native-gesture-handler": "~2.8.0",
|
||||||
"react-native-htmlview": "^0.16.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-language-detection": "^0.1.0",
|
||||||
"react-native-live-text-image-view": "^0.4.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": "^2.13.0",
|
||||||
"react-native-reanimated-zoom": "^0.3.3",
|
"react-native-reanimated-zoom": "^0.3.3",
|
||||||
"react-native-safe-area-context": "^4.4.1",
|
"react-native-safe-area-context": "^4.4.1",
|
||||||
|
@ -88,24 +88,25 @@
|
||||||
"react-native-share-menu": "^6.0.0",
|
"react-native-share-menu": "^6.0.0",
|
||||||
"react-native-svg": "^13.6.0",
|
"react-native-svg": "^13.6.0",
|
||||||
"react-native-swipe-list-view": "^3.2.9",
|
"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-query": "^3.39.2",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"rn-placeholder": "^3.0.3",
|
"rn-placeholder": "^3.0.3",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9",
|
||||||
|
"zeego": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.2",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@expo/config": "^7.0.3",
|
"@expo/config": "^7.0.3",
|
||||||
"@types/linkify-it": "^3.0.2",
|
"@types/linkify-it": "^3.0.2",
|
||||||
"@types/lodash": "^4.14.189",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/react": "~18.0.25",
|
"@types/react": "~18.0.26",
|
||||||
"@types/react-dom": "~18.0.9",
|
"@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-base64": "^0.2.0",
|
||||||
"@types/react-native-share-menu": "^5.0.2",
|
"@types/react-native-share-menu": "^5.0.2",
|
||||||
"@types/react-timeago": "^4.1.3",
|
"@types/react-timeago": "^4.1.3",
|
||||||
|
|
|
@ -274,6 +274,7 @@ declare namespace Mastodon {
|
||||||
type List = {
|
type List = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
replies_policy: 'none' | 'list' | 'followed'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
|
|
26
src/App.tsx
26
src/App.tsx
|
@ -1,5 +1,4 @@
|
||||||
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||||
import getLanguage from '@helpers/getLanguage'
|
|
||||||
import queryClient from '@helpers/queryClient'
|
import queryClient from '@helpers/queryClient'
|
||||||
import i18n from '@root/i18n/i18n'
|
import i18n from '@root/i18n/i18n'
|
||||||
import Screens from '@root/Screens'
|
import Screens from '@root/Screens'
|
||||||
|
@ -13,11 +12,12 @@ import timezone from '@root/startup/timezone'
|
||||||
import { persistor, store } from '@root/store'
|
import { persistor, store } from '@root/store'
|
||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
|
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 ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import * as Localization from 'expo-localization'
|
import * as Localization from 'expo-localization'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { IntlProvider } from 'react-intl'
|
||||||
import { LogBox, Platform } from 'react-native'
|
import { LogBox, Platform } from 'react-native'
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
|
@ -85,7 +85,7 @@ const App: React.FC = () => {
|
||||||
if (bootstrapped) {
|
if (bootstrapped) {
|
||||||
log('log', 'App', 'loading actual app :)')
|
log('log', 'App', 'loading actual app :)')
|
||||||
log('log', 'App', `Locale: ${Localization.locale}`)
|
log('log', 'App', `Locale: ${Localization.locale}`)
|
||||||
const language = getLanguage()
|
const language = getSettingsLanguage(store.getState())
|
||||||
if (!language) {
|
if (!language) {
|
||||||
if (Platform.OS !== 'ios') {
|
if (Platform.OS !== 'ios') {
|
||||||
store.dispatch(changeLanguage('en'))
|
store.dispatch(changeLanguage('en'))
|
||||||
|
@ -96,15 +96,17 @@ const App: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<IntlProvider locale={language}>
|
||||||
<ActionSheetProvider>
|
<SafeAreaProvider>
|
||||||
<AccessibilityManager>
|
<ActionSheetProvider>
|
||||||
<ThemeManager>
|
<AccessibilityManager>
|
||||||
<Screens localCorrupt={localCorrupt} />
|
<ThemeManager>
|
||||||
</ThemeManager>
|
<Screens localCorrupt={localCorrupt} />
|
||||||
</AccessibilityManager>
|
</ThemeManager>
|
||||||
</ActionSheetProvider>
|
</AccessibilityManager>
|
||||||
</SafeAreaProvider>
|
</ActionSheetProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</IntlProvider>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import { HeaderLeft } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import { displayMessage, Message } from '@components/Message'
|
import { displayMessage, Message } from '@components/Message'
|
||||||
import navigationRef from '@helpers/navigationRef'
|
import navigationRef from '@helpers/navigationRef'
|
||||||
|
@ -28,7 +27,6 @@ import * as Linking from 'expo-linking'
|
||||||
import { addScreenshotListener } from 'expo-screen-capture'
|
import { addScreenshotListener } from 'expo-screen-capture'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { IntlProvider } from 'react-intl'
|
|
||||||
import { Alert, Platform, StatusBar } from 'react-native'
|
import { Alert, Platform, StatusBar } from 'react-native'
|
||||||
import ShareMenu from 'react-native-share-menu'
|
import ShareMenu from 'react-native-share-menu'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
@ -113,7 +111,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousRoute?.name !== currentRoute?.name) {
|
if (previousRoute?.name !== currentRoute?.name) {
|
||||||
analytics('screen_view', { screen_name: currentRoute?.name })
|
|
||||||
Sentry.setContext('page', {
|
Sentry.setContext('page', {
|
||||||
previous: previousRoute,
|
previous: previousRoute,
|
||||||
current: currentRoute
|
current: currentRoute
|
||||||
|
@ -273,7 +270,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={i18n.language}>
|
<>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
backgroundColor={colors.backgroundDefault}
|
backgroundColor={colors.backgroundDefault}
|
||||||
{...(Platform.OS === 'android' && {
|
{...(Platform.OS === 'android' && {
|
||||||
|
@ -350,7 +347,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
|
|
||||||
<Message />
|
<Message />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</IntlProvider>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,30 +7,23 @@ const handleError = (error: any) => {
|
||||||
// The request was made and the server responded with a status code
|
// The request was made and the server responded with a status code
|
||||||
// that falls out of the range of 2xx
|
// that falls out of the range of 2xx
|
||||||
console.error(
|
console.error(
|
||||||
ctx.bold(' API instance '),
|
ctx.bold(' API '),
|
||||||
ctx.bold('response'),
|
ctx.bold('response'),
|
||||||
error.response.status,
|
error.response.status,
|
||||||
error?.response.data?.error || error?.response.message || 'Unknown error'
|
error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||||
)
|
)
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: error?.response.status,
|
status: error?.response.status,
|
||||||
message:
|
message: error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||||
error?.response.data?.error ||
|
|
||||||
error?.response.message ||
|
|
||||||
'Unknown error'
|
|
||||||
})
|
})
|
||||||
} else if (error?.request) {
|
} else if (error?.request) {
|
||||||
// The request was made but no response was received
|
// The request was made but no response was received
|
||||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||||
// http.ClientRequest in node.js
|
// http.ClientRequest in node.js
|
||||||
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
|
console.error(ctx.bold(' API '), ctx.bold('request'), error)
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
|
||||||
ctx.bold(' API instance '),
|
|
||||||
ctx.bold('internal'),
|
|
||||||
error?.message
|
|
||||||
)
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,7 @@ export type Params = {
|
||||||
}
|
}
|
||||||
headers?: { [key: string]: string }
|
headers?: { [key: string]: string }
|
||||||
body?: FormData
|
body?: FormData
|
||||||
extras?: Omit<
|
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
|
||||||
AxiosRequestConfig,
|
|
||||||
'method' | 'url' | 'params' | 'headers' | 'data'
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceResponse<T = unknown> = {
|
export type InstanceResponse<T = unknown> = {
|
||||||
|
@ -35,9 +32,7 @@ const apiInstance = async <T = unknown>({
|
||||||
}: Params): Promise<InstanceResponse<T>> => {
|
}: Params): Promise<InstanceResponse<T>> => {
|
||||||
const { store } = require('@root/store')
|
const { store } = require('@root/store')
|
||||||
const state = store.getState() as RootState
|
const state = store.getState() as RootState
|
||||||
const instanceActive = state.instances.instances.findIndex(
|
const instanceActive = state.instances.instances.findIndex(instance => instance.active)
|
||||||
instance => instance.active
|
|
||||||
)
|
|
||||||
|
|
||||||
let domain
|
let domain
|
||||||
let token
|
let token
|
||||||
|
@ -45,21 +40,19 @@ const apiInstance = async <T = unknown>({
|
||||||
domain = state.instances.instances[instanceActive].url
|
domain = state.instances.instances[instanceActive].url
|
||||||
token = state.instances.instances[instanceActive].token
|
token = state.instances.instances[instanceActive].token
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided')
|
||||||
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
|
||||||
)
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
ctx.bgGreen.bold(' API instance ') +
|
ctx.bgGreen.bold(' API instance ') +
|
||||||
' ' +
|
' ' +
|
||||||
domain +
|
domain +
|
||||||
' ' +
|
' ' +
|
||||||
method +
|
method +
|
||||||
ctx.green(' -> ') +
|
ctx.green(' -> ') +
|
||||||
`/${url}` +
|
`/${url}` +
|
||||||
(params ? ctx.green(' -> ') : ''),
|
(params ? ctx.green(' -> ') : ''),
|
||||||
params ? params : ''
|
params ? params : ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,10 +63,7 @@ const apiInstance = async <T = unknown>({
|
||||||
url,
|
url,
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type':
|
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
|
||||||
body && body instanceof FormData
|
|
||||||
? 'multipart/form-data'
|
|
||||||
: 'application/json',
|
|
||||||
Accept: '*/*',
|
Accept: '*/*',
|
||||||
...userAgent,
|
...userAgent,
|
||||||
...headers,
|
...headers,
|
||||||
|
@ -87,10 +77,10 @@ const apiInstance = async <T = unknown>({
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let prev
|
let prev
|
||||||
let next
|
let next
|
||||||
if (response.headers.link) {
|
if (response.headers?.link) {
|
||||||
const headersLinks = li.parse(response.headers.link)
|
const headersLinks = li.parse(response.headers?.link)
|
||||||
prev = headersLinks.prev?.match(/_id=([0-9]*)/)[1]
|
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1]
|
||||||
next = headersLinks.next?.match(/_id=([0-9]*)/)[1]
|
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1]
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
body: response.data,
|
body: response.data,
|
||||||
|
|
|
@ -4,49 +4,47 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback } from 'react'
|
import React, { PropsWithChildren } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import analytics from './analytics'
|
|
||||||
import GracefullyImage from './GracefullyImage'
|
import GracefullyImage from './GracefullyImage'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account
|
||||||
onPress?: () => void
|
Component?: typeof View | typeof Pressable
|
||||||
origin?: string
|
props?: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentAccount: React.FC<Props> = ({
|
const ComponentAccount: React.FC<PropsWithChildren & Props> = ({
|
||||||
account,
|
account,
|
||||||
onPress: customOnPress,
|
Component,
|
||||||
origin
|
props,
|
||||||
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation =
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
if (!props) {
|
||||||
analytics('search_account_press', { page: origin })
|
props = { onPress: () => navigation.push('Tab-Shared-Account', { account }) }
|
||||||
navigation.push('Tab-Shared-Account', { account })
|
}
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return React.createElement(
|
||||||
<Pressable
|
Component || Pressable,
|
||||||
accessibilityRole='button'
|
{
|
||||||
style={{
|
...props,
|
||||||
|
style: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
paddingVertical: StyleConstants.Spacing.M,
|
paddingVertical: StyleConstants.Spacing.M,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignSelf: 'flex-start',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}
|
}
|
||||||
onPress={customOnPress || onPress}
|
},
|
||||||
>
|
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
uri={{ original: account.avatar, static: account.avatar_static }}
|
uri={{ original: account.avatar, static: account.avatar_static }}
|
||||||
style={{
|
style={{
|
||||||
alignSelf: 'flex-start',
|
|
||||||
width: StyleConstants.Avatar.S,
|
width: StyleConstants.Avatar.S,
|
||||||
height: StyleConstants.Avatar.S,
|
height: StyleConstants.Avatar.S,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
@ -72,7 +70,8 @@ const ComponentAccount: React.FC<Props> = ({
|
||||||
@{account.acct}
|
@{account.acct}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</View>
|
</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 { displayMessage } from '@components/Message'
|
||||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Platform } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { ContextMenuAction } from 'react-native-context-menu-view'
|
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
export interface Props {
|
const menuInstance = ({
|
||||||
actions: ContextMenuAction[]
|
status,
|
||||||
status: Mastodon.Status
|
queryKey,
|
||||||
queryKey: QueryKeyTimeline
|
rootQueryKey
|
||||||
|
}: {
|
||||||
|
status?: Mastodon.Status
|
||||||
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
}
|
}): ContextMenu[][] => {
|
||||||
|
if (!status || !queryKey) return []
|
||||||
|
|
||||||
const contextMenuInstance = ({ actions, status, queryKey, rootQueryKey }: Props) => {
|
|
||||||
const { t } = useTranslation('componentContextMenu')
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const { t } = useTranslation('componentContextMenu')
|
||||||
const currentInstance = useSelector(getInstanceUrl)
|
|
||||||
const instance = status?.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
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) {
|
if (currentInstance !== instance && instance) {
|
||||||
switch (Platform.OS) {
|
menus.push([
|
||||||
case 'ios':
|
{
|
||||||
actions.push({
|
key: 'instance-block',
|
||||||
id: 'instance',
|
item: {
|
||||||
title: t('instance.title'),
|
onSelect: () =>
|
||||||
actions: [
|
Alert.alert(
|
||||||
{
|
t('instance.block.alert.title', { instance }),
|
||||||
id: 'instance-block',
|
t('instance.block.alert.message'),
|
||||||
title: t('instance.block.action', { instance }),
|
[
|
||||||
destructive: true
|
{
|
||||||
}
|
text: t('instance.block.alert.buttons.confirm'),
|
||||||
]
|
style: 'destructive',
|
||||||
})
|
onPress: () => {
|
||||||
break
|
mutation.mutate({
|
||||||
default:
|
type: 'domainBlock',
|
||||||
actions.push({
|
queryKey,
|
||||||
id: 'instance-block',
|
domain: instance
|
||||||
title: t('instance.block.action', { instance }),
|
})
|
||||||
destructive: true
|
}
|
||||||
})
|
},
|
||||||
break
|
{
|
||||||
}
|
text: t('common:buttons.cancel')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
disabled: false,
|
||||||
|
destructive: true,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
title: t('instance.block.action', { instance }),
|
||||||
|
icon: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
return (index: number) => {
|
return menus
|
||||||
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')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contextMenuInstance
|
export default menuInstance
|
||||||
|
|
|
@ -1,64 +1,76 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Share } from 'react-native'
|
import { Platform, Share } from 'react-native'
|
||||||
import { ContextMenuAction } from 'react-native-context-menu-view'
|
|
||||||
|
|
||||||
export interface Props {
|
const menuShare = (
|
||||||
copiableContent?: React.MutableRefObject<{
|
params:
|
||||||
content?: string | undefined
|
| {
|
||||||
complete: boolean
|
visibility?: Mastodon.Status['visibility']
|
||||||
}>
|
copiableContent?: React.MutableRefObject<{
|
||||||
actions: ContextMenuAction[]
|
content?: string | undefined
|
||||||
type: 'status' | 'account'
|
complete: boolean
|
||||||
url: string
|
}>
|
||||||
}
|
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 { theme } = useTheme()
|
||||||
const { t } = useTranslation('componentContextMenu')
|
const { t } = useTranslation('componentContextMenu')
|
||||||
|
|
||||||
actions.push({
|
const menus: ContextMenu[][] = [[]]
|
||||||
id: 'share',
|
|
||||||
title: t(`share.${type}.action`),
|
if (params.url) {
|
||||||
systemIcon: 'square.and.arrow.up'
|
const url = params.url
|
||||||
})
|
menus[0].push({
|
||||||
Platform.OS !== 'android' &&
|
key: 'share',
|
||||||
type === 'status' &&
|
item: {
|
||||||
actions.push({
|
onSelect: () => {
|
||||||
id: 'copy',
|
switch (Platform.OS) {
|
||||||
title: t(`copy.action`),
|
case 'ios':
|
||||||
systemIcon: 'doc.on.doc',
|
Share.share({ url })
|
||||||
disabled: !copiableContent?.current.content?.length
|
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) => {
|
return menus
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contextMenuShare
|
export default menuShare
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import apiInstance from '@api/instance'
|
import apiInstance from '@api/instance'
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { ContextMenuAction } from 'react-native-context-menu-view'
|
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
export interface Props {
|
const menuStatus = ({
|
||||||
actions: ContextMenuAction[]
|
status,
|
||||||
status: Mastodon.Status
|
queryKey,
|
||||||
queryKey: QueryKeyTimeline
|
rootQueryKey
|
||||||
|
}: {
|
||||||
|
status?: Mastodon.Status
|
||||||
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
}
|
}): ContextMenu[][] => {
|
||||||
|
if (!status || !queryKey) return []
|
||||||
|
|
||||||
const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) => {
|
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Screen-Tabs'>>()
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Screen-Tabs'>>()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { t } = useTranslation('componentContextMenu')
|
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 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) {
|
if (ownAccount) {
|
||||||
const accountMenuItems: ContextMenuAction[] = [
|
menus.push([
|
||||||
{
|
{
|
||||||
id: 'status-delete',
|
key: 'status-edit',
|
||||||
title: t('status.delete.action'),
|
item: {
|
||||||
systemIcon: 'trash',
|
onSelect: async () => {
|
||||||
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
|
|
||||||
})
|
|
||||||
let replyToStatus: Mastodon.Status | undefined = undefined
|
let replyToStatus: Mastodon.Status | undefined = undefined
|
||||||
if (status.in_reply_to_id) {
|
if (status.in_reply_to_id) {
|
||||||
replyToStatus = await apiInstance<Mastodon.Status>({
|
replyToStatus = await apiInstance<Mastodon.Status>({
|
||||||
|
@ -151,96 +75,166 @@ const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) =
|
||||||
url: `statuses/${status.in_reply_to_id}`
|
url: `statuses/${status.in_reply_to_id}`
|
||||||
}).then(res => res.body)
|
}).then(res => res.body)
|
||||||
}
|
}
|
||||||
mutation
|
apiInstance<{
|
||||||
.mutateAsync({
|
id: Mastodon.Status['id']
|
||||||
type: 'deleteItem',
|
text: NonNullable<Mastodon.Status['text']>
|
||||||
source: 'statuses',
|
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,
|
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 }),
|
disabled: false,
|
||||||
queryKey,
|
destructive: false,
|
||||||
rootQueryKey
|
hidden: !canEditPost
|
||||||
})
|
},
|
||||||
})
|
title: t('status.edit.action'),
|
||||||
}
|
icon: 'square.and.pencil'
|
||||||
if (actions[index].id === 'status-pin') {
|
},
|
||||||
// Also note that reblogs cannot be pinned.
|
{
|
||||||
analytics('timeline_shared_headeractions_status_pin_press', {
|
key: 'status-delete-edit',
|
||||||
page: queryKey && queryKey[1].page
|
item: {
|
||||||
})
|
onSelect: () =>
|
||||||
mutation.mutate({
|
Alert.alert(t('status.deleteEdit.alert.title'), t('status.deleteEdit.alert.message'), [
|
||||||
type: 'updateStatusProperty',
|
{
|
||||||
queryKey,
|
text: t('status.deleteEdit.alert.buttons.confirm'),
|
||||||
rootQueryKey,
|
style: 'destructive',
|
||||||
id: status.id,
|
onPress: async () => {
|
||||||
payload: {
|
let replyToStatus: Mastodon.Status | undefined = undefined
|
||||||
property: 'pinned',
|
if (status.in_reply_to_id) {
|
||||||
currentValue: status.pinned,
|
replyToStatus = await apiInstance<Mastodon.Status>({
|
||||||
propertyCount: undefined,
|
method: 'get',
|
||||||
countValue: undefined
|
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 { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Pressable } from 'react-native'
|
import { Dimensions, Pressable } from 'react-native'
|
||||||
import analytics from './analytics'
|
import Sparkline from './Sparkline'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
hashtag: Mastodon.Tag
|
hashtag: Mastodon.Tag
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
origin?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentHashtag: React.FC<Props> = ({
|
const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress }) => {
|
||||||
hashtag,
|
|
||||||
onPress: customOnPress,
|
|
||||||
origin
|
|
||||||
}) => {
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation =
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
analytics('search_account_press', { page: origin })
|
|
||||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
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 (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
style={{ padding: StyleConstants.Spacing.S * 1.5 }}
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding
|
||||||
|
}}
|
||||||
onPress={customOnPress || onPress}
|
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}
|
#{hashtag.name}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
|
<Sparkline
|
||||||
|
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import browserPackage from '@helpers/browserPackage'
|
||||||
import { useAppsQuery } from '@utils/queryHooks/apps'
|
import { useAppsQuery } from '@utils/queryHooks/apps'
|
||||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
import { getInstances } from '@utils/slices/instancesSlice'
|
import { getInstances } from '@utils/slices/instancesSlice'
|
||||||
|
@ -9,18 +10,10 @@ import * as WebBrowser from 'expo-web-browser'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { RefObject, useCallback, useMemo, useState } from 'react'
|
import React, { RefObject, useCallback, useMemo, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import {
|
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||||
Alert,
|
|
||||||
Image,
|
|
||||||
KeyboardAvoidingView,
|
|
||||||
Platform,
|
|
||||||
TextInput,
|
|
||||||
View
|
|
||||||
} from 'react-native'
|
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Placeholder } from 'rn-placeholder'
|
import { Placeholder } from 'rn-placeholder'
|
||||||
import analytics from './analytics'
|
|
||||||
import InstanceAuth from './Instance/Auth'
|
import InstanceAuth from './Instance/Auth'
|
||||||
import InstanceInfo from './Instance/Info'
|
import InstanceInfo from './Instance/Info'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
@ -65,18 +58,14 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
|
|
||||||
const processUpdate = useCallback(() => {
|
const processUpdate = useCallback(() => {
|
||||||
if (domain) {
|
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'), [
|
Alert.alert(t('update.alert.title'), t('update.alert.message'), [
|
||||||
{
|
{
|
||||||
text: t('update.alert.buttons.cancel'),
|
text: t('common:buttons.cancel'),
|
||||||
style: 'cancel'
|
style: 'cancel'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t('update.alert.buttons.continue'),
|
text: t('common:buttons.continue'),
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
appsQuery.refetch()
|
appsQuery.refetch()
|
||||||
}
|
}
|
||||||
|
@ -142,9 +131,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
...StyleConstants.FontStyle.M,
|
...StyleConstants.FontStyle.M,
|
||||||
color: colors.primaryDefault,
|
color: colors.primaryDefault,
|
||||||
borderBottomColor: instanceQuery.isError
|
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
|
||||||
? colors.red
|
|
||||||
: colors.border
|
|
||||||
}}
|
}}
|
||||||
editable={false}
|
editable={false}
|
||||||
defaultValue='https://'
|
defaultValue='https://'
|
||||||
|
@ -156,9 +143,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
...StyleConstants.FontStyle.M,
|
...StyleConstants.FontStyle.M,
|
||||||
marginRight: StyleConstants.Spacing.M,
|
marginRight: StyleConstants.Spacing.M,
|
||||||
color: colors.primaryDefault,
|
color: colors.primaryDefault,
|
||||||
borderBottomColor: instanceQuery.isError
|
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
|
||||||
? colors.red
|
|
||||||
: colors.border
|
|
||||||
}}
|
}}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
|
@ -166,7 +151,6 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
keyboardType='url'
|
keyboardType='url'
|
||||||
textContentType='URL'
|
textContentType='URL'
|
||||||
onSubmitEditing={({ nativeEvent: { text } }) => {
|
onSubmitEditing={({ nativeEvent: { text } }) => {
|
||||||
analytics('instance_textinput_submit', { match: text === domain })
|
|
||||||
if (
|
if (
|
||||||
text === domain &&
|
text === domain &&
|
||||||
instanceQuery.isSuccess &&
|
instanceQuery.isSuccess &&
|
||||||
|
@ -182,11 +166,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
keyboardAppearance={mode}
|
keyboardAppearance={mode}
|
||||||
{...(scrollViewRef && {
|
{...(scrollViewRef && {
|
||||||
onFocus: () =>
|
onFocus: () =>
|
||||||
setTimeout(
|
setTimeout(() => scrollViewRef.current?.scrollTo({ y: 0, animated: true }), 150)
|
||||||
() =>
|
|
||||||
scrollViewRef.current?.scrollTo({ y: 0, animated: true }),
|
|
||||||
150
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
@ -211,27 +191,19 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
<InstanceInfo
|
<InstanceInfo
|
||||||
style={{ alignItems: 'flex-start' }}
|
style={{ alignItems: 'flex-start' }}
|
||||||
header={t('server.information.accounts')}
|
header={t('server.information.accounts')}
|
||||||
content={
|
content={instanceQuery.data?.stats?.user_count?.toString() || undefined}
|
||||||
instanceQuery.data?.stats?.user_count?.toString() || undefined
|
|
||||||
}
|
|
||||||
potentialWidth={4}
|
potentialWidth={4}
|
||||||
/>
|
/>
|
||||||
<InstanceInfo
|
<InstanceInfo
|
||||||
style={{ alignItems: 'center' }}
|
style={{ alignItems: 'center' }}
|
||||||
header={t('server.information.statuses')}
|
header={t('server.information.statuses')}
|
||||||
content={
|
content={instanceQuery.data?.stats?.status_count?.toString() || undefined}
|
||||||
instanceQuery.data?.stats?.status_count?.toString() ||
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
potentialWidth={4}
|
potentialWidth={4}
|
||||||
/>
|
/>
|
||||||
<InstanceInfo
|
<InstanceInfo
|
||||||
style={{ alignItems: 'flex-end' }}
|
style={{ alignItems: 'flex-end' }}
|
||||||
header={t('server.information.domains')}
|
header={t('server.information.domains')}
|
||||||
content={
|
content={instanceQuery.data?.stats?.domain_count?.toString() || undefined}
|
||||||
instanceQuery.data?.stats?.domain_count?.toString() ||
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
potentialWidth={4}
|
potentialWidth={4}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -248,17 +220,11 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={colors.secondary}
|
color={colors.secondary}
|
||||||
style={{
|
style={{
|
||||||
marginTop:
|
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
|
||||||
(StyleConstants.Font.LineHeight.S -
|
|
||||||
StyleConstants.Font.Size.S) /
|
|
||||||
2,
|
|
||||||
marginRight: StyleConstants.Spacing.XS
|
marginRight: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomText
|
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
|
||||||
fontStyle='S'
|
|
||||||
style={{ flex: 1, color: colors.secondary }}
|
|
||||||
>
|
|
||||||
{t('server.disclaimer.base')}
|
{t('server.disclaimer.base')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</View>
|
</View>
|
||||||
|
@ -274,10 +240,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={colors.secondary}
|
color={colors.secondary}
|
||||||
style={{
|
style={{
|
||||||
marginTop:
|
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
|
||||||
(StyleConstants.Font.LineHeight.S -
|
|
||||||
StyleConstants.Font.Size.S) /
|
|
||||||
2,
|
|
||||||
marginRight: StyleConstants.Spacing.XS
|
marginRight: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -292,22 +255,20 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
<CustomText
|
<CustomText
|
||||||
accessible
|
accessible
|
||||||
style={{ color: colors.blue }}
|
style={{ color: colors.blue }}
|
||||||
onPress={() => {
|
onPress={async () =>
|
||||||
analytics('view_privacy')
|
WebBrowser.openBrowserAsync('https://tooot.app/privacy-policy', {
|
||||||
WebBrowser.openBrowserAsync(
|
browserPackage: await browserPackage()
|
||||||
'https://tooot.app/privacy-policy'
|
})
|
||||||
)
|
}
|
||||||
}}
|
|
||||||
/>,
|
/>,
|
||||||
<CustomText
|
<CustomText
|
||||||
accessible
|
accessible
|
||||||
style={{ color: colors.blue }}
|
style={{ color: colors.blue }}
|
||||||
onPress={() => {
|
onPress={async () =>
|
||||||
analytics('view_tos')
|
WebBrowser.openBrowserAsync('https://tooot.app/terms-of-service', {
|
||||||
WebBrowser.openBrowserAsync(
|
browserPackage: await browserPackage()
|
||||||
'https://tooot.app/terms-of-service'
|
})
|
||||||
)
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import browserPackage from '@helpers/browserPackage'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAppDispatch } from '@root/store'
|
import { useAppDispatch } from '@root/store'
|
||||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||||
|
@ -24,14 +25,11 @@ const InstanceAuth = React.memo(
|
||||||
useProxy: false
|
useProxy: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const navigation =
|
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||||
useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const deprecateAuthFollow = useSelector(
|
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
||||||
checkInstanceFeature('deprecate_auth_follow')
|
|
||||||
)
|
|
||||||
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
||||||
{
|
{
|
||||||
clientId: appData.clientId,
|
clientId: appData.clientId,
|
||||||
|
@ -48,7 +46,7 @@ const InstanceAuth = React.memo(
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
if (request?.clientId) {
|
if (request?.clientId) {
|
||||||
await promptAsync()
|
await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e))
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [request])
|
}, [request])
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -7,14 +7,16 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuContainer: React.FC<Props> = ({ children }) => {
|
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
|
export default MenuContainer
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { ColorDefinitions } from '@utils/styles/themes'
|
import { ColorDefinitions } from '@utils/styles/themes'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { View } from 'react-native'
|
import { Text, View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ const MenuRow: React.FC<Props> = ({
|
||||||
>
|
>
|
||||||
<TapGestureHandler
|
<TapGestureHandler
|
||||||
onHandlerStateChange={async ({ nativeEvent }) => {
|
onHandlerStateChange={async ({ nativeEvent }) => {
|
||||||
|
if (typeof iconBack !== 'string') return // Let icon back handles the gesture
|
||||||
if (nativeEvent.state === State.ACTIVE && !loading) {
|
if (nativeEvent.state === State.ACTIVE && !loading) {
|
||||||
if (screenReaderEnabled && switchOnValueChange) {
|
if (screenReaderEnabled && switchOnValueChange) {
|
||||||
switchOnValueChange()
|
switchOnValueChange()
|
||||||
|
@ -79,12 +80,13 @@ const MenuRow: React.FC<Props> = ({
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingTop: StyleConstants.Spacing.S
|
justifyContent: 'space-between',
|
||||||
|
marginTop: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexGrow: 3,
|
flex: 3,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -125,7 +125,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||||
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
|
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
paddingRight: StyleConstants.Spacing.M * 2,
|
paddingRight: StyleConstants.Spacing.M * 2,
|
||||||
marginTop: insets.top
|
marginTop: ref ? undefined : insets.top
|
||||||
}}
|
}}
|
||||||
titleStyle={{
|
titleStyle={{
|
||||||
color: colors.primaryDefault,
|
color: colors.primaryDefault,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import openLink from '@components/openLink'
|
import openLink from '@components/openLink'
|
||||||
import ParseEmojis from '@components/Parse/Emojis'
|
import ParseEmojis from '@components/Parse/Emojis'
|
||||||
|
@ -63,7 +62,6 @@ const renderNode = ({
|
||||||
lineHeight: adaptedLineheight
|
lineHeight: adaptedLineheight
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('status_hashtag_press')
|
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
differentTag &&
|
differentTag &&
|
||||||
navigation.push('Tab-Shared-Hashtag', {
|
navigation.push('Tab-Shared-Hashtag', {
|
||||||
|
@ -89,7 +87,6 @@ const renderNode = ({
|
||||||
lineHeight: adaptedLineheight
|
lineHeight: adaptedLineheight
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('status_mention_press')
|
|
||||||
accountIndex !== -1 &&
|
accountIndex !== -1 &&
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
differentAccount &&
|
differentAccount &&
|
||||||
|
@ -118,7 +115,6 @@ const renderNode = ({
|
||||||
lineHeight: adaptedLineheight
|
lineHeight: adaptedLineheight
|
||||||
}}
|
}}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
analytics('status_link_press')
|
|
||||||
if (!disableDetails) {
|
if (!disableDetails) {
|
||||||
if (shouldBeTag) {
|
if (shouldBeTag) {
|
||||||
navigation.push('Tab-Shared-Hashtag', {
|
navigation.push('Tab-Shared-Hashtag', {
|
||||||
|
@ -172,6 +168,7 @@ export interface Props {
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
disableDetails?: boolean
|
disableDetails?: boolean
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseHTML = React.memo(
|
const ParseHTML = React.memo(
|
||||||
|
@ -187,7 +184,8 @@ const ParseHTML = React.memo(
|
||||||
expandHint,
|
expandHint,
|
||||||
highlighted = false,
|
highlighted = false,
|
||||||
disableDetails = false,
|
disableDetails = false,
|
||||||
selectable = false
|
selectable = false,
|
||||||
|
setSpoilerExpanded
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||||
const adaptedFontsize = adaptiveScale(
|
const adaptedFontsize = adaptiveScale(
|
||||||
|
@ -255,9 +253,11 @@ const ParseHTML = React.memo(
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={t('HTML.accessibilityHint')}
|
accessibilityLabel={t('HTML.accessibilityHint')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('status_readmore', { totalLines, expanded })
|
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
setExpanded(!expanded)
|
setExpanded(!expanded)
|
||||||
|
if (setSpoilerExpanded) {
|
||||||
|
setSpoilerExpanded(!expanded)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import {
|
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
||||||
QueryKeyRelationship,
|
|
||||||
useRelationshipMutation
|
|
||||||
} from '@utils/queryHooks/relationship'
|
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
@ -23,17 +19,12 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
||||||
const queryKeyNotification: QueryKeyTimeline = [
|
const queryKeyNotification: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||||
'Timeline',
|
|
||||||
{ page: 'Notifications' }
|
|
||||||
]
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useRelationshipMutation({
|
const mutation = useRelationshipMutation({
|
||||||
onSuccess: res => {
|
onSuccess: res => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [
|
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||||
res
|
|
||||||
])
|
|
||||||
queryClient.refetchQueries(queryKeyNotification)
|
queryClient.refetchQueries(queryKeyNotification)
|
||||||
},
|
},
|
||||||
onError: (err: any, { type }) => {
|
onError: (err: any, { type }) => {
|
||||||
|
@ -62,28 +53,26 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||||
type='icon'
|
type='icon'
|
||||||
content='X'
|
content='X'
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
onPress={() => {
|
onPress={() =>
|
||||||
analytics('relationship_incoming_press_reject')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
payload: { action: 'reject' }
|
payload: { action: 'reject' }
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
type='icon'
|
type='icon'
|
||||||
content='Check'
|
content='Check'
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
onPress={() => {
|
onPress={() =>
|
||||||
analytics('relationship_incoming_press_authorize')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
payload: { action: 'authorize' }
|
payload: { action: 'authorize' }
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
style={styles.approve}
|
style={styles.approve}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
|
@ -29,10 +28,7 @@ const RelationshipOutgoing = React.memo(
|
||||||
const mutation = useRelationshipMutation({
|
const mutation = useRelationshipMutation({
|
||||||
onSuccess: (res, { payload: { action } }) => {
|
onSuccess: (res, { payload: { action } }) => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.setQueryData<Mastodon.Relationship[]>(
|
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||||
queryKeyRelationship,
|
|
||||||
[res]
|
|
||||||
)
|
|
||||||
if (action === 'block') {
|
if (action === 'block') {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
|
@ -64,17 +60,12 @@ const RelationshipOutgoing = React.memo(
|
||||||
onPress = () => {}
|
onPress = () => {}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.blocked_by) {
|
if (query.data?.blocked_by) {
|
||||||
analytics('relationship_outgoing_blocked_by')
|
|
||||||
content = t('button.blocked_by')
|
content = t('button.blocked_by')
|
||||||
onPress = () => {
|
onPress = () => {}
|
||||||
analytics('relationship_outgoing_blocked_by_press')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.blocking) {
|
if (query.data?.blocking) {
|
||||||
analytics('relationship_outgoing_blocking')
|
|
||||||
content = t('button.blocking')
|
content = t('button.blocking')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
analytics('relationship_outgoing_blocking_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
@ -86,10 +77,8 @@ const RelationshipOutgoing = React.memo(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.following) {
|
if (query.data?.following) {
|
||||||
analytics('relationship_outgoing_following')
|
|
||||||
content = t('button.following')
|
content = t('button.following')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
analytics('relationship_outgoing_following_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
@ -101,10 +90,8 @@ const RelationshipOutgoing = React.memo(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.requested) {
|
if (query.data?.requested) {
|
||||||
analytics('relationship_outgoing_requested')
|
|
||||||
content = t('button.requested')
|
content = t('button.requested')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
analytics('relationship_outgoing_requested_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
@ -115,10 +102,8 @@ const RelationshipOutgoing = React.memo(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
analytics('relationship_outgoing_default')
|
|
||||||
content = t('button.default')
|
content = t('button.default')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
analytics('relationship_outgoing_default_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
|
|
@ -9,7 +9,7 @@ export interface Props {
|
||||||
const RelativeTime: React.FC<Props> = ({ time }) => {
|
const RelativeTime: React.FC<Props> = ({ time }) => {
|
||||||
const [now, setNow] = useState(new Date().getTime())
|
const [now, setNow] = useState(new Date().getTime())
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const appStateListener = AppState.addEventListener('change', state => {
|
const appStateListener = AppState.addEventListener('change', () => {
|
||||||
setNow(new Date().getTime())
|
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 { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
extraMarginLeft?: number
|
extraMarginLeft?: number
|
||||||
extraMarginRight?: number
|
extraMarginRight?: number
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentSeparator = React.memo(
|
const ComponentSeparator: React.FC<Props> = ({
|
||||||
({ extraMarginLeft = 0, extraMarginRight = 0 }: Props) => {
|
extraMarginLeft = 0,
|
||||||
const { colors } = useTheme()
|
extraMarginRight = 0,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={[
|
||||||
|
style,
|
||||||
|
{
|
||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
marginLeft:
|
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
||||||
StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
||||||
marginRight:
|
}
|
||||||
StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
]}
|
||||||
}}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ComponentSeparator
|
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 apiInstance from '@api/instance'
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { isEqual } from 'lodash'
|
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import TimelineActions from './Shared/Actions'
|
import TimelineActions from './Shared/Actions'
|
||||||
import TimelineContent from './Shared/Content'
|
import TimelineContent from './Shared/Content'
|
||||||
|
import StatusContext from './Shared/Context'
|
||||||
import TimelineHeaderConversation from './Shared/HeaderConversation'
|
import TimelineHeaderConversation from './Shared/HeaderConversation'
|
||||||
import TimelinePoll from './Shared/Poll'
|
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 {
|
export interface Props {
|
||||||
conversation: Mastodon.Conversation
|
conversation: Mastodon.Conversation
|
||||||
queryKey: QueryKeyTimeline
|
queryKey: QueryKeyTimeline
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineConversation = React.memo(
|
const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlighted = false }) => {
|
||||||
({ conversation, queryKey, highlighted = false }: Props) => {
|
const { colors } = useTheme()
|
||||||
const instanceAccount = useSelector(
|
|
||||||
getInstanceAccount,
|
|
||||||
(prev, next) => prev?.id === next?.id
|
|
||||||
)
|
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(() => {
|
const fireMutation = useCallback(() => {
|
||||||
return apiInstance<Mastodon.Conversation>({
|
return apiInstance<Mastodon.Conversation>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `conversations/${conversation.id}/read`
|
url: `conversations/${conversation.id}/read`
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
const { mutate } = useMutation(fireMutation, {
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries(queryKey)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}, [])
|
||||||
|
const { mutate } = useMutation(fireMutation, {
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries(queryKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const navigation =
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const onPress = useCallback(() => {
|
||||||
const onPress = useCallback(() => {
|
if (conversation.last_status) {
|
||||||
analytics('timeline_conversation_press')
|
conversation.unread && mutate()
|
||||||
if (conversation.last_status) {
|
navigation.push('Tab-Shared-Toot', {
|
||||||
conversation.unread && mutate()
|
toot: conversation.last_status,
|
||||||
navigation.push('Tab-Shared-Toot', {
|
rootQueryKey: queryKey
|
||||||
toot: conversation.last_status,
|
})
|
||||||
rootQueryKey: queryKey
|
}
|
||||||
})
|
}, [])
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<StatusContext.Provider value={{ queryKey, status: conversation.last_status }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
|
@ -102,19 +62,39 @@ const TimelineConversation = React.memo(
|
||||||
conversation.unread && {
|
conversation.unread && {
|
||||||
borderLeftWidth: StyleConstants.Spacing.XS,
|
borderLeftWidth: StyleConstants.Spacing.XS,
|
||||||
borderLeftColor: colors.blue,
|
borderLeftColor: colors.blue,
|
||||||
paddingLeft:
|
paddingLeft: StyleConstants.Spacing.Global.PagePadding - StyleConstants.Spacing.XS
|
||||||
StyleConstants.Spacing.Global.PagePadding -
|
|
||||||
StyleConstants.Spacing.XS
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||||
<Avatars accounts={conversation.accounts} />
|
<View
|
||||||
<TimelineHeaderConversation
|
style={{
|
||||||
queryKey={queryKey}
|
borderRadius: 4,
|
||||||
conversation={conversation}
|
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>
|
</View>
|
||||||
|
|
||||||
{conversation.last_status ? (
|
{conversation.last_status ? (
|
||||||
|
@ -122,40 +102,19 @@ const TimelineConversation = React.memo(
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||||
paddingLeft: highlighted
|
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||||
? 0
|
|
||||||
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TimelineContent
|
<TimelineContent />
|
||||||
status={conversation.last_status}
|
<TimelinePoll />
|
||||||
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}
|
|
||||||
</View>
|
</View>
|
||||||
<TimelineActions
|
|
||||||
queryKey={queryKey}
|
<TimelineActions />
|
||||||
status={conversation.last_status}
|
|
||||||
highlighted={highlighted}
|
|
||||||
accts={conversation.accounts.map(account => account.acct)}
|
|
||||||
reblog={false}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
</StatusContext.Provider>
|
||||||
},
|
)
|
||||||
(prev, next) => isEqual(prev.conversation, next.conversation)
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default TimelineConversation
|
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 TimelineActioned from '@components/Timeline/Shared/Actioned'
|
||||||
import TimelineActions from '@components/Timeline/Shared/Actions'
|
import TimelineActions from '@components/Timeline/Shared/Actions'
|
||||||
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
|
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
|
||||||
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
|
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
|
||||||
import TimelineCard from '@components/Timeline/Shared/Card'
|
import TimelineCard from '@components/Timeline/Shared/Card'
|
||||||
import TimelineContent from '@components/Timeline/Shared/Content'
|
import TimelineContent from '@components/Timeline/Shared/Content'
|
||||||
// @ts-ignore
|
|
||||||
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
|
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
|
||||||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
@ -15,21 +16,21 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { uniqBy } from 'lodash'
|
import React, { useRef, useState } from 'react'
|
||||||
import React, { useRef } from 'react'
|
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { Platform, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
|
||||||
import { useSelector } from 'react-redux'
|
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 TimelineFeedback from './Shared/Feedback'
|
||||||
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
|
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
|
||||||
import TimelineFullConversation from './Shared/FullConversation'
|
import TimelineFullConversation from './Shared/FullConversation'
|
||||||
|
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
|
||||||
import TimelineTranslate from './Shared/Translate'
|
import TimelineTranslate from './Shared/Translate'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
|
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
|
||||||
queryKey?: QueryKeyTimeline
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
origin?: string
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
disableDetails?: boolean
|
disableDetails?: boolean
|
||||||
disableOnPress?: boolean
|
disableOnPress?: boolean
|
||||||
|
@ -40,40 +41,33 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
origin,
|
|
||||||
highlighted = false,
|
highlighted = false,
|
||||||
disableDetails = false,
|
disableDetails = false,
|
||||||
disableOnPress = false
|
disableOnPress = false
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const actualStatus = item.reblog ? item.reblog : item
|
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||||
|
|
||||||
const ownAccount = actualStatus.account?.id === instanceAccount?.id
|
|
||||||
|
|
||||||
|
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 }>({
|
const copiableContent = useRef<{ content: string; complete: boolean }>({
|
||||||
content: '',
|
content: '',
|
||||||
complete: false
|
complete: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const filtered = queryKey && shouldFilter({ copiableContent, status: actualStatus, queryKey })
|
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
|
||||||
if (queryKey && filtered && !highlighted) {
|
if (queryKey && filtered && !highlighted) {
|
||||||
return <TimelineFiltered phrase={filtered} />
|
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> = {
|
const mainStyle: StyleProp<ViewStyle> = {
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
|
@ -82,22 +76,14 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
const main = () => (
|
const main = () => (
|
||||||
<>
|
<>
|
||||||
{item.reblog ? (
|
{item.reblog ? (
|
||||||
<TimelineActioned action='reblog' account={item.account} />
|
<TimelineActioned action='reblog' />
|
||||||
) : item._pinned ? (
|
) : item._pinned ? (
|
||||||
<TimelineActioned action='pinned' account={item.account} />
|
<TimelineActioned action='pinned' />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||||
<TimelineAvatar
|
<TimelineAvatar />
|
||||||
queryKey={disableOnPress ? undefined : queryKey}
|
<TimelineHeaderDefault />
|
||||||
account={actualStatus.account}
|
|
||||||
highlighted={highlighted}
|
|
||||||
/>
|
|
||||||
<TimelineHeaderDefault
|
|
||||||
queryKey={disableOnPress ? undefined : queryKey}
|
|
||||||
status={actualStatus}
|
|
||||||
highlighted={highlighted}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -106,73 +92,103 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? (
|
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
|
||||||
<TimelineContent
|
<TimelinePoll />
|
||||||
status={actualStatus}
|
<TimelineAttachment />
|
||||||
highlighted={highlighted}
|
<TimelineCard />
|
||||||
disableDetails={disableDetails}
|
<TimelineFullConversation />
|
||||||
/>
|
<TimelineTranslate />
|
||||||
) : null}
|
<TimelineFeedback />
|
||||||
{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} />
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{queryKey && !disableDetails ? (
|
<TimelineActions />
|
||||||
<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}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return disableOnPress ? (
|
const mShare = menuShare({
|
||||||
<View style={mainStyle}>{main()}</View>
|
visibility: status.visibility,
|
||||||
) : (
|
type: 'status',
|
||||||
<TimelineContextMenu
|
url: status.url || status.uri,
|
||||||
copiableContent={copiableContent}
|
copiableContent
|
||||||
status={actualStatus}
|
})
|
||||||
queryKey={queryKey}
|
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
||||||
rootQueryKey={rootQueryKey}
|
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusContext.Provider
|
||||||
|
value={{
|
||||||
|
queryKey,
|
||||||
|
rootQueryKey,
|
||||||
|
status,
|
||||||
|
isReblog: !!item.reblog,
|
||||||
|
ownAccount,
|
||||||
|
spoilerHidden,
|
||||||
|
copiableContent,
|
||||||
|
highlighted,
|
||||||
|
disableDetails,
|
||||||
|
disableOnPress
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Pressable
|
{disableOnPress ? (
|
||||||
accessible={highlighted ? false : true}
|
<View style={mainStyle}>{main()}</View>
|
||||||
style={mainStyle}
|
) : (
|
||||||
onPress={onPress}
|
<>
|
||||||
onLongPress={() => {}}
|
<ContextMenu.Root>
|
||||||
>
|
<ContextMenu.Trigger>
|
||||||
{main()}
|
<Pressable
|
||||||
</Pressable>
|
accessible={highlighted ? false : true}
|
||||||
</TimelineContextMenu>
|
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 Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
@ -27,20 +26,11 @@ const TimelineEmpty = React.memo(
|
||||||
const children = () => {
|
const children = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
return (
|
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||||
<Circle
|
|
||||||
size={StyleConstants.Font.Size.L}
|
|
||||||
color={colors.secondary}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'error':
|
case 'error':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||||
name='Frown'
|
|
||||||
size={StyleConstants.Font.Size.L}
|
|
||||||
color={colors.primaryDefault}
|
|
||||||
/>
|
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='M'
|
fontStyle='M'
|
||||||
style={{
|
style={{
|
||||||
|
@ -51,14 +41,7 @@ const TimelineEmpty = React.memo(
|
||||||
>
|
>
|
||||||
{t('empty.error.message')}
|
{t('empty.error.message')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
<Button
|
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
|
||||||
type='text'
|
|
||||||
content={t('empty.error.button')}
|
|
||||||
onPress={() => {
|
|
||||||
analytics('timeline_error_press_refetch')
|
|
||||||
refetch()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'success':
|
case 'success':
|
||||||
|
@ -74,7 +57,7 @@ const TimelineEmpty = React.memo(
|
||||||
style={{
|
style={{
|
||||||
marginTop: StyleConstants.Spacing.S,
|
marginTop: StyleConstants.Spacing.S,
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
marginBottom: StyleConstants.Spacing.L,
|
||||||
color: colors.primaryDefault
|
color: colors.secondary
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('empty.success.message')}
|
{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 TimelineActioned from '@components/Timeline/Shared/Actioned'
|
||||||
import TimelineActions from '@components/Timeline/Shared/Actions'
|
import TimelineActions from '@components/Timeline/Shared/Actions'
|
||||||
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
|
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
|
||||||
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
|
import TimelineAvatar from '@components/Timeline/Shared/Avatar'
|
||||||
import TimelineCard from '@components/Timeline/Shared/Card'
|
import TimelineCard from '@components/Timeline/Shared/Card'
|
||||||
import TimelineContent from '@components/Timeline/Shared/Content'
|
import TimelineContent from '@components/Timeline/Shared/Content'
|
||||||
// @ts-ignore
|
|
||||||
import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotification'
|
import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotification'
|
||||||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
@ -15,13 +16,14 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { isEqual, uniqBy } from 'lodash'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import React, { useCallback, useRef } from 'react'
|
import { Pressable, View } from 'react-native'
|
||||||
import { Platform, Pressable, View } from 'react-native'
|
|
||||||
import { useSelector } from 'react-redux'
|
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 TimelineFiltered, { shouldFilter } from './Shared/Filtered'
|
||||||
import TimelineFullConversation from './Shared/FullConversation'
|
import TimelineFullConversation from './Shared/FullConversation'
|
||||||
|
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
notification: Mastodon.Notification
|
notification: Mastodon.Notification
|
||||||
|
@ -34,6 +36,17 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||||
queryKey,
|
queryKey,
|
||||||
highlighted = false
|
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 }>({
|
const copiableContent = useRef<{ content: string; complete: boolean }>({
|
||||||
content: '',
|
content: '',
|
||||||
complete: false
|
complete: false
|
||||||
|
@ -51,13 +64,9 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const actualAccount = notification.status ? notification.status.account : notification.account
|
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
analytics('timeline_notification_press')
|
|
||||||
notification.status &&
|
notification.status &&
|
||||||
navigation.push('Tab-Shared-Toot', {
|
navigation.push('Tab-Shared-Toot', {
|
||||||
toot: notification.status,
|
toot: notification.status,
|
||||||
|
@ -69,11 +78,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notification.type !== 'mention' ? (
|
{notification.type !== 'mention' ? (
|
||||||
<TimelineActioned
|
<TimelineActioned action={notification.type} isNotification account={account} />
|
||||||
action={notification.type}
|
|
||||||
account={notification.account}
|
|
||||||
notification
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -88,8 +93,8 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||||
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
|
<TimelineAvatar account={account} />
|
||||||
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
|
<TimelineHeaderNotification notification={notification} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{notification.status ? (
|
{notification.status ? (
|
||||||
|
@ -99,75 +104,92 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{notification.status.content.length > 0 ? (
|
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
|
||||||
<TimelineContent status={notification.status} highlighted={highlighted} />
|
<TimelinePoll />
|
||||||
) : null}
|
<TimelineAttachment />
|
||||||
{notification.status.poll ? (
|
<TimelineCard />
|
||||||
<TimelinePoll
|
<TimelineFullConversation />
|
||||||
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} />
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{notification.status ? (
|
<TimelineActions />
|
||||||
<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}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Platform.OS === 'android' ? (
|
const mShare = menuShare({
|
||||||
<Pressable
|
visibility: notification.status?.visibility,
|
||||||
style={{
|
type: 'status',
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
url: notification.status?.url || notification.status?.uri,
|
||||||
backgroundColor: colors.backgroundDefault,
|
copiableContent
|
||||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
})
|
||||||
|
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()}
|
<ContextMenu.Root>
|
||||||
</Pressable>
|
<ContextMenu.Trigger>
|
||||||
) : (
|
<Pressable
|
||||||
<TimelineContextMenu
|
style={{
|
||||||
copiableContent={copiableContent}
|
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||||
status={notification.status}
|
backgroundColor: colors.backgroundDefault,
|
||||||
queryKey={queryKey}
|
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
||||||
>
|
}}
|
||||||
<Pressable
|
onPress={onPress}
|
||||||
style={{
|
onLongPress={() => {}}
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
children={main()}
|
||||||
backgroundColor: colors.backgroundDefault,
|
/>
|
||||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
</ContextMenu.Trigger>
|
||||||
}}
|
|
||||||
onPress={onPress}
|
<ContextMenu.Content>
|
||||||
onLongPress={() => {}}
|
{mShare.map((mGroup, index) => (
|
||||||
>
|
<ContextMenu.Group key={index}>
|
||||||
{main()}
|
{mGroup.map(menu => (
|
||||||
</Pressable>
|
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||||
</TimelineContextMenu>
|
<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 Icon from '@components/Icon'
|
||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
@ -6,167 +5,164 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
||||||
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
|
isNotification?: boolean
|
||||||
notification?: boolean
|
account?: Mastodon.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineActioned = React.memo(
|
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||||
({ account, action, notification = false }: Props) => {
|
const { status } = useContext(StatusContext)
|
||||||
const { t } = useTranslation('componentTimeline')
|
const account = isNotification ? rest.account : status?.account
|
||||||
const { colors } = useTheme()
|
if (!status || !account) return null
|
||||||
const navigation =
|
|
||||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
|
||||||
const name = account?.display_name || account?.username
|
|
||||||
const iconColor = colors.primaryDefault
|
|
||||||
|
|
||||||
const content = (content: string) => (
|
const { t } = useTranslation('componentTimeline')
|
||||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
const { colors } = useTheme()
|
||||||
)
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
const name = account?.display_name || account?.username
|
||||||
|
const iconColor = colors.primaryDefault
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const content = (content: string) => (
|
||||||
analytics('timeline_shared_actioned_press', { action })
|
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||||
navigation.push('Tab-Shared-Account', { account })
|
)
|
||||||
}, [])
|
|
||||||
|
|
||||||
const children = () => {
|
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
||||||
switch (action) {
|
|
||||||
case 'pinned':
|
const children = () => {
|
||||||
return (
|
switch (action) {
|
||||||
<>
|
case 'pinned':
|
||||||
<Icon
|
return (
|
||||||
name='Anchor'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='Anchor'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
{content(t('shared.actioned.pinned'))}
|
style={styles.icon}
|
||||||
</>
|
/>
|
||||||
)
|
{content(t('shared.actioned.pinned'))}
|
||||||
case 'favourite':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'favourite':
|
||||||
<Icon
|
return (
|
||||||
name='Heart'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='Heart'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
<Pressable onPress={onPress}>
|
style={styles.icon}
|
||||||
{content(t('shared.actioned.favourite', { name }))}
|
/>
|
||||||
</Pressable>
|
<Pressable onPress={onPress}>
|
||||||
</>
|
{content(t('shared.actioned.favourite', { name }))}
|
||||||
)
|
</Pressable>
|
||||||
case 'follow':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'follow':
|
||||||
<Icon
|
return (
|
||||||
name='UserPlus'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='UserPlus'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
<Pressable onPress={onPress}>
|
style={styles.icon}
|
||||||
{content(t('shared.actioned.follow', { name }))}
|
/>
|
||||||
</Pressable>
|
<Pressable onPress={onPress}>
|
||||||
</>
|
{content(t('shared.actioned.follow', { name }))}
|
||||||
)
|
</Pressable>
|
||||||
case 'follow_request':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'follow_request':
|
||||||
<Icon
|
return (
|
||||||
name='UserPlus'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='UserPlus'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
<Pressable onPress={onPress}>
|
style={styles.icon}
|
||||||
{content(t('shared.actioned.follow_request', { name }))}
|
/>
|
||||||
</Pressable>
|
<Pressable onPress={onPress}>
|
||||||
</>
|
{content(t('shared.actioned.follow_request', { name }))}
|
||||||
)
|
</Pressable>
|
||||||
case 'poll':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'poll':
|
||||||
<Icon
|
return (
|
||||||
name='BarChart2'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='BarChart2'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
{content(t('shared.actioned.poll'))}
|
style={styles.icon}
|
||||||
</>
|
/>
|
||||||
)
|
{content(t('shared.actioned.poll'))}
|
||||||
case 'reblog':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'reblog':
|
||||||
<Icon
|
return (
|
||||||
name='Repeat'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='Repeat'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
<Pressable onPress={onPress}>
|
style={styles.icon}
|
||||||
{content(
|
/>
|
||||||
notification
|
<Pressable onPress={onPress}>
|
||||||
? t('shared.actioned.reblog.notification', { name })
|
{content(
|
||||||
: t('shared.actioned.reblog.default', { name })
|
isNotification
|
||||||
)}
|
? t('shared.actioned.reblog.notification', { name })
|
||||||
</Pressable>
|
: t('shared.actioned.reblog.default', { name })
|
||||||
</>
|
)}
|
||||||
)
|
</Pressable>
|
||||||
case 'status':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'status':
|
||||||
<Icon
|
return (
|
||||||
name='Activity'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='Activity'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
<Pressable onPress={onPress}>
|
style={styles.icon}
|
||||||
{content(t('shared.actioned.status', { name }))}
|
/>
|
||||||
</Pressable>
|
<Pressable onPress={onPress}>
|
||||||
</>
|
{content(t('shared.actioned.status', { name }))}
|
||||||
)
|
</Pressable>
|
||||||
case 'update':
|
</>
|
||||||
return (
|
)
|
||||||
<>
|
case 'update':
|
||||||
<Icon
|
return (
|
||||||
name='BarChart2'
|
<>
|
||||||
size={StyleConstants.Font.Size.S}
|
<Icon
|
||||||
color={iconColor}
|
name='BarChart2'
|
||||||
style={styles.icon}
|
size={StyleConstants.Font.Size.S}
|
||||||
/>
|
color={iconColor}
|
||||||
{content(t('shared.actioned.update'))}
|
style={styles.icon}
|
||||||
</>
|
/>
|
||||||
)
|
{content(t('shared.actioned.update'))}
|
||||||
default:
|
</>
|
||||||
return <></>
|
)
|
||||||
}
|
default:
|
||||||
|
return <></>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: StyleConstants.Spacing.S,
|
marginBottom: StyleConstants.Spacing.S,
|
||||||
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
|
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
>
|
children={children()}
|
||||||
{children()}
|
/>
|
||||||
</View>
|
)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
@ -11,32 +10,22 @@ import {
|
||||||
QueryKeyTimeline,
|
QueryKeyTimeline,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
|
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import { uniqBy } from 'lodash'
|
||||||
|
import React, { useCallback, useContext, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineActions: React.FC = () => {
|
||||||
queryKey: QueryKeyTimeline
|
const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } =
|
||||||
rootQueryKey?: QueryKeyTimeline
|
useContext(StatusContext)
|
||||||
highlighted: boolean
|
if (!queryKey || !status || disableDetails) return null
|
||||||
status: Mastodon.Status
|
|
||||||
ownAccount?: boolean
|
|
||||||
accts: Mastodon.Account['acct'][] // When replying to conversations
|
|
||||||
reblog: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineActions: React.FC<Props> = ({
|
|
||||||
queryKey,
|
|
||||||
rootQueryKey,
|
|
||||||
highlighted,
|
|
||||||
status,
|
|
||||||
ownAccount = false,
|
|
||||||
accts,
|
|
||||||
reblog
|
|
||||||
}) => {
|
|
||||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
|
@ -84,11 +73,14 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||||
const onPressReply = useCallback(() => {
|
const onPressReply = useCallback(() => {
|
||||||
analytics('timeline_shared_actions_reply_press', {
|
const accts = uniqBy(
|
||||||
page: queryKey[1].page,
|
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||||
count: status.replies_count
|
.concat(status.mentions)
|
||||||
})
|
.filter(d => d?.id !== instanceAccount?.id),
|
||||||
|
d => d?.id
|
||||||
|
).map(d => d?.acct)
|
||||||
navigation.navigate('Screen-Compose', {
|
navigation.navigate('Screen-Compose', {
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
incomingStatus: status,
|
incomingStatus: status,
|
||||||
|
@ -112,17 +104,12 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
(selectedIndex: number) => {
|
(selectedIndex: number) => {
|
||||||
switch (selectedIndex) {
|
switch (selectedIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
analytics('timeline_shared_actions_reblog_public_press', {
|
|
||||||
page: queryKey[1].page,
|
|
||||||
count: status.reblogs_count,
|
|
||||||
current: status.reblogged
|
|
||||||
})
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
property: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
currentValue: status.reblogged,
|
||||||
|
@ -133,17 +120,12 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
analytics('timeline_shared_actions_reblog_unlisted_press', {
|
|
||||||
page: queryKey[1].page,
|
|
||||||
count: status.reblogs_count,
|
|
||||||
current: status.reblogged
|
|
||||||
})
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
property: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
currentValue: status.reblogged,
|
||||||
|
@ -157,17 +139,12 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
analytics('timeline_shared_actions_reblog_press', {
|
|
||||||
page: queryKey[1].page,
|
|
||||||
count: status.reblogs_count,
|
|
||||||
current: status.reblogged
|
|
||||||
})
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
property: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
currentValue: status.reblogged,
|
||||||
|
@ -179,17 +156,12 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}, [status.reblogged, status.reblogs_count])
|
||||||
const onPressFavourite = useCallback(() => {
|
const onPressFavourite = useCallback(() => {
|
||||||
analytics('timeline_shared_actions_favourite_press', {
|
|
||||||
page: queryKey[1].page,
|
|
||||||
count: status.favourites_count,
|
|
||||||
current: status.favourited
|
|
||||||
})
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'favourited',
|
property: 'favourited',
|
||||||
currentValue: status.favourited,
|
currentValue: status.favourited,
|
||||||
|
@ -199,16 +171,12 @@ const TimelineActions: React.FC<Props> = ({
|
||||||
})
|
})
|
||||||
}, [status.favourited, status.favourites_count])
|
}, [status.favourited, status.favourites_count])
|
||||||
const onPressBookmark = useCallback(() => {
|
const onPressBookmark = useCallback(() => {
|
||||||
analytics('timeline_shared_actions_bookmark_press', {
|
|
||||||
page: queryKey[1].page,
|
|
||||||
current: status.bookmarked
|
|
||||||
})
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'bookmarked',
|
property: 'bookmarked',
|
||||||
currentValue: status.bookmarked,
|
currentValue: status.bookmarked,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import AttachmentAudio from '@components/Timeline/Shared/Attachment/Audio'
|
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 { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import React, { useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineAttachment = () => {
|
||||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
const { status, disableDetails } = useContext(StatusContext)
|
||||||
}
|
if (
|
||||||
|
!status ||
|
||||||
|
disableDetails ||
|
||||||
|
!Array.isArray(status.media_attachments) ||
|
||||||
|
!status.media_attachments.length
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
const TimelineAttachment = React.memo(
|
const { t } = useTranslation('componentTimeline')
|
||||||
({ status }: Props) => {
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
|
||||||
|
|
||||||
const account = useSelector(
|
const account = useSelector(
|
||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
(prev, next) =>
|
(prev, next) =>
|
||||||
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
|
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
|
||||||
)
|
)
|
||||||
const defaultSensitive = () => {
|
const defaultSensitive = () => {
|
||||||
switch (account.preferences['reading:expand:media']) {
|
switch (account.preferences['reading:expand:media']) {
|
||||||
case 'show_all':
|
case 'show_all':
|
||||||
return false
|
return false
|
||||||
case 'hide_all':
|
case 'hide_all':
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return status.sensitive
|
return status.sensitive
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
|
}
|
||||||
|
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] =
|
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
|
||||||
status.media_attachments
|
.map(attachment => {
|
||||||
.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) {
|
switch (attachment.type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return {
|
return (
|
||||||
id: attachment.id,
|
<AttachmentImage
|
||||||
preview_url: attachment.preview_url,
|
key={index}
|
||||||
url: attachment.url,
|
total={status.media_attachments.length}
|
||||||
remote_url: attachment.remote_url,
|
index={index}
|
||||||
blurhash: attachment.blurhash,
|
sensitiveShown={sensitiveShown}
|
||||||
width: attachment.meta?.original?.width,
|
image={attachment}
|
||||||
height: attachment.meta?.original?.height
|
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:
|
default:
|
||||||
if (
|
if (
|
||||||
attachment.preview_url?.endsWith('.jpg') ||
|
attachment.preview_url?.endsWith('.jpg') ||
|
||||||
|
@ -67,178 +155,74 @@ const TimelineAttachment = React.memo(
|
||||||
attachment.remote_url?.endsWith('.png') ||
|
attachment.remote_url?.endsWith('.png') ||
|
||||||
attachment.remote_url?.endsWith('.gif')
|
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 (
|
return (
|
||||||
<AttachmentImage
|
<AttachmentImage
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
|
// @ts-ignore
|
||||||
image={attachment}
|
image={attachment}
|
||||||
navigateToImagesViewer={navigateToImagesViewer}
|
navigateToImagesViewer={navigateToImagesViewer}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'video':
|
} else {
|
||||||
return (
|
return (
|
||||||
<AttachmentVideo
|
<AttachmentUnsupported
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
video={attachment}
|
attachment={attachment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'gifv':
|
}
|
||||||
return (
|
}
|
||||||
<AttachmentVideo
|
})}
|
||||||
key={index}
|
</View>
|
||||||
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>
|
|
||||||
|
|
||||||
{defaultSensitive() &&
|
{defaultSensitive() &&
|
||||||
(sensitiveShown ? (
|
(sensitiveShown ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: '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>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
type='icon'
|
type='text'
|
||||||
content='EyeOff'
|
content={t('shared.attachment.sensitive.button')}
|
||||||
round
|
|
||||||
overlay
|
overlay
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('timeline_shared_attachment_blurview_press_hide')
|
layoutAnimation()
|
||||||
setSensitiveShown(true)
|
setSensitiveShown(false)
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: StyleConstants.Spacing.S * 2,
|
|
||||||
left: StyleConstants.Spacing.S
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</Pressable>
|
||||||
</View>
|
) : (
|
||||||
)
|
<Button
|
||||||
},
|
type='icon'
|
||||||
(prev, next) => {
|
content='EyeOff'
|
||||||
let isEqual = true
|
round
|
||||||
|
overlay
|
||||||
if (prev.status.media_attachments.length !== next.status.media_attachments.length) {
|
onPress={() => {
|
||||||
isEqual = false
|
setSensitiveShown(true)
|
||||||
return isEqual
|
haptics('Light')
|
||||||
}
|
}}
|
||||||
|
style={{
|
||||||
prev.status.media_attachments.forEach((attachment, index) => {
|
position: 'absolute',
|
||||||
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) {
|
top: StyleConstants.Spacing.S * 2,
|
||||||
isEqual = false
|
left: StyleConstants.Spacing.S
|
||||||
}
|
}}
|
||||||
})
|
/>
|
||||||
|
))}
|
||||||
return isEqual
|
</View>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export default TimelineAttachment
|
export default TimelineAttachment
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { Slider } from '@sharcoux/slider'
|
import { Slider } from '@sharcoux/slider'
|
||||||
|
@ -25,7 +24,6 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
|
||||||
const [audioPlaying, setAudioPlaying] = useState(false)
|
const [audioPlaying, setAudioPlaying] = useState(false)
|
||||||
const [audioPosition, setAudioPosition] = useState(0)
|
const [audioPosition, setAudioPosition] = useState(0)
|
||||||
const playAudio = useCallback(async () => {
|
const playAudio = useCallback(async () => {
|
||||||
analytics('timeline_shared_attachment_audio_play_press', { id: audio.id })
|
|
||||||
if (!audioPlayer) {
|
if (!audioPlayer) {
|
||||||
const { sound } = await Audio.Sound.createAsync(
|
const { sound } = await Audio.Sound.createAsync(
|
||||||
{ uri: audio.url },
|
{ uri: audio.url },
|
||||||
|
@ -41,7 +39,6 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
|
||||||
}
|
}
|
||||||
}, [audioPlayer, audioPosition])
|
}, [audioPlayer, audioPosition])
|
||||||
const pauseAudio = useCallback(async () => {
|
const pauseAudio = useCallback(async () => {
|
||||||
analytics('timeline_shared_attachment_audio_pause_press', { id: audio.id })
|
|
||||||
audioPlayer!.pauseAsync()
|
audioPlayer!.pauseAsync()
|
||||||
setAudioPlaying(false)
|
setAudioPlaying(false)
|
||||||
}, [audioPlayer])
|
}, [audioPlayer])
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -34,27 +33,17 @@ const AttachmentImage = ({
|
||||||
hidden={sensitiveShown}
|
hidden={sensitiveShown}
|
||||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||||
blurhash={image.blurhash}
|
blurhash={image.blurhash}
|
||||||
onPress={() => {
|
onPress={() => navigateToImagesViewer(image.id)}
|
||||||
analytics('timeline_shared_attachment_image_press', {
|
|
||||||
id: image.id
|
|
||||||
})
|
|
||||||
navigateToImagesViewer(image.id)
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
aspectRatio:
|
aspectRatio:
|
||||||
total > 1 ||
|
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height
|
||||||
!image.meta?.original?.width ||
|
|
||||||
!image.meta?.original?.height
|
|
||||||
? attachmentAspectRatio({ total, index })
|
? attachmentAspectRatio({ total, index })
|
||||||
: image.meta.original.height / image.meta.original.width > 1
|
: image.meta.original.height / image.meta.original.width > 1
|
||||||
? 1
|
? 1
|
||||||
: image.meta.original.width / image.meta.original.height
|
: image.meta.original.width / image.meta.original.height
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AttachmentAltText
|
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
|
||||||
sensitiveShown={sensitiveShown}
|
|
||||||
text={image.description}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import openLink from '@components/openLink'
|
import openLink from '@components/openLink'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
@ -18,12 +17,7 @@ export interface Props {
|
||||||
attachment: Mastodon.AttachmentUnknown
|
attachment: Mastodon.AttachmentUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentUnsupported: React.FC<Props> = ({
|
const AttachmentUnsupported: React.FC<Props> = ({ total, index, sensitiveShown, attachment }) => {
|
||||||
total,
|
|
||||||
index,
|
|
||||||
sensitiveShown,
|
|
||||||
attachment
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
@ -55,9 +49,7 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: StyleConstants.Spacing.S,
|
marginBottom: StyleConstants.Spacing.S,
|
||||||
color: attachment.blurhash
|
color: attachment.blurhash ? colors.backgroundDefault : colors.primaryDefault
|
||||||
? colors.backgroundDefault
|
|
||||||
: colors.primaryDefault
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('shared.attachment.unsupported.text')}
|
{t('shared.attachment.unsupported.text')}
|
||||||
|
@ -69,17 +61,13 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
||||||
size='S'
|
size='S'
|
||||||
overlay
|
overlay
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('timeline_shared_attachment_unsupported_press')
|
|
||||||
attachment.remote_url && openLink(attachment.remote_url)
|
attachment.remote_url && openLink(attachment.remote_url)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<AttachmentAltText
|
<AttachmentAltText sensitiveShown={sensitiveShown} text={attachment.description} />
|
||||||
sensitiveShown={sensitiveShown}
|
|
||||||
text={attachment.description}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { AppState, AppStateStatus, Pressable, View } from 'react-native'
|
import { AppState, AppStateStatus, Pressable, View } from 'react-native'
|
||||||
import { Blurhash } from 'react-native-blurhash'
|
import { Blurhash } from 'react-native-blurhash'
|
||||||
import attachmentAspectRatio from './aspectRatio'
|
import attachmentAspectRatio from './aspectRatio'
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import AttachmentAltText from './AltText'
|
import AttachmentAltText from './AltText'
|
||||||
import { Platform } from 'expo-modules-core'
|
import { Platform } from 'expo-modules-core'
|
||||||
|
|
||||||
|
@ -30,13 +29,6 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
const [videoPosition, setVideoPosition] = useState<number>(0)
|
const [videoPosition, setVideoPosition] = useState<number>(0)
|
||||||
const [videoResizeMode, setVideoResizeMode] = useState<ResizeMode>(ResizeMode.COVER)
|
const [videoResizeMode, setVideoResizeMode] = useState<ResizeMode>(ResizeMode.COVER)
|
||||||
const playOnPress = useCallback(async () => {
|
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)
|
setVideoLoading(true)
|
||||||
if (!videoLoaded) {
|
if (!videoLoaded) {
|
||||||
await videoPlayer.current?.loadAsync({ uri: video.url })
|
await videoPlayer.current?.loadAsync({ uri: video.url })
|
||||||
|
|
|
@ -1,56 +1,49 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey?: QueryKeyTimeline
|
account?: Mastodon.Account
|
||||||
account: Mastodon.Account
|
|
||||||
highlighted: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineAvatar = React.memo(
|
const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
||||||
({ queryKey, account, highlighted }: Props) => {
|
const { status, highlighted, disableOnPress } = useContext(StatusContext)
|
||||||
const { t } = useTranslation('componentTimeline')
|
const actualAccount = account || status?.account
|
||||||
const navigation =
|
if (!actualAccount) return null
|
||||||
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 })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
const { t } = useTranslation('componentTimeline')
|
||||||
<GracefullyImage
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
{...(highlighted && {
|
|
||||||
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
|
return (
|
||||||
name: account.display_name
|
<GracefullyImage
|
||||||
}),
|
{...(highlighted && {
|
||||||
accessibilityHint: t('shared.avatar.accessibilityHint', {
|
accessibilityLabel: t('shared.avatar.accessibilityLabel', {
|
||||||
name: account.display_name
|
name: actualAccount.display_name
|
||||||
})
|
}),
|
||||||
})}
|
accessibilityHint: t('shared.avatar.accessibilityHint', {
|
||||||
onPress={onPress}
|
name: actualAccount.display_name
|
||||||
uri={{ original: account?.avatar, static: account?.avatar_static }}
|
})
|
||||||
dimension={{
|
})}
|
||||||
width: StyleConstants.Avatar.M,
|
onPress={() =>
|
||||||
height: StyleConstants.Avatar.M
|
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
|
||||||
}}
|
}
|
||||||
style={{
|
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
||||||
borderRadius: StyleConstants.Avatar.M,
|
dimension={{
|
||||||
overflow: 'hidden',
|
width: StyleConstants.Avatar.M,
|
||||||
marginRight: StyleConstants.Spacing.S
|
height: StyleConstants.Avatar.M
|
||||||
}}
|
}}
|
||||||
/>
|
style={{
|
||||||
)
|
borderRadius: StyleConstants.Avatar.M,
|
||||||
}
|
overflow: 'hidden',
|
||||||
)
|
marginRight: StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default TimelineAvatar
|
export default TimelineAvatar
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import ComponentAccount from '@components/Account'
|
import ComponentAccount from '@components/Account'
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import openLink from '@components/openLink'
|
import openLink from '@components/openLink'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
@ -10,23 +9,23 @@ import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
import { useStatusQuery } from '@utils/queryHooks/status'
|
import { useStatusQuery } from '@utils/queryHooks/status'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import TimelineDefault from '../Default'
|
import TimelineDefault from '../Default'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineCard: React.FC = () => {
|
||||||
card: Pick<Mastodon.Card, 'url' | 'image' | 'blurhash' | 'title' | 'description'>
|
const { status, spoilerHidden, disableDetails } = useContext(StatusContext)
|
||||||
}
|
if (!status || !status.card) return null
|
||||||
|
|
||||||
const TimelineCard = React.memo(({ card }: Props) => {
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const isStatus = matchStatus(card.url)
|
const isStatus = matchStatus(status.card.url)
|
||||||
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
||||||
const isAccount = matchAccount(card.url)
|
const isAccount = matchAccount(status.card.url)
|
||||||
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
|
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
|
||||||
|
|
||||||
const searchQuery = useSearchQuery({
|
const searchQuery = useSearchQuery({
|
||||||
|
@ -39,7 +38,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
if (isStatus.sameInstance) {
|
if (isStatus.sameInstance) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
return card.url
|
return status.card.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isAccount) {
|
if (isAccount) {
|
||||||
|
@ -50,7 +49,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
return isAccount.username
|
return isAccount.username
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return card.url
|
return status.card.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
|
@ -130,17 +129,17 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isStatus && foundStatus) {
|
if (isStatus && foundStatus) {
|
||||||
return <TimelineDefault item={foundStatus} disableDetails disableOnPress origin='card' />
|
return <TimelineDefault item={foundStatus} disableDetails disableOnPress />
|
||||||
}
|
}
|
||||||
if (isAccount && foundAccount) {
|
if (isAccount && foundAccount) {
|
||||||
return <ComponentAccount account={foundAccount} origin='card' />
|
return <ComponentAccount account={foundAccount} />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{card.image ? (
|
{status.card?.image ? (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
uri={{ original: card.image }}
|
uri={{ original: status.card.image }}
|
||||||
blurhash={card.blurhash}
|
blurhash={status.card.blurhash}
|
||||||
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
|
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
|
||||||
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
|
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
|
||||||
/>
|
/>
|
||||||
|
@ -156,9 +155,9 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
fontWeight='Bold'
|
fontWeight='Bold'
|
||||||
testID='title'
|
testID='title'
|
||||||
>
|
>
|
||||||
{card.title}
|
{status.card?.title}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
{card.description ? (
|
{status.card?.description ? (
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='S'
|
fontStyle='S'
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
|
@ -168,17 +167,19 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
}}
|
}}
|
||||||
testID='description'
|
testID='description'
|
||||||
>
|
>
|
||||||
{card.description}
|
{status.card.description}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
) : null}
|
) : null}
|
||||||
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
|
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
|
||||||
{card.url}
|
{status.card?.url}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spoilerHidden || disableDetails) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessible
|
accessible
|
||||||
|
@ -193,13 +194,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderColor: colors.border
|
borderColor: colors.border
|
||||||
}}
|
}}
|
||||||
onPress={async () => {
|
onPress={async () => status.card && (await openLink(status.card.url, navigation))}
|
||||||
analytics('timeline_shared_card_press')
|
children={cardContent()}
|
||||||
await openLink(card.url, navigation)
|
|
||||||
}}
|
|
||||||
children={cardContent}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
export default TimelineCard
|
export default TimelineCard
|
||||||
|
|
|
@ -1,52 +1,36 @@
|
||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & {
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
mentions?: Mastodon.Status['mentions']
|
|
||||||
tags?: Mastodon.Status['tags']
|
|
||||||
}
|
|
||||||
highlighted?: boolean
|
|
||||||
disableDetails?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineContent = React.memo(
|
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
|
||||||
({ status, highlighted = false, disableDetails = false }: Props) => {
|
const { status, highlighted, disableDetails } = useContext(StatusContext)
|
||||||
const { t } = useTranslation('componentTimeline')
|
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
|
||||||
|
|
||||||
return (
|
const { t } = useTranslation('componentTimeline')
|
||||||
<>
|
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||||
{status.spoiler_text ? (
|
|
||||||
<>
|
return (
|
||||||
<ParseHTML
|
<>
|
||||||
content={status.spoiler_text}
|
{status.spoiler_text?.length ? (
|
||||||
size={highlighted ? 'L' : 'M'}
|
<>
|
||||||
adaptiveSize
|
<ParseHTML
|
||||||
emojis={status.emojis}
|
content={status.spoiler_text}
|
||||||
mentions={status.mentions}
|
size={highlighted ? 'L' : 'M'}
|
||||||
tags={status.tags}
|
adaptiveSize
|
||||||
numberOfLines={999}
|
emojis={status.emojis}
|
||||||
highlighted={highlighted}
|
mentions={status.mentions}
|
||||||
disableDetails={disableDetails}
|
tags={status.tags}
|
||||||
/>
|
numberOfLines={999}
|
||||||
<ParseHTML
|
highlighted={highlighted}
|
||||||
content={status.content}
|
disableDetails={disableDetails}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ParseHTML
|
<ParseHTML
|
||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
|
@ -54,16 +38,27 @@ const TimelineContent = React.memo(
|
||||||
emojis={status.emojis}
|
emojis={status.emojis}
|
||||||
mentions={status.mentions}
|
mentions={status.mentions}
|
||||||
tags={status.tags}
|
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}
|
disableDetails={disableDetails}
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
</>
|
) : (
|
||||||
)
|
<ParseHTML
|
||||||
},
|
content={status.content}
|
||||||
(prev, next) =>
|
size={highlighted ? 'L' : 'M'}
|
||||||
prev.status.content === next.status.content &&
|
adaptiveSize
|
||||||
prev.status.spoiler_text === next.status.spoiler_text
|
emojis={status.emojis}
|
||||||
)
|
mentions={status.mentions}
|
||||||
|
tags={status.tags}
|
||||||
|
numberOfLines={highlighted ? 999 : undefined}
|
||||||
|
disableDetails={disableDetails}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default TimelineContent
|
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 CustomText from '@components/Text'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
@ -6,133 +5,92 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
|
import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineFeedback = () => {
|
||||||
status: Pick<
|
const { status, highlighted } = useContext(StatusContext)
|
||||||
Mastodon.Status,
|
if (!status || !highlighted) return null
|
||||||
'id' | 'edited_at' | 'reblogs_count' | 'favourites_count'
|
|
||||||
>
|
|
||||||
highlighted: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineFeedback = React.memo(
|
const { t } = useTranslation('componentTimeline')
|
||||||
({ status, highlighted }: Props) => {
|
const { colors } = useTheme()
|
||||||
if (!highlighted) {
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { data } = useStatusHistory({
|
||||||
const { colors } = useTheme()
|
id: status.id,
|
||||||
const navigation =
|
options: { enabled: status.edited_at !== undefined }
|
||||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
})
|
||||||
|
|
||||||
const { data } = useStatusHistory({
|
return (
|
||||||
id: status.id,
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||||
options: { enabled: status.edited_at !== undefined }
|
<View style={{ flexDirection: 'row' }}>
|
||||||
})
|
{status.reblogs_count > 0 ? (
|
||||||
|
<CustomText
|
||||||
return (
|
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
count: status.reblogs_count
|
||||||
<View style={{ flexDirection: 'row' }}>
|
})}
|
||||||
{status.reblogs_count > 0 ? (
|
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
|
||||||
<CustomText
|
accessibilityRole='button'
|
||||||
accessibilityLabel={t(
|
style={[styles.text, { color: colors.blue }]}
|
||||||
'shared.actionsUsers.reblogged_by.accessibilityLabel',
|
onPress={() =>
|
||||||
{
|
navigation.push('Tab-Shared-Users', {
|
||||||
count: status.reblogs_count
|
reference: 'statuses',
|
||||||
}
|
id: status.id,
|
||||||
)}
|
type: 'reblogged_by',
|
||||||
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', {
|
|
||||||
count: status.reblogs_count
|
count: status.reblogs_count
|
||||||
})}
|
})
|
||||||
</CustomText>
|
}
|
||||||
) : null}
|
>
|
||||||
{status.favourites_count > 0 ? (
|
{t('shared.actionsUsers.reblogged_by.text', {
|
||||||
<CustomText
|
count: status.reblogs_count
|
||||||
accessibilityLabel={t(
|
})}
|
||||||
'shared.actionsUsers.favourited_by.accessibilityLabel',
|
</CustomText>
|
||||||
{
|
) : null}
|
||||||
count: status.reblogs_count
|
{status.favourites_count > 0 ? (
|
||||||
}
|
<CustomText
|
||||||
)}
|
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
|
||||||
accessibilityHint={t(
|
count: status.reblogs_count
|
||||||
'shared.actionsUsers.favourited_by.accessibilityHint'
|
})}
|
||||||
)}
|
accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
style={[styles.text, { color: colors.blue }]}
|
style={[styles.text, { color: colors.blue }]}
|
||||||
onPress={() => {
|
onPress={() =>
|
||||||
analytics('timeline_shared_feedback_press_favourite', {
|
navigation.push('Tab-Shared-Users', {
|
||||||
count: status.favourites_count
|
reference: 'statuses',
|
||||||
})
|
id: status.id,
|
||||||
navigation.push('Tab-Shared-Users', {
|
type: 'favourited_by',
|
||||||
reference: 'statuses',
|
|
||||||
id: status.id,
|
|
||||||
type: 'favourited_by',
|
|
||||||
count: status.favourites_count
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('shared.actionsUsers.favourited_by.text', {
|
|
||||||
count: status.favourites_count
|
count: status.favourites_count
|
||||||
})}
|
})
|
||||||
</CustomText>
|
}
|
||||||
) : null}
|
>
|
||||||
</View>
|
{t('shared.actionsUsers.favourited_by.text', {
|
||||||
<View>
|
count: status.favourites_count
|
||||||
{data && data.length > 1 ? (
|
})}
|
||||||
<CustomText
|
</CustomText>
|
||||||
accessibilityLabel={t(
|
) : null}
|
||||||
'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>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
<View>
|
||||||
},
|
{data && data.length > 1 ? (
|
||||||
(prev, next) =>
|
<CustomText
|
||||||
prev.status.edited_at === next.status.edited_at &&
|
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
|
||||||
prev.status.reblogs_count === next.status.reblogs_count &&
|
count: data.length - 1
|
||||||
prev.status.favourites_count === next.status.favourites_count
|
})}
|
||||||
)
|
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({
|
const styles = StyleSheet.create({
|
||||||
text: {
|
text: {
|
||||||
|
|
|
@ -1,39 +1,32 @@
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineFullConversation = () => {
|
||||||
queryKey?: QueryKeyTimeline
|
const { queryKey, status, disableDetails } = useContext(StatusContext)
|
||||||
status: Mastodon.Status
|
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
|
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 Icon from '@components/Icon'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import {
|
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
QueryKeyTimeline,
|
|
||||||
useTimelineMutation
|
|
||||||
} from '@utils/queryHooks/timeline'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
import StatusContext from './Context'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
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 {
|
export interface Props {
|
||||||
queryKey: QueryKeyTimeline
|
|
||||||
conversation: Mastodon.Conversation
|
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 { colors, theme } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
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 (
|
return (
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||||
<View style={{ flex: 3 }}>
|
<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
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -116,8 +85,15 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center' }}
|
style={{ flex: 1, flexDirection: 'row', justifyContent: 'center' }}
|
||||||
onPress={actionOnPress}
|
onPress={() =>
|
||||||
children={actionChildren}
|
mutation.mutate({
|
||||||
|
type: 'deleteItem',
|
||||||
|
source: 'conversations',
|
||||||
|
queryKey,
|
||||||
|
id: conversation.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
children={<Icon name='Trash' color={colors.secondary} size={StyleConstants.Font.Size.L} />}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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 openLink from '@components/openLink'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
@ -20,9 +19,6 @@ const HeaderSharedApplication = React.memo(
|
||||||
fontStyle='S'
|
fontStyle='S'
|
||||||
accessibilityRole='link'
|
accessibilityRole='link'
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
analytics('timeline_shared_header_application_press', {
|
|
||||||
application
|
|
||||||
})
|
|
||||||
application.website && (await openLink(application.website))
|
application.website && (await openLink(application.website))
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
@ -8,41 +7,28 @@ import RelativeTime from '@components/RelativeTime'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
QueryKeyTimeline,
|
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
|
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { maxBy } from 'lodash'
|
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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelinePoll: React.FC = () => {
|
||||||
queryKey: QueryKeyTimeline
|
const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } =
|
||||||
rootQueryKey?: QueryKeyTimeline
|
useContext(StatusContext)
|
||||||
statusId: Mastodon.Status['id']
|
if (!queryKey || !status || !status.poll) return null
|
||||||
poll: NonNullable<Mastodon.Status['poll']>
|
const poll = status.poll
|
||||||
reblog: boolean
|
|
||||||
sameAccount: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelinePoll: React.FC<Props> = ({
|
|
||||||
queryKey,
|
|
||||||
rootQueryKey,
|
|
||||||
statusId,
|
|
||||||
poll,
|
|
||||||
reblog,
|
|
||||||
sameAccount
|
|
||||||
}) => {
|
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { t, i18n } = useTranslation('componentTimeline')
|
const { t, i18n } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const [allOptions, setAllOptions] = useState(
|
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
|
||||||
new Array(poll.options.length).fill(false)
|
|
||||||
)
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
const mutation = useTimelineMutation({
|
||||||
|
@ -82,18 +68,17 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
|
|
||||||
const pollButton = useMemo(() => {
|
const pollButton = useMemo(() => {
|
||||||
if (!poll.expired) {
|
if (!poll.expired) {
|
||||||
if (!sameAccount && !poll.voted) {
|
if (!ownAccount && !poll.voted) {
|
||||||
return (
|
return (
|
||||||
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() =>
|
||||||
analytics('timeline_shared_vote_vote_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: statusId,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
property: 'poll',
|
||||||
id: poll.id,
|
id: poll.id,
|
||||||
|
@ -101,7 +86,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
options: allOptions
|
options: allOptions
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.vote')}
|
content={t('shared.poll.meta.button.vote')}
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
|
@ -113,21 +98,20 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() =>
|
||||||
analytics('timeline_shared_vote_refresh_press')
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
id: statusId,
|
id: status.id,
|
||||||
reblog,
|
isReblog,
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
property: 'poll',
|
||||||
id: poll.id,
|
id: poll.id,
|
||||||
type: 'refresh'
|
type: 'refresh'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.refresh')}
|
content={t('shared.poll.meta.button.refresh')}
|
||||||
loading={mutation.isLoading}
|
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(
|
const isSelected = useCallback(
|
||||||
(index: number): string =>
|
(index: number): string =>
|
||||||
|
@ -154,20 +131,13 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const pollBodyDisallow = useMemo(() => {
|
const pollBodyDisallow = useMemo(() => {
|
||||||
const maxValue = maxBy(
|
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
||||||
poll.options,
|
|
||||||
option => option.votes_count
|
|
||||||
)?.votes_count
|
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<View
|
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
||||||
key={index}
|
|
||||||
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
|
||||||
>
|
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||||
<Icon
|
<Icon
|
||||||
style={{
|
style={{
|
||||||
paddingTop:
|
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
|
||||||
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
|
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginRight: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
name={
|
name={
|
||||||
|
@ -176,9 +146,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
}` as any
|
}` as any
|
||||||
}
|
}
|
||||||
size={StyleConstants.Font.Size.M}
|
size={StyleConstants.Font.Size.M}
|
||||||
color={
|
color={poll.own_votes?.includes(index) ? colors.blue : colors.disabled}
|
||||||
poll.own_votes?.includes(index) ? colors.blue : colors.disabled
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<CustomText style={{ flex: 1 }}>
|
<CustomText style={{ flex: 1 }}>
|
||||||
<ParseEmojis content={option.title} emojis={poll.emojis} />
|
<ParseEmojis content={option.title} emojis={poll.emojis} />
|
||||||
|
@ -194,11 +162,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{poll.votes_count
|
{poll.votes_count
|
||||||
? Math.round(
|
? Math.round((option.votes_count / (poll.voters_count || poll.votes_count)) * 100)
|
||||||
(option.votes_count /
|
|
||||||
(poll.voters_count || poll.votes_count)) *
|
|
||||||
100
|
|
||||||
)
|
|
||||||
: 0}
|
: 0}
|
||||||
%
|
%
|
||||||
</CustomText>
|
</CustomText>
|
||||||
|
@ -213,11 +177,9 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
marginTop: StyleConstants.Spacing.XS,
|
marginTop: StyleConstants.Spacing.XS,
|
||||||
marginBottom: StyleConstants.Spacing.S,
|
marginBottom: StyleConstants.Spacing.S,
|
||||||
width: `${Math.round(
|
width: `${Math.round(
|
||||||
(option.votes_count / (poll.voters_count || poll.votes_count)) *
|
(option.votes_count / (poll.voters_count || poll.votes_count)) * 100
|
||||||
100
|
|
||||||
)}%`,
|
)}%`,
|
||||||
backgroundColor:
|
backgroundColor: option.votes_count === maxValue ? colors.blue : colors.disabled
|
||||||
option.votes_count === maxValue ? colors.blue : colors.disabled
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -229,21 +191,15 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
key={index}
|
key={index}
|
||||||
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('timeline_shared_vote_option_press')
|
|
||||||
!allOptions[index] && haptics('Light')
|
!allOptions[index] && haptics('Light')
|
||||||
if (poll.multiple) {
|
if (poll.multiple) {
|
||||||
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
|
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
|
||||||
} else {
|
} else {
|
||||||
{
|
{
|
||||||
const otherOptions =
|
const otherOptions = allOptions[index] === false ? false : undefined
|
||||||
allOptions[index] === false ? false : undefined
|
|
||||||
setAllOptions(
|
setAllOptions(
|
||||||
allOptions.map((o, i) =>
|
allOptions.map((o, i) =>
|
||||||
i === index
|
i === index ? !o : otherOptions !== undefined ? otherOptions : o
|
||||||
? !o
|
|
||||||
: otherOptions !== undefined
|
|
||||||
? otherOptions
|
|
||||||
: o
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -253,8 +209,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||||
<Icon
|
<Icon
|
||||||
style={{
|
style={{
|
||||||
paddingTop:
|
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
|
||||||
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
|
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginRight: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
name={isSelected(index)}
|
name={isSelected(index)}
|
||||||
|
@ -271,13 +226,9 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
|
|
||||||
const pollVoteCounts = () => {
|
const pollVoteCounts = () => {
|
||||||
if (poll.voters_count !== null) {
|
if (poll.voters_count !== null) {
|
||||||
return (
|
return t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
|
||||||
t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
|
|
||||||
)
|
|
||||||
} else if (poll.votes_count !== null) {
|
} else if (poll.votes_count !== null) {
|
||||||
return (
|
return t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
|
||||||
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 (
|
return (
|
||||||
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
||||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
||||||
|
@ -308,10 +261,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pollButton}
|
{pollButton}
|
||||||
<CustomText
|
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
||||||
fontStyle='S'
|
|
||||||
style={{ flexShrink: 1, color: colors.secondary }}
|
|
||||||
>
|
|
||||||
{pollVoteCounts()}
|
{pollVoteCounts()}
|
||||||
{pollExpiration()}
|
{pollExpiration()}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import getLanguage from '@helpers/getLanguage'
|
import getLanguage from '@helpers/getLanguage'
|
||||||
|
@ -6,137 +5,119 @@ import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as Localization from 'expo-localization'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { Pressable } from 'react-native'
|
import { Pressable } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import detectLanguage from 'react-native-language-detection'
|
import detectLanguage from 'react-native-language-detection'
|
||||||
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
const TimelineTranslate = () => {
|
||||||
highlighted: boolean
|
const { status, highlighted } = useContext(StatusContext)
|
||||||
status: Pick<Mastodon.Status, 'language' | 'spoiler_text' | 'content' | 'emojis'>
|
if (!status || !highlighted) return null
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineTranslate = React.memo(
|
const { t } = useTranslation('componentTimeline')
|
||||||
({ highlighted, status }: Props) => {
|
const { colors } = useTheme()
|
||||||
if (!highlighted) {
|
|
||||||
return null
|
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 [detectedLanguage, setDetectedLanguage] = useState<string>('')
|
||||||
const { colors } = useTheme()
|
useEffect(() => {
|
||||||
|
const detect = async () => {
|
||||||
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
|
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
|
||||||
|
// No need to log language detection failure
|
||||||
for (const i in text) {
|
})
|
||||||
for (const emoji of status.emojis) {
|
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
|
||||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
|
|
||||||
}
|
|
||||||
text[i] = text[i]
|
|
||||||
.replace(/(<([^>]+)>)/gi, ' ')
|
|
||||||
.replace(/@.*? /gi, ' ')
|
|
||||||
.replace(/#.*? /gi, ' ')
|
|
||||||
.replace(/http(s):\/\/.*? /gi, ' ')
|
|
||||||
}
|
}
|
||||||
|
detect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
|
const settingsLanguage = getLanguage()
|
||||||
useEffect(() => {
|
const targetLanguage = settingsLanguage?.startsWith('en')
|
||||||
const detect = async () => {
|
? Localization.locale || settingsLanguage || 'en'
|
||||||
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
|
: settingsLanguage || Localization.locale || 'en'
|
||||||
// No need to log language detection failure
|
|
||||||
})
|
|
||||||
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
|
|
||||||
}
|
|
||||||
detect()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const settingsLanguage = getLanguage()
|
const [enabled, setEnabled] = useState(false)
|
||||||
const targetLanguage = settingsLanguage?.startsWith('en')
|
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||||
? Localization.locale || settingsLanguage || 'en'
|
source: detectedLanguage,
|
||||||
: settingsLanguage || Localization.locale || 'en'
|
target: targetLanguage,
|
||||||
|
text,
|
||||||
|
options: { enabled }
|
||||||
|
})
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState(false)
|
if (!detectedLanguage) {
|
||||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
return null
|
||||||
source: detectedLanguage,
|
}
|
||||||
target: targetLanguage,
|
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
|
||||||
text,
|
return null
|
||||||
options: { enabled }
|
}
|
||||||
})
|
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!detectedLanguage) {
|
return (
|
||||||
return null
|
<>
|
||||||
}
|
<Pressable
|
||||||
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
|
style={{
|
||||||
return null
|
flexDirection: 'row',
|
||||||
}
|
alignItems: 'center',
|
||||||
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
|
paddingVertical: StyleConstants.Spacing.S,
|
||||||
return null
|
paddingBottom: isSuccess ? 0 : undefined
|
||||||
}
|
}}
|
||||||
|
onPress={() => {
|
||||||
return (
|
if (enabled) {
|
||||||
<>
|
if (!isSuccess) {
|
||||||
<Pressable
|
refetch()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setEnabled(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CustomText
|
||||||
|
fontStyle='M'
|
||||||
|
style={{
|
||||||
|
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CustomText
|
{isError
|
||||||
fontStyle='M'
|
? t('shared.translate.failed')
|
||||||
style={{
|
: isSuccess
|
||||||
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
|
? typeof data?.error === 'string'
|
||||||
}}
|
? t(`shared.translate.${data.error}`)
|
||||||
>
|
: t('shared.translate.succeed', {
|
||||||
{isError
|
provider: data?.provider,
|
||||||
? t('shared.translate.failed')
|
source: data?.sourceLanguage
|
||||||
: isSuccess
|
})
|
||||||
? typeof data?.error === 'string'
|
: t('shared.translate.default')}
|
||||||
? t(`shared.translate.${data.error}`)
|
</CustomText>
|
||||||
: t('shared.translate.succeed', {
|
<CustomText>
|
||||||
provider: data?.provider,
|
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
|
||||||
source: data?.sourceLanguage
|
</CustomText>
|
||||||
})
|
{isLoading ? (
|
||||||
: t('shared.translate.default')}
|
<Circle
|
||||||
</CustomText>
|
size={StyleConstants.Font.Size.M}
|
||||||
<CustomText>
|
color={colors.disabled}
|
||||||
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
|
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||||
</CustomText>
|
/>
|
||||||
{isLoading ? (
|
) : null}
|
||||||
<Circle
|
</Pressable>
|
||||||
size={StyleConstants.Font.Size.M}
|
{data && data.error === undefined
|
||||||
color={colors.disabled}
|
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
|
||||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
: null}
|
||||||
/>
|
</>
|
||||||
) : 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
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TimelineTranslate
|
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 apiInstance from '@api/instance'
|
||||||
|
import browserPackage from '@helpers/browserPackage'
|
||||||
import navigationRef from '@helpers/navigationRef'
|
import navigationRef from '@helpers/navigationRef'
|
||||||
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
||||||
import { store } from '@root/store'
|
import { store } from '@root/store'
|
||||||
|
@ -91,7 +92,8 @@ const openLink = async (url: string, navigation?: any) => {
|
||||||
case 'internal':
|
case 'internal':
|
||||||
await WebBrowser.openBrowserAsync(encodeURI(url), {
|
await WebBrowser.openBrowserAsync(encodeURI(url), {
|
||||||
dismissButtonStyle: 'close',
|
dismissButtonStyle: 'close',
|
||||||
enableBarCollapsing: true
|
enableBarCollapsing: true,
|
||||||
|
browserPackage: await browserPackage()
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'external':
|
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,
|
"version": 3.5,
|
||||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
"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",
|
"feature": "follow_tags",
|
||||||
"version": 4.0,
|
"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