mirror of https://github.com/tooot-app/app
commit
36e61e9f95
|
@ -9,22 +9,18 @@ jobs:
|
||||||
build-ios:
|
build-ios:
|
||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
steps:
|
steps:
|
||||||
- name: -- Step 0 -- Extract branch name
|
- uses: tj-actions/branch-names@v6
|
||||||
uses: tj-actions/branch-names@v6
|
|
||||||
id: branch
|
id: branch
|
||||||
- name: -- Step 1 -- Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
- uses: actions/setup-node@v3
|
||||||
- name: -- Step 2 -- Setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- name: -- Step 3 -- Install node dependencies
|
- run: corepack enable
|
||||||
run: yarn install
|
- run: yarn install
|
||||||
- name: -- Step 4 -- Install ruby dependencies
|
- run: bundle install
|
||||||
run: bundle install
|
- run: yarn app:build ios
|
||||||
- name: -- Step 5 -- Run fastlane
|
|
||||||
env:
|
env:
|
||||||
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
|
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
|
||||||
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
LC_ALL: en_US.UTF-8
|
LC_ALL: en_US.UTF-8
|
||||||
|
@ -40,30 +36,24 @@ jobs:
|
||||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||||
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||||
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: yarn app:build ios
|
|
||||||
|
|
||||||
build-android:
|
build-android:
|
||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
steps:
|
steps:
|
||||||
- name: -- Step 0 -- Extract branch name
|
- uses: tj-actions/branch-names@v6
|
||||||
uses: tj-actions/branch-names@v6
|
|
||||||
id: branch
|
id: branch
|
||||||
- name: -- Step 1 -- Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
- uses: actions/setup-node@v3
|
||||||
- name: -- Step 2 -- Setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- name: -- Step 3 -- Setup Java
|
- uses: actions/setup-java@v3
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
- name: -- Step 4 -- Install node dependencies
|
- run: corepack enable
|
||||||
run: yarn install
|
- run: yarn install
|
||||||
- name: -- Step 5 -- Install ruby dependencies
|
- run: bundle install
|
||||||
run: bundle install
|
- run: yarn app:build android
|
||||||
- name: -- Step 6 -- Run fastlane
|
|
||||||
env:
|
env:
|
||||||
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
|
@ -75,31 +65,25 @@ jobs:
|
||||||
ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
||||||
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
|
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
|
||||||
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: yarn app:build android
|
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
needs: [build-ios, build-android]
|
needs: [build-ios, build-android]
|
||||||
steps:
|
steps:
|
||||||
- name: -- Step 0 -- Extract branch name
|
- uses: tj-actions/branch-names@v6
|
||||||
uses: tj-actions/branch-names@v6
|
|
||||||
id: branch
|
id: branch
|
||||||
- name: -- Step 1 -- Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
- uses: actions/setup-node@v3
|
||||||
- name: -- Step 2 -- Setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- name: -- Step 3 -- Setup Java
|
- uses: actions/setup-java@v3
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
- name: -- Step 4 -- Install node dependencies
|
- run: corepack enable
|
||||||
run: yarn install
|
- run: yarn install
|
||||||
- name: -- Step 5 -- Install ruby dependencies
|
- run: bundle install
|
||||||
run: bundle install
|
- run: yarn app:build release
|
||||||
- name: -- Step 6 -- Run fastlane
|
|
||||||
env:
|
env:
|
||||||
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||||
|
@ -113,4 +97,3 @@ jobs:
|
||||||
ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
|
ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
|
||||||
ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
ANDROID_KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
||||||
FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }}
|
FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: yarn app:build release
|
|
||||||
|
|
|
@ -66,4 +66,13 @@ buck-out/
|
||||||
web-build/
|
web-build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# @end expo-cli
|
# @end expo-cli
|
||||||
|
|
||||||
|
# yarn 3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
|
@ -1,7 +1,7 @@
|
||||||
diff --git a/node_modules/@types/react-native-share-menu/index.d.ts b/node_modules/@types/react-native-share-menu/index.d.ts
|
diff --git a/index.d.ts b/index.d.ts
|
||||||
index f52822c..ee98565 100755
|
index f52822c8bed928f387baf90fdb7342c7416a775a..6d9d480d18342832c4b07af2b10f4a63ff538e7b 100755
|
||||||
--- a/node_modules/@types/react-native-share-menu/index.d.ts
|
--- a/index.d.ts
|
||||||
+++ b/node_modules/@types/react-native-share-menu/index.d.ts
|
+++ b/index.d.ts
|
||||||
@@ -5,11 +5,9 @@
|
@@ -5,11 +5,9 @@
|
||||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||||
// Minimum TypeScript Version: 3.7
|
// Minimum TypeScript Version: 3.7
|
||||||
|
@ -17,12 +17,18 @@ index f52822c..ee98565 100755
|
||||||
|
|
||||||
export type ShareCallback = (share?: ShareData) => void;
|
export type ShareCallback = (share?: ShareData) => void;
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ interface ShareMenuReactView {
|
@@ -25,10 +23,10 @@ interface ShareMenu {
|
||||||
dismissExtension(error?: string): void;
|
}
|
||||||
openApp(): void;
|
|
||||||
continueInApp(extraData?: object): void;
|
interface ShareMenuReactView {
|
||||||
|
- dismissExtension(error?: string): void;
|
||||||
|
- openApp(): void;
|
||||||
|
- continueInApp(extraData?: object): void;
|
||||||
- data(): Promise<{mimeType: string, data: string}>;
|
- data(): Promise<{mimeType: string, data: string}>;
|
||||||
+ data(): Promise<{data: {mimeType: string; data: string}[]}>;
|
+ dismissExtension(error?: string): void
|
||||||
|
+ openApp(): void
|
||||||
|
+ continueInApp(extraData?: object): void
|
||||||
|
+ data(): Promise<{ data: { mimeType: string; data: string }[] }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShareMenuReactView: ShareMenuReactView;
|
export const ShareMenuReactView: ShareMenuReactView;
|
|
@ -0,0 +1,13 @@
|
||||||
|
diff --git a/ios/EXAV/EXAudioSessionManager.m b/ios/EXAV/EXAudioSessionManager.m
|
||||||
|
index 81dce13366c3947b12c863f7b39c0237882a6c36..fa27e0a354d48a994ca46e19642a5e224d42d9a8 100644
|
||||||
|
--- a/ios/EXAV/EXAudioSessionManager.m
|
||||||
|
+++ b/ios/EXAV/EXAudioSessionManager.m
|
||||||
|
@@ -170,7 +170,7 @@ - (void)moduleDidBackground:(id)backgroundingModule
|
||||||
|
[_foregroundedModules compact];
|
||||||
|
|
||||||
|
// Any possible failures are silent
|
||||||
|
- [self _updateSessionConfiguration];
|
||||||
|
+ // [self _updateSessionConfiguration];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)moduleDidForeground:(id)module
|
|
@ -0,0 +1,23 @@
|
||||||
|
diff --git a/RNFastImage.podspec b/RNFastImage.podspec
|
||||||
|
index db0fada63fc06191f8620d336d244edde6c3dba3..b6ffe6c77ab1fd5b821525f6f0b7363a13cba3a0 100644
|
||||||
|
--- a/RNFastImage.podspec
|
||||||
|
+++ b/RNFastImage.podspec
|
||||||
|
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
|
||||||
|
s.source_files = "ios/**/*.{h,m}"
|
||||||
|
|
||||||
|
s.dependency 'React-Core'
|
||||||
|
- s.dependency 'SDWebImage', '~> 5.11.1'
|
||||||
|
- s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
|
||||||
|
+ s.dependency 'SDWebImage', '~> 5.14.3'
|
||||||
|
+ s.dependency 'SDWebImageWebPCoder', '~> 0.9.1'
|
||||||
|
end
|
||||||
|
diff --git a/android/build.gradle b/android/build.gradle
|
||||||
|
index 5b21cd59c40a5754f5d19c77e2a0eb0229925911..19d82f826e88125c5e6d87ee7c348fac621f548c 100644
|
||||||
|
--- a/android/build.gradle
|
||||||
|
+++ b/android/build.gradle
|
||||||
|
@@ -65,4 +65,5 @@ dependencies {
|
||||||
|
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||||
|
implementation "com.github.bumptech.glide:okhttp3-integration:${glideVersion}"
|
||||||
|
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||||
|
+ implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.12.0'
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
diff --git a/node_modules/react-native-share-menu/android/build.gradle b/node_modules/react-native-share-menu/android/build.gradle
|
diff --git a/android/build.gradle b/android/build.gradle
|
||||||
index 9557fdb..ebdeb6f 100644
|
index 9557fdbf2fbf97b7f7aeaf7ce86d301a8ced213d..ebdeb6f4de7846d3241101001755595c52a4b05e 100644
|
||||||
--- a/node_modules/react-native-share-menu/android/build.gradle
|
--- a/android/build.gradle
|
||||||
+++ b/node_modules/react-native-share-menu/android/build.gradle
|
+++ b/android/build.gradle
|
||||||
@@ -1,12 +1,12 @@
|
@@ -1,12 +1,12 @@
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@ index 9557fdb..ebdeb6f 100644
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
ndk {
|
ndk {
|
||||||
diff --git a/node_modules/react-native-share-menu/ios/ReactShareViewController.swift b/node_modules/react-native-share-menu/ios/ReactShareViewController.swift
|
diff --git a/ios/ReactShareViewController.swift b/ios/ReactShareViewController.swift
|
||||||
index f42bce6..ee36062 100644
|
index f42bce6ce7e3f48a7ddc83f3366b68fd0664b1a0..ee360622b1d03cc9661c78c6f210b84c3b19a725 100644
|
||||||
--- a/node_modules/react-native-share-menu/ios/ReactShareViewController.swift
|
--- a/ios/ReactShareViewController.swift
|
||||||
+++ b/node_modules/react-native-share-menu/ios/ReactShareViewController.swift
|
+++ b/ios/ReactShareViewController.swift
|
||||||
@@ -13,7 +13,7 @@ class ReactShareViewController: ShareViewController, RCTBridgeDelegate, ReactSha
|
@@ -13,7 +13,7 @@ class ReactShareViewController: ShareViewController, RCTBridgeDelegate, ReactSha
|
||||||
func sourceURL(for bridge: RCTBridge!) -> URL! {
|
func sourceURL(for bridge: RCTBridge!) -> URL! {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
@ -32,10 +32,10 @@ index f42bce6..ee36062 100644
|
||||||
#else
|
#else
|
||||||
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||||
#endif
|
#endif
|
||||||
diff --git a/node_modules/react-native-share-menu/ios/ShareViewController.swift b/node_modules/react-native-share-menu/ios/ShareViewController.swift
|
diff --git a/ios/ShareViewController.swift b/ios/ShareViewController.swift
|
||||||
index 12d8c92..64aa72b 100644
|
index 12d8c92dda20fabd9e7b55fec57b3d867414063c..8a1db0de285b18a9358a10b2ca8293a8c7d56a8e 100644
|
||||||
--- a/node_modules/react-native-share-menu/ios/ShareViewController.swift
|
--- a/ios/ShareViewController.swift
|
||||||
+++ b/node_modules/react-native-share-menu/ios/ShareViewController.swift
|
+++ b/ios/ShareViewController.swift
|
||||||
@@ -19,8 +19,8 @@ class ShareViewController: SLComposeServiceViewController {
|
@@ -19,8 +19,8 @@ class ShareViewController: SLComposeServiceViewController {
|
||||||
var hostAppUrlScheme: String?
|
var hostAppUrlScheme: String?
|
||||||
var sharedItems: [Any] = []
|
var sharedItems: [Any] = []
|
||||||
|
@ -78,7 +78,7 @@ index 12d8c92..64aa72b 100644
|
||||||
override func configurationItems() -> [Any]! {
|
override func configurationItems() -> [Any]! {
|
||||||
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
|
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
|
||||||
return []
|
return []
|
||||||
@@ -238,11 +235,10 @@ class ShareViewController: SLComposeServiceViewController {
|
@@ -238,7 +235,7 @@ class ShareViewController: SLComposeServiceViewController {
|
||||||
|
|
||||||
func completeRequest() {
|
func completeRequest() {
|
||||||
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
|
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
|
||||||
|
@ -87,7 +87,3 @@ index 12d8c92..64aa72b 100644
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelRequest() {
|
func cancelRequest() {
|
||||||
extensionContext!.cancelRequest(withError: NSError())
|
|
||||||
}
|
|
||||||
-
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||||
|
spec: "@yarnpkg/plugin-typescript"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-interactive-tools"
|
34
Gemfile.lock
34
Gemfile.lock
|
@ -17,16 +17,16 @@ GEM
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.653.0)
|
aws-partitions (1.687.0)
|
||||||
aws-sdk-core (3.166.0)
|
aws-sdk-core (3.168.4)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.59.0)
|
aws-sdk-kms (1.61.0)
|
||||||
aws-sdk-core (~> 3, >= 3.165.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.117.1)
|
aws-sdk-s3 (1.117.2)
|
||||||
aws-sdk-core (~> 3, >= 3.165.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
|
@ -86,7 +86,7 @@ GEM
|
||||||
escape (0.0.4)
|
escape (0.0.4)
|
||||||
ethon (0.15.0)
|
ethon (0.15.0)
|
||||||
ffi (>= 1.15.0)
|
ffi (>= 1.15.0)
|
||||||
excon (0.93.1)
|
excon (0.95.0)
|
||||||
faraday (1.10.2)
|
faraday (1.10.2)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
@ -116,7 +116,7 @@ GEM
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.6)
|
fastimage (2.2.6)
|
||||||
fastlane (2.210.1)
|
fastlane (2.211.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
|
@ -156,7 +156,7 @@ GEM
|
||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.3.0)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
fastlane-plugin-json (1.1.0)
|
fastlane-plugin-json (1.1.0)
|
||||||
fastlane-plugin-sentry (1.14.0)
|
fastlane-plugin-sentry (1.15.0)
|
||||||
os (~> 1.1, >= 1.1.4)
|
os (~> 1.1, >= 1.1.4)
|
||||||
fastlane-plugin-versioning_android (0.1.0)
|
fastlane-plugin-versioning_android (0.1.0)
|
||||||
fastlane-plugin-yarn (1.2)
|
fastlane-plugin-yarn (1.2)
|
||||||
|
@ -164,9 +164,9 @@ GEM
|
||||||
fourflusher (2.3.1)
|
fourflusher (2.3.1)
|
||||||
fuzzy_match (2.0.4)
|
fuzzy_match (2.0.4)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.29.0)
|
google-apis-androidpublisher_v3 (0.32.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-core (0.9.1)
|
google-apis-core (0.9.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
@ -175,8 +175,8 @@ GEM
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.15.0)
|
google-apis-iamcredentials_v1 (0.16.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.12.0)
|
google-apis-playcustomapp_v1 (0.12.0)
|
||||||
google-apis-core (>= 0.9.1, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-storage_v1 (0.19.0)
|
||||||
|
@ -187,7 +187,7 @@ GEM
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.3.0)
|
google-cloud-errors (1.3.0)
|
||||||
google-cloud-storage (1.43.0)
|
google-cloud-storage (1.44.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
@ -208,11 +208,11 @@ GEM
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.12.0)
|
i18n (1.12.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jmespath (1.6.1)
|
jmespath (1.6.2)
|
||||||
json (2.6.2)
|
json (2.6.3)
|
||||||
jwt (2.5.0)
|
jwt (2.6.0)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
minitest (5.16.3)
|
minitest (5.16.3)
|
||||||
molinillo (0.8.0)
|
molinillo (0.8.0)
|
||||||
|
|
42
README.md
42
README.md
|
@ -1,8 +1,8 @@
|
||||||
# [tooot](https://tooot.app/) app for Mastodon
|
# [tooot](https://tooot.app/) app for Mastodon compatible platforms
|
||||||
|
|
||||||
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
|
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
|
||||||
|
|
||||||
![GitHub Workflow Status (candidate)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release)
|
![GitHub Workflow Status (candidate)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=candidate&label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=release&label=build%20release)
|
||||||
|
|
||||||
## Contribute to translation
|
## Contribute to translation
|
||||||
|
|
||||||
|
@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati
|
||||||
|
|
||||||
## Special thanks
|
## Special thanks
|
||||||
|
|
||||||
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
- [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish 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
|
||||||
[@pat](https://piaille.fr/@pat) for French 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
|
||||||
[@andrigamerita](https://github.com/andrigamerita) for Italian translation
|
- [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
||||||
|
- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
||||||
[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
|
- [@janlindblom](https://github.com/janlindblom) for Swedish
|
||||||
|
- [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
|
||||||
[@hellojaccc](https://github.com/hellojaccc) for Korean translation
|
- [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
|
||||||
|
- [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
|
||||||
[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
- [@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
|
||||||
|
|
||||||
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
|
||||||
|
|
||||||
[@janlindblom](https://github.com/janlindblom) for Swedish
|
|
||||||
|
|
||||||
[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
|
|
||||||
|
|
||||||
[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
|
|
||||||
|
|
||||||
[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
|
|
||||||
|
|
||||||
[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
apply plugin: "com.android.application"
|
apply plugin: "com.android.application"
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
||||||
import com.android.build.OutputFile
|
import com.android.build.OutputFile
|
||||||
import org.apache.tools.ant.taskdefs.condition.Os
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
@ -231,6 +232,7 @@ android {
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
namespace 'com.xmflsct.app.tooot'
|
||||||
|
|
||||||
// applicationVariants are e.g. debug, release
|
// applicationVariants are e.g. debug, release
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"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-erabggnp958v10r0tvsrh3pg880qnvqn.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.xmflsct.app.tooot",
|
||||||
|
"certificate_hash": "53162f104230ee8b7b1372e4f378e2b9607ca16f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-65g8ce369ugck3ii4ulk6jhb3ijg51kl.apps.googleusercontent.com",
|
||||||
|
"client_type": 2,
|
||||||
|
"ios_info": {
|
||||||
|
"bundle_id": "com.xmflsct.app.tooot",
|
||||||
|
"app_store_id": "1549772269"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools" package="com.xmflsct.app.tooot">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.xmflsct.app.tooot">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true">
|
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true">
|
||||||
<!-- [Custom] Expo Notifications -->
|
<!-- [Custom] Expo Notifications -->
|
||||||
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/ic_stat_notifications" />
|
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/ic_stat_notifications" />
|
||||||
|
|
|
@ -22,9 +22,10 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.2.1")
|
classpath('com.android.tools.build:gradle:7.2.2')
|
||||||
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")
|
||||||
|
classpath 'com.google.gms:google-services:4.3.14'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
|
@ -26,7 +26,7 @@ android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
||||||
# Version of flipper SDK to use with React Native
|
# Version of flipper SDK to use with React Native
|
||||||
FLIPPER_VERSION=0.125.0
|
FLIPPER_VERSION=0.176.1
|
||||||
|
|
||||||
# Use this property to specify which architecture you want to build.
|
# Use this property to specify which architecture you want to build.
|
||||||
# You can also override it from the CLI using
|
# You can also override it from the CLI using
|
||||||
|
|
|
@ -15,8 +15,9 @@ export default (): ExpoConfig => ({
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
package: 'com.xmflsct.app.tooot',
|
package: 'com.xmflsct.app.tooot',
|
||||||
permissions: ['CAMERA', 'VIBRATE'],
|
permissions: ['NOTIFICATIONS', 'CAMERA', 'VIBRATE'],
|
||||||
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
|
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT'],
|
||||||
|
googleServicesFile: './android/app/google-services.json'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
api.cache(true)
|
api.cache(false)
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
'@babel/plugin-proposal-optional-chaining',
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
@ -8,11 +8,8 @@ module.exports = function (api) {
|
||||||
{
|
{
|
||||||
root: ['./'],
|
root: ['./'],
|
||||||
alias: {
|
alias: {
|
||||||
'@assets': './assets',
|
|
||||||
'@root': './src',
|
|
||||||
'@api': './src/api',
|
|
||||||
'@helpers': './src/helpers',
|
|
||||||
'@components': './src/components',
|
'@components': './src/components',
|
||||||
|
'@i18n': './src/i18n',
|
||||||
'@screens': './src/screens',
|
'@screens': './src/screens',
|
||||||
'@utils': './src/utils'
|
'@utils': './src/utils'
|
||||||
}
|
}
|
||||||
|
@ -21,27 +18,9 @@ module.exports = function (api) {
|
||||||
'react-native-reanimated/plugin'
|
'react-native-reanimated/plugin'
|
||||||
]
|
]
|
||||||
|
|
||||||
if (
|
if (process.env.NODE_ENV === 'production' || process.env.BABEL_ENV === 'production') {
|
||||||
process.env.NODE_ENV === 'production' ||
|
|
||||||
process.env.BABEL_ENV === 'production'
|
|
||||||
) {
|
|
||||||
plugins.push('transform-remove-console')
|
plugins.push('transform-remove-console')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { presets: ['babel-preset-expo'], plugins }
|
||||||
presets: [
|
|
||||||
'babel-preset-expo',
|
|
||||||
[
|
|
||||||
'@babel/preset-react',
|
|
||||||
{
|
|
||||||
importSource: '@welldone-software/why-did-you-render',
|
|
||||||
runtime: 'automatic',
|
|
||||||
development:
|
|
||||||
process.env.NODE_ENV === 'development' ||
|
|
||||||
process.env.BABEL_ENV === 'development'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
plugins
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 251 KiB |
|
@ -36,7 +36,7 @@ private_lane :build_ios do
|
||||||
export_method: "app-store",
|
export_method: "app-store",
|
||||||
include_symbols: true,
|
include_symbols: true,
|
||||||
output_directory: BUILD_DIRECTORY,
|
output_directory: BUILD_DIRECTORY,
|
||||||
silent: false
|
silent: true
|
||||||
)
|
)
|
||||||
|
|
||||||
case ENVIRONMENT
|
case ENVIRONMENT
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
Enjoy toooting! This version includes following improvements and fixes:
|
Enjoy toooting! This version includes following improvements and fixes:
|
||||||
- Fixed wrongly update notification
|
- Auto fetch remote content in conversations!
|
||||||
- Fix some random crashes
|
- Remember last read position in timeline!
|
||||||
|
- Follow a user with other logged in accounts
|
||||||
|
- Allowing adding more context of reports
|
||||||
|
- Option to disable autoplay gif
|
||||||
|
- Hide boosts from users
|
||||||
|
- Followed hashtags are underlined
|
||||||
|
- Support GoToSocial
|
|
@ -1,3 +1,9 @@
|
||||||
toooting愉快!此版本包括以下改进和修复:
|
toooting愉快!此版本包括以下改进和修复:
|
||||||
- 修复错误的升级通知
|
- 主动获取对话的远程内容
|
||||||
- 修复部分应用崩溃
|
- 自动加载上次我的关注的阅读位置
|
||||||
|
- 用其它已登陆的账户关注用户
|
||||||
|
- 可添加举报细节
|
||||||
|
- 新增暂停自动播放gif动画选项
|
||||||
|
- 隐藏用户的转嘟
|
||||||
|
- 下划线高亮正在关注的话题标签
|
||||||
|
- 支持GoToSocial
|
6
index.js
6
index.js
|
@ -1,8 +1,4 @@
|
||||||
import { registerRootComponent } from 'expo'
|
import { registerRootComponent } from 'expo'
|
||||||
|
import App from './src/App'
|
||||||
|
|
||||||
import App from '@root/App'
|
|
||||||
|
|
||||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
|
||||||
// It also ensures that whether you load the app in the Expo client or in a native build,
|
|
||||||
// the environment is set up appropriately
|
|
||||||
registerRootComponent(App)
|
registerRootComponent(App)
|
||||||
|
|
|
@ -19,6 +19,7 @@ target 'tooot' do
|
||||||
:path => config[:reactNativePath],
|
:path => config[:reactNativePath],
|
||||||
:hermes_enabled => true,
|
:hermes_enabled => true,
|
||||||
:fabric_enabled => flags[:fabric_enabled],
|
:fabric_enabled => flags[:fabric_enabled],
|
||||||
|
# :flipper_configuration => FlipperConfiguration.enabled(["Debug"], { 'Flipper' => '0.159.0' }),
|
||||||
:flipper_configuration => FlipperConfiguration.disabled,
|
:flipper_configuration => FlipperConfiguration.disabled,
|
||||||
# An absolute path to your application root.
|
# An absolute path to your application root.
|
||||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||||
|
|
|
@ -3,10 +3,10 @@ PODS:
|
||||||
- DoubleConversion (1.1.6)
|
- DoubleConversion (1.1.6)
|
||||||
- EXApplication (5.0.1):
|
- EXApplication (5.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXAV (13.0.2):
|
- EXAV (13.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- EXConstants (14.0.2):
|
- EXConstants (14.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXErrorRecovery (4.0.1):
|
- EXErrorRecovery (4.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
@ -16,34 +16,37 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXNotifications (0.17.0):
|
- EXNotifications (0.17.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- Expo (47.0.8):
|
- Expo (47.0.12):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoCrypto (12.0.0):
|
- ExpoCrypto (12.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoHaptics (12.0.1):
|
- ExpoHaptics (12.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoKeepAwake (11.0.1):
|
- ExpoKeepAwake (11.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoLocalization (14.0.0):
|
- ExpoLocalization (14.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoModulesCore (1.0.3):
|
- ExpoModulesCore (1.1.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- ExpoRandom (13.0.0):
|
- ExpoRandom (13.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoStoreReview (6.0.0):
|
- ExpoStoreReview (6.1.0):
|
||||||
|
- ExpoModulesCore
|
||||||
|
- ExpoVideoThumbnails (7.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoWebBrowser (12.0.0):
|
- ExpoWebBrowser (12.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXScreenCapture (5.0.0):
|
- EXScreenCapture (5.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- EXScreenOrientation (5.0.1):
|
||||||
|
- ExpoModulesCore
|
||||||
|
- React-Core
|
||||||
- EXSecureStore (12.0.0):
|
- EXSecureStore (12.0.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXSplashScreen (0.17.5):
|
- EXSplashScreen (0.17.5):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
- EXVideoThumbnails (7.0.0):
|
|
||||||
- ExpoModulesCore
|
|
||||||
- FBLazyVector (0.70.6)
|
- FBLazyVector (0.70.6)
|
||||||
- FBReactNativeSpec (0.70.6):
|
- FBReactNativeSpec (0.70.6):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
|
@ -65,6 +68,9 @@ 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)
|
||||||
|
- MMKV (1.2.14):
|
||||||
|
- MMKVCore (~> 1.2.14)
|
||||||
|
- MMKVCore (1.2.14)
|
||||||
- RCT-Folly (2021.07.22.00):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
|
@ -295,16 +301,19 @@ PODS:
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-blurhash (1.1.10):
|
- react-native-blurhash (1.1.10):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-cameraroll (5.2.0):
|
- react-native-cameraroll (5.2.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-image-picker (4.10.2):
|
- react-native-image-picker (4.10.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-ios-context-menu (1.15.1):
|
- react-native-ios-context-menu (1.15.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-language-detection (0.2.2):
|
- react-native-language-detection (0.2.2):
|
||||||
- React
|
- React
|
||||||
- react-native-menu (0.7.2):
|
- react-native-menu (0.7.3):
|
||||||
- React
|
- React
|
||||||
|
- react-native-mmkv (2.5.1):
|
||||||
|
- MMKV (>= 1.2.13)
|
||||||
|
- React-Core
|
||||||
- react-native-netinfo (9.3.7):
|
- react-native-netinfo (9.3.7):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-pager-view (6.1.2):
|
- react-native-pager-view (6.1.2):
|
||||||
|
@ -392,7 +401,7 @@ PODS:
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNFastImage (8.6.3):
|
- RNFastImage (8.6.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
- SDWebImage (~> 5.14.2)
|
- SDWebImage (~> 5.14.3)
|
||||||
- SDWebImageWebPCoder (~> 0.9.1)
|
- SDWebImageWebPCoder (~> 0.9.1)
|
||||||
- RNGestureHandler (2.8.0):
|
- RNGestureHandler (2.8.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -433,9 +442,9 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNSVG (13.6.0):
|
- RNSVG (13.6.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- SDWebImage (5.14.2):
|
- SDWebImage (5.14.3):
|
||||||
- SDWebImage/Core (= 5.14.2)
|
- SDWebImage/Core (= 5.14.3)
|
||||||
- SDWebImage/Core (5.14.2)
|
- SDWebImage/Core (5.14.3)
|
||||||
- SDWebImageWebPCoder (0.9.1):
|
- SDWebImageWebPCoder (0.9.1):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.13)
|
- SDWebImage/Core (~> 5.13)
|
||||||
|
@ -461,11 +470,12 @@ DEPENDENCIES:
|
||||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||||
- ExpoRandom (from `../node_modules/expo-random/ios`)
|
- ExpoRandom (from `../node_modules/expo-random/ios`)
|
||||||
- ExpoStoreReview (from `../node_modules/expo-store-review/ios`)
|
- ExpoStoreReview (from `../node_modules/expo-store-review/ios`)
|
||||||
|
- ExpoVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
|
||||||
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||||
- EXScreenCapture (from `../node_modules/expo-screen-capture/ios`)
|
- EXScreenCapture (from `../node_modules/expo-screen-capture/ios`)
|
||||||
|
- EXScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
|
||||||
- EXSecureStore (from `../node_modules/expo-secure-store/ios`)
|
- EXSecureStore (from `../node_modules/expo-secure-store/ios`)
|
||||||
- EXSplashScreen (from `../node_modules/expo-splash-screen/ios`)
|
- EXSplashScreen (from `../node_modules/expo-splash-screen/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`)
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
|
@ -494,6 +504,7 @@ DEPENDENCIES:
|
||||||
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
|
- 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-menu (from `../node_modules/@react-native-menu/menu`)"
|
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
|
||||||
|
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
|
||||||
- "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`)"
|
||||||
|
@ -527,6 +538,8 @@ SPEC REPOS:
|
||||||
- fmt
|
- fmt
|
||||||
- libevent
|
- libevent
|
||||||
- libwebp
|
- libwebp
|
||||||
|
- MMKV
|
||||||
|
- MMKVCore
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
- Sentry
|
- Sentry
|
||||||
|
@ -567,16 +580,18 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-random/ios"
|
:path: "../node_modules/expo-random/ios"
|
||||||
ExpoStoreReview:
|
ExpoStoreReview:
|
||||||
:path: "../node_modules/expo-store-review/ios"
|
:path: "../node_modules/expo-store-review/ios"
|
||||||
|
ExpoVideoThumbnails:
|
||||||
|
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||||
ExpoWebBrowser:
|
ExpoWebBrowser:
|
||||||
:path: "../node_modules/expo-web-browser/ios"
|
:path: "../node_modules/expo-web-browser/ios"
|
||||||
EXScreenCapture:
|
EXScreenCapture:
|
||||||
:path: "../node_modules/expo-screen-capture/ios"
|
:path: "../node_modules/expo-screen-capture/ios"
|
||||||
|
EXScreenOrientation:
|
||||||
|
:path: "../node_modules/expo-screen-orientation/ios"
|
||||||
EXSecureStore:
|
EXSecureStore:
|
||||||
:path: "../node_modules/expo-secure-store/ios"
|
:path: "../node_modules/expo-secure-store/ios"
|
||||||
EXSplashScreen:
|
EXSplashScreen:
|
||||||
:path: "../node_modules/expo-splash-screen/ios"
|
:path: "../node_modules/expo-splash-screen/ios"
|
||||||
EXVideoThumbnails:
|
|
||||||
:path: "../node_modules/expo-video-thumbnails/ios"
|
|
||||||
FBLazyVector:
|
FBLazyVector:
|
||||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
||||||
FBReactNativeSpec:
|
FBReactNativeSpec:
|
||||||
|
@ -629,6 +644,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native-language-detection"
|
:path: "../node_modules/react-native-language-detection"
|
||||||
react-native-menu:
|
react-native-menu:
|
||||||
:path: "../node_modules/@react-native-menu/menu"
|
:path: "../node_modules/@react-native-menu/menu"
|
||||||
|
react-native-mmkv:
|
||||||
|
:path: "../node_modules/react-native-mmkv"
|
||||||
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:
|
||||||
|
@ -688,25 +705,26 @@ SPEC CHECKSUMS:
|
||||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||||
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
|
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
|
||||||
EXAV: 9a45d37772c5329294c054a041dcc39931fc5032
|
EXAV: 4b92292fb107520a25956bea940a94a3bb4911ca
|
||||||
EXConstants: 3c86653c422dd77e40d10cbbabb3025003977415
|
EXConstants: 44f7d347d0432a66f469d0ce1dc4e3a0ca1b8b2d
|
||||||
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
|
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
|
||||||
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
||||||
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
||||||
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
|
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
|
||||||
Expo: 36b5f625d36728adbdd1934d4d57182f319ab832
|
Expo: f48d305fda3e4e501d686e6bad7d8c8373828279
|
||||||
ExpoCrypto: 51e7662c7f5bfeab25b7909b8a5d545ec15d4877
|
ExpoCrypto: 6eb2a5ede7d95b7359a5f0391ee0c5d2ecd144b3
|
||||||
ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f
|
ExpoHaptics: 129d3f8d44c2205adcdf8db760602818463d5437
|
||||||
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
||||||
ExpoLocalization: e202d1e2a4950df17ac8d0889d65a1ffd7532d7e
|
ExpoLocalization: e202d1e2a4950df17ac8d0889d65a1ffd7532d7e
|
||||||
ExpoModulesCore: b5d21c8880afda6fb6ee95469f9ac2ec9b98e995
|
ExpoModulesCore: 485dff3a59b036a33b6050c0a5aea3cf1037fdd1
|
||||||
ExpoRandom: 58b7e0a5fe1adf1cb6dc1cbe503a6fe9524f36ce
|
ExpoRandom: 58b7e0a5fe1adf1cb6dc1cbe503a6fe9524f36ce
|
||||||
ExpoStoreReview: ff6d631f2949eb7e4b2d14146ef6af25a16d770d
|
ExpoStoreReview: 713336ff504db3a6983475bf7c67519cc5efc86f
|
||||||
|
ExpoVideoThumbnails: 424db02cedfbbe2d498bcb2712ea4ba8a9dcb453
|
||||||
ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda
|
ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda
|
||||||
EXScreenCapture: d9f1ec31042dfef109290d06c2b4789b7444d16d
|
EXScreenCapture: d9f1ec31042dfef109290d06c2b4789b7444d16d
|
||||||
|
EXScreenOrientation: 07e5aeff07bce09a2b214981e612d87fd7719997
|
||||||
EXSecureStore: daec0117c922a67c658cb229152a9e252e5c1750
|
EXSecureStore: daec0117c922a67c658cb229152a9e252e5c1750
|
||||||
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
|
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
|
||||||
EXVideoThumbnails: 8b3e48f3716679dd0cbf949217a31eab5c555799
|
|
||||||
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
|
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
|
||||||
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
|
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
|
||||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||||
|
@ -714,6 +732,8 @@ SPEC CHECKSUMS:
|
||||||
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
|
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
|
||||||
|
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
|
||||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||||
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
||||||
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
||||||
|
@ -731,11 +751,12 @@ SPEC CHECKSUMS:
|
||||||
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
||||||
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
||||||
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
|
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
|
||||||
react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
|
react-native-cameraroll: f94bf9f46c998963ecd2bb6e9a3f9cca59b6d9f1
|
||||||
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
|
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
|
||||||
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
||||||
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
||||||
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
react-native-menu: 9d7d6f819cc7fa14a15cf86888c53f3240d86f1b
|
||||||
|
react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3
|
||||||
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||||
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
||||||
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
|
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
|
||||||
|
@ -755,19 +776,19 @@ SPEC CHECKSUMS:
|
||||||
ReactCommon: 349be31adeecffc7986a0de875d7fb0dcf4e251c
|
ReactCommon: 349be31adeecffc7986a0de875d7fb0dcf4e251c
|
||||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||||
RNFastImage: c5dd1b551779c5826fe43b7d36788385da2021e2
|
RNFastImage: 756ab178acb5e3f11d8b0a931956fbd9da8d6e54
|
||||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||||
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
|
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
|
||||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||||
RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d
|
RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d
|
||||||
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
||||||
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
|
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
|
||||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
SDWebImage: 9c36e66c8ce4620b41a7407698dda44211a96764
|
||||||
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
|
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
|
||||||
Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf
|
Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf
|
||||||
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
||||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||||
|
|
||||||
PODFILE CHECKSUM: 05bf71d31ba782dfda5a6b47d38e98a6f6bc079a
|
PODFILE CHECKSUM: 08742f25aa1cdb93d6d5d5efeafd8803ba02b689
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
|
|
@ -86,5 +86,7 @@
|
||||||
<string>Automatic</string>
|
<string>Automatic</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
86
package.json
86
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tooot",
|
"name": "tooot",
|
||||||
"version": "4.7.2",
|
"version": "4.8.0",
|
||||||
"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",
|
||||||
|
@ -14,8 +14,7 @@
|
||||||
"iphone": "react-native run-ios --simulator 'iPhone 14 Pro'",
|
"iphone": "react-native run-ios --simulator 'iPhone 14 Pro'",
|
||||||
"ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (4th generation)'",
|
"ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (4th generation)'",
|
||||||
"app:build": "bundle exec fastlane",
|
"app:build": "bundle exec fastlane",
|
||||||
"clean": "react-native-clean-project",
|
"clean": "react-native-clean-project"
|
||||||
"postinstall": "patch-package"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/react-native-action-sheet": "^4.0.1",
|
"@expo/react-native-action-sheet": "^4.0.1",
|
||||||
|
@ -28,58 +27,59 @@
|
||||||
"@mattermost/react-native-paste-input": "^0.5.2",
|
"@mattermost/react-native-paste-input": "^0.5.2",
|
||||||
"@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.2.0",
|
"@react-native-camera-roll/camera-roll": "^5.2.1",
|
||||||
"@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.7",
|
"@react-native-community/netinfo": "9.3.7",
|
||||||
"@react-native-community/segmented-control": "^2.2.2",
|
"@react-native-community/segmented-control": "^2.2.2",
|
||||||
"@react-native-menu/menu": "^0.7.2",
|
"@react-native-firebase/app": "^16.5.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.1",
|
"@react-native-menu/menu": "^0.7.3",
|
||||||
|
"@react-navigation/bottom-tabs": "^6.5.2",
|
||||||
"@react-navigation/native": "^6.1.1",
|
"@react-navigation/native": "^6.1.1",
|
||||||
"@react-navigation/native-stack": "^6.9.6",
|
"@react-navigation/native-stack": "^6.9.7",
|
||||||
"@react-navigation/stack": "^6.3.9",
|
"@react-navigation/stack": "^6.3.10",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
|
||||||
"@sentry/react-native": "4.12.0",
|
"@sentry/react-native": "4.12.0",
|
||||||
"@sharcoux/slider": "^6.1.1",
|
"@sharcoux/slider": "^6.1.1",
|
||||||
"@tanstack/react-query": "^4.20.4",
|
"@tanstack/react-query": "^4.20.9",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.2",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"expo": "^47.0.8",
|
"expo": "^47.0.12",
|
||||||
"expo-auth-session": "^3.7.3",
|
"expo-auth-session": "^3.8.0",
|
||||||
"expo-av": "^13.0.2",
|
"expo-av": "^13.1.0",
|
||||||
"expo-constants": "^14.0.2",
|
"expo-constants": "^14.1.0",
|
||||||
"expo-crypto": "^12.0.0",
|
"expo-crypto": "^12.1.0",
|
||||||
"expo-file-system": "^15.1.1",
|
"expo-file-system": "^15.1.1",
|
||||||
"expo-haptics": "^12.0.1",
|
"expo-haptics": "^12.1.0",
|
||||||
"expo-linking": "^3.2.3",
|
"expo-linking": "^3.3.0",
|
||||||
"expo-localization": "^14.0.0",
|
"expo-localization": "^14.0.0",
|
||||||
"expo-notifications": "^0.17.0",
|
"expo-notifications": "^0.17.0",
|
||||||
"expo-random": "^13.0.0",
|
"expo-random": "^13.0.0",
|
||||||
"expo-screen-capture": "^5.0.0",
|
"expo-screen-capture": "^5.0.0",
|
||||||
|
"expo-screen-orientation": "^5.0.1",
|
||||||
"expo-secure-store": "^12.0.0",
|
"expo-secure-store": "^12.0.0",
|
||||||
"expo-splash-screen": "^0.17.5",
|
"expo-splash-screen": "^0.17.5",
|
||||||
"expo-store-review": "^6.0.0",
|
"expo-store-review": "^6.1.0",
|
||||||
"expo-video-thumbnails": "^7.0.0",
|
"expo-video-thumbnails": "^7.1.0",
|
||||||
"expo-web-browser": "~12.0.0",
|
"expo-web-browser": "~12.0.0",
|
||||||
"i18next": "^22.4.5",
|
"htmlparser2": "^8.0.1",
|
||||||
|
"i18next": "^22.4.8",
|
||||||
"linkify-it": "^4.0.1",
|
"linkify-it": "^4.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.1.1",
|
"react-i18next": "^12.1.4",
|
||||||
"react-intl": "^6.2.5",
|
"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-blurhash": "^1.1.10",
|
"react-native-blurhash": "^1.1.10",
|
||||||
"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.4.0",
|
||||||
"react-native-gesture-handler": "~2.8.0",
|
"react-native-gesture-handler": "~2.8.0",
|
||||||
"react-native-htmlview": "^0.16.0",
|
"react-native-image-picker": "^4.10.3",
|
||||||
"react-native-image-picker": "^4.10.2",
|
|
||||||
"react-native-ios-context-menu": "^1.15.1",
|
"react-native-ios-context-menu": "^1.15.1",
|
||||||
"react-native-language-detection": "^0.2.2",
|
"react-native-language-detection": "^0.2.2",
|
||||||
|
"react-native-mmkv": "^2.5.1",
|
||||||
"react-native-pager-view": "^6.1.2",
|
"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",
|
||||||
|
@ -90,37 +90,35 @@
|
||||||
"react-native-swipe-list-view": "^3.2.9",
|
"react-native-swipe-list-view": "^3.2.9",
|
||||||
"react-native-tab-view": "^3.3.4",
|
"react-native-tab-view": "^3.3.4",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
|
||||||
"rn-placeholder": "^3.0.3",
|
"rn-placeholder": "^3.0.3",
|
||||||
"rtl-detect": "^1.0.4",
|
"url-parse": "^1.5.10",
|
||||||
"valid-url": "^1.0.9",
|
"zeego": "^1.0.2"
|
||||||
"zeego": "^0.5.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.12",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
|
||||||
"@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/diff": "^5.0.2",
|
"@types/diff": "^5.0.2",
|
||||||
"@types/linkify-it": "^3.0.2",
|
"@types/linkify-it": "^3.0.2",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/react": "~18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "~18.0.9",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/react-native": "~0.70.8",
|
"@types/react-native": "^0.70.8",
|
||||||
"@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/url-parse": "^1.4.8",
|
||||||
"@types/valid-url": "^1.0.3",
|
|
||||||
"@welldone-software/why-did-you-render": "^7.0.1",
|
|
||||||
"babel-plugin-module-resolver": "^4.1.0",
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"expo-cli": "^6.0.8",
|
|
||||||
"patch-package": "^6.5.0",
|
|
||||||
"postinstall-postinstall": "^2.1.0",
|
|
||||||
"react-native-clean-project": "^4.0.1",
|
"react-native-clean-project": "^4.0.1",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.3.1",
|
||||||
|
"resolutions": {
|
||||||
|
"react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch",
|
||||||
|
"expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch",
|
||||||
|
"react-native-share-menu@^6.0.0": "patch:react-native-share-menu@npm%3A6.0.0#./.yarn/patches/react-native-share-menu-npm-6.0.0-f1094c3204.patch",
|
||||||
|
"@types/react-native-share-menu@^5.0.2": "patch:@types/react-native-share-menu@npm%3A5.0.2#./.yarn/patches/@types-react-native-share-menu-npm-5.0.2-373df17ecc.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
diff --git a/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m b/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
|
|
||||||
index 81dce13..8664b90 100644
|
|
||||||
--- a/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
|
|
||||||
+++ b/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
|
|
||||||
@@ -168,9 +168,6 @@ - (void)moduleDidBackground:(id)backgroundingModule
|
|
||||||
// compact doesn't work, that's why we need the `|| !pointer` above
|
|
||||||
// http://www.openradar.me/15396578
|
|
||||||
[_foregroundedModules compact];
|
|
||||||
-
|
|
||||||
- // Any possible failures are silent
|
|
||||||
- [self _updateSessionConfiguration];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)moduleDidForeground:(id)module
|
|
|
@ -1,23 +0,0 @@
|
||||||
diff --git a/node_modules/react-native-fast-image/RNFastImage.podspec b/node_modules/react-native-fast-image/RNFastImage.podspec
|
|
||||||
index db0fada..b23cd91 100644
|
|
||||||
--- a/node_modules/react-native-fast-image/RNFastImage.podspec
|
|
||||||
+++ b/node_modules/react-native-fast-image/RNFastImage.podspec
|
|
||||||
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
|
|
||||||
s.source_files = "ios/**/*.{h,m}"
|
|
||||||
|
|
||||||
s.dependency 'React-Core'
|
|
||||||
- s.dependency 'SDWebImage', '~> 5.11.1'
|
|
||||||
- s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
|
|
||||||
+ s.dependency 'SDWebImage', '~> 5.14.2'
|
|
||||||
+ s.dependency 'SDWebImageWebPCoder', '~> 0.9.1'
|
|
||||||
end
|
|
||||||
diff --git a/node_modules/react-native-fast-image/android/build.gradle b/node_modules/react-native-fast-image/android/build.gradle
|
|
||||||
index 5b21cd5..19d82f8 100644
|
|
||||||
--- a/node_modules/react-native-fast-image/android/build.gradle
|
|
||||||
+++ b/node_modules/react-native-fast-image/android/build.gradle
|
|
||||||
@@ -65,4 +65,5 @@ dependencies {
|
|
||||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:${glideVersion}"
|
|
||||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
|
||||||
+ implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.12.0'
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
diff --git a/node_modules/react-native-htmlview/HTMLView.js b/node_modules/react-native-htmlview/HTMLView.js
|
|
||||||
index 43f8b7e..728112b 100644
|
|
||||||
--- a/node_modules/react-native-htmlview/HTMLView.js
|
|
||||||
+++ b/node_modules/react-native-htmlview/HTMLView.js
|
|
||||||
@@ -1,7 +1,7 @@
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import htmlToElement from './htmlToElement';
|
|
||||||
-import {Linking, Platform, StyleSheet, View, ViewPropTypes} from 'react-native';
|
|
||||||
+import {Linking, Platform, StyleSheet, View} from 'react-native';
|
|
||||||
|
|
||||||
const boldStyle = {fontWeight: 'bold'};
|
|
||||||
const italicStyle = {fontStyle: 'italic'};
|
|
||||||
@@ -146,7 +146,7 @@ HtmlView.propTypes = {
|
|
||||||
renderNode: PropTypes.func,
|
|
||||||
RootComponent: PropTypes.func,
|
|
||||||
rootComponentProps: PropTypes.object,
|
|
||||||
- style: ViewPropTypes.style,
|
|
||||||
+ style: PropTypes.any,
|
|
||||||
stylesheet: PropTypes.object,
|
|
||||||
TextComponent: PropTypes.func,
|
|
||||||
textComponentProps: PropTypes.object,
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
dependencies: {
|
||||||
|
'@react-native-firebase/app': {
|
||||||
|
platforms: {
|
||||||
|
ios: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,44 @@
|
||||||
import 'i18next'
|
import 'i18next'
|
||||||
|
|
||||||
|
import common from '../i18n/en/common.json'
|
||||||
|
import screens from '../i18n/en/screens.json'
|
||||||
|
|
||||||
|
import screenAccountSelection from '../i18n/en/screens/accountSelection.json'
|
||||||
|
import screenAnnouncements from '../i18n/en/screens/announcements.json'
|
||||||
|
import screenCompose from '../i18n/en/screens/compose.json'
|
||||||
|
import screenImageViewer from '../i18n/en/screens/imageViewer.json'
|
||||||
|
import screenTabs from '../i18n/en/screens/tabs.json'
|
||||||
|
|
||||||
|
import componentContextMenu from '../i18n/en/components/contextMenu.json'
|
||||||
|
import componentEmojis from '../i18n/en/components/emojis.json'
|
||||||
|
import componentInstance from '../i18n/en/components/instance.json'
|
||||||
|
import componentMediaSelector from '../i18n/en/components/mediaSelector.json'
|
||||||
|
import componentParse from '../i18n/en/components/parse.json'
|
||||||
|
import componentRelationship from '../i18n/en/components/relationship.json'
|
||||||
|
import componentTimeline from '../i18n/en/components/timeline.json'
|
||||||
|
|
||||||
declare module 'i18next' {
|
declare module 'i18next' {
|
||||||
interface CustomTypeOptions {
|
interface CustomTypeOptions {
|
||||||
defaultNS: 'common',
|
defaultNS: 'common'
|
||||||
|
resources: {
|
||||||
|
common: typeof common
|
||||||
|
screens: typeof screens
|
||||||
|
|
||||||
|
screenAccountSelection: typeof screenAccountSelection
|
||||||
|
screenAnnouncements: typeof screenAnnouncements
|
||||||
|
screenCompose: typeof screenCompose
|
||||||
|
screenImageViewer: typeof screenImageViewer
|
||||||
|
screenTabs: typeof screenTabs
|
||||||
|
|
||||||
|
componentContextMenu: typeof componentContextMenu
|
||||||
|
componentEmojis: typeof componentEmojis
|
||||||
|
componentInstance: typeof componentInstance
|
||||||
|
componentMediaSelector: typeof componentMediaSelector
|
||||||
|
componentParse: typeof componentParse
|
||||||
|
componentRelationship: typeof componentRelationship
|
||||||
|
componentTimeline: typeof componentTimeline
|
||||||
|
}
|
||||||
returnNull: false
|
returnNull: false
|
||||||
|
returnEmptyString: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,9 @@ declare namespace Mastodon {
|
||||||
source?: Source
|
source?: Source
|
||||||
suspended?: boolean
|
suspended?: boolean
|
||||||
role?: Role
|
role?: Role
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
_remote?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Announcement = {
|
type Announcement = {
|
||||||
|
@ -264,14 +267,6 @@ declare namespace Mastodon {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
|
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
|
||||||
type Filter_V1 = {
|
|
||||||
id: string
|
|
||||||
phrase: string
|
|
||||||
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
|
|
||||||
expires_at?: string
|
|
||||||
irreversible: boolean
|
|
||||||
whole_word: boolean
|
|
||||||
}
|
|
||||||
type Filter_V2 = {
|
type Filter_V2 = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
@ -281,6 +276,14 @@ declare namespace Mastodon {
|
||||||
keywords: FilterKeyword[]
|
keywords: FilterKeyword[]
|
||||||
statuses: FilterStatus[]
|
statuses: FilterStatus[]
|
||||||
}
|
}
|
||||||
|
type Filter_V1 = {
|
||||||
|
id: string
|
||||||
|
phrase: string
|
||||||
|
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
|
||||||
|
expires_at?: string
|
||||||
|
irreversible: boolean
|
||||||
|
whole_word: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
|
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
|
||||||
|
|
||||||
|
@ -298,7 +301,48 @@ declare namespace Mastodon {
|
||||||
replies_policy: 'none' | 'list' | 'followed'
|
replies_policy: 'none' | 'list' | 'followed'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Instance = {
|
type Instance<T extends 'v1' | 'v2'> = T extends 'v2' ? Instance_V2 : Instance_V1
|
||||||
|
type Instance_V2 = {
|
||||||
|
domain: string
|
||||||
|
title: string
|
||||||
|
version: string
|
||||||
|
source_url: string
|
||||||
|
description: string
|
||||||
|
usage: { users: { active_month: number } }
|
||||||
|
thumbnail: { url: string; blurhash?: string; versions?: { '@1x'?: string; '@2x'?: string } }
|
||||||
|
languages: string[]
|
||||||
|
configuration: {
|
||||||
|
urls: { streaming_api: string }
|
||||||
|
accounts: { max_featured_tags: number }
|
||||||
|
statuses: {
|
||||||
|
max_characters: number
|
||||||
|
max_media_attachments: number
|
||||||
|
characters_reserved_per_url: number
|
||||||
|
}
|
||||||
|
media_attachments: {
|
||||||
|
supported_mime_types: string[]
|
||||||
|
image_size_limit: number
|
||||||
|
image_matrix_limit: number
|
||||||
|
video_size_limit: number
|
||||||
|
video_frame_rate_limit: number
|
||||||
|
video_matrix_limit: number
|
||||||
|
}
|
||||||
|
polls: {
|
||||||
|
max_options: number
|
||||||
|
max_characters_per_option: number
|
||||||
|
min_expiration: number
|
||||||
|
max_expiration: number
|
||||||
|
}
|
||||||
|
translation: { enabled: boolean }
|
||||||
|
registrations: { enabled: boolean; approval_required: boolean; message?: string }
|
||||||
|
contact: { email: string; account: Account }
|
||||||
|
rules: Rule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gotosocial
|
||||||
|
account_domain?: string
|
||||||
|
}
|
||||||
|
type Instance_V1 = {
|
||||||
// Base
|
// Base
|
||||||
uri: string
|
uri: string
|
||||||
title: string
|
title: string
|
||||||
|
@ -343,6 +387,9 @@ declare namespace Mastodon {
|
||||||
max_expiration: number
|
max_expiration: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gotosocial
|
||||||
|
account_domain?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mention = {
|
type Mention = {
|
||||||
|
@ -351,6 +398,9 @@ declare namespace Mastodon {
|
||||||
username: string
|
username: string
|
||||||
acct: string
|
acct: string
|
||||||
url: string
|
url: string
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
_remote?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification =
|
type Notification =
|
||||||
|
@ -470,6 +520,11 @@ declare namespace Mastodon {
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Rule = {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
type Status = {
|
type Status = {
|
||||||
// Base
|
// Base
|
||||||
id: string
|
id: string
|
||||||
|
@ -509,6 +564,10 @@ declare namespace Mastodon {
|
||||||
language?: string
|
language?: string
|
||||||
text?: string
|
text?: string
|
||||||
filtered?: FilterResult[]
|
filtered?: FilterResult[]
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
_level?: number
|
||||||
|
_remote?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusHistory = {
|
type StatusHistory = {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
declare module 'gl-react-blurhash'
|
declare module 'gl-react-blurhash'
|
||||||
declare module 'htmlparser2-without-node-native'
|
|
||||||
declare module 'react-native-feather'
|
declare module 'react-native-feather'
|
||||||
declare module 'react-native-htmlview'
|
|
||||||
declare module 'react-native-toast-message'
|
declare module 'react-native-toast-message'
|
||||||
declare module 'rtl-detect'
|
declare module 'rtl-detect'
|
||||||
|
|
||||||
|
|
178
src/App.tsx
178
src/App.tsx
|
@ -1,31 +1,35 @@
|
||||||
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 i18n from '@root/i18n/i18n'
|
|
||||||
import Screens from '@root/Screens'
|
|
||||||
import audio from '@root/startup/audio'
|
|
||||||
import dev from '@root/startup/dev'
|
|
||||||
import log from '@root/startup/log'
|
|
||||||
import netInfo from '@root/startup/netInfo'
|
|
||||||
import push from '@root/startup/push'
|
|
||||||
import sentry from '@root/startup/sentry'
|
|
||||||
import timezone from '@root/startup/timezone'
|
|
||||||
import { persistor, store } from '@root/store'
|
|
||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
|
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
|
||||||
import { changeLanguage } from '@utils/slices/settingsSlice'
|
import getLanguage from '@utils/helpers/getLanguage'
|
||||||
|
import { queryClient } from '@utils/queryHooks'
|
||||||
|
import audio from '@utils/startup/audio'
|
||||||
|
import { dev } from '@utils/startup/dev'
|
||||||
|
import log from '@utils/startup/log'
|
||||||
|
import netInfo from '@utils/startup/netInfo'
|
||||||
|
import push from '@utils/startup/push'
|
||||||
|
import sentry from '@utils/startup/sentry'
|
||||||
|
import timezone from '@utils/startup/timezone'
|
||||||
|
import { storage } from '@utils/storage'
|
||||||
|
import {
|
||||||
|
getGlobalStorage,
|
||||||
|
removeAccount,
|
||||||
|
setAccount,
|
||||||
|
setGlobalStorage
|
||||||
|
} from '@utils/storage/actions'
|
||||||
|
import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
|
||||||
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 { MMKV } from 'react-native-mmkv'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
import { enableFreeze } from 'react-native-screens'
|
import { enableFreeze } from 'react-native-screens'
|
||||||
import { QueryClientProvider } from '@tanstack/react-query'
|
import i18n from './i18n'
|
||||||
import { Provider } from 'react-redux'
|
import Screens from './screens'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
|
||||||
|
|
||||||
Platform.select({
|
Platform.select({
|
||||||
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||||
|
@ -38,83 +42,95 @@ push()
|
||||||
timezone()
|
timezone()
|
||||||
enableFreeze(true)
|
enableFreeze(true)
|
||||||
|
|
||||||
|
log('log', 'App', 'delay splash')
|
||||||
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
log('log', 'App', 'rendering App')
|
log('log', 'App', 'rendering App')
|
||||||
|
const [appIsReady, setAppIsReady] = useState(false)
|
||||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||||
|
|
||||||
|
const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delaySplash = async () => {
|
const prepare = async () => {
|
||||||
log('log', 'App', 'delay splash')
|
if (!hasMigrated) {
|
||||||
try {
|
try {
|
||||||
await SplashScreen.preventAutoHideAsync()
|
await migrateFromAsyncStorage()
|
||||||
} catch (e) {
|
setHasMigrated(true)
|
||||||
console.warn(e)
|
} catch {}
|
||||||
|
} else {
|
||||||
|
log('log', 'App', 'loading from MMKV')
|
||||||
|
const account = getGlobalStorage.string('account.active')
|
||||||
|
if (account) {
|
||||||
|
const storageAccount = new MMKV({ id: account })
|
||||||
|
const token = storageAccount.getString('auth.token')
|
||||||
|
if (token) {
|
||||||
|
log('log', 'App', `Binding storage of ${account}`)
|
||||||
|
storage.account = storageAccount
|
||||||
|
} else {
|
||||||
|
log('log', 'App', `Token not found for ${account}`)
|
||||||
|
removeAccount(account)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('log', 'App', 'No active account available')
|
||||||
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
if (accounts?.length) {
|
||||||
|
log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`)
|
||||||
|
setAccount(accounts[accounts.length - 1])
|
||||||
|
} else {
|
||||||
|
setGlobalStorage('account.active', undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let netInfoRes = undefined
|
||||||
|
try {
|
||||||
|
netInfoRes = await netInfo()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
||||||
|
setLocalCorrupt(netInfoRes.corrupted)
|
||||||
|
}
|
||||||
|
|
||||||
|
log('log', 'App', `locale: ${Localization.locale}`)
|
||||||
|
const language = getLanguage()
|
||||||
|
if (!language) {
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
setGlobalStorage('app.language', 'en')
|
||||||
|
}
|
||||||
|
i18n.changeLanguage('en')
|
||||||
|
} else {
|
||||||
|
i18n.changeLanguage(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppIsReady(true)
|
||||||
}
|
}
|
||||||
delaySplash()
|
|
||||||
|
prepare()
|
||||||
}, [])
|
}, [])
|
||||||
|
const onLayoutRootView = useCallback(async () => {
|
||||||
const onBeforeLift = useCallback(async () => {
|
if (appIsReady) {
|
||||||
let netInfoRes = undefined
|
log('log', 'App', 'hide splash')
|
||||||
try {
|
|
||||||
netInfoRes = await netInfo()
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
|
||||||
setLocalCorrupt(netInfoRes.corrupted)
|
|
||||||
}
|
|
||||||
|
|
||||||
log('log', 'App', 'hide splash')
|
|
||||||
try {
|
|
||||||
await SplashScreen.hideAsync()
|
await SplashScreen.hideAsync()
|
||||||
return Promise.resolve()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [appIsReady])
|
||||||
|
if (!appIsReady) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }} onLayout={onLayoutRootView}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Provider store={store}>
|
<SafeAreaProvider>
|
||||||
<PersistGate
|
<ActionSheetProvider>
|
||||||
persistor={persistor}
|
<AccessibilityManager>
|
||||||
onBeforeLift={onBeforeLift}
|
<ThemeManager>
|
||||||
children={bootstrapped => {
|
<Screens localCorrupt={localCorrupt} />
|
||||||
log('log', 'App', 'bootstrapped')
|
</ThemeManager>
|
||||||
if (bootstrapped) {
|
</AccessibilityManager>
|
||||||
log('log', 'App', 'loading actual app :)')
|
</ActionSheetProvider>
|
||||||
log('log', 'App', `Locale: ${Localization.locale}`)
|
</SafeAreaProvider>
|
||||||
const language = getLanguage()
|
|
||||||
if (!language) {
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
store.dispatch(changeLanguage('en'))
|
|
||||||
}
|
|
||||||
i18n.changeLanguage('en')
|
|
||||||
} else {
|
|
||||||
i18n.changeLanguage(language)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={language}>
|
|
||||||
<SafeAreaProvider>
|
|
||||||
<ActionSheetProvider>
|
|
||||||
<AccessibilityManager>
|
|
||||||
<ThemeManager>
|
|
||||||
<Screens localCorrupt={localCorrupt} />
|
|
||||||
</ThemeManager>
|
|
||||||
</AccessibilityManager>
|
|
||||||
</ActionSheetProvider>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
</IntlProvider>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
)
|
)
|
||||||
|
|
348
src/Screens.tsx
348
src/Screens.tsx
|
@ -1,348 +0,0 @@
|
||||||
import { HeaderLeft } from '@components/Header'
|
|
||||||
import { displayMessage, Message } from '@components/Message'
|
|
||||||
import navigationRef from '@helpers/navigationRef'
|
|
||||||
import { NavigationContainer } from '@react-navigation/native'
|
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|
||||||
import ScreenAccountSelection from '@screens/AccountSelection'
|
|
||||||
import ScreenActions from '@screens/Actions'
|
|
||||||
import ScreenAnnouncements from '@screens/Announcements'
|
|
||||||
import ScreenCompose from '@screens/Compose'
|
|
||||||
import ScreenImagesViewer from '@screens/ImagesViewer'
|
|
||||||
import ScreenTabs from '@screens/Tabs'
|
|
||||||
import * as Sentry from '@sentry/react-native'
|
|
||||||
import initQuery from '@utils/initQuery'
|
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
|
||||||
import pushUseConnect from '@utils/push/useConnect'
|
|
||||||
import pushUseReceive from '@utils/push/useReceive'
|
|
||||||
import pushUseRespond from '@utils/push/useRespond'
|
|
||||||
import { updatePreviousTab } from '@utils/slices/contextsSlice'
|
|
||||||
import { checkEmojis } from '@utils/slices/instances/checkEmojis'
|
|
||||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
|
||||||
import { updateConfiguration } from '@utils/slices/instances/updateConfiguration'
|
|
||||||
import { updateFilters } from '@utils/slices/instances/updateFilters'
|
|
||||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import { themes } from '@utils/styles/themes'
|
|
||||||
import * as Linking from 'expo-linking'
|
|
||||||
import { addScreenshotListener } from 'expo-screen-capture'
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { Alert, Platform, StatusBar } from 'react-native'
|
|
||||||
import ShareMenu from 'react-native-share-menu'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { useAppDispatch } from './store'
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>()
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
localCorrupt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|
||||||
const { t } = useTranslation('screens')
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
|
||||||
const { colors, theme } = useTheme()
|
|
||||||
|
|
||||||
const routeRef = useRef<{ name?: string; params?: {} }>()
|
|
||||||
|
|
||||||
// Push hooks
|
|
||||||
const instances = useSelector(getInstances, (prev, next) => prev.length === next.length)
|
|
||||||
pushUseConnect()
|
|
||||||
pushUseReceive()
|
|
||||||
pushUseRespond()
|
|
||||||
|
|
||||||
// Prevent screenshot alert
|
|
||||||
useEffect(() => {
|
|
||||||
const screenshotListener = addScreenshotListener(() =>
|
|
||||||
Alert.alert(t('screenshot.title'), t('screenshot.message'), [
|
|
||||||
{ text: t('common:buttons.confirm'), style: 'destructive' }
|
|
||||||
])
|
|
||||||
)
|
|
||||||
Platform.select({ ios: screenshotListener })
|
|
||||||
return () => screenshotListener.remove()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// On launch display login credentials corrupt information
|
|
||||||
useEffect(() => {
|
|
||||||
const showLocalCorrect = () => {
|
|
||||||
if (localCorrupt) {
|
|
||||||
displayMessage({
|
|
||||||
message: t('localCorrupt.message'),
|
|
||||||
description: localCorrupt.length ? localCorrupt : undefined,
|
|
||||||
type: 'danger'
|
|
||||||
})
|
|
||||||
// @ts-ignore
|
|
||||||
navigationRef.navigate('Screen-Tabs', {
|
|
||||||
screen: 'Tab-Me'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return showLocalCorrect()
|
|
||||||
}, [localCorrupt])
|
|
||||||
|
|
||||||
// Lazily update users's preferences, for e.g. composing default visibility
|
|
||||||
useEffect(() => {
|
|
||||||
if (instanceActive !== -1) {
|
|
||||||
dispatch(updateConfiguration())
|
|
||||||
dispatch(updateFilters())
|
|
||||||
dispatch(updateAccountPreferences())
|
|
||||||
dispatch(checkEmojis())
|
|
||||||
}
|
|
||||||
}, [instanceActive])
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
const navigationContainerOnReady = useCallback(() => {
|
|
||||||
const currentRoute = navigationRef.getCurrentRoute()
|
|
||||||
routeRef.current = {
|
|
||||||
name: currentRoute?.name,
|
|
||||||
params: currentRoute?.params ? JSON.stringify(currentRoute.params) : undefined
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
const navigationContainerOnStateChange = useCallback(() => {
|
|
||||||
const previousRoute = routeRef.current
|
|
||||||
const currentRoute = navigationRef.getCurrentRoute()
|
|
||||||
|
|
||||||
const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
|
|
||||||
if (matchTabName) {
|
|
||||||
//@ts-ignore
|
|
||||||
dispatch(updatePreviousTab(matchTabName[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousRoute?.name !== currentRoute?.name) {
|
|
||||||
Sentry.setContext('page', {
|
|
||||||
previous: previousRoute,
|
|
||||||
current: currentRoute
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
routeRef.current = currentRoute
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Deep linking for compose
|
|
||||||
const [deeplinked, setDeeplinked] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const getUrlAsync = async () => {
|
|
||||||
setDeeplinked(true)
|
|
||||||
|
|
||||||
const initialUrl = await Linking.parseInitialURLAsync()
|
|
||||||
|
|
||||||
if (initialUrl.path) {
|
|
||||||
const paths = initialUrl.path.split('/')
|
|
||||||
|
|
||||||
if (paths && paths.length) {
|
|
||||||
const instanceIndex = instances.findIndex(
|
|
||||||
instance => paths[0] === `@${instance.account.acct}@${instance.uri}`
|
|
||||||
)
|
|
||||||
if (instanceIndex !== -1 && instanceActive !== instanceIndex) {
|
|
||||||
initQuery({ instance: instances[instanceIndex] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialUrl.hostname === 'compose') {
|
|
||||||
navigationRef.navigate('Screen-Compose')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!deeplinked) {
|
|
||||||
getUrlAsync()
|
|
||||||
}
|
|
||||||
}, [instanceActive, instances, deeplinked])
|
|
||||||
|
|
||||||
// Share Extension
|
|
||||||
const handleShare = useCallback(
|
|
||||||
(
|
|
||||||
item?:
|
|
||||||
| {
|
|
||||||
data: { mimeType: string; data: string }[]
|
|
||||||
mimeType: undefined
|
|
||||||
}
|
|
||||||
| { data: string | string[]; mimeType: string }
|
|
||||||
) => {
|
|
||||||
if (instanceActive < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!item || !item.data) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let text: string | undefined = undefined
|
|
||||||
let media: { uri: string; mime: string }[] = []
|
|
||||||
|
|
||||||
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
|
|
||||||
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
|
|
||||||
const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => {
|
|
||||||
if (mime.startsWith('image/')) {
|
|
||||||
if (!typesImage.includes(mime.split('/')[1])) {
|
|
||||||
console.warn('Image type not supported:', mime.split('/')[1])
|
|
||||||
displayMessage({
|
|
||||||
message: t('shareError.imageNotSupported', {
|
|
||||||
type: mime.split('/')[1]
|
|
||||||
}),
|
|
||||||
type: 'danger'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media.push({ uri, mime })
|
|
||||||
} else if (mime.startsWith('video/')) {
|
|
||||||
if (!typesVideo.includes(mime.split('/')[1])) {
|
|
||||||
console.warn('Video type not supported:', mime.split('/')[1])
|
|
||||||
displayMessage({
|
|
||||||
message: t('shareError.videoNotSupported', {
|
|
||||||
type: mime.split('/')[1]
|
|
||||||
}),
|
|
||||||
type: 'danger'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media.push({ uri, mime })
|
|
||||||
} else {
|
|
||||||
if (typesImage.includes(uri.split('.').pop() || '')) {
|
|
||||||
media.push({ uri, mime: 'image/jpg' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (typesVideo.includes(uri.split('.').pop() || '')) {
|
|
||||||
media.push({ uri, mime: 'video/mp4' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text = !text ? uri : text.concat(text, `\n${uri}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (Platform.OS) {
|
|
||||||
case 'ios':
|
|
||||||
if (!Array.isArray(item.data) || !item.data) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const d of item.data) {
|
|
||||||
if (typeof d !== 'string') {
|
|
||||||
filterMedia({ uri: d.data, mime: d.mimeType })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'android':
|
|
||||||
if (!item.mimeType) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (Array.isArray(item.data)) {
|
|
||||||
for (const d of item.data) {
|
|
||||||
filterMedia({ uri: d, mime: item.mimeType })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filterMedia({ uri: item.data, mime: item.mimeType })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text && !media.length) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
if (instances.length > 1) {
|
|
||||||
navigationRef.navigate('Screen-AccountSelection', {
|
|
||||||
share: { text, media }
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
navigationRef.navigate('Screen-Compose', {
|
|
||||||
type: 'share',
|
|
||||||
text,
|
|
||||||
media
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
ShareMenu.getInitialShare(handleShare)
|
|
||||||
}, [])
|
|
||||||
useEffect(() => {
|
|
||||||
const listener = ShareMenu.addNewShareListener(handleShare)
|
|
||||||
return () => {
|
|
||||||
listener.remove()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StatusBar
|
|
||||||
backgroundColor={colors.backgroundDefault}
|
|
||||||
{...(Platform.OS === 'android' && {
|
|
||||||
barStyle: theme === 'light' ? 'dark-content' : 'light-content'
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<NavigationContainer
|
|
||||||
ref={navigationRef}
|
|
||||||
theme={themes[theme]}
|
|
||||||
onReady={navigationContainerOnReady}
|
|
||||||
onStateChange={navigationContainerOnStateChange}
|
|
||||||
>
|
|
||||||
<Stack.Navigator initialRouteName='Screen-Tabs'>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Tabs'
|
|
||||||
component={ScreenTabs}
|
|
||||||
options={{ headerShown: false }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Actions'
|
|
||||||
component={ScreenActions}
|
|
||||||
options={{
|
|
||||||
presentation: 'transparentModal',
|
|
||||||
animation: 'fade',
|
|
||||||
headerShown: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Announcements'
|
|
||||||
component={ScreenAnnouncements}
|
|
||||||
options={({ navigation }) => ({
|
|
||||||
presentation: 'transparentModal',
|
|
||||||
animation: 'fade',
|
|
||||||
headerShown: true,
|
|
||||||
headerShadowVisible: false,
|
|
||||||
headerTransparent: true,
|
|
||||||
headerStyle: { backgroundColor: 'transparent' },
|
|
||||||
headerLeft: () => <HeaderLeft content='X' onPress={() => navigation.goBack()} />,
|
|
||||||
title: t('screenAnnouncements:heading')
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Compose'
|
|
||||||
component={ScreenCompose}
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: 'fullScreenModal'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-ImagesViewer'
|
|
||||||
component={ScreenImagesViewer}
|
|
||||||
options={{ headerShown: false, animation: 'fade' }}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-AccountSelection'
|
|
||||||
component={ScreenAccountSelection}
|
|
||||||
options={({ navigation }) => ({
|
|
||||||
title: t('screenAccountSelection:heading'),
|
|
||||||
headerShadowVisible: false,
|
|
||||||
presentation: 'modal',
|
|
||||||
gestureEnabled: false,
|
|
||||||
headerLeft: () => (
|
|
||||||
<HeaderLeft
|
|
||||||
type='text'
|
|
||||||
content={t('common:buttons.cancel')}
|
|
||||||
onPress={() => navigation.goBack()}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
|
|
||||||
<Message />
|
|
||||||
</NavigationContainer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(Screens, () => true)
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { RootState } from '@root/store'
|
|
||||||
import axios, { AxiosRequestConfig } from 'axios'
|
|
||||||
import { ctx, handleError, PagedResponse, userAgent } from './helpers'
|
|
||||||
|
|
||||||
export type Params = {
|
|
||||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
|
||||||
version?: 'v1' | 'v2'
|
|
||||||
url: string
|
|
||||||
params?: {
|
|
||||||
[key: string]: string | number | boolean | string[] | number[] | boolean[]
|
|
||||||
}
|
|
||||||
headers?: { [key: string]: string }
|
|
||||||
body?: FormData
|
|
||||||
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiInstance = async <T = unknown>({
|
|
||||||
method,
|
|
||||||
version = 'v1',
|
|
||||||
url,
|
|
||||||
params,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
extras
|
|
||||||
}: Params): Promise<PagedResponse<T>> => {
|
|
||||||
const { store } = require('@root/store')
|
|
||||||
const state = store.getState() as RootState
|
|
||||||
const instanceActive = state.instances.instances.findIndex(instance => instance.active)
|
|
||||||
|
|
||||||
let domain
|
|
||||||
let token
|
|
||||||
if (instanceActive !== -1 && state.instances.instances[instanceActive]) {
|
|
||||||
domain = state.instances.instances[instanceActive].url
|
|
||||||
token = state.instances.instances[instanceActive].token
|
|
||||||
} else {
|
|
||||||
console.warn(ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided')
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
ctx.bgGreen.bold(' API instance ') +
|
|
||||||
' ' +
|
|
||||||
domain +
|
|
||||||
' ' +
|
|
||||||
method +
|
|
||||||
ctx.green(' -> ') +
|
|
||||||
`/${url}` +
|
|
||||||
(params ? ctx.green(' -> ') : ''),
|
|
||||||
params ? params : ''
|
|
||||||
)
|
|
||||||
|
|
||||||
return axios({
|
|
||||||
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
|
|
||||||
method,
|
|
||||||
baseURL: `https://${domain}/api/${version}/`,
|
|
||||||
url,
|
|
||||||
params,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
|
|
||||||
Accept: '*/*',
|
|
||||||
...userAgent,
|
|
||||||
...headers,
|
|
||||||
...(token && {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
})
|
|
||||||
},
|
|
||||||
...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
|
|
||||||
...extras
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
let links: {
|
|
||||||
prev?: { id: string; isOffset: boolean }
|
|
||||||
next?: { id: string; isOffset: boolean }
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
if (response.headers?.link) {
|
|
||||||
const linksParsed = response.headers.link.matchAll(
|
|
||||||
new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
|
|
||||||
)
|
|
||||||
for (const link of linksParsed) {
|
|
||||||
switch (link[3]) {
|
|
||||||
case 'prev':
|
|
||||||
links.prev = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
links.next = { id: link[2], isOffset: link[1].includes('offset') }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve({ body: response.data, links })
|
|
||||||
})
|
|
||||||
.catch(handleError())
|
|
||||||
}
|
|
||||||
|
|
||||||
export default apiInstance
|
|
|
@ -11,7 +11,7 @@ import Icon from './Icon'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'>
|
||||||
props?: PressableProps
|
props?: PressableProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +42,11 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
|
||||||
style={{
|
style={{
|
||||||
width: StyleConstants.Avatar.S,
|
width: StyleConstants.Avatar.S,
|
||||||
height: StyleConstants.Avatar.S,
|
height: StyleConstants.Avatar.S,
|
||||||
borderRadius: 6,
|
borderRadius: 8,
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginRight: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View>
|
<View style={{ flex: 1 }}>
|
||||||
<CustomText numberOfLines={1}>
|
<CustomText numberOfLines={1}>
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
content={account.display_name || account.username}
|
content={account.display_name || account.username}
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import initQuery from '@utils/initQuery'
|
import { ReadableAccountType, setAccount } from '@utils/storage/actions'
|
||||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import haptics from './haptics'
|
import haptics from './haptics'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instance: InstanceLatest
|
account: ReadableAccountType
|
||||||
selected?: boolean
|
|
||||||
additionalActions?: () => void
|
additionalActions?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
|
const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
selected={selected}
|
selected={account.active}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: StyleConstants.Spacing.M,
|
marginBottom: StyleConstants.Spacing.M,
|
||||||
marginRight: StyleConstants.Spacing.M
|
marginRight: StyleConstants.Spacing.M
|
||||||
}}
|
}}
|
||||||
content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`}
|
content={account.acct}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
initQuery({ instance })
|
setAccount(account.key)
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
if (additionalActions) {
|
if (additionalActions) {
|
||||||
additionalActions()
|
additionalActions()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({
|
||||||
overlay = false,
|
overlay = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<View style={{ position: 'absolute' }}>
|
||||||
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const mainColor = useMemo(() => {
|
const mainColor = () => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return colors.blue
|
return colors.blue
|
||||||
} else if (overlay) {
|
} else if (overlay) {
|
||||||
|
@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({
|
||||||
return colors.primaryDefault
|
return colors.primaryDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, disabled, loading, selected])
|
}
|
||||||
|
|
||||||
const colorBackground = useMemo(() => {
|
const children = () => {
|
||||||
if (overlay) {
|
|
||||||
return colors.backgroundOverlayInvert
|
|
||||||
} else {
|
|
||||||
return colors.backgroundDefault
|
|
||||||
}
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name={content}
|
name={content}
|
||||||
color={mainColor}
|
color={mainColor()}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
style={{ opacity: loading ? 0 : 1 }}
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
|
@ -103,19 +93,19 @@ const Button: React.FC<Props> = ({
|
||||||
<>
|
<>
|
||||||
<CustomText
|
<CustomText
|
||||||
style={{
|
style={{
|
||||||
color: mainColor,
|
color: mainColor(),
|
||||||
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
||||||
opacity: loading ? 0 : 1
|
opacity: loading ? 0 : 1
|
||||||
}}
|
}}
|
||||||
fontWeight={fontBold ? 'Bold' : 'Normal'}
|
fontWeight={fontBold || selected ? 'Bold' : 'Normal'}
|
||||||
children={content}
|
children={content}
|
||||||
testID='text'
|
testID='text'
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, content, loading, disabled])
|
}
|
||||||
|
|
||||||
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
||||||
|
|
||||||
|
@ -135,9 +125,9 @@ const Button: React.FC<Props> = ({
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: overlay ? 0 : 1,
|
borderWidth: overlay ? 0 : selected ? 1.5 : 1,
|
||||||
borderColor: mainColor,
|
borderColor: mainColor(),
|
||||||
backgroundColor: colorBackground,
|
backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault,
|
||||||
paddingVertical: StyleConstants.Spacing[spacing],
|
paddingVertical: StyleConstants.Spacing[spacing],
|
||||||
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
||||||
width: round && layoutHeight ? layoutHeight : undefined
|
width: round && layoutHeight ? layoutHeight : undefined
|
||||||
|
@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({
|
||||||
})}
|
})}
|
||||||
testID='base'
|
testID='base'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={selected || disabled || loading}
|
disabled={selected || disabled || loading}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Keyboard, Pressable, View } from 'react-native'
|
import { Keyboard, Pressable, View } from 'react-native'
|
||||||
import EmojisContext from './helpers/EmojisContext'
|
import EmojisContext from './Context'
|
||||||
|
|
||||||
const EmojisButton: React.FC = () => {
|
const EmojisButton: React.FC = () => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { emojis } from '@components/Emojis'
|
import { emojis } from '@components/Emojis'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { countInstanceEmoji } from '@utils/slices/instancesSlice'
|
import { StorageAccount } from '@utils/storage/account'
|
||||||
|
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
|
||||||
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 { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
@ -19,16 +19,14 @@ import {
|
||||||
View
|
View
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import validUrl from 'valid-url'
|
import EmojisContext from './Context'
|
||||||
import EmojisContext from './helpers/EmojisContext'
|
|
||||||
|
|
||||||
const EmojisList = () => {
|
const EmojisList = () => {
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
|
|
||||||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||||
const { colors, mode } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const addEmoji = (shortcode: string) => {
|
const addEmoji = (shortcode: string) => {
|
||||||
if (emojisState.targetIndex === -1) {
|
if (emojisState.targetIndex === -1) {
|
||||||
|
@ -69,31 +67,77 @@ const EmojisList = () => {
|
||||||
>
|
>
|
||||||
{item.map(emoji => {
|
{item.map(emoji => {
|
||||||
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||||
if (validUrl.isHttpsUri(uri)) {
|
return (
|
||||||
return (
|
<Pressable
|
||||||
<Pressable
|
key={emoji.shortcode}
|
||||||
key={emoji.shortcode}
|
onPress={() => {
|
||||||
onPress={() => {
|
addEmoji(`:${emoji.shortcode}:`)
|
||||||
addEmoji(`:${emoji.shortcode}:`)
|
|
||||||
dispatch(countInstanceEmoji(emoji))
|
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
|
||||||
}}
|
const calculateScore = (
|
||||||
style={{ padding: StyleConstants.Spacing.S }}
|
emoji: StorageAccount['emojis_frequent'][number]
|
||||||
>
|
): number => {
|
||||||
<FastImage
|
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
|
||||||
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
|
var score = emoji.count + 1
|
||||||
emoji: emoji.shortcode
|
var order = Math.log(Math.max(score, 1)) / Math.LN10
|
||||||
})}
|
var sign = score > 0 ? 1 : score === 0 ? 0 : -1
|
||||||
accessibilityHint={t(
|
return (sign * order + seconds / HALF_LIFE) * 10
|
||||||
'screenCompose:content.root.footer.emojis.accessibilityHint'
|
}
|
||||||
)}
|
|
||||||
source={{ uri }}
|
const currentEmojis = getAccountStorage.object('emojis_frequent')
|
||||||
style={{ width: 32, height: 32 }}
|
const foundEmojiIndex = currentEmojis?.findIndex(
|
||||||
/>
|
e => e.emoji.shortcode === emoji.shortcode && e.emoji.url === emoji.url
|
||||||
</Pressable>
|
)
|
||||||
)
|
|
||||||
} else {
|
let newEmojisSort: StorageAccount['emojis_frequent']
|
||||||
return null
|
if (foundEmojiIndex === -1) {
|
||||||
}
|
newEmojisSort = currentEmojis || []
|
||||||
|
const temp = {
|
||||||
|
emoji,
|
||||||
|
score: 0,
|
||||||
|
count: 0,
|
||||||
|
lastUsed: new Date().getTime()
|
||||||
|
}
|
||||||
|
newEmojisSort.push({
|
||||||
|
...temp,
|
||||||
|
score: calculateScore(temp),
|
||||||
|
count: temp.count + 1
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
newEmojisSort =
|
||||||
|
currentEmojis
|
||||||
|
?.map((e, i) =>
|
||||||
|
i === foundEmojiIndex
|
||||||
|
? {
|
||||||
|
...e,
|
||||||
|
score: calculateScore(e),
|
||||||
|
count: e.count + 1,
|
||||||
|
lastUsed: new Date().getTime()
|
||||||
|
}
|
||||||
|
: e
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.score - a.score) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountStorage([
|
||||||
|
{
|
||||||
|
key: 'emojis_frequent',
|
||||||
|
value: newEmojisSort.sort((a, b) => b.score - a.score).slice(0, 20)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
style={{ padding: StyleConstants.Spacing.S }}
|
||||||
|
>
|
||||||
|
<FastImage
|
||||||
|
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
|
||||||
|
emoji: emoji.shortcode
|
||||||
|
})}
|
||||||
|
accessibilityHint={t('screenCompose:content.root.footer.emojis.accessibilityHint')}
|
||||||
|
source={{ uri }}
|
||||||
|
style={{ width: 32, height: 32 }}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -158,7 +202,6 @@ const EmojisList = () => {
|
||||||
onChangeText={setSearch}
|
onChangeText={setSearch}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
clearButtonMode='always'
|
clearButtonMode='always'
|
||||||
keyboardAppearance={mode}
|
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,15 +2,13 @@ import EmojisButton from '@components/Emojis/Button'
|
||||||
import EmojisList from '@components/Emojis/List'
|
import EmojisList from '@components/Emojis/List'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
||||||
import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
|
import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Keyboard, KeyboardAvoidingView, View } from 'react-native'
|
import { Keyboard, KeyboardAvoidingView, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
|
||||||
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useSelector } from 'react-redux'
|
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Context'
|
||||||
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Emojis/helpers/EmojisContext'
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
inputProps: EmojisState['inputProps']
|
inputProps: EmojisState['inputProps']
|
||||||
|
@ -35,9 +33,9 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
|
||||||
emojisDispatch({ type: 'input', payload: inputProps })
|
emojisDispatch({ type: 'input', payload: inputProps })
|
||||||
}, [inputProps])
|
}, [inputProps])
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation(['componentEmojis'])
|
||||||
const { data } = useEmojisQuery({})
|
const { data } = useEmojisQuery({})
|
||||||
const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
|
const [frequentEmojis] = useAccountStorage.object('emojis_frequent')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data.length) {
|
if (data && data.length) {
|
||||||
let sortedEmojis: NonNullable<Emojis['current']> = []
|
let sortedEmojis: NonNullable<Emojis['current']> = []
|
|
@ -1,6 +1,6 @@
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
AccessibilityProps,
|
||||||
Image,
|
Image,
|
||||||
|
@ -10,8 +10,8 @@ import {
|
||||||
View,
|
View,
|
||||||
ViewStyle
|
ViewStyle
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
|
||||||
import { Blurhash } from 'react-native-blurhash'
|
import { Blurhash } from 'react-native-blurhash'
|
||||||
|
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||||
|
|
||||||
// blurhas -> if blurhash, show before any loading succeed
|
// blurhas -> if blurhash, show before any loading succeed
|
||||||
// original -> load original
|
// original -> load original
|
||||||
|
@ -55,17 +55,12 @@ const GracefullyImage = ({
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const [imageLoaded, setImageLoaded] = useState(false)
|
const [imageLoaded, setImageLoaded] = useState(false)
|
||||||
|
|
||||||
|
const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote)
|
||||||
const source = {
|
const source = {
|
||||||
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
|
uri: reduceMotionEnabled && uri.static ? uri.static : currentUri
|
||||||
}
|
|
||||||
const onLoad = () => {
|
|
||||||
setImageLoaded(true)
|
|
||||||
if (setImageDimensions && source.uri) {
|
|
||||||
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const blurhashView = useMemo(() => {
|
const blurhashView = () => {
|
||||||
if (hidden || !imageLoaded) {
|
if (hidden || !imageLoaded) {
|
||||||
if (blurhash) {
|
if (blurhash) {
|
||||||
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
||||||
|
@ -75,7 +70,7 @@ const GracefullyImage = ({
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}, [hidden, imageLoaded])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
@ -92,13 +87,21 @@ const GracefullyImage = ({
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{
|
source={source}
|
||||||
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
|
|
||||||
}}
|
|
||||||
style={[{ flex: 1 }, imageStyle]}
|
style={[{ flex: 1 }, imageStyle]}
|
||||||
onLoad={onLoad}
|
onLoad={() => {
|
||||||
|
setImageLoaded(true)
|
||||||
|
if (setImageDimensions && source.uri) {
|
||||||
|
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
if (uri.original && uri.original === currentUri && uri.remote) {
|
||||||
|
setCurrentUri(uri.remote)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{blurhashView}
|
{blurhashView()}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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, { PropsWithChildren, useCallback, useState } from 'react'
|
import React, { PropsWithChildren, useState } from 'react'
|
||||||
import { Dimensions, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable, View } from 'react-native'
|
||||||
import Sparkline from './Sparkline'
|
import Sparkline from './Sparkline'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = () => {
|
||||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const padding = StyleConstants.Spacing.Global.PagePadding
|
const padding = StyleConstants.Spacing.Global.PagePadding
|
||||||
const width = Dimensions.get('window').width / 4
|
const width = Dimensions.get('window').width / 4
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { Pressable } from 'react-native'
|
import { Pressable } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({
|
||||||
background = false,
|
background = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
|
@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<CustomText
|
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} />
|
||||||
fontStyle='M'
|
|
||||||
style={{ color: colors.primaryDefault }}
|
|
||||||
children={content}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: background
|
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||||
? colors.backgroundOverlayDefault
|
|
||||||
: undefined,
|
|
||||||
minHeight: 44,
|
minHeight: 44,
|
||||||
minWidth: 44,
|
minWidth: 44,
|
||||||
marginLeft: native
|
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||||
? -StyleConstants.Spacing.S
|
|
||||||
: StyleConstants.Spacing.S,
|
|
||||||
...(type === 'icon' && {
|
...(type === 'icon' && {
|
||||||
borderRadius: 100
|
borderRadius: 100
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { AccessibilityProps, Pressable, View } from 'react-native'
|
import { AccessibilityProps, Pressable, View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export interface Props {
|
||||||
|
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
destructive?: boolean
|
||||||
|
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
}
|
}
|
||||||
|
@ -34,23 +35,19 @@ const HeaderRight: React.FC<Props> = ({
|
||||||
background = false,
|
background = false,
|
||||||
loading,
|
loading,
|
||||||
disabled,
|
disabled,
|
||||||
|
destructive = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<View style={{ position: 'absolute' }}>
|
||||||
<Flow
|
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
size={StyleConstants.Font.Size.M * 1.25}
|
|
||||||
color={colors.secondary}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
|
@ -59,9 +56,9 @@ const HeaderRight: React.FC<Props> = ({
|
||||||
name={content}
|
name={content}
|
||||||
style={{ opacity: loading ? 0 : 1 }}
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
size={StyleConstants.Spacing.M * 1.25}
|
size={StyleConstants.Spacing.M * 1.25}
|
||||||
color={disabled ? colors.secondary : colors.primaryDefault}
|
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
|
@ -69,17 +66,22 @@ const HeaderRight: React.FC<Props> = ({
|
||||||
<>
|
<>
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='M'
|
fontStyle='M'
|
||||||
|
fontWeight={destructive ? 'Bold' : 'Normal'}
|
||||||
style={{
|
style={{
|
||||||
color: disabled ? colors.secondary : colors.primaryDefault,
|
color: disabled
|
||||||
|
? colors.secondary
|
||||||
|
: destructive
|
||||||
|
? colors.red
|
||||||
|
: colors.primaryDefault,
|
||||||
opacity: loading ? 0 : 1
|
opacity: loading ? 0 : 1
|
||||||
}}
|
}}
|
||||||
children={content}
|
children={content}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, loading, disabled])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
@ -88,20 +90,16 @@ const HeaderRight: React.FC<Props> = ({
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityState={accessibilityState}
|
accessibilityState={accessibilityState}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: background
|
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||||
? colors.backgroundOverlayDefault
|
|
||||||
: undefined,
|
|
||||||
minHeight: 44,
|
minHeight: 44,
|
||||||
minWidth: 44,
|
minWidth: 44,
|
||||||
marginRight: native
|
marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||||
? -StyleConstants.Spacing.S
|
|
||||||
: StyleConstants.Spacing.S,
|
|
||||||
...(type === 'icon' && {
|
...(type === 'icon' && {
|
||||||
borderRadius: 100
|
borderRadius: 100
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import HeaderLeft from '@components/Header/Left'
|
|
||||||
import HeaderCenter from '@components/Header/Center'
|
import HeaderCenter from '@components/Header/Center'
|
||||||
|
import HeaderLeft from '@components/Header/Left'
|
||||||
import HeaderRight from '@components/Header/Right'
|
import HeaderRight from '@components/Header/Right'
|
||||||
|
|
||||||
export { HeaderLeft, HeaderCenter, HeaderRight }
|
export { HeaderLeft, HeaderCenter, HeaderRight }
|
|
@ -3,7 +3,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { forwardRef, RefObject } from 'react'
|
import React, { forwardRef, RefObject } from 'react'
|
||||||
import { Platform, TextInput, TextInputProps, View } from 'react-native'
|
import { Platform, TextInput, TextInputProps, View } from 'react-native'
|
||||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||||
import { EmojisState } from './Emojis/helpers/EmojisContext'
|
import { EmojisState } from './Emojis/Context'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
@ -85,7 +85,6 @@ const ComponentInput = forwardRef(
|
||||||
multiline,
|
multiline,
|
||||||
numberOfLines: Platform.OS === 'android' ? 5 : undefined
|
numberOfLines: Platform.OS === 'android' ? 5 : undefined
|
||||||
})}
|
})}
|
||||||
keyboardAppearance={mode}
|
|
||||||
textAlignVertical='top'
|
textAlignVertical='top'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import CustomText from '@components/Text'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import React from 'react'
|
|
||||||
import { View, ViewStyle } from 'react-native'
|
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
style?: ViewStyle
|
|
||||||
header: string
|
|
||||||
content?: string
|
|
||||||
potentialWidth?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const InstanceInfo = React.memo(
|
|
||||||
({ style, header, content, potentialWidth }: Props) => {
|
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
flex: 1,
|
|
||||||
marginTop: StyleConstants.Spacing.M,
|
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
|
||||||
style
|
|
||||||
]}
|
|
||||||
accessible
|
|
||||||
>
|
|
||||||
<CustomText
|
|
||||||
fontStyle='S'
|
|
||||||
style={{
|
|
||||||
marginBottom: StyleConstants.Spacing.XS,
|
|
||||||
color: colors.primaryDefault
|
|
||||||
}}
|
|
||||||
fontWeight='Bold'
|
|
||||||
children={header}
|
|
||||||
/>
|
|
||||||
{content ? (
|
|
||||||
<CustomText
|
|
||||||
fontStyle='M'
|
|
||||||
style={{ color: colors.primaryDefault }}
|
|
||||||
children={content}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PlaceholderLine
|
|
||||||
width={
|
|
||||||
potentialWidth
|
|
||||||
? potentialWidth * StyleConstants.Font.Size.M
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
height={StyleConstants.Font.LineHeight.M}
|
|
||||||
color={colors.shimmerDefault}
|
|
||||||
noMargin
|
|
||||||
style={{ borderRadius: 0 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
(prev, next) => prev.content === next.content
|
|
||||||
)
|
|
||||||
|
|
||||||
export default InstanceInfo
|
|
|
@ -1,9 +1,21 @@
|
||||||
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 { useNavigation } from '@react-navigation/native'
|
||||||
|
import apiGeneral from '@utils/api/general'
|
||||||
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
|
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
||||||
|
import { queryClient } from '@utils/queryHooks'
|
||||||
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
||||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
import { checkInstanceFeature, getInstances } from '@utils/slices/instancesSlice'
|
import { StorageAccount } from '@utils/storage/account'
|
||||||
|
import {
|
||||||
|
generateAccountKey,
|
||||||
|
getGlobalStorage,
|
||||||
|
setAccount,
|
||||||
|
setAccountStorage,
|
||||||
|
setGlobalStorage
|
||||||
|
} from '@utils/storage/actions'
|
||||||
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 AuthSession from 'expo-auth-session'
|
import * as AuthSession from 'expo-auth-session'
|
||||||
|
@ -13,16 +25,8 @@ import React, { RefObject, useCallback, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useSelector } from 'react-redux'
|
import parse from 'url-parse'
|
||||||
import { Placeholder } from 'rn-placeholder'
|
|
||||||
import validUrl from 'valid-url'
|
|
||||||
import InstanceInfo from './Info'
|
|
||||||
import CustomText from '../Text'
|
import CustomText from '../Text'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
|
||||||
import queryClient from '@helpers/queryClient'
|
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import addInstance from '@utils/slices/instances/add'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollViewRef?: RefObject<ScrollView>
|
scrollViewRef?: RefObject<ScrollView>
|
||||||
|
@ -35,7 +39,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
disableHeaderImage,
|
disableHeaderImage,
|
||||||
goBack = false
|
goBack = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('componentInstance')
|
const { t } = useTranslation(['common', 'componentInstance'])
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||||
|
|
||||||
|
@ -44,11 +48,9 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
const whitelisted: boolean =
|
const whitelisted: boolean =
|
||||||
!!domain.length &&
|
!!domain.length &&
|
||||||
!!errorCode &&
|
!!errorCode &&
|
||||||
!!validUrl.isHttpsUri(`https://${domain}`) &&
|
!!(parse(`https://${domain}/`).hostname === domain) &&
|
||||||
errorCode === 401
|
errorCode === 401
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const instances = useSelector(getInstances, () => true)
|
|
||||||
const instanceQuery = useInstanceQuery({
|
const instanceQuery = useInstanceQuery({
|
||||||
domain,
|
domain,
|
||||||
options: {
|
options: {
|
||||||
|
@ -62,8 +64,6 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
|
||||||
|
|
||||||
const appsMutation = useAppsMutation({
|
const appsMutation = useAppsMutation({
|
||||||
retry: false,
|
retry: false,
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, variables) => {
|
||||||
|
@ -75,56 +75,145 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
const request = new AuthSession.AuthRequest({
|
const request = new AuthSession.AuthRequest({
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
scopes: deprecateAuthFollow
|
scopes: variables.scopes,
|
||||||
? ['read', 'write', 'push']
|
|
||||||
: ['read', 'write', 'follow', 'push'],
|
|
||||||
redirectUri
|
redirectUri
|
||||||
})
|
})
|
||||||
await request.makeAuthUrlAsync(discovery)
|
await request.makeAuthUrlAsync(discovery)
|
||||||
|
|
||||||
const promptResult = await request.promptAsync(discovery)
|
const promptResult = await request.promptAsync(discovery, await browserPackage())
|
||||||
|
|
||||||
if (promptResult?.type === 'success') {
|
if (promptResult.type === 'success') {
|
||||||
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
||||||
{
|
{
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
scopes: ['read', 'write', 'follow', 'push'],
|
scopes: variables.scopes,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
code: promptResult.params.code,
|
code: promptResult.params.code,
|
||||||
extraParams: { grant_type: 'authorization_code' }
|
extraParams: {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
...(request.codeVerifier && { code_verifier: request.codeVerifier })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
|
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
|
||||||
)
|
)
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
dispatch(
|
|
||||||
addInstance({
|
const {
|
||||||
domain,
|
body: { id, acct, avatar_static }
|
||||||
token: accessToken,
|
} = await apiGeneral<Mastodon.Account>({
|
||||||
instance: instanceQuery.data!,
|
method: 'get',
|
||||||
appData: { clientId, clientSecret }
|
domain,
|
||||||
})
|
url: `api/v1/accounts/verify_credentials`,
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
const accountKey = generateAccountKey({ domain, id })
|
||||||
|
const account = accounts?.find(account => account === accountKey)
|
||||||
|
|
||||||
|
const accountDetails: StorageAccount = {
|
||||||
|
'auth.clientId': clientId,
|
||||||
|
'auth.clientSecret': clientSecret,
|
||||||
|
'auth.token': accessToken,
|
||||||
|
'auth.domain': domain,
|
||||||
|
'auth.account.id': id,
|
||||||
|
'auth.account.acct': acct,
|
||||||
|
'auth.account.domain':
|
||||||
|
(instanceQuery.data as Mastodon.Instance_V2)?.domain ||
|
||||||
|
instanceQuery.data?.account_domain ||
|
||||||
|
((instanceQuery.data as Mastodon.Instance_V1)?.uri
|
||||||
|
? parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname
|
||||||
|
: undefined) ||
|
||||||
|
(instanceQuery.data as Mastodon.Instance_V1)?.uri,
|
||||||
|
'auth.account.avatar_static': avatar_static,
|
||||||
|
version: instanceQuery.data?.version || '0',
|
||||||
|
preferences: undefined,
|
||||||
|
notifications: {
|
||||||
|
follow: true,
|
||||||
|
follow_request: true,
|
||||||
|
favourite: true,
|
||||||
|
reblog: true,
|
||||||
|
mention: true,
|
||||||
|
poll: true,
|
||||||
|
status: true,
|
||||||
|
update: true,
|
||||||
|
'admin.sign_up': true,
|
||||||
|
'admin.report': true
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
global: false,
|
||||||
|
decode: false,
|
||||||
|
alerts: {
|
||||||
|
follow: true,
|
||||||
|
follow_request: true,
|
||||||
|
favourite: true,
|
||||||
|
reblog: true,
|
||||||
|
mention: true,
|
||||||
|
poll: true,
|
||||||
|
status: true,
|
||||||
|
update: true,
|
||||||
|
'admin.sign_up': false,
|
||||||
|
'admin.report': false
|
||||||
|
},
|
||||||
|
key: Math.random().toString(36).slice(2, 12)
|
||||||
|
},
|
||||||
|
page_local: {
|
||||||
|
showBoosts: true,
|
||||||
|
showReplies: true
|
||||||
|
},
|
||||||
|
page_me: {
|
||||||
|
followedTags: { shown: false },
|
||||||
|
lists: { shown: false },
|
||||||
|
announcements: { shown: false, unread: 0 }
|
||||||
|
},
|
||||||
|
drafts: [],
|
||||||
|
emojis_frequent: []
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountStorage(
|
||||||
|
Object.keys(accountDetails).map((key: keyof StorageAccount) => ({
|
||||||
|
key,
|
||||||
|
value: accountDetails[key]
|
||||||
|
})),
|
||||||
|
accountKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
setGlobalStorage('accounts', accounts?.concat([accountKey]))
|
||||||
|
}
|
||||||
|
setAccount(accountKey)
|
||||||
|
|
||||||
goBack && navigation.goBack()
|
goBack && navigation.goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const scopes = featureCheck('deprecate_auth_follow')
|
||||||
|
? ['read', 'write', 'push']
|
||||||
|
: ['read', 'write', 'follow', 'push']
|
||||||
const processUpdate = useCallback(() => {
|
const processUpdate = useCallback(() => {
|
||||||
if (domain) {
|
if (domain) {
|
||||||
if (instances && instances.filter(instance => instance.url === domain).length) {
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
Alert.alert(t('update.alert.title'), t('update.alert.message'), [
|
if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) {
|
||||||
{
|
Alert.alert(
|
||||||
text: t('common:buttons.cancel'),
|
t('componentInstance:update.alert.title'),
|
||||||
style: 'cancel'
|
t('componentInstance:update.alert.message'),
|
||||||
},
|
[
|
||||||
{
|
{
|
||||||
text: t('common:buttons.continue'),
|
text: t('common:buttons.cancel'),
|
||||||
onPress: () => appsMutation.mutate({ domain })
|
style: 'cancel'
|
||||||
}
|
},
|
||||||
])
|
{
|
||||||
|
text: t('common:buttons.continue'),
|
||||||
|
onPress: () => appsMutation.mutate({ domain, scopes })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
appsMutation.mutate({ domain })
|
appsMutation.mutate({ domain, scopes })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [domain])
|
}, [domain])
|
||||||
|
@ -204,12 +293,13 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
text === domain &&
|
text === domain &&
|
||||||
instanceQuery.isSuccess &&
|
instanceQuery.isSuccess &&
|
||||||
instanceQuery.data &&
|
instanceQuery.data &&
|
||||||
instanceQuery.data.uri
|
// @ts-ignore
|
||||||
|
(instanceQuery.data.domain || instanceQuery.data.uri)
|
||||||
) {
|
) {
|
||||||
processUpdate()
|
processUpdate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={' ' + t('server.textInput.placeholder')}
|
placeholder={' ' + t('componentInstance:server.textInput.placeholder')}
|
||||||
placeholderTextColor={colors.secondary}
|
placeholderTextColor={colors.secondary}
|
||||||
returnKeyType='go'
|
returnKeyType='go'
|
||||||
keyboardAppearance={mode}
|
keyboardAppearance={mode}
|
||||||
|
@ -222,9 +312,10 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
content={t('server.button')}
|
content={t('componentInstance:server.button')}
|
||||||
onPress={processUpdate}
|
onPress={processUpdate}
|
||||||
disabled={!instanceQuery.data?.uri && !whitelisted}
|
// @ts-ignore
|
||||||
|
disabled={!(instanceQuery.data?.domain || instanceQuery.data?.uri) && !whitelisted}
|
||||||
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -239,37 +330,9 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
paddingTop: StyleConstants.Spacing.XS
|
paddingTop: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('server.whitelisted')}
|
{t('componentInstance:server.whitelisted')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
) : (
|
) : null}
|
||||||
<Placeholder>
|
|
||||||
<InstanceInfo
|
|
||||||
header={t('server.information.name')}
|
|
||||||
content={instanceQuery.data?.title || undefined}
|
|
||||||
potentialWidth={2}
|
|
||||||
/>
|
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'flex-start' }}
|
|
||||||
header={t('server.information.accounts')}
|
|
||||||
content={instanceQuery.data?.stats?.user_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'center' }}
|
|
||||||
header={t('server.information.statuses')}
|
|
||||||
content={instanceQuery.data?.stats?.status_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'flex-end' }}
|
|
||||||
header={t('server.information.domains')}
|
|
||||||
content={instanceQuery.data?.stats?.domain_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Placeholder>
|
|
||||||
)}
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -287,7 +350,7 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
|
||||||
{t('server.disclaimer.base')}
|
{t('componentInstance:server.disclaimer.base')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
|
@ -312,7 +375,8 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
accessibilityRole='link'
|
accessibilityRole='link'
|
||||||
>
|
>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='componentInstance:server.terms.base'
|
ns='componentInstance'
|
||||||
|
i18nKey='server.terms.base'
|
||||||
components={[
|
components={[
|
||||||
<CustomText
|
<CustomText
|
||||||
accessible
|
accessible
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react'
|
import CustomText from '@components/Text'
|
||||||
import { View } from 'react-native'
|
|
||||||
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 CustomText from '@components/Text'
|
import React from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
heading: string
|
heading: string
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { ColorDefinitions } from '@utils/styles/themes'
|
import { ColorDefinitions } from '@utils/styles/themes'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { 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'
|
||||||
|
@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { screenReaderEnabled } = useAccessibility()
|
const { screenReaderEnabled } = useAccessibility()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
|
||||||
() => (
|
|
||||||
<View style={{ position: 'absolute' }}>
|
|
||||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{ minHeight: 50 }}
|
style={{ minHeight: 50 }}
|
||||||
|
@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({
|
||||||
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{loading && loadingSpinkit}
|
{loading ? (
|
||||||
|
<View style={{ position: 'absolute' }}>
|
||||||
|
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject } from 'react'
|
import React, { RefObject } from 'react'
|
||||||
import { AccessibilityInfo } from 'react-native'
|
import { AccessibilityInfo } from 'react-native'
|
||||||
import FlashMessage, { MessageType, showMessage } from 'react-native-flash-message'
|
import FlashMessage, { MessageType, showMessage } from 'react-native-flash-message'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
||||||
import haptics from './haptics'
|
import haptics from './haptics'
|
||||||
|
|
||||||
const displayMessage = ({
|
const displayMessage = ({
|
||||||
|
@ -53,7 +52,6 @@ const displayMessage = ({
|
||||||
|
|
||||||
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
|
|
||||||
enum iconMapping {
|
enum iconMapping {
|
||||||
success = 'CheckCircle',
|
success = 'CheckCircle',
|
||||||
|
@ -96,8 +94,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
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: ref ? undefined : insets.top
|
|
||||||
}}
|
}}
|
||||||
titleStyle={{
|
titleStyle={{
|
||||||
color: colors.primaryDefault,
|
color: colors.primaryDefault,
|
||||||
|
@ -109,7 +106,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||||
...StyleConstants.FontStyle.S
|
...StyleConstants.FontStyle.S
|
||||||
}}
|
}}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
textProps={{ numberOfLines: 2 }}
|
textProps={{ numberOfLines: 3 }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import ParseEmojis from './Parse/Emojis'
|
|
||||||
import ParseHTML from './Parse/HTML'
|
|
||||||
|
|
||||||
export { ParseEmojis, ParseHTML }
|
|
|
@ -1,19 +1,17 @@
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Platform, TextStyle } from 'react-native'
|
import { Platform, TextStyle } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import validUrl from 'valid-url'
|
|
||||||
|
|
||||||
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
content: string
|
content?: string
|
||||||
emojis?: Mastodon.Emoji[]
|
emojis?: Mastodon.Emoji[]
|
||||||
size?: 'S' | 'M' | 'L'
|
size?: 'S' | 'M' | 'L'
|
||||||
adaptiveSize?: boolean
|
adaptiveSize?: boolean
|
||||||
|
@ -21,79 +19,81 @@ export interface Props {
|
||||||
style?: TextStyle
|
style?: TextStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseEmojis = React.memo(
|
const ParseEmojis: React.FC<Props> = ({
|
||||||
({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
|
content,
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
emojis,
|
||||||
|
size = 'M',
|
||||||
|
adaptiveSize = false,
|
||||||
|
fontBold = false,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
if (!content) return null
|
||||||
|
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const adaptedFontsize = adaptiveScale(
|
|
||||||
StyleConstants.Font.Size[size],
|
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
|
||||||
)
|
|
||||||
const adaptedLineheight = adaptiveScale(
|
|
||||||
StyleConstants.Font.LineHeight[size],
|
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const { colors, theme } = useTheme()
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
|
const adaptedFontsize = adaptiveScale(
|
||||||
|
StyleConstants.Font.Size[size],
|
||||||
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
|
)
|
||||||
|
const adaptedLineheight = adaptiveScale(
|
||||||
|
StyleConstants.Font.LineHeight[size],
|
||||||
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
const { colors, theme } = useTheme()
|
||||||
<CustomText
|
|
||||||
style={[
|
return (
|
||||||
{
|
<CustomText
|
||||||
color: colors.primaryDefault,
|
style={[
|
||||||
fontSize: adaptedFontsize,
|
{
|
||||||
lineHeight: adaptedLineheight
|
color: colors.primaryDefault,
|
||||||
},
|
fontSize: adaptedFontsize,
|
||||||
style
|
lineHeight: adaptedLineheight
|
||||||
]}
|
},
|
||||||
fontWeight={fontBold ? 'Bold' : undefined}
|
style
|
||||||
>
|
]}
|
||||||
{emojis ? (
|
fontWeight={fontBold ? 'Bold' : undefined}
|
||||||
content
|
>
|
||||||
.split(regexEmoji)
|
{emojis ? (
|
||||||
.filter(f => f)
|
content
|
||||||
.map((str, i) => {
|
.split(regexEmoji)
|
||||||
if (str.match(regexEmoji)) {
|
.filter(f => f)
|
||||||
const emojiShortcode = str.split(regexEmoji)[1]
|
.map((str, i) => {
|
||||||
const emojiIndex = emojis.findIndex(emoji => {
|
if (str.match(regexEmoji)) {
|
||||||
return emojiShortcode === `:${emoji.shortcode}:`
|
const emojiShortcode = str.split(regexEmoji)[1]
|
||||||
})
|
const emojiIndex = emojis.findIndex(emoji => {
|
||||||
if (emojiIndex === -1) {
|
return emojiShortcode === `:${emoji.shortcode}:`
|
||||||
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
|
})
|
||||||
} else {
|
if (emojiIndex === -1) {
|
||||||
const uri = reduceMotionEnabled
|
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
|
||||||
? emojis[emojiIndex].static_url
|
|
||||||
: emojis[emojiIndex].url
|
|
||||||
if (validUrl.isHttpsUri(uri)) {
|
|
||||||
return (
|
|
||||||
<CustomText key={emojiShortcode + i}>
|
|
||||||
{i === 0 ? ' ' : undefined}
|
|
||||||
<FastImage
|
|
||||||
source={{ uri }}
|
|
||||||
style={{
|
|
||||||
width: adaptedFontsize,
|
|
||||||
height: adaptedFontsize,
|
|
||||||
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CustomText>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return <CustomText key={i}>{str}</CustomText>
|
const uri = reduceMotionEnabled
|
||||||
|
? emojis[emojiIndex].static_url
|
||||||
|
: emojis[emojiIndex].url
|
||||||
|
return (
|
||||||
|
<CustomText key={emojiShortcode + i}>
|
||||||
|
{i === 0 ? ' ' : undefined}
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: uri.trim() }}
|
||||||
|
style={{
|
||||||
|
width: adaptedFontsize,
|
||||||
|
height: adaptedFontsize,
|
||||||
|
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CustomText>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
) : (
|
return <CustomText key={i}>{str}</CustomText>
|
||||||
<CustomText>{content}</CustomText>
|
}
|
||||||
)}
|
})
|
||||||
</CustomText>
|
) : (
|
||||||
)
|
<CustomText>{content}</CustomText>
|
||||||
},
|
)}
|
||||||
(prev, next) => prev.content === next.content
|
</CustomText>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ParseEmojis
|
export default ParseEmojis
|
||||||
|
|
|
@ -1,315 +1,302 @@
|
||||||
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'
|
||||||
import CustomText from '@components/Text'
|
import StatusContext from '@components/Timeline/Shared/Context'
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native'
|
import { useNavigation, useRoute } 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 { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
import { useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
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 { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { isEqual } from 'lodash'
|
import { ChildNode } from 'domhandler'
|
||||||
import React, { useCallback, useState } from 'react'
|
import { ElementType, parseDocument } from 'htmlparser2'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import React, { useContext, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Pressable, TextStyleIOS, View } from 'react-native'
|
import { Platform, Pressable, Text, View } from 'react-native'
|
||||||
import HTMLView from 'react-native-htmlview'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
// Prevent going to the same hashtag multiple times
|
|
||||||
const renderNode = ({
|
|
||||||
routeParams,
|
|
||||||
colors,
|
|
||||||
node,
|
|
||||||
index,
|
|
||||||
adaptedFontsize,
|
|
||||||
adaptedLineheight,
|
|
||||||
navigation,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
showFullLink,
|
|
||||||
disableDetails
|
|
||||||
}: {
|
|
||||||
routeParams?: any
|
|
||||||
colors: any
|
|
||||||
node: any
|
|
||||||
index: number
|
|
||||||
adaptedFontsize: number
|
|
||||||
adaptedLineheight: number
|
|
||||||
navigation: StackNavigationProp<TabLocalStackParamList>
|
|
||||||
mentions?: Mastodon.Mention[]
|
|
||||||
tags?: Mastodon.Tag[]
|
|
||||||
showFullLink: boolean
|
|
||||||
disableDetails: boolean
|
|
||||||
}) => {
|
|
||||||
switch (node.name) {
|
|
||||||
case 'a':
|
|
||||||
const classes = node.attribs.class
|
|
||||||
const href = node.attribs.href
|
|
||||||
if (classes) {
|
|
||||||
if (classes.includes('hashtag')) {
|
|
||||||
const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
|
|
||||||
const differentTag = routeParams?.hashtag
|
|
||||||
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
|
|
||||||
: true
|
|
||||||
return (
|
|
||||||
<CustomText
|
|
||||||
accessible
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
color: colors.blue,
|
|
||||||
fontSize: adaptedFontsize,
|
|
||||||
lineHeight: adaptedLineheight
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
!disableDetails &&
|
|
||||||
differentTag &&
|
|
||||||
navigation.push('Tab-Shared-Hashtag', {
|
|
||||||
hashtag: tag[1] || tag[2]
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.children[0].data}
|
|
||||||
{node.children[1]?.children[0].data}
|
|
||||||
</CustomText>
|
|
||||||
)
|
|
||||||
} else if (classes.includes('mention') && mentions) {
|
|
||||||
const accountIndex = mentions.findIndex(mention => mention.url === href)
|
|
||||||
const differentAccount = routeParams?.account
|
|
||||||
? routeParams.account.id !== mentions[accountIndex]?.id
|
|
||||||
: true
|
|
||||||
return (
|
|
||||||
<CustomText
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
color: accountIndex !== -1 ? colors.blue : colors.primaryDefault,
|
|
||||||
fontSize: adaptedFontsize,
|
|
||||||
lineHeight: adaptedLineheight
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
accountIndex !== -1 &&
|
|
||||||
!disableDetails &&
|
|
||||||
differentAccount &&
|
|
||||||
navigation.push('Tab-Shared-Account', {
|
|
||||||
account: mentions[accountIndex]
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.children[0].data}
|
|
||||||
{node.children[1]?.children[0].data}
|
|
||||||
</CustomText>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const domain = href?.split(new RegExp(/:\/\/(.[^\/]+\/.{3})/))
|
|
||||||
// Need example here
|
|
||||||
const content = node.children && node.children[0] && node.children[0].data
|
|
||||||
const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
|
||||||
return (
|
|
||||||
<CustomText
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
color: colors.blue,
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: adaptedFontsize,
|
|
||||||
lineHeight: adaptedLineheight
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
|
||||||
if (!disableDetails) {
|
|
||||||
if (shouldBeTag) {
|
|
||||||
navigation.push('Tab-Shared-Hashtag', {
|
|
||||||
hashtag: content.substring(1)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await openLink(href, navigation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content && content !== href ? content : showFullLink ? href : domain?.[1]}
|
|
||||||
{!shouldBeTag ? '...' : null}
|
|
||||||
</CustomText>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'p':
|
|
||||||
if (!node.children.length) {
|
|
||||||
return <View key={index} /> // bug when the tag is empty
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
content: string
|
content: string
|
||||||
size?: 'S' | 'M' | 'L'
|
size?: 'S' | 'M' | 'L'
|
||||||
textStyles?: TextStyleIOS
|
|
||||||
adaptiveSize?: boolean
|
adaptiveSize?: boolean
|
||||||
emojis?: Mastodon.Emoji[]
|
|
||||||
mentions?: Mastodon.Mention[]
|
|
||||||
tags?: Mastodon.Tag[]
|
|
||||||
showFullLink?: boolean
|
showFullLink?: boolean
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
expandHint?: string
|
expandHint?: string
|
||||||
highlighted?: boolean
|
|
||||||
disableDetails?: boolean
|
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
emojis?: Mastodon.Emoji[]
|
||||||
|
mentions?: Mastodon.Mention[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseHTML = React.memo(
|
const ParseHTML: React.FC<Props> = ({
|
||||||
({
|
content,
|
||||||
content,
|
size = 'M',
|
||||||
size = 'M',
|
adaptiveSize = false,
|
||||||
textStyles,
|
showFullLink = false,
|
||||||
adaptiveSize = false,
|
numberOfLines = 10,
|
||||||
emojis,
|
expandHint,
|
||||||
mentions,
|
selectable = false,
|
||||||
tags,
|
setSpoilerExpanded,
|
||||||
showFullLink = false,
|
emojis,
|
||||||
numberOfLines = 10,
|
mentions
|
||||||
expandHint,
|
}) => {
|
||||||
highlighted = false,
|
const { status, highlighted, disableDetails, excludeMentions } = useContext(StatusContext)
|
||||||
disableDetails = false,
|
|
||||||
selectable = false,
|
|
||||||
setSpoilerExpanded
|
|
||||||
}: Props) => {
|
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
|
||||||
const adaptedFontsize = adaptiveScale(
|
|
||||||
StyleConstants.Font.Size[size],
|
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
|
||||||
)
|
|
||||||
const adaptedLineheight = adaptiveScale(
|
|
||||||
StyleConstants.Font.LineHeight[size],
|
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const route = useRoute()
|
const adaptedFontsize = adaptiveScale(
|
||||||
const { colors, theme } = useTheme()
|
StyleConstants.Font.Size[size],
|
||||||
const { t } = useTranslation('componentParse')
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
if (!expandHint) {
|
)
|
||||||
expandHint = t('HTML.defaultHint')
|
const adaptedLineheight =
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? adaptiveScale(StyleConstants.Font.LineHeight[size], adaptiveSize ? adaptiveFontsize : 0)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
const { params } = useRoute()
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const { t } = useTranslation('componentParse')
|
||||||
|
if (!expandHint) {
|
||||||
|
expandHint = t('HTML.defaultHint')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableDetails) {
|
||||||
|
numberOfLines = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const [followedTags] = useAccountStorage.object('followed_tags')
|
||||||
|
|
||||||
|
const [totalLines, setTotalLines] = useState<number>()
|
||||||
|
const [expanded, setExpanded] = useState(highlighted)
|
||||||
|
|
||||||
|
const document = parseDocument(content)
|
||||||
|
const unwrapNode = (node: ChildNode): string => {
|
||||||
|
switch (node.type) {
|
||||||
|
case ElementType.Text:
|
||||||
|
return node.data
|
||||||
|
case ElementType.Tag:
|
||||||
|
if (node.name === 'span') {
|
||||||
|
if (node.attribs.class?.includes('invisible') && !showFullLink) return ''
|
||||||
|
if (node.attribs.class?.includes('ellipsis') && !showFullLink)
|
||||||
|
return node.children.map(child => unwrapNode(child)).join('') + '...'
|
||||||
|
}
|
||||||
|
return node.children.map(child => unwrapNode(child)).join('')
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const prevMentionRemoved = useRef<boolean>(false)
|
||||||
|
const renderNode = (node: ChildNode, index: number) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case ElementType.Text:
|
||||||
|
let content: string = node.data
|
||||||
|
if (prevMentionRemoved.current) {
|
||||||
|
prevMentionRemoved.current = false // Removing empty spaces appeared between tags and mentions
|
||||||
|
if (node.data.trim().length) {
|
||||||
|
content = excludeMentions?.current.length
|
||||||
|
? node.data.replace(new RegExp(/^\s+/), '')
|
||||||
|
: node.data
|
||||||
|
} else {
|
||||||
|
content = node.data.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (disableDetails) {
|
|
||||||
numberOfLines = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderNodeCallback = useCallback(
|
|
||||||
(node: any, index: any) =>
|
|
||||||
renderNode({
|
|
||||||
routeParams: route.params,
|
|
||||||
colors,
|
|
||||||
node,
|
|
||||||
index,
|
|
||||||
adaptedFontsize,
|
|
||||||
adaptedLineheight,
|
|
||||||
navigation,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
showFullLink,
|
|
||||||
disableDetails
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const textComponent = useCallback(({ children }: any) => {
|
|
||||||
if (children) {
|
|
||||||
return (
|
return (
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
content={children?.toString()}
|
key={index}
|
||||||
emojis={emojis}
|
content={content}
|
||||||
|
emojis={status?.emojis || emojis}
|
||||||
size={size}
|
size={size}
|
||||||
adaptiveSize={adaptiveSize}
|
adaptiveSize={adaptiveSize}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
case ElementType.Tag:
|
||||||
return null
|
switch (node.name) {
|
||||||
}
|
case 'a':
|
||||||
}, [])
|
const classes = node.attribs.class
|
||||||
const rootComponent = useCallback(
|
const href = node.attribs.href
|
||||||
({ children }: any) => {
|
if (classes) {
|
||||||
const { t } = useTranslation('componentParse')
|
if (classes.includes('hashtag')) {
|
||||||
|
const children = node.children.map(unwrapNode).join('')
|
||||||
|
const tag =
|
||||||
|
href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1]?.toLowerCase() ||
|
||||||
|
children.match(new RegExp(/#(\S+)/))?.[1]?.toLowerCase()
|
||||||
|
|
||||||
const [totalLines, setTotalLines] = useState<number>()
|
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
|
||||||
const [expanded, setExpanded] = useState(highlighted)
|
?.hashtag
|
||||||
|
const sameHashtag = paramsHashtag === tag
|
||||||
|
const isFollowing = followedTags?.find(t => t.name === tag)
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
{ color: tag?.length ? colors.blue : colors.red },
|
||||||
|
isFollowing
|
||||||
|
? {
|
||||||
|
textDecorationColor: tag?.length ? colors.blue : colors.red,
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
textDecorationStyle: 'dotted'
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
]}
|
||||||
|
onPress={() =>
|
||||||
|
tag?.length &&
|
||||||
|
!disableDetails &&
|
||||||
|
!sameHashtag &&
|
||||||
|
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
|
||||||
|
}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (classes.includes('mention') && (status?.mentions?.length || mentions?.length)) {
|
||||||
|
const matchedMention = (status?.mentions || mentions || []).find(
|
||||||
|
mention => mention.url === href
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
matchedMention &&
|
||||||
|
excludeMentions?.current.find(eM => eM.id === matchedMention.id)
|
||||||
|
) {
|
||||||
|
prevMentionRemoved.current = true
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
|
||||||
|
const sameAccount = paramsAccount?.id === matchedMention?.id
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{ color: matchedMention ? colors.blue : undefined }}
|
||||||
|
onPress={() =>
|
||||||
|
matchedMention &&
|
||||||
|
!disableDetails &&
|
||||||
|
!sameAccount &&
|
||||||
|
navigation.push('Tab-Shared-Account', { account: matchedMention })
|
||||||
|
}
|
||||||
|
children={node.children.map(unwrapNode).join('')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const content = node.children.map(child => unwrapNode(child)).join('')
|
||||||
<View style={{ overflow: 'hidden' }}>
|
const shouldBeTag = status?.tags?.find(tag => `#${tag.name}` === content)
|
||||||
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
return (
|
||||||
<Pressable
|
<Text
|
||||||
accessibilityLabel={t('HTML.accessibilityHint')}
|
key={index}
|
||||||
onPress={() => {
|
style={{ color: colors.blue }}
|
||||||
layoutAnimation()
|
onPress={async () => {
|
||||||
setExpanded(!expanded)
|
if (!disableDetails) {
|
||||||
if (setSpoilerExpanded) {
|
if (shouldBeTag) {
|
||||||
setSpoilerExpanded(!expanded)
|
navigation.push('Tab-Shared-Hashtag', {
|
||||||
|
hashtag: content.substring(1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await openLink(href, navigation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
children={content}
|
||||||
flexDirection: 'row',
|
/>
|
||||||
justifyContent: 'center',
|
)
|
||||||
alignItems: 'center',
|
break
|
||||||
minHeight: 44,
|
case 'br':
|
||||||
backgroundColor: colors.backgroundDefault
|
return (
|
||||||
}}
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
|
||||||
>
|
>
|
||||||
<CustomText
|
{'\n'}
|
||||||
style={{
|
</Text>
|
||||||
textAlign: 'center',
|
)
|
||||||
...StyleConstants.FontStyle.S,
|
case 'p':
|
||||||
color: colors.primaryDefault,
|
if (index < document.children.length - 1) {
|
||||||
marginRight: StyleConstants.Spacing.S
|
return (
|
||||||
}}
|
<Text key={index}>
|
||||||
children={t('HTML.expanded', {
|
{node.children.map((c, i) => renderNode(c, i))}
|
||||||
hint: expandHint,
|
<Text
|
||||||
moreLines:
|
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
|
||||||
numberOfLines > 1 && typeof totalLines === 'number'
|
>
|
||||||
? t('HTML.moreLines', { count: totalLines - numberOfLines })
|
{'\n\n'}
|
||||||
: ''
|
</Text>
|
||||||
})}
|
</Text>
|
||||||
/>
|
)
|
||||||
<Icon
|
} else {
|
||||||
name={expanded ? 'Minimize2' : 'Maximize2'}
|
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||||
color={colors.primaryDefault}
|
}
|
||||||
strokeWidth={2}
|
default:
|
||||||
size={StyleConstants.Font.Size[size]}
|
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||||
/>
|
}
|
||||||
</Pressable>
|
}
|
||||||
) : null}
|
return null
|
||||||
<CustomText
|
}
|
||||||
children={children}
|
return (
|
||||||
onTextLayout={({ nativeEvent }) => {
|
<View style={{ overflow: 'hidden' }}>
|
||||||
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
|
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
||||||
setTotalLines(nativeEvent.lines.length)
|
<Pressable
|
||||||
}
|
accessibilityLabel={t('HTML.accessibilityHint')}
|
||||||
}}
|
onPress={() => {
|
||||||
style={{
|
layoutAnimation()
|
||||||
...textStyles,
|
setExpanded(!expanded)
|
||||||
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
if (setSpoilerExpanded) {
|
||||||
}}
|
setSpoilerExpanded(!expanded)
|
||||||
numberOfLines={
|
}
|
||||||
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
|
}}
|
||||||
}
|
style={{
|
||||||
selectable={selectable}
|
flexDirection: 'row',
|
||||||
/>
|
justifyContent: 'center',
|
||||||
</View>
|
alignItems: 'center',
|
||||||
)
|
minHeight: 44,
|
||||||
},
|
backgroundColor: colors.backgroundDefault
|
||||||
[theme]
|
}}
|
||||||
)
|
>
|
||||||
|
<Text
|
||||||
return (
|
style={{
|
||||||
<HTMLView
|
textAlign: 'center',
|
||||||
value={content}
|
...StyleConstants.FontStyle.S,
|
||||||
TextComponent={textComponent}
|
color: colors.primaryDefault,
|
||||||
RootComponent={rootComponent}
|
marginRight: StyleConstants.Spacing.S
|
||||||
renderNode={renderNodeCallback}
|
}}
|
||||||
|
children={t('HTML.expanded', {
|
||||||
|
hint: expandHint,
|
||||||
|
moreLines:
|
||||||
|
numberOfLines > 1 && typeof totalLines === 'number'
|
||||||
|
? t('HTML.moreLines', { count: totalLines - numberOfLines })
|
||||||
|
: ''
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name={expanded ? 'Minimize2' : 'Maximize2'}
|
||||||
|
color={colors.primaryDefault}
|
||||||
|
strokeWidth={2}
|
||||||
|
size={StyleConstants.Font.Size[size]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
<Text
|
||||||
|
children={document.children.map(renderNode)}
|
||||||
|
onTextLayout={({ nativeEvent }) => {
|
||||||
|
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
|
||||||
|
setTotalLines(nativeEvent.lines.length)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
fontSize: adaptedFontsize,
|
||||||
|
lineHeight: adaptedLineheight,
|
||||||
|
...(Platform.OS === 'ios' &&
|
||||||
|
status?.language &&
|
||||||
|
i18next.dir(status.language) === 'rtl' &&
|
||||||
|
({ writingDirection: 'rtl' } as { writingDirection: 'rtl' })),
|
||||||
|
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
||||||
|
}}
|
||||||
|
numberOfLines={
|
||||||
|
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
|
||||||
|
}
|
||||||
|
selectable={selectable}
|
||||||
/>
|
/>
|
||||||
)
|
</View>
|
||||||
},
|
)
|
||||||
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default ParseHTML
|
export default ParseHTML
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import ParseEmojis from './Emojis'
|
||||||
|
import ParseHTML from './HTML'
|
||||||
|
|
||||||
|
export { ParseEmojis, ParseHTML }
|
|
@ -1,6 +1,7 @@
|
||||||
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 { useQueryClient } from '@tanstack/react-query'
|
||||||
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
import { 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'
|
||||||
|
@ -8,7 +9,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React 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 { useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
|
@ -16,7 +16,7 @@ export interface Props {
|
||||||
|
|
||||||
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation(['common', 'componentRelationship'])
|
||||||
|
|
||||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
||||||
const queryKeyNotification: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
const queryKeyNotification: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||||
|
@ -33,7 +33,7 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
theme,
|
theme,
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`relationship:${type}.function`)
|
function: t(`componentRelationship:${type}.function` as any)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
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 { useRoute } from '@react-navigation/native'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import {
|
import {
|
||||||
QueryKeyRelationship,
|
QueryKeyRelationship,
|
||||||
useRelationshipMutation,
|
useRelationshipMutation,
|
||||||
useRelationshipQuery
|
useRelationshipQuery
|
||||||
} from '@utils/queryHooks/relationship'
|
} from '@utils/queryHooks/relationship'
|
||||||
|
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 { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { useRoute } from '@react-navigation/native'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
|
@ -22,9 +21,9 @@ export interface Props {
|
||||||
|
|
||||||
const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { t } = useTranslation('componentRelationship')
|
const { t } = useTranslation(['common', 'componentRelationship'])
|
||||||
|
|
||||||
const canFollowNotify = useSelector(checkInstanceFeature('account_follow_notify'))
|
const canFollowNotify = featureCheck('account_follow_notify')
|
||||||
|
|
||||||
const query = useRelationshipQuery({ id })
|
const query = useRelationshipQuery({ id })
|
||||||
|
|
||||||
|
@ -44,7 +43,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
theme,
|
theme,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`${action}.function`)
|
function: t(`componentRelationship:${action}.function` as any)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -61,15 +60,15 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
let onPress: () => void
|
let onPress: () => void
|
||||||
|
|
||||||
if (query.isError) {
|
if (query.isError) {
|
||||||
content = t('button.error')
|
content = t('componentRelationship:button.error')
|
||||||
onPress = () => {}
|
onPress = () => {}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.blocked_by) {
|
if (query.data?.blocked_by) {
|
||||||
content = t('button.blocked_by')
|
content = t('componentRelationship:button.blocked_by')
|
||||||
onPress = () => {}
|
onPress = () => {}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.blocking) {
|
if (query.data?.blocking) {
|
||||||
content = t('button.blocking')
|
content = t('componentRelationship:button.blocking')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
|
@ -82,7 +81,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.following) {
|
if (query.data?.following) {
|
||||||
content = t('button.following')
|
content = t('componentRelationship:button.following')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
|
@ -95,7 +94,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (query.data?.requested) {
|
if (query.data?.requested) {
|
||||||
content = t('button.requested')
|
content = t('componentRelationship:button.requested')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
|
@ -107,7 +106,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = t('button.default')
|
content = t('componentRelationship:button.default')
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -11,9 +11,15 @@ export interface Props {
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
options: { selected: boolean; content: string }[]
|
options: { selected: boolean; content: string }[]
|
||||||
setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>>
|
setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>>
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Selections: React.FC<Props> = ({ multiple = false, options, setOptions }) => {
|
const Selections: React.FC<Props> = ({
|
||||||
|
multiple = false,
|
||||||
|
options,
|
||||||
|
setOptions,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const isSelected = (index: number): string =>
|
const isSelected = (index: number): string =>
|
||||||
|
@ -22,10 +28,11 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
|
||||||
: `${multiple ? 'Square' : 'Circle'}`
|
: `${multiple ? 'Square' : 'Circle'}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
|
disabled={disabled}
|
||||||
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
|
@ -56,15 +63,18 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
|
||||||
}}
|
}}
|
||||||
name={isSelected(index)}
|
name={isSelected(index)}
|
||||||
size={StyleConstants.Font.Size.M}
|
size={StyleConstants.Font.Size.M}
|
||||||
color={colors.primaryDefault}
|
color={disabled ? colors.disabled : colors.primaryDefault}
|
||||||
/>
|
/>
|
||||||
<CustomText style={{ flex: 1 }}>
|
<CustomText style={{ flex: 1 }}>
|
||||||
<ParseEmojis content={option.content} />
|
<ParseEmojis
|
||||||
|
content={option.content}
|
||||||
|
style={{ color: disabled ? colors.disabled : colors.primaryDefault }}
|
||||||
|
/>
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
import { StyleProp, View, ViewStyle } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
extraMarginLeft?: number
|
extraMarginLeft?: number
|
||||||
|
@ -23,7 +23,7 @@ const ComponentSeparator: React.FC<Props> = ({
|
||||||
{
|
{
|
||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: 1,
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
||||||
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import apiInstance from '@api/instance'
|
|
||||||
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 { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
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 { 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 } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
||||||
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 StatusContext from './Shared/Context'
|
||||||
|
@ -41,10 +41,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
if (conversation.last_status) {
|
if (conversation.last_status) {
|
||||||
conversation.unread && mutate()
|
conversation.unread && mutate()
|
||||||
navigation.push('Tab-Shared-Toot', {
|
navigation.push('Tab-Shared-Toot', { toot: conversation.last_status })
|
||||||
toot: conversation.last_status,
|
|
||||||
rootQueryKey: queryKey
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -115,4 +112,4 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineConversation
|
export default React.memo(TimelineConversation, () => true)
|
||||||
|
|
|
@ -9,17 +9,18 @@ import TimelineCard from '@components/Timeline/Shared/Card'
|
||||||
import TimelineContent from '@components/Timeline/Shared/Content'
|
import TimelineContent from '@components/Timeline/Shared/Content'
|
||||||
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 removeHTML from '@helpers/removeHTML'
|
|
||||||
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 { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
|
import removeHTML from '@utils/helpers/removeHTML'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
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, { useRef, useState } from 'react'
|
import React, { Fragment, useRef, useState } from 'react'
|
||||||
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as ContextMenu from 'zeego/context-menu'
|
import * as ContextMenu from 'zeego/context-menu'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
import TimelineFeedback from './Shared/Feedback'
|
import TimelineFeedback from './Shared/Feedback'
|
||||||
|
@ -31,7 +32,6 @@ 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
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
disableDetails?: boolean
|
disableDetails?: boolean
|
||||||
disableOnPress?: boolean
|
disableOnPress?: boolean
|
||||||
|
@ -42,7 +42,6 @@ export interface Props {
|
||||||
const TimelineDefault: React.FC<Props> = ({
|
const TimelineDefault: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
|
||||||
highlighted = false,
|
highlighted = false,
|
||||||
disableDetails = false,
|
disableDetails = false,
|
||||||
disableOnPress = false,
|
disableOnPress = false,
|
||||||
|
@ -50,7 +49,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const status = item.reblog ? item.reblog : item
|
const status = item.reblog ? item.reblog : item
|
||||||
const rawContent = useRef<string[]>([])
|
const rawContent = useRef<string[]>([])
|
||||||
if (highlighted) {
|
if (highlighted || isConversation) {
|
||||||
rawContent.current = [
|
rawContent.current = [
|
||||||
removeHTML(status.content),
|
removeHTML(status.content),
|
||||||
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
|
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
|
||||||
|
@ -60,16 +59,18 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
const ownAccount = status.account?.id === instanceAccount?.id
|
const ownAccount = status.account?.id === accountId
|
||||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||||
instanceAccount?.preferences?.['reading:expand:spoilers'] || false
|
preferences?.['reading:expand:spoilers'] || false
|
||||||
)
|
)
|
||||||
const spoilerHidden = status.spoiler_text?.length
|
const spoilerHidden = status.spoiler_text?.length
|
||||||
? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||||
: false
|
: false
|
||||||
const detectedLanguage = useRef<string>(status.language || '')
|
const detectedLanguage = useRef<string>(status.language || '')
|
||||||
|
const excludeMentions = useRef<Mastodon.Mention[]>([])
|
||||||
|
|
||||||
const mainStyle: StyleProp<ViewStyle> = {
|
const mainStyle: StyleProp<ViewStyle> = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
@ -82,9 +83,9 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
const main = () => (
|
const main = () => (
|
||||||
<>
|
<>
|
||||||
{item.reblog ? (
|
{item.reblog ? (
|
||||||
<TimelineActioned action='reblog' />
|
<TimelineActioned action='reblog' rootStatus={item} />
|
||||||
) : item._pinned ? (
|
) : item._pinned ? (
|
||||||
<TimelineActioned action='pinned' />
|
<TimelineActioned action='pinned' rootStatus={item} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -128,13 +129,13 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
url: status.url || status.uri,
|
url: status.url || status.uri,
|
||||||
rawContent
|
rawContent
|
||||||
})
|
})
|
||||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
const mStatus = menuStatus({ status, queryKey })
|
||||||
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
|
const mInstance = menuInstance({ status, queryKey })
|
||||||
|
|
||||||
if (!ownAccount) {
|
if (!ownAccount) {
|
||||||
let filterResults: FilteredProps['filterResults'] = []
|
let filterResults: FilteredProps['filterResults'] = []
|
||||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||||
if (hasFilterServerSide) {
|
if (hasFilterServerSide) {
|
||||||
if (status.filtered?.length) {
|
if (status.filtered?.length) {
|
||||||
filterResults = status.filtered?.map(filter => filter.filter)
|
filterResults = status.filtered?.map(filter => filter.filter)
|
||||||
|
@ -160,18 +161,18 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
<StatusContext.Provider
|
<StatusContext.Provider
|
||||||
value={{
|
value={{
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
|
||||||
status,
|
status,
|
||||||
reblogStatus: item.reblog ? item : undefined,
|
|
||||||
ownAccount,
|
ownAccount,
|
||||||
spoilerHidden,
|
spoilerHidden,
|
||||||
rawContent,
|
rawContent,
|
||||||
detectedLanguage,
|
detectedLanguage,
|
||||||
|
excludeMentions,
|
||||||
highlighted,
|
highlighted,
|
||||||
inThread: queryKey?.[1].page === 'Toot',
|
inThread: queryKey?.[1].page === 'Toot',
|
||||||
disableDetails,
|
disableDetails,
|
||||||
disableOnPress,
|
disableOnPress,
|
||||||
isConversation
|
isConversation,
|
||||||
|
isRemote: item._remote
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{disableOnPress ? (
|
{disableOnPress ? (
|
||||||
|
@ -184,49 +185,58 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
accessible={highlighted ? false : true}
|
accessible={highlighted ? false : true}
|
||||||
style={mainStyle}
|
style={mainStyle}
|
||||||
disabled={highlighted}
|
disabled={highlighted}
|
||||||
onPress={() =>
|
onPress={() => navigation.push('Tab-Shared-Toot', { toot: status })}
|
||||||
navigation.push('Tab-Shared-Toot', {
|
|
||||||
toot: status,
|
|
||||||
rootQueryKey: queryKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onLongPress={() => {}}
|
onLongPress={() => {}}
|
||||||
children={main()}
|
children={main()}
|
||||||
/>
|
/>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
{mShare.map((mGroup, index) => (
|
{[mShare, mStatus, mInstance].map((menu, i) => (
|
||||||
<ContextMenu.Group key={index}>
|
<Fragment key={i}>
|
||||||
{mGroup.map(menu => (
|
{menu.map((group, index) => (
|
||||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
<ContextMenu.Group key={index}>
|
||||||
<ContextMenu.ItemTitle children={menu.title} />
|
{group.map(item => {
|
||||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
switch (item.type) {
|
||||||
</ContextMenu.Item>
|
case 'item':
|
||||||
|
return (
|
||||||
|
<ContextMenu.Item key={item.key} {...item.props}>
|
||||||
|
<ContextMenu.ItemTitle children={item.title} />
|
||||||
|
{item.icon ? (
|
||||||
|
<ContextMenu.ItemIcon ios={{ name: item.icon }} />
|
||||||
|
) : null}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
)
|
||||||
|
case 'sub':
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<ContextMenu.Sub key={item.key}>
|
||||||
|
<ContextMenu.SubTrigger
|
||||||
|
key={item.trigger.key}
|
||||||
|
{...item.trigger.props}
|
||||||
|
>
|
||||||
|
<ContextMenu.ItemTitle children={item.trigger.title} />
|
||||||
|
{item.trigger.icon ? (
|
||||||
|
<ContextMenu.ItemIcon ios={{ name: item.trigger.icon }} />
|
||||||
|
) : null}
|
||||||
|
</ContextMenu.SubTrigger>
|
||||||
|
<ContextMenu.SubContent>
|
||||||
|
{item.items.map(sub => (
|
||||||
|
<ContextMenu.Item key={sub.key} {...sub.props}>
|
||||||
|
<ContextMenu.ItemTitle children={sub.title} />
|
||||||
|
{sub.icon ? (
|
||||||
|
<ContextMenu.ItemIcon ios={{ name: sub.icon }} />
|
||||||
|
) : null}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
))}
|
||||||
|
</ContextMenu.SubContent>
|
||||||
|
</ContextMenu.Sub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ContextMenu.Group>
|
||||||
))}
|
))}
|
||||||
</ContextMenu.Group>
|
</Fragment>
|
||||||
))}
|
|
||||||
|
|
||||||
{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.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
|
@ -237,4 +247,4 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineDefault
|
export default React.memo(TimelineDefault, () => true)
|
||||||
|
|
|
@ -13,74 +13,71 @@ export interface Props {
|
||||||
queryKey: QueryKeyTimeline
|
queryKey: QueryKeyTimeline
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineEmpty = React.memo(
|
const TimelineEmpty: React.FC<Props> = ({ queryKey }) => {
|
||||||
({ queryKey }: Props) => {
|
const { status, refetch } = useTimelineQuery({
|
||||||
const { status, refetch } = useTimelineQuery({
|
...queryKey[1],
|
||||||
...queryKey[1],
|
options: { notifyOnChangeProps: ['status'] }
|
||||||
options: { notifyOnChangeProps: ['status'] }
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const children = () => {
|
const children = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||||
case 'error':
|
case 'error':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='M'
|
fontStyle='M'
|
||||||
style={{
|
style={{
|
||||||
marginTop: StyleConstants.Spacing.S,
|
marginTop: StyleConstants.Spacing.S,
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
marginBottom: StyleConstants.Spacing.L,
|
||||||
color: colors.primaryDefault
|
color: colors.primaryDefault
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('empty.error.message')}
|
{t('empty.error.message')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
|
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'success':
|
case 'success':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Smartphone'
|
name='Smartphone'
|
||||||
size={StyleConstants.Font.Size.L}
|
size={StyleConstants.Font.Size.L}
|
||||||
color={colors.primaryDefault}
|
color={colors.primaryDefault}
|
||||||
/>
|
/>
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='M'
|
fontStyle='M'
|
||||||
style={{
|
style={{
|
||||||
marginTop: StyleConstants.Spacing.S,
|
marginTop: StyleConstants.Spacing.S,
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
marginBottom: StyleConstants.Spacing.L,
|
||||||
color: colors.secondary
|
color: colors.secondary
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('empty.success.message')}
|
{t('empty.success.message')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (
|
}
|
||||||
<View
|
return (
|
||||||
style={{
|
<View
|
||||||
flex: 1,
|
style={{
|
||||||
minHeight: '100%',
|
flex: 1,
|
||||||
justifyContent: 'center',
|
minHeight: '100%',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: colors.backgroundDefault
|
alignItems: 'center',
|
||||||
}}
|
backgroundColor: colors.backgroundDefault
|
||||||
>
|
}}
|
||||||
{children()}
|
>
|
||||||
</View>
|
{children()}
|
||||||
)
|
</View>
|
||||||
},
|
)
|
||||||
() => true
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default TimelineEmpty
|
export default TimelineEmpty
|
||||||
|
|
|
@ -13,49 +13,38 @@ export interface Props {
|
||||||
disableInfinity: boolean
|
disableInfinity: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineFooter = React.memo(
|
const TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
|
||||||
({ queryKey, disableInfinity }: Props) => {
|
const { hasNextPage } = useTimelineQuery({
|
||||||
const { hasNextPage } = useTimelineQuery({
|
...queryKey[1],
|
||||||
...queryKey[1],
|
options: { enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'] }
|
||||||
options: {
|
})
|
||||||
enabled: !disableInfinity,
|
|
||||||
notifyOnChangeProps: ['hasNextPage'],
|
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage?.links?.next && {
|
|
||||||
...(lastPage.links.next.isOffset
|
|
||||||
? { offset: lastPage.links.next.id }
|
|
||||||
: { max_id: lastPage.links.next.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: StyleConstants.Spacing.M
|
padding: StyleConstants.Spacing.M
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!disableInfinity && hasNextPage ? (
|
{!disableInfinity && hasNextPage ? (
|
||||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||||
) : (
|
) : (
|
||||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='componentTimeline:end.message'
|
ns='componentTimeline'
|
||||||
components={[
|
i18nKey='end.message'
|
||||||
<Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
|
components={[
|
||||||
]}
|
<Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
|
||||||
/>
|
]}
|
||||||
</CustomText>
|
/>
|
||||||
)}
|
</CustomText>
|
||||||
</View>
|
)}
|
||||||
)
|
</View>
|
||||||
},
|
)
|
||||||
() => true
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default TimelineFooter
|
export default TimelineFooter
|
||||||
|
|
|
@ -11,14 +11,15 @@ import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotifi
|
||||||
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'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
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, useRef, useState } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as ContextMenu from 'zeego/context-menu'
|
import * as ContextMenu from 'zeego/context-menu'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
|
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
|
||||||
|
@ -31,7 +32,8 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
const status = notification.status?.reblog ? notification.status.reblog : notification.status
|
const status = notification.status?.reblog ? notification.status.reblog : notification.status
|
||||||
const account =
|
const account =
|
||||||
|
@ -40,25 +42,17 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
: notification.status
|
: notification.status
|
||||||
? notification.status.account
|
? notification.status.account
|
||||||
: notification.account
|
: notification.account
|
||||||
const ownAccount = notification.account?.id === instanceAccount?.id
|
const ownAccount = notification.account?.id === accountId
|
||||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||||
instanceAccount.preferences?.['reading:expand:spoilers'] || false
|
preferences?.['reading:expand:spoilers'] || false
|
||||||
)
|
)
|
||||||
const spoilerHidden = notification.status?.spoiler_text?.length
|
const spoilerHidden = notification.status?.spoiler_text?.length
|
||||||
? !instanceAccount.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||||
: false
|
: false
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
notification.status &&
|
|
||||||
navigation.push('Tab-Shared-Toot', {
|
|
||||||
toot: notification.status,
|
|
||||||
rootQueryKey: queryKey
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -67,6 +61,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
action={notification.type}
|
action={notification.type}
|
||||||
isNotification
|
isNotification
|
||||||
account={notification.account}
|
account={notification.account}
|
||||||
|
rootStatus={notification.status}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -117,7 +112,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
if (!ownAccount) {
|
if (!ownAccount) {
|
||||||
let filterResults: FilteredProps['filterResults'] = []
|
let filterResults: FilteredProps['filterResults'] = []
|
||||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||||
if (notification.status) {
|
if (notification.status) {
|
||||||
if (hasFilterServerSide) {
|
if (hasFilterServerSide) {
|
||||||
if (notification.status.filtered?.length) {
|
if (notification.status.filtered?.length) {
|
||||||
|
@ -131,7 +126,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterResults?.length && !filterRevealed) {
|
if (filterResults?.length && !filterRevealed) {
|
||||||
return !filterResults.filter(result => result.filter_action === 'hide').length ? (
|
return !filterResults.filter(result => result.filter_action === 'hide')?.length ? (
|
||||||
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
|
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
|
||||||
<TimelineFiltered filterResults={filterResults} />
|
<TimelineFiltered filterResults={filterResults} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
@ -157,44 +152,56 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
onPress={onPress}
|
onPress={() =>
|
||||||
|
notification.status &&
|
||||||
|
navigation.push('Tab-Shared-Toot', { toot: notification.status })
|
||||||
|
}
|
||||||
onLongPress={() => {}}
|
onLongPress={() => {}}
|
||||||
children={main()}
|
children={main()}
|
||||||
/>
|
/>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
{mShare.map((mGroup, index) => (
|
{[mShare, mStatus, mInstance].map((menu, i) => (
|
||||||
<ContextMenu.Group key={index}>
|
<Fragment key={i}>
|
||||||
{mGroup.map(menu => (
|
{menu.map((group, index) => (
|
||||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
<ContextMenu.Group key={index}>
|
||||||
<ContextMenu.ItemTitle children={menu.title} />
|
{group.map(item => {
|
||||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
switch (item.type) {
|
||||||
</ContextMenu.Item>
|
case 'item':
|
||||||
|
return (
|
||||||
|
<ContextMenu.Item key={item.key} {...item.props}>
|
||||||
|
<ContextMenu.ItemTitle children={item.title} />
|
||||||
|
{item.icon ? <ContextMenu.ItemIcon ios={{ name: item.icon }} /> : null}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
)
|
||||||
|
case 'sub':
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<ContextMenu.Sub key={item.key}>
|
||||||
|
<ContextMenu.SubTrigger key={item.trigger.key} {...item.trigger.props}>
|
||||||
|
<ContextMenu.ItemTitle children={item.trigger.title} />
|
||||||
|
{item.trigger.icon ? (
|
||||||
|
<ContextMenu.ItemIcon ios={{ name: item.trigger.icon }} />
|
||||||
|
) : null}
|
||||||
|
</ContextMenu.SubTrigger>
|
||||||
|
<ContextMenu.SubContent>
|
||||||
|
{item.items.map(sub => (
|
||||||
|
<ContextMenu.Item key={sub.key} {...sub.props}>
|
||||||
|
<ContextMenu.ItemTitle children={sub.title} />
|
||||||
|
{sub.icon ? (
|
||||||
|
<ContextMenu.ItemIcon ios={{ name: sub.icon }} />
|
||||||
|
) : null}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
))}
|
||||||
|
</ContextMenu.SubContent>
|
||||||
|
</ContextMenu.Sub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ContextMenu.Group>
|
||||||
))}
|
))}
|
||||||
</ContextMenu.Group>
|
</Fragment>
|
||||||
))}
|
|
||||||
|
|
||||||
{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.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
|
@ -203,4 +210,4 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineNotifications
|
export default React.memo(TimelineNotifications, () => true)
|
||||||
|
|
|
@ -1,29 +1,37 @@
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
|
import {
|
||||||
|
queryFunctionTimeline,
|
||||||
|
QueryKeyTimeline,
|
||||||
|
useTimelineQuery
|
||||||
|
} from '@utils/queryHooks/timeline'
|
||||||
|
import { setAccountStorage } from '@utils/storage/actions'
|
||||||
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, { RefObject, useCallback, useRef, useState } from 'react'
|
import React, { RefObject, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native'
|
import { FlatList, Platform, Text, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
interpolate,
|
interpolate,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming
|
withTiming
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
flRef: RefObject<FlatList<any>>
|
flRef: RefObject<FlatList<any>>
|
||||||
queryKey: QueryKeyTimeline
|
queryKey: QueryKeyTimeline
|
||||||
|
fetchingActive: React.MutableRefObject<boolean>
|
||||||
scrollY: Animated.SharedValue<number>
|
scrollY: Animated.SharedValue<number>
|
||||||
fetchingType: Animated.SharedValue<0 | 1 | 2>
|
fetchingType: Animated.SharedValue<0 | 1 | 2>
|
||||||
disableRefresh?: boolean
|
disableRefresh?: boolean
|
||||||
|
readMarker?: 'read_marker_following'
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
|
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
|
||||||
|
@ -33,9 +41,11 @@ export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Siz
|
||||||
const TimelineRefresh: React.FC<Props> = ({
|
const TimelineRefresh: React.FC<Props> = ({
|
||||||
flRef,
|
flRef,
|
||||||
queryKey,
|
queryKey,
|
||||||
|
fetchingActive,
|
||||||
scrollY,
|
scrollY,
|
||||||
fetchingType,
|
fetchingType,
|
||||||
disableRefresh = false
|
disableRefresh = false,
|
||||||
|
readMarker
|
||||||
}) => {
|
}) => {
|
||||||
if (Platform.OS !== 'ios') {
|
if (Platform.OS !== 'ios') {
|
||||||
return null
|
return null
|
||||||
|
@ -44,86 +54,25 @@ const TimelineRefresh: React.FC<Props> = ({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchingLatestIndex = useRef(0)
|
const PREV_PER_BATCH = 1
|
||||||
const refetchActive = useRef(false)
|
const prevActive = useRef<boolean>(false)
|
||||||
|
const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>()
|
||||||
|
const prevStatusId = useRef<Mastodon.Status['id']>()
|
||||||
|
|
||||||
const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } =
|
const queryClient = useQueryClient()
|
||||||
useTimelineQuery({
|
const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] })
|
||||||
...queryKey[1],
|
|
||||||
options: {
|
useDerivedValue(() => {
|
||||||
getPreviousPageParam: firstPage =>
|
if (prevActive.current || isRefetching) {
|
||||||
firstPage?.links?.prev && {
|
fetchingActive.current = true
|
||||||
...(firstPage.links.prev.isOffset
|
} else {
|
||||||
? { offset: firstPage.links.prev.id }
|
fetchingActive.current = false
|
||||||
: { min_id: firstPage.links.prev.id }),
|
}
|
||||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
}, [prevActive.current, isRefetching])
|
||||||
limit: '3'
|
|
||||||
},
|
|
||||||
select: data => {
|
|
||||||
if (refetchActive.current) {
|
|
||||||
data.pageParams = [data.pageParams[0]]
|
|
||||||
data.pages = [data.pages[0]]
|
|
||||||
refetchActive.current = false
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
if (fetchingLatestIndex.current > 0) {
|
|
||||||
if (fetchingLatestIndex.current > 5) {
|
|
||||||
clearFirstPage()
|
|
||||||
fetchingLatestIndex.current = 0
|
|
||||||
} else {
|
|
||||||
if (hasPreviousPage) {
|
|
||||||
fetchPreviousPage()
|
|
||||||
fetchingLatestIndex.current++
|
|
||||||
} else {
|
|
||||||
clearFirstPage()
|
|
||||||
fetchingLatestIndex.current = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const clearFirstPage = () => {
|
|
||||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
|
|
||||||
if (data?.pages[0] && data.pages[0].body.length === 0) {
|
|
||||||
return {
|
|
||||||
pages: data.pages.slice(1),
|
|
||||||
pageParams: data.pageParams.slice(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const prepareRefetch = () => {
|
|
||||||
refetchActive.current = true
|
|
||||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
|
|
||||||
if (data) {
|
|
||||||
data.pageParams = [undefined]
|
|
||||||
const newFirstPage: TimelineData = { body: [] }
|
|
||||||
for (let page of data.pages) {
|
|
||||||
// @ts-ignore
|
|
||||||
newFirstPage.body.push(...page.body)
|
|
||||||
if (newFirstPage.body.length > 10) break
|
|
||||||
}
|
|
||||||
data.pages = [newFirstPage]
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const callRefetch = async () => {
|
|
||||||
await refetch()
|
|
||||||
setTimeout(() => flRef.current?.scrollToOffset({ offset: 1 }), 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [textRight, setTextRight] = useState(0)
|
const [textRight, setTextRight] = useState(0)
|
||||||
const arrowY = useAnimatedStyle(() => ({
|
const arrowY = useAnimatedStyle(() => ({
|
||||||
transform: [
|
transform: [
|
||||||
|
@ -145,17 +94,9 @@ const TimelineRefresh: React.FC<Props> = ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const arrowStage = useSharedValue(0)
|
const arrowStage = useSharedValue(0)
|
||||||
const onLayout = useCallback(
|
|
||||||
({ nativeEvent }: LayoutChangeEvent) => {
|
|
||||||
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
|
||||||
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[textRight]
|
|
||||||
)
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => {
|
() => {
|
||||||
if (isFetching) {
|
if (fetchingActive.current) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch (arrowStage.value) {
|
switch (arrowStage.value) {
|
||||||
|
@ -188,10 +129,96 @@ const TimelineRefresh: React.FC<Props> = ({
|
||||||
runOnJS(haptics)('Light')
|
runOnJS(haptics)('Light')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isFetching]
|
[fetchingActive.current]
|
||||||
)
|
)
|
||||||
const wrapperStartLatest = () => {
|
|
||||||
fetchingLatestIndex.current = 1
|
const fetchAndScrolled = useSharedValue(false)
|
||||||
|
const runFetchPrevious = async () => {
|
||||||
|
if (prevActive.current) return
|
||||||
|
|
||||||
|
const firstPage =
|
||||||
|
queryClient.getQueryData<
|
||||||
|
InfiniteData<
|
||||||
|
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
|
||||||
|
>
|
||||||
|
>(queryKey)?.pages[0]
|
||||||
|
|
||||||
|
prevActive.current = true
|
||||||
|
prevStatusId.current = firstPage?.body[0]?.id
|
||||||
|
|
||||||
|
await queryFunctionTimeline({
|
||||||
|
queryKey,
|
||||||
|
pageParam: firstPage?.links?.prev,
|
||||||
|
meta: {}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.body.length) return
|
||||||
|
|
||||||
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<
|
||||||
|
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
|
||||||
|
>
|
||||||
|
>(queryKey, old => {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
prevCache.current = res.body.slice(0, -PREV_PER_BATCH)
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.body.length - PREV_PER_BATCH
|
||||||
|
})
|
||||||
|
.then(async nextLength => {
|
||||||
|
if (!nextLength) {
|
||||||
|
prevActive.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let [index] of Array(Math.ceil(nextLength / PREV_PER_BATCH)).entries()) {
|
||||||
|
if (!fetchAndScrolled.value && index < 3 && scrollY.value > 15) {
|
||||||
|
fetchAndScrolled.value = true
|
||||||
|
flRef.current?.scrollToOffset({ offset: scrollY.value - 15, animated: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(promise => setTimeout(promise, 64))
|
||||||
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<
|
||||||
|
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
|
||||||
|
>
|
||||||
|
>(queryKey, old => {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map((page, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
const insert = prevCache.current?.slice(-PREV_PER_BATCH)
|
||||||
|
prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH)
|
||||||
|
if (insert) {
|
||||||
|
return { ...page, body: [...insert, ...page.body] }
|
||||||
|
} else {
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prevActive.current = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const runFetchLatest = async () => {
|
||||||
|
queryClient.invalidateQueries(queryKey)
|
||||||
|
if (readMarker) {
|
||||||
|
setAccountStorage([{ key: readMarker, value: undefined }])
|
||||||
|
}
|
||||||
|
await refetch()
|
||||||
|
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
|
@ -202,95 +229,76 @@ const TimelineRefresh: React.FC<Props> = ({
|
||||||
fetchingType.value = 0
|
fetchingType.value = 0
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case 1:
|
case 1:
|
||||||
runOnJS(wrapperStartLatest)()
|
runOnJS(runFetchPrevious)()
|
||||||
runOnJS(clearFirstPage)()
|
return
|
||||||
runOnJS(fetchPreviousPage)()
|
|
||||||
break
|
|
||||||
case 2:
|
case 2:
|
||||||
runOnJS(prepareRefetch)()
|
runOnJS(runFetchLatest)()
|
||||||
runOnJS(callRefetch)()
|
return
|
||||||
break
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerPadding = useAnimatedStyle(
|
|
||||||
() => ({
|
|
||||||
paddingTop:
|
|
||||||
fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage)
|
|
||||||
? withTiming(StyleConstants.Spacing.M * 2.5)
|
|
||||||
: withTiming(0)
|
|
||||||
}),
|
|
||||||
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={headerPadding}>
|
<Animated.View
|
||||||
<View style={styles.base}>
|
style={{
|
||||||
{isFetching ? (
|
position: 'absolute',
|
||||||
<View style={styles.container2}>
|
top: 0,
|
||||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
left: 0,
|
||||||
</View>
|
right: 0,
|
||||||
) : (
|
height: CONTAINER_HEIGHT * 2,
|
||||||
<>
|
alignItems: 'center'
|
||||||
<View style={styles.container1}>
|
}}
|
||||||
<Text
|
>
|
||||||
style={[styles.explanation, { color: colors.primaryDefault }]}
|
<View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
|
||||||
onLayout={onLayout}
|
<Text
|
||||||
children={t('refresh.fetchPreviousPage')}
|
style={{
|
||||||
/>
|
fontSize: StyleConstants.Font.Size.S,
|
||||||
<Animated.View
|
lineHeight: CONTAINER_HEIGHT,
|
||||||
style={[
|
color: colors.primaryDefault
|
||||||
{
|
}}
|
||||||
position: 'absolute',
|
onLayout={({ nativeEvent }) => {
|
||||||
left: textRight + StyleConstants.Spacing.S
|
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
||||||
},
|
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
||||||
arrowY,
|
}
|
||||||
arrowTop
|
}}
|
||||||
]}
|
children={t('refresh.fetchPreviousPage')}
|
||||||
children={
|
/>
|
||||||
<Icon
|
<Animated.View
|
||||||
name='ArrowLeft'
|
style={[
|
||||||
size={StyleConstants.Font.Size.M}
|
{
|
||||||
color={colors.primaryDefault}
|
position: 'absolute',
|
||||||
/>
|
left: textRight + StyleConstants.Spacing.S
|
||||||
}
|
},
|
||||||
/>
|
arrowY,
|
||||||
</View>
|
arrowTop
|
||||||
<View style={styles.container2}>
|
]}
|
||||||
<Text
|
children={
|
||||||
style={[styles.explanation, { color: colors.primaryDefault }]}
|
<Icon
|
||||||
onLayout={onLayout}
|
name='ArrowLeft'
|
||||||
children={t('refresh.refetch')}
|
size={StyleConstants.Font.Size.M}
|
||||||
/>
|
color={colors.primaryDefault}
|
||||||
</View>
|
/>
|
||||||
</>
|
}
|
||||||
)}
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: StyleConstants.Font.Size.S,
|
||||||
|
lineHeight: CONTAINER_HEIGHT,
|
||||||
|
color: colors.primaryDefault
|
||||||
|
}}
|
||||||
|
onLayout={({ nativeEvent }) => {
|
||||||
|
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
||||||
|
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
children={t('refresh.refetch')}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
base: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: CONTAINER_HEIGHT * 2,
|
|
||||||
alignItems: 'center'
|
|
||||||
},
|
|
||||||
container1: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
height: CONTAINER_HEIGHT
|
|
||||||
},
|
|
||||||
container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' },
|
|
||||||
explanation: {
|
|
||||||
fontSize: StyleConstants.Font.Size.S,
|
|
||||||
lineHeight: CONTAINER_HEIGHT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default TimelineRefresh
|
export default TimelineRefresh
|
||||||
|
|
|
@ -14,11 +14,12 @@ export interface Props {
|
||||||
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
||||||
isNotification?: boolean
|
isNotification?: boolean
|
||||||
account?: Mastodon.Account // For notification
|
account?: Mastodon.Account // For notification
|
||||||
|
rootStatus?: Mastodon.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||||
const { status, reblogStatus } = useContext(StatusContext)
|
const { status } = useContext(StatusContext)
|
||||||
const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account)
|
const account = rest.account || (rest.rootStatus || status)?.account
|
||||||
if (!account) return null
|
if (!account) return null
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
@ -36,7 +37,8 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
const onPress = () =>
|
||||||
|
navigation.push('Tab-Shared-Account', { account })
|
||||||
|
|
||||||
const children = () => {
|
const children = () => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
|
|
@ -2,34 +2,33 @@ 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'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
|
||||||
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 { RootStackParamList } from '@utils/navigation/navigators'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
|
import { RootStackParamList, useNavState } from '@utils/navigation/navigators'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
QueryKeyTimeline,
|
QueryKeyTimeline,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
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 { uniqBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, 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 { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineActions: React.FC = () => {
|
const TimelineActions: React.FC = () => {
|
||||||
const { queryKey, rootQueryKey, status, reblogStatus, ownAccount, highlighted, disableDetails } =
|
const { queryKey, status, ownAccount, highlighted, disableDetails } = useContext(StatusContext)
|
||||||
useContext(StatusContext)
|
|
||||||
if (!queryKey || !status || disableDetails) return null
|
if (!queryKey || !status || disableDetails) return null
|
||||||
|
|
||||||
|
const navigationState = useNavState()
|
||||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
const iconColor = colors.secondary
|
const iconColor = colors.secondary
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
@ -39,28 +38,29 @@ const TimelineActions: React.FC = () => {
|
||||||
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
||||||
if (
|
if (
|
||||||
// Un-bookmark from bookmarks page
|
// Un-bookmark from bookmarks page
|
||||||
(queryKey[1].page === 'Bookmarks' && theParams.payload.property === 'bookmarked') ||
|
(queryKey[1].page === 'Bookmarks' && theParams.payload.type === 'bookmarked') ||
|
||||||
// Un-favourite from favourites page
|
// Un-favourite from favourites page
|
||||||
(queryKey[1].page === 'Favourites' && theParams.payload.property === 'favourited')
|
(queryKey[1].page === 'Favourites' && theParams.payload.type === 'favourited')
|
||||||
) {
|
) {
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
} else if (theParams.payload.property === 'favourited') {
|
} else if (theParams.payload.type === 'favourited') {
|
||||||
// When favourited, update favourited page
|
// When favourited, update favourited page
|
||||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||||
queryClient.invalidateQueries(tempQueryKey)
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
} else if (theParams.payload.property === 'bookmarked') {
|
} else if (theParams.payload.type === 'bookmarked') {
|
||||||
// When bookmarked, update bookmark page
|
// When bookmarked, update bookmark page
|
||||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||||
queryClient.invalidateQueries(tempQueryKey)
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any, params, oldData) => {
|
onError: (err: any, params) => {
|
||||||
const correctParam = params as MutationVarsTimelineUpdateStatusProperty
|
const correctParam = params as MutationVarsTimelineUpdateStatusProperty
|
||||||
displayMessage({
|
displayMessage({
|
||||||
theme,
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`shared.actions.${correctParam.payload.property}.function`)
|
function: t(
|
||||||
|
`componentTimeline:shared.actions.${correctParam.payload.type}.function` as any
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -74,30 +74,30 @@ const TimelineActions: React.FC = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
const onPressReply = useCallback(() => {
|
const onPressReply = () => {
|
||||||
const accts = uniqBy(
|
const accts = uniqBy(
|
||||||
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||||
.concat(status.mentions)
|
.concat(status.mentions)
|
||||||
.filter(d => d?.id !== instanceAccount?.id),
|
.filter(d => d?.id !== accountId),
|
||||||
d => d?.id
|
d => d?.id
|
||||||
).map(d => d?.acct)
|
).map(d => d?.acct)
|
||||||
navigation.navigate('Screen-Compose', {
|
navigation.navigate('Screen-Compose', {
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
incomingStatus: status,
|
incomingStatus: status,
|
||||||
accts,
|
accts,
|
||||||
queryKey
|
navigationState
|
||||||
})
|
})
|
||||||
}, [status.replies_count])
|
}
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const onPressReblog = useCallback(() => {
|
const onPressReblog = () => {
|
||||||
if (!status.reblogged) {
|
if (!status.reblogged) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
title: t('shared.actions.reblogged.options.title'),
|
title: t('componentTimeline:shared.actions.reblogged.options.title'),
|
||||||
options: [
|
options: [
|
||||||
t('shared.actions.reblogged.options.public'),
|
t('componentTimeline:shared.actions.reblogged.options.public'),
|
||||||
t('shared.actions.reblogged.options.unlisted'),
|
t('componentTimeline:shared.actions.reblogged.options.unlisted'),
|
||||||
t('common:buttons.cancel')
|
t('common:buttons.cancel')
|
||||||
],
|
],
|
||||||
cancelButtonIndex: 2,
|
cancelButtonIndex: 2,
|
||||||
|
@ -108,32 +108,22 @@ const TimelineActions: React.FC = () => {
|
||||||
case 0:
|
case 0:
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
visibility: 'public',
|
||||||
propertyCount: 'reblogs_count',
|
to: true
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'public'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
visibility: 'unlisted',
|
||||||
propertyCount: 'reblogs_count',
|
to: true
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'unlisted'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
@ -143,91 +133,72 @@ const TimelineActions: React.FC = () => {
|
||||||
} else {
|
} else {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'reblogged',
|
type: 'reblogged',
|
||||||
currentValue: status.reblogged,
|
visibility: 'public',
|
||||||
propertyCount: 'reblogs_count',
|
to: false
|
||||||
countValue: status.reblogs_count,
|
|
||||||
visibility: 'public'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const onPressFavourite = useCallback(() => {
|
const onPressFavourite = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'favourited',
|
type: 'favourited',
|
||||||
currentValue: status.favourited,
|
to: !status.favourited
|
||||||
propertyCount: 'favourites_count',
|
|
||||||
countValue: status.favourites_count
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const onPressBookmark = useCallback(() => {
|
const onPressBookmark = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'bookmarked',
|
type: 'bookmarked',
|
||||||
currentValue: status.bookmarked,
|
to: !status.bookmarked
|
||||||
propertyCount: undefined,
|
|
||||||
countValue: undefined
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
const childrenReply = useMemo(
|
const childrenReply = () => (
|
||||||
() => (
|
<>
|
||||||
<>
|
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||||
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
{status.replies_count > 0 ? (
|
||||||
{status.replies_count > 0 ? (
|
<CustomText
|
||||||
<CustomText
|
fontStyle='S'
|
||||||
style={{
|
style={{
|
||||||
color: colors.secondary,
|
color: colors.secondary,
|
||||||
fontSize: StyleConstants.Font.Size.M,
|
marginLeft: StyleConstants.Spacing.XS
|
||||||
marginLeft: StyleConstants.Spacing.XS
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{status.replies_count}
|
||||||
{status.replies_count}
|
</CustomText>
|
||||||
</CustomText>
|
) : null}
|
||||||
) : null}
|
</>
|
||||||
</>
|
|
||||||
),
|
|
||||||
[status.replies_count]
|
|
||||||
)
|
)
|
||||||
const childrenReblog = useMemo(() => {
|
const childrenReblog = () => {
|
||||||
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
||||||
|
const disabled =
|
||||||
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Repeat'
|
name='Repeat'
|
||||||
color={
|
color={disabled ? colors.disabled : color(status.reblogged)}
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
crossOut={disabled}
|
||||||
? colors.disabled
|
|
||||||
: color(status.reblogged)
|
|
||||||
}
|
|
||||||
size={StyleConstants.Font.Size.L}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
{status.reblogs_count > 0 ? (
|
{status.reblogs_count > 0 ? (
|
||||||
<CustomText
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
style={{
|
style={{
|
||||||
color:
|
color:
|
||||||
status.visibility === 'private' && !ownAccount
|
status.visibility === 'private' && !ownAccount
|
||||||
? colors.disabled
|
? colors.disabled
|
||||||
: color(status.reblogged),
|
: color(status.reblogged),
|
||||||
fontSize: StyleConstants.Font.Size.M,
|
|
||||||
marginLeft: StyleConstants.Spacing.XS
|
marginLeft: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -236,58 +207,56 @@ const TimelineActions: React.FC = () => {
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const childrenFavourite = useMemo(() => {
|
const childrenFavourite = () => {
|
||||||
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon name='Heart' color={color(status.favourited)} size={StyleConstants.Font.Size.L} />
|
<Icon name='Heart' color={color(status.favourited)} size={StyleConstants.Font.Size.L} />
|
||||||
{status.favourites_count > 0 ? (
|
{status.favourites_count > 0 ? (
|
||||||
<CustomText
|
<CustomText
|
||||||
style={{
|
fontStyle='S'
|
||||||
color: color(status.favourited),
|
style={{ color: color(status.favourited), marginLeft: StyleConstants.Spacing.XS }}
|
||||||
fontSize: StyleConstants.Font.Size.M,
|
|
||||||
marginLeft: StyleConstants.Spacing.XS,
|
|
||||||
marginTop: 0
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{status.favourites_count}
|
{status.favourites_count}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const childrenBookmark = useMemo(() => {
|
const childrenBookmark = () => {
|
||||||
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
||||||
)
|
)
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row' }}>
|
<View style={{ flexDirection: 'row' }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
{...(highlighted
|
{...(highlighted
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t('shared.actions.reply.accessibilityLabel'),
|
accessibilityLabel: t('componentTimeline:shared.actions.reply.accessibilityLabel'),
|
||||||
accessibilityRole: 'button'
|
accessibilityRole: 'button'
|
||||||
}
|
}
|
||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReply}
|
onPress={onPressReply}
|
||||||
children={childrenReply}
|
children={childrenReply()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
{...(highlighted
|
{...(highlighted
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t('shared.actions.reblogged.accessibilityLabel'),
|
accessibilityLabel: t(
|
||||||
|
'componentTimeline:shared.actions.reblogged.accessibilityLabel'
|
||||||
|
),
|
||||||
accessibilityRole: 'button'
|
accessibilityRole: 'button'
|
||||||
}
|
}
|
||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReblog}
|
onPress={onPressReblog}
|
||||||
children={childrenReblog}
|
children={childrenReblog()}
|
||||||
disabled={
|
disabled={
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
}
|
}
|
||||||
|
@ -296,25 +265,29 @@ const TimelineActions: React.FC = () => {
|
||||||
<Pressable
|
<Pressable
|
||||||
{...(highlighted
|
{...(highlighted
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t('shared.actions.favourited.accessibilityLabel'),
|
accessibilityLabel: t(
|
||||||
|
'componentTimeline:shared.actions.favourited.accessibilityLabel'
|
||||||
|
),
|
||||||
accessibilityRole: 'button'
|
accessibilityRole: 'button'
|
||||||
}
|
}
|
||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressFavourite}
|
onPress={onPressFavourite}
|
||||||
children={childrenFavourite}
|
children={childrenFavourite()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
{...(highlighted
|
{...(highlighted
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t('shared.actions.bookmarked.accessibilityLabel'),
|
accessibilityLabel: t(
|
||||||
|
'componentTimeline:shared.actions.bookmarked.accessibilityLabel'
|
||||||
|
),
|
||||||
accessibilityRole: 'button'
|
accessibilityRole: 'button'
|
||||||
}
|
}
|
||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressBookmark}
|
onPress={onPressBookmark}
|
||||||
children={childrenBookmark}
|
children={childrenBookmark()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -326,8 +299,7 @@ const styles = StyleSheet.create({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
|
paddingVertical: StyleConstants.Spacing.S * 1.5
|
||||||
marginHorizontal: StyleConstants.Spacing.S
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,12 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
|
||||||
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 { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
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, { useContext, 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 StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineAttachment = () => {
|
const TimelineAttachment = () => {
|
||||||
|
@ -28,13 +27,10 @@ const TimelineAttachment = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const account = useSelector(
|
const { data: preferences } = usePreferencesQuery()
|
||||||
getInstanceAccount,
|
|
||||||
(prev, next) =>
|
|
||||||
prev.preferences?.['reading:expand:media'] === next.preferences?.['reading:expand:media']
|
|
||||||
)
|
|
||||||
const defaultSensitive = () => {
|
const defaultSensitive = () => {
|
||||||
switch (account.preferences?.['reading:expand:media']) {
|
switch (preferences?.['reading:expand:media']) {
|
||||||
case 'show_all':
|
case 'show_all':
|
||||||
return false
|
return false
|
||||||
case 'hide_all':
|
case 'hide_all':
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Alert } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
|
@ -14,7 +13,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
return !sensitiveShown ? (
|
return !sensitiveShown ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -28,7 +27,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
||||||
type='text'
|
type='text'
|
||||||
content='ALT'
|
content='ALT'
|
||||||
fontBold
|
fontBold
|
||||||
onPress={() => navigation.navigate('Screen-Actions', { type: 'alt_text', text })}
|
onPress={() => Alert.alert(t('shared.attachment.altText'), text)}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
|
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
|
||||||
|
import { Platform } from 'expo-modules-core'
|
||||||
|
import * as ScreenOrientation from 'expo-screen-orientation'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { Blurhash } from 'react-native-blurhash'
|
import { Blurhash } from 'react-native-blurhash'
|
||||||
import AttachmentAltText from './AltText'
|
import AttachmentAltText from './AltText'
|
||||||
import { Platform } from 'expo-modules-core'
|
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
|
||||||
import { aspectRatio } from './dimensions'
|
import { aspectRatio } from './dimensions'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -25,6 +27,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
gifv = false
|
gifv = false
|
||||||
}) => {
|
}) => {
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
|
const [autoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv')
|
||||||
|
|
||||||
const videoPlayer = useRef<Video>(null)
|
const videoPlayer = useRef<Video>(null)
|
||||||
const [videoLoading, setVideoLoading] = useState(false)
|
const [videoLoading, setVideoLoading] = useState(false)
|
||||||
|
@ -60,7 +63,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
resizeMode={videoResizeMode}
|
resizeMode={videoResizeMode}
|
||||||
{...(gifv
|
{...(gifv
|
||||||
? {
|
? {
|
||||||
shouldPlay: reduceMotionEnabled ? false : true,
|
shouldPlay: reduceMotionEnabled || !autoplayGifv ? false : true,
|
||||||
isMuted: true,
|
isMuted: true,
|
||||||
isLooping: true,
|
isLooping: true,
|
||||||
source: { uri: video.url }
|
source: { uri: video.url }
|
||||||
|
@ -70,14 +73,21 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
posterStyle: { resizeMode: ResizeMode.COVER }
|
posterStyle: { resizeMode: ResizeMode.COVER }
|
||||||
})}
|
})}
|
||||||
useNativeControls={false}
|
useNativeControls={false}
|
||||||
onFullscreenUpdate={event => {
|
onFullscreenUpdate={async ({ fullscreenUpdate }) => {
|
||||||
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
|
switch (fullscreenUpdate) {
|
||||||
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
|
case VideoFullscreenUpdate.PLAYER_DID_PRESENT:
|
||||||
if (gifv && !reduceMotionEnabled) {
|
Platform.OS === 'android' && (await ScreenOrientation.unlockAsync())
|
||||||
videoPlayer.current?.playAsync()
|
break
|
||||||
} else {
|
case VideoFullscreenUpdate.PLAYER_WILL_DISMISS:
|
||||||
videoPlayer.current?.pauseAsync()
|
Platform.OS === 'android' &&
|
||||||
}
|
(await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT))
|
||||||
|
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
|
||||||
|
if (gifv && !reduceMotionEnabled && autoplayGifv) {
|
||||||
|
videoPlayer.current?.playAsync()
|
||||||
|
} else {
|
||||||
|
videoPlayer.current?.pauseAsync()
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPlaybackStatusUpdate={event => {
|
onPlaybackStatusUpdate={event => {
|
||||||
|
@ -106,7 +116,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
video.blurhash ? (
|
video.blurhash ? (
|
||||||
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
|
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
|
||||||
) : null
|
) : null
|
||||||
) : !gifv || (gifv && reduceMotionEnabled) ? (
|
) : !gifv || (gifv && (reduceMotionEnabled || !autoplayGifv)) ? (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
overlay
|
overlay
|
||||||
|
@ -119,6 +129,21 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||||
) : null}
|
) : null}
|
||||||
<AttachmentAltText sensitiveShown={sensitiveShown} text={video.description} />
|
<AttachmentAltText sensitiveShown={sensitiveShown} text={video.description} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{gifv && !autoplayGifv ? (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: StyleConstants.Spacing.S,
|
||||||
|
bottom: StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
|
overlay
|
||||||
|
size='S'
|
||||||
|
type='text'
|
||||||
|
content='GIF'
|
||||||
|
fontBold
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
||||||
})
|
})
|
||||||
})}
|
})}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
|
!disableOnPress &&
|
||||||
|
navigation.push('Tab-Shared-Account', { account: actualAccount })
|
||||||
}
|
}
|
||||||
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
|
||||||
dimension={
|
dimension={
|
||||||
|
|
|
@ -2,15 +2,16 @@ import ComponentAccount from '@components/Account'
|
||||||
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'
|
||||||
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { urlMatcher } from '@utils/helpers/urlMatcher'
|
||||||
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { useAccountQuery } from '@utils/queryHooks/account'
|
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
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, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, 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'
|
import StatusContext from './Context'
|
||||||
|
@ -20,96 +21,44 @@ const TimelineCard: React.FC = () => {
|
||||||
if (!status || !status.card) return null
|
if (!status || !status.card) return null
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const isStatus = matchStatus(status.card.url)
|
const match = urlMatcher(status.card.url)
|
||||||
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
||||||
const isAccount = matchAccount(status.card.url)
|
|
||||||
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
|
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
|
||||||
|
|
||||||
const searchQuery = useSearchQuery({
|
|
||||||
type: (() => {
|
|
||||||
if (isStatus) return 'statuses'
|
|
||||||
if (isAccount) return 'accounts'
|
|
||||||
})(),
|
|
||||||
term: (() => {
|
|
||||||
if (isStatus) {
|
|
||||||
if (isStatus.sameInstance) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
return status.card.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isAccount) {
|
|
||||||
if (isAccount.sameInstance) {
|
|
||||||
if (isAccount.style === 'default') {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
return isAccount.username
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return status.card.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
limit: 1,
|
|
||||||
options: { enabled: false }
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusQuery = useStatusQuery({
|
const statusQuery = useStatusQuery({
|
||||||
id: isStatus?.id || '',
|
status: match?.status ? { ...match.status, uri: status.card.url } : undefined,
|
||||||
options: { enabled: false }
|
options: { enabled: false, retry: false }
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStatus) {
|
if (match?.status) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
if (isStatus.sameInstance) {
|
statusQuery
|
||||||
statusQuery
|
.refetch()
|
||||||
.refetch()
|
.then(res => {
|
||||||
.then(res => {
|
res.data && setFoundStatus(res.data)
|
||||||
res.data && setFoundStatus(res.data)
|
setLoading(false)
|
||||||
setLoading(false)
|
})
|
||||||
})
|
.catch(() => setLoading(false))
|
||||||
.catch(() => setLoading(false))
|
|
||||||
} else {
|
|
||||||
searchQuery
|
|
||||||
.refetch()
|
|
||||||
.then(res => {
|
|
||||||
const status = (res.data as any)?.statuses?.[0]
|
|
||||||
status && setFoundStatus(status)
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
.catch(() => setLoading(false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const accountQuery = useAccountQuery({
|
const accountQuery = useAccountQuery({
|
||||||
id: isAccount?.style === 'default' ? isAccount.id : '',
|
account: match?.account ? { ...match?.account, url: status.card.url } : undefined,
|
||||||
options: { enabled: false }
|
options: { enabled: false, retry: false }
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAccount) {
|
if (match?.account) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
if (isAccount.sameInstance && isAccount.style === 'default') {
|
accountQuery
|
||||||
accountQuery
|
.refetch()
|
||||||
.refetch()
|
.then(res => {
|
||||||
.then(res => {
|
res.data && setFoundAccount(res.data)
|
||||||
res.data && setFoundAccount(res.data)
|
setLoading(false)
|
||||||
setLoading(false)
|
})
|
||||||
})
|
.catch(() => setLoading(false))
|
||||||
.catch(() => setLoading(false))
|
|
||||||
} else {
|
|
||||||
searchQuery
|
|
||||||
.refetch()
|
|
||||||
.then(res => {
|
|
||||||
const account = (res.data as any)?.accounts?.[0]
|
|
||||||
account && setFoundAccount(account)
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
.catch(() => setLoading(false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -128,10 +77,10 @@ const TimelineCard: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isStatus && foundStatus) {
|
if (match?.status && foundStatus) {
|
||||||
return <TimelineDefault item={foundStatus} disableDetails disableOnPress />
|
return <TimelineDefault item={foundStatus} disableDetails disableOnPress />
|
||||||
}
|
}
|
||||||
if (isAccount && foundAccount) {
|
if (match?.account && foundAccount) {
|
||||||
return <ComponentAccount account={foundAccount} />
|
return <ComponentAccount account={foundAccount} />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -192,12 +141,23 @@ const TimelineCard: React.FC = () => {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: StyleConstants.Spacing.M,
|
marginTop: StyleConstants.Spacing.M,
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: 1,
|
||||||
borderRadius: StyleConstants.Spacing.S,
|
borderRadius: StyleConstants.Spacing.S,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderColor: colors.border
|
borderColor: colors.border
|
||||||
}}
|
}}
|
||||||
onPress={async () => status.card && (await openLink(status.card.url, navigation))}
|
onPress={async () => {
|
||||||
|
if (match?.status && foundStatus) {
|
||||||
|
navigation.push('Tab-Shared-Toot', { toot: foundStatus })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (match?.account && foundAccount) {
|
||||||
|
navigation.push('Tab-Shared-Account', { account: foundAccount })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status.card?.url && (await openLink(status.card.url, navigation))
|
||||||
|
}}
|
||||||
children={cardContent()}
|
children={cardContent()}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, StyleSheet, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { Path, Svg } from 'react-native-svg'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { isRtlLang } from 'rtl-detect'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -17,35 +14,37 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
|
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
|
||||||
const { status, highlighted, inThread, disableDetails } = useContext(StatusContext)
|
const { status, highlighted, inThread } = useContext(StatusContext)
|
||||||
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
if (!status || typeof status.content !== 'string' || !status.content.length) return null
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View>
|
||||||
|
{/* <CustomText
|
||||||
|
children={excludeMentions?.current.map(mention => mention.username).join(' - ')}
|
||||||
|
style={{ color: colors.secondary }}
|
||||||
|
/> */}
|
||||||
{status.spoiler_text?.length ? (
|
{status.spoiler_text?.length ? (
|
||||||
<>
|
<>
|
||||||
<ParseHTML
|
<ParseHTML
|
||||||
content={status.spoiler_text}
|
content={status.spoiler_text}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={999}
|
numberOfLines={999}
|
||||||
highlighted={highlighted}
|
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={
|
|
||||||
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
|
|
||||||
? { writingDirection: 'rtl' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{inThread ? (
|
{inThread ? (
|
||||||
<CustomText fontStyle='S' style={{ textAlign: 'center', color: colors.secondary, paddingVertical: StyleConstants.Spacing.XS }}>
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: colors.secondary,
|
||||||
|
paddingVertical: StyleConstants.Spacing.XS
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('shared.content.expandHint')}
|
{t('shared.content.expandHint')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -53,11 +52,8 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={
|
numberOfLines={
|
||||||
instanceAccount.preferences?.['reading:expand:spoilers'] || inThread
|
preferences?.['reading:expand:spoilers'] || inThread
|
||||||
? notificationOwnToot
|
? notificationOwnToot
|
||||||
? 2
|
? 2
|
||||||
: 999
|
: 999
|
||||||
|
@ -65,13 +61,6 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
||||||
}
|
}
|
||||||
expandHint={t('shared.content.expandHint')}
|
expandHint={t('shared.content.expandHint')}
|
||||||
setSpoilerExpanded={setSpoilerExpanded}
|
setSpoilerExpanded={setSpoilerExpanded}
|
||||||
highlighted={highlighted}
|
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={
|
|
||||||
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
|
|
||||||
? { writingDirection: 'rtl' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -79,19 +68,10 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
|
numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={
|
|
||||||
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
|
|
||||||
? { writingDirection: 'rtl' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ export type HighlightedStatusContextType = {}
|
||||||
|
|
||||||
type StatusContextType = {
|
type StatusContextType = {
|
||||||
queryKey?: QueryKeyTimeline
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
|
||||||
|
|
||||||
status?: Mastodon.Status
|
status?: Mastodon.Status
|
||||||
|
|
||||||
reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
|
|
||||||
ownAccount?: boolean
|
ownAccount?: boolean
|
||||||
spoilerHidden?: boolean
|
spoilerHidden?: boolean
|
||||||
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
||||||
detectedLanguage?: React.MutableRefObject<string>
|
detectedLanguage?: React.MutableRefObject<string>
|
||||||
|
excludeMentions?: React.MutableRefObject<Mastodon.Mention[]>
|
||||||
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
inThread?: boolean
|
inThread?: boolean
|
||||||
disableDetails?: boolean
|
disableDetails?: boolean
|
||||||
disableOnPress?: boolean
|
disableOnPress?: boolean
|
||||||
isConversation?: boolean
|
isConversation?: boolean
|
||||||
|
isRemote?: boolean
|
||||||
}
|
}
|
||||||
const StatusContext = createContext<StatusContextType>({} as StatusContextType)
|
const StatusContext = createContext<StatusContextType>({} as StatusContextType)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ const TimelineFeedback = () => {
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const { data } = useStatusHistory({
|
const { data } = useStatusHistory({
|
||||||
id: status.id,
|
status,
|
||||||
options: { enabled: status.edited_at !== undefined }
|
options: { enabled: status.edited_at !== undefined }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ const TimelineFeedback = () => {
|
||||||
style={[styles.text, { marginRight: 0, color: colors.blue }]}
|
style={[styles.text, { marginRight: 0, color: colors.blue }]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.push('Tab-Shared-History', {
|
navigation.push('Tab-Shared-History', {
|
||||||
id: status.id,
|
status,
|
||||||
detectedLanguage: detectedLanguage?.current || status.language || ''
|
detectedLanguage: detectedLanguage?.current || status.language || ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import removeHTML from '@helpers/removeHTML'
|
import removeHTML from '@utils/helpers/removeHTML'
|
||||||
import { store } from '@root/store'
|
import { queryClient } from '@utils/queryHooks'
|
||||||
|
import { QueryKeyFilters } from '@utils/queryHooks/filters'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstance } 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 from 'react'
|
import React from 'react'
|
||||||
|
@ -15,7 +15,7 @@ export interface FilteredProps {
|
||||||
|
|
||||||
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
if (!filterResults?.length) {
|
if (!filterResults?.length) {
|
||||||
|
@ -23,18 +23,27 @@ const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
||||||
}
|
}
|
||||||
switch (typeof filterResults[0]) {
|
switch (typeof filterResults[0]) {
|
||||||
case 'string': // v1 filter
|
case 'string': // v1 filter
|
||||||
return <>{t('shared.filtered.match', { context: 'v1', phrase: filterResults[0] })}</>
|
return (
|
||||||
|
<>
|
||||||
|
{t('componentTimeline:shared.filtered.match', {
|
||||||
|
defaultValue: 'v1',
|
||||||
|
context: 'v1',
|
||||||
|
phrase: filterResults[0]
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{t('shared.filtered.match', {
|
{t('componentTimeline:shared.filtered.match', {
|
||||||
|
defaultValue: 'v2',
|
||||||
context: 'v2',
|
context: 'v2',
|
||||||
count: filterResults.length,
|
count: filterResults.length,
|
||||||
filters: filterResults.map(result => result.title).join(t('common:separator'))
|
filters: filterResults.map(result => result.title).join(t('common:separator'))
|
||||||
})}
|
})}
|
||||||
<CustomText
|
<CustomText
|
||||||
style={{ color: colors.blue }}
|
style={{ color: colors.blue }}
|
||||||
children={`\n${t('shared.filtered.reveal')}`}
|
children={`\n${t('componentTimeline:shared.filtered.reveal')}`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -66,7 +75,6 @@ export const shouldFilter = ({
|
||||||
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
|
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
|
||||||
}): FilteredProps['filterResults'] | undefined => {
|
}): FilteredProps['filterResults'] | undefined => {
|
||||||
const page = queryKey[1]
|
const page = queryKey[1]
|
||||||
const instance = getInstance(store.getState())
|
|
||||||
|
|
||||||
let returnFilter: FilteredProps['filterResults'] | undefined
|
let returnFilter: FilteredProps['filterResults'] | undefined
|
||||||
|
|
||||||
|
@ -91,7 +99,8 @@ export const shouldFilter = ({
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instance?.filters?.forEach(filter => {
|
const queryKeyFilters: QueryKeyFilters = ['Filters']
|
||||||
|
queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => {
|
||||||
if (returnFilter) {
|
if (returnFilter) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,13 @@ import menuStatus from '@components/contextMenu/status'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { Platform, View } from 'react-native'
|
import { Platform, View } from 'react-native'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineHeaderAndroid: React.FC = () => {
|
const TimelineHeaderAndroid: React.FC = () => {
|
||||||
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress, rawContent } =
|
const { queryKey, status, disableDetails, disableOnPress, rawContent } = useContext(StatusContext)
|
||||||
useContext(StatusContext)
|
|
||||||
|
|
||||||
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
|
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
|
||||||
|
|
||||||
|
@ -28,9 +27,9 @@ const TimelineHeaderAndroid: React.FC = () => {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
openChange,
|
openChange,
|
||||||
account: status.account,
|
account: status.account,
|
||||||
queryKey
|
...(status && { status })
|
||||||
})
|
})
|
||||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
const mStatus = menuStatus({ status, queryKey })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ position: 'absolute', top: 0, right: 0 }}>
|
<View style={{ position: 'absolute', top: 0, right: 0 }}>
|
||||||
|
@ -52,34 +51,51 @@ const TimelineHeaderAndroid: React.FC = () => {
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{mShare.map((mGroup, index) => (
|
{[mShare, mAccount, mStatus].map((menu, i) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<Fragment key={i}>
|
||||||
{mGroup.map(menu => (
|
{menu.map((group, index) => (
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
<DropdownMenu.Group key={index}>
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
{group.map(item => {
|
||||||
</DropdownMenu.Item>
|
switch (item.type) {
|
||||||
|
case 'item':
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item key={item.key} {...item.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={item.title} />
|
||||||
|
{item.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: item.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
case 'sub':
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<DropdownMenu.Sub key={item.key}>
|
||||||
|
<DropdownMenu.SubTrigger
|
||||||
|
key={item.trigger.key}
|
||||||
|
{...item.trigger.props}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle children={item.trigger.title} />
|
||||||
|
{item.trigger.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: item.trigger.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
{item.items.map(sub => (
|
||||||
|
<DropdownMenu.Item key={sub.key} {...sub.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={sub.title} />
|
||||||
|
{sub.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: sub.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</Fragment>
|
||||||
))}
|
|
||||||
|
|
||||||
{mAccount.map((mGroup, index) => (
|
|
||||||
<DropdownMenu.Group key={index}>
|
|
||||||
{mGroup.map(menu => (
|
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
|
||||||
</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.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Group>
|
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
|
@ -2,13 +2,13 @@ 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 { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { 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, { useContext } 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 '@tanstack/react-query'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||||
|
@ -22,7 +22,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
||||||
if (!queryKey) return null
|
if (!queryKey) return null
|
||||||
|
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
const mutation = useTimelineMutation({
|
||||||
|
@ -32,7 +32,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
||||||
theme,
|
theme,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`shared.header.conversation.delete.function`)
|
function: t(`componentTimeline:shared.header.conversation.delete.function`)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -53,7 +53,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
|
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
|
||||||
>
|
>
|
||||||
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
|
<CustomText>{t('componentTimeline:shared.header.conversation.withAccounts')}</CustomText>
|
||||||
{conversation.accounts.map((account, index) => (
|
{conversation.accounts.map((account, index) => (
|
||||||
<CustomText key={account.id} numberOfLines={1}>
|
<CustomText key={account.id} numberOfLines={1}>
|
||||||
{index !== 0 ? t('common:separator') : undefined}
|
{index !== 0 ? t('common:separator') : undefined}
|
||||||
|
@ -73,13 +73,8 @@ const HeaderConversation = ({ conversation }: Props) => {
|
||||||
marginBottom: StyleConstants.Spacing.S
|
marginBottom: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{conversation.last_status?.created_at ? (
|
{conversation.last_status?.created_at ? <HeaderSharedCreated /> : null}
|
||||||
<HeaderSharedCreated
|
<HeaderSharedMuted />
|
||||||
created_at={conversation.last_status?.created_at}
|
|
||||||
edited_at={conversation.last_status?.edited_at}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<HeaderSharedMuted muted={conversation.last_status?.muted} />
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -89,7 +84,6 @@ const HeaderConversation = ({ conversation }: Props) => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'deleteItem',
|
type: 'deleteItem',
|
||||||
source: 'conversations',
|
source: 'conversations',
|
||||||
queryKey,
|
|
||||||
id: conversation.id
|
id: conversation.id
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Pressable, View } from 'react-native'
|
import { Platform, Pressable, View } from 'react-native'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
|
@ -13,11 +13,11 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
||||||
import HeaderSharedApplication from './HeaderShared/Application'
|
import HeaderSharedApplication from './HeaderShared/Application'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||||
|
import HeaderSharedReplies from './HeaderShared/Replies'
|
||||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||||
|
|
||||||
const TimelineHeaderDefault: React.FC = () => {
|
const TimelineHeaderDefault: React.FC = () => {
|
||||||
const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } =
|
const { queryKey, status, disableDetails, rawContent, isRemote } = useContext(StatusContext)
|
||||||
useContext(StatusContext)
|
|
||||||
if (!status) return null
|
if (!status) return null
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
@ -34,9 +34,9 @@ const TimelineHeaderDefault: React.FC = () => {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
openChange,
|
openChange,
|
||||||
account: status.account,
|
account: status.account,
|
||||||
queryKey
|
...(status && { status })
|
||||||
})
|
})
|
||||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
const mStatus = menuStatus({ status, queryKey })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||||
|
@ -56,14 +56,19 @@ const TimelineHeaderDefault: React.FC = () => {
|
||||||
: { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S })
|
: { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HeaderSharedCreated
|
{isRemote ? (
|
||||||
created_at={status.created_at}
|
<Icon
|
||||||
edited_at={status.edited_at}
|
name='Wifi'
|
||||||
highlighted={highlighted}
|
size={StyleConstants.Font.Size.M}
|
||||||
/>
|
color={colors.secondary}
|
||||||
<HeaderSharedVisibility visibility={status.visibility} />
|
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||||
<HeaderSharedMuted muted={status.muted} />
|
/>
|
||||||
<HeaderSharedApplication application={status.application} />
|
) : null}
|
||||||
|
<HeaderSharedCreated />
|
||||||
|
<HeaderSharedVisibility />
|
||||||
|
<HeaderSharedMuted />
|
||||||
|
<HeaderSharedReplies />
|
||||||
|
<HeaderSharedApplication />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -82,37 +87,51 @@ const TimelineHeaderDefault: React.FC = () => {
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{mShare.map((mGroup, index) => (
|
{[mShare, mAccount, mStatus].map((menu, i) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<Fragment key={i}>
|
||||||
{mGroup.map(menu => (
|
{menu.map((group, index) => (
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
<DropdownMenu.Group key={index}>
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
{group.map(item => {
|
||||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
switch (item.type) {
|
||||||
</DropdownMenu.Item>
|
case 'item':
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item key={item.key} {...item.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={item.title} />
|
||||||
|
{item.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: item.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
case 'sub':
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<DropdownMenu.Sub key={item}>
|
||||||
|
<DropdownMenu.SubTrigger
|
||||||
|
key={item.trigger.key}
|
||||||
|
{...item.trigger.props}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle children={item.trigger.title} />
|
||||||
|
{item.trigger.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: item.trigger.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
{item.items.map(sub => (
|
||||||
|
<DropdownMenu.Item key={sub.key} {...sub.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={sub.title} />
|
||||||
|
{sub.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: sub.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</Fragment>
|
||||||
))}
|
|
||||||
|
|
||||||
{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>
|
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
|
@ -5,15 +5,14 @@ import menuShare from '@components/contextMenu/share'
|
||||||
import menuStatus from '@components/contextMenu/status'
|
import menuStatus from '@components/contextMenu/status'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
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 WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Pressable, View } from 'react-native'
|
import { Platform, Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
import HeaderSharedAccount from './HeaderShared/Account'
|
import HeaderSharedAccount from './HeaderShared/Account'
|
||||||
|
@ -42,13 +41,11 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
openChange,
|
openChange,
|
||||||
account: status?.account,
|
account: status?.account,
|
||||||
queryKey
|
...(status && { status })
|
||||||
})
|
})
|
||||||
const mStatus = menuStatus({ status, queryKey })
|
const mStatus = menuStatus({ status, queryKey })
|
||||||
const mInstance = menuInstance({ status, queryKey })
|
const mInstance = menuInstance({ status, queryKey })
|
||||||
|
|
||||||
const url = useSelector(getInstanceUrl)
|
|
||||||
|
|
||||||
const actions = () => {
|
const actions = () => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
@ -62,7 +59,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||||
content={t('shared.actions.openReport')}
|
content={t('shared.actions.openReport')}
|
||||||
onPress={async () =>
|
onPress={async () =>
|
||||||
WebBrowser.openAuthSessionAsync(
|
WebBrowser.openAuthSessionAsync(
|
||||||
`https://${url}/admin/reports/${notification.report.id}`,
|
`https://${getAccountStorage.string('auth.domain')}/admin/reports/${
|
||||||
|
notification.report.id
|
||||||
|
}`,
|
||||||
'tooot://tooot',
|
'tooot://tooot',
|
||||||
{
|
{
|
||||||
...(await browserPackage()),
|
...(await browserPackage()),
|
||||||
|
@ -74,7 +73,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
if (status) {
|
if (status && Platform.OS !== 'android') {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{ flex: 1, alignItems: 'center' }}
|
style={{ flex: 1, alignItems: 'center' }}
|
||||||
|
@ -89,48 +88,53 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{mShare.map((mGroup, index) => (
|
{[mShare, mStatus, mAccount, mInstance].map((menu, i) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<Fragment key={i}>
|
||||||
{mGroup.map(menu => (
|
{menu.map((group, index) => (
|
||||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
<DropdownMenu.Group key={index}>
|
||||||
<DropdownMenu.ItemTitle children={menu.title} />
|
{group.map(item => {
|
||||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
switch (item.type) {
|
||||||
</DropdownMenu.Item>
|
case 'item':
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item key={item.key} {...item.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={item.title} />
|
||||||
|
{item.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: item.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
case 'sub':
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<DropdownMenu.Sub key={item.key}>
|
||||||
|
<DropdownMenu.SubTrigger
|
||||||
|
key={item.trigger.key}
|
||||||
|
{...item.trigger.props}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle children={item.trigger.title} />
|
||||||
|
{item.trigger.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon
|
||||||
|
ios={{ name: item.trigger.icon }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
{item.items.map(sub => (
|
||||||
|
<DropdownMenu.Item key={sub.key} {...sub.props}>
|
||||||
|
<DropdownMenu.ItemTitle children={sub.title} />
|
||||||
|
{sub.icon ? (
|
||||||
|
<DropdownMenu.ItemIcon ios={{ name: sub.icon }} />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</Fragment>
|
||||||
))}
|
|
||||||
|
|
||||||
{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.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
@ -175,31 +179,24 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||||
marginBottom: StyleConstants.Spacing.S
|
marginBottom: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HeaderSharedCreated
|
<HeaderSharedCreated />
|
||||||
created_at={notification.status?.created_at || notification.created_at}
|
{notification.status?.visibility ? <HeaderSharedVisibility /> : null}
|
||||||
edited_at={notification.status?.edited_at}
|
<HeaderSharedMuted />
|
||||||
/>
|
<HeaderSharedApplication />
|
||||||
{notification.status?.visibility ? (
|
|
||||||
<HeaderSharedVisibility visibility={notification.status.visibility} />
|
|
||||||
) : null}
|
|
||||||
<HeaderSharedMuted muted={notification.status?.muted} />
|
|
||||||
<HeaderSharedApplication application={notification.status?.application} />
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{Platform.OS !== 'android' ? (
|
<View
|
||||||
<View
|
style={[
|
||||||
style={[
|
{ marginLeft: StyleConstants.Spacing.M },
|
||||||
{ marginLeft: StyleConstants.Spacing.M },
|
notification.type === 'follow' ||
|
||||||
notification.type === 'follow' ||
|
notification.type === 'follow_request' ||
|
||||||
notification.type === 'follow_request' ||
|
notification.type === 'admin.report'
|
||||||
notification.type === 'admin.report'
|
? { flexShrink: 1 }
|
||||||
? { flexShrink: 1 }
|
: { flex: 1 }
|
||||||
: { flex: 1 }
|
]}
|
||||||
]}
|
children={actions()}
|
||||||
children={actions()}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { ParseEmojis } from '@root/components/Parse'
|
|
||||||
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'
|
||||||
|
|
|
@ -2,38 +2,34 @@ 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'
|
||||||
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 HeaderSharedApplication: React.FC = () => {
|
||||||
application?: Mastodon.Application
|
const { status, isConversation } = useContext(StatusContext)
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
|
return !isConversation && status?.application?.name && status.application.name !== 'Web' ? (
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
accessibilityRole='link'
|
||||||
|
onPress={async () => {
|
||||||
|
status.application?.website && (await openLink(status.application.website))
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: StyleConstants.Spacing.S,
|
||||||
|
color: colors.secondary
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{t('shared.header.shared.application', {
|
||||||
|
application: status.application.name
|
||||||
|
})}
|
||||||
|
</CustomText>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSharedApplication = React.memo(
|
|
||||||
({ application }: Props) => {
|
|
||||||
const { colors } = useTheme()
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
|
||||||
|
|
||||||
return application && application.name !== 'Web' ? (
|
|
||||||
<CustomText
|
|
||||||
fontStyle='S'
|
|
||||||
accessibilityRole='link'
|
|
||||||
onPress={async () => {
|
|
||||||
application.website && (await openLink(application.website))
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
marginLeft: StyleConstants.Spacing.S,
|
|
||||||
color: colors.secondary
|
|
||||||
}}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{t('shared.header.shared.application', {
|
|
||||||
application: application.name
|
|
||||||
})}
|
|
||||||
</CustomText>
|
|
||||||
) : null
|
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
export default HeaderSharedApplication
|
export default HeaderSharedApplication
|
||||||
|
|
|
@ -3,53 +3,46 @@ import RelativeTime from '@components/RelativeTime'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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 { FormattedDate } from 'react-intl'
|
import { FormattedDate } from 'react-intl'
|
||||||
|
import StatusContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
created_at: Mastodon.Status['created_at'] | number
|
created_at?: Mastodon.Status['created_at'] | number
|
||||||
edited_at?: Mastodon.Status['edited_at']
|
|
||||||
highlighted?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSharedCreated = React.memo(
|
const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
||||||
({ created_at, edited_at, highlighted = false }: Props) => {
|
const { status, highlighted } = useContext(StatusContext)
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const actualTime = edited_at || created_at
|
if (!status) return null
|
||||||
|
|
||||||
return (
|
const actualTime = created_at || status.edited_at || status.created_at
|
||||||
<>
|
|
||||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
return (
|
||||||
{highlighted ? (
|
<>
|
||||||
<>
|
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||||
<FormattedDate
|
{highlighted ? (
|
||||||
value={new Date(actualTime)}
|
<>
|
||||||
dateStyle='medium'
|
<FormattedDate value={new Date(actualTime)} dateStyle='medium' timeStyle='short' />
|
||||||
timeStyle='short'
|
</>
|
||||||
/>
|
) : (
|
||||||
</>
|
<RelativeTime time={actualTime} />
|
||||||
) : (
|
)}
|
||||||
<RelativeTime time={actualTime} />
|
</CustomText>
|
||||||
)}
|
{status.edited_at && !highlighted ? (
|
||||||
</CustomText>
|
<Icon
|
||||||
{edited_at ? (
|
accessibilityLabel={t('shared.header.shared.edited.accessibilityLabel')}
|
||||||
<Icon
|
name='Edit'
|
||||||
accessibilityLabel={t(
|
size={StyleConstants.Font.Size.S}
|
||||||
'shared.header.shared.edited.accessibilityLabel'
|
color={colors.secondary}
|
||||||
)}
|
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||||
name='Edit'
|
/>
|
||||||
size={StyleConstants.Font.Size.S}
|
) : null}
|
||||||
color={colors.secondary}
|
</>
|
||||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
)
|
||||||
/>
|
}
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
(prev, next) => prev.edited_at === next.edited_at
|
|
||||||
)
|
|
||||||
|
|
||||||
export default HeaderSharedCreated
|
export default HeaderSharedCreated
|
||||||
|
|
|
@ -1,36 +1,24 @@
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import StatusContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const HeaderSharedMuted: React.FC = () => {
|
||||||
muted?: Mastodon.Status['muted']
|
const { status } = useContext(StatusContext)
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
return status?.muted ? (
|
||||||
|
<Icon
|
||||||
|
accessibilityLabel={t('shared.header.shared.muted.accessibilityLabel')}
|
||||||
|
name='VolumeX'
|
||||||
|
size={StyleConstants.Font.Size.M}
|
||||||
|
color={colors.secondary}
|
||||||
|
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSharedMuted = React.memo(
|
|
||||||
({ muted }: Props) => {
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
return muted ? (
|
|
||||||
<Icon
|
|
||||||
accessibilityLabel={t('shared.header.shared.muted.accessibilityLabel')}
|
|
||||||
name='VolumeX'
|
|
||||||
size={StyleConstants.Font.Size.S}
|
|
||||||
color={colors.secondary}
|
|
||||||
style={styles.visibility}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
visibility: {
|
|
||||||
marginLeft: StyleConstants.Spacing.S
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default HeaderSharedMuted
|
export default HeaderSharedMuted
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import CustomText from '@components/Text'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React, { Fragment, useContext } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import StatusContext from '../Context'
|
||||||
|
|
||||||
|
const HeaderSharedReplies: React.FC = () => {
|
||||||
|
const { status, rawContent, excludeMentions, isConversation } = useContext(StatusContext)
|
||||||
|
if (!isConversation) return null
|
||||||
|
|
||||||
|
const navigation = useNavigation<any>()
|
||||||
|
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
const mentionsBeginning = rawContent?.current?.[0]
|
||||||
|
.match(new RegExp(/^(?:@\S+\s+)+/))?.[0]
|
||||||
|
?.match(new RegExp(/@\S+/, 'g'))
|
||||||
|
excludeMentions &&
|
||||||
|
(excludeMentions.current =
|
||||||
|
mentionsBeginning?.length && status?.mentions
|
||||||
|
? status.mentions.filter(mention => mentionsBeginning.includes(`@${mention.username}`))
|
||||||
|
: [])
|
||||||
|
|
||||||
|
return excludeMentions?.current.length ? (
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
style={{ flex: 1, marginLeft: StyleConstants.Spacing.S, color: colors.secondary }}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
<Trans
|
||||||
|
ns='componentTimeline'
|
||||||
|
i18nKey='shared.header.shared.replies'
|
||||||
|
components={[
|
||||||
|
<>
|
||||||
|
{excludeMentions.current.map((mention, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{index > 0 ? t('common:separator') : null}
|
||||||
|
<CustomText
|
||||||
|
style={{ color: colors.blue, paddingLeft: StyleConstants.Spacing.S }}
|
||||||
|
children={`@${mention.username}`}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.push('Tab-Shared-Account', {
|
||||||
|
account: mention,
|
||||||
|
isRemote: status?._remote
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</CustomText>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderSharedReplies
|
|
@ -1,57 +1,52 @@
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
import StatusContext from '../Context'
|
||||||
|
|
||||||
export interface Props {
|
const HeaderSharedVisibility: React.FC = () => {
|
||||||
visibility: Mastodon.Status['visibility']
|
const { status } = useContext(StatusContext)
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
switch (status?.visibility) {
|
||||||
|
case 'unlisted':
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
|
||||||
|
name='Unlock'
|
||||||
|
size={StyleConstants.Font.Size.S}
|
||||||
|
color={colors.secondary}
|
||||||
|
style={styles.visibility}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'private':
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
|
||||||
|
name='Lock'
|
||||||
|
size={StyleConstants.Font.Size.S}
|
||||||
|
color={colors.secondary}
|
||||||
|
style={styles.visibility}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'direct':
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
accessibilityLabel={t('shared.header.shared.visibility.direct.accessibilityLabel')}
|
||||||
|
name='Mail'
|
||||||
|
size={StyleConstants.Font.Size.S}
|
||||||
|
color={colors.secondary}
|
||||||
|
style={styles.visibility}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSharedVisibility = React.memo(
|
|
||||||
({ visibility }: Props) => {
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
|
||||||
const { colors } = useTheme()
|
|
||||||
|
|
||||||
switch (visibility) {
|
|
||||||
case 'unlisted':
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
|
|
||||||
name='Unlock'
|
|
||||||
size={StyleConstants.Font.Size.S}
|
|
||||||
color={colors.secondary}
|
|
||||||
style={styles.visibility}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'private':
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
|
|
||||||
name='Lock'
|
|
||||||
size={StyleConstants.Font.Size.S}
|
|
||||||
color={colors.secondary}
|
|
||||||
style={styles.visibility}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'direct':
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
accessibilityLabel={t('shared.header.shared.visibility.direct.accessibilityLabel')}
|
|
||||||
name='Mail'
|
|
||||||
size={StyleConstants.Font.Size.S}
|
|
||||||
color={colors.secondary}
|
|
||||||
style={styles.visibility}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
visibility: {
|
visibility: {
|
||||||
marginLeft: StyleConstants.Spacing.S
|
marginLeft: StyleConstants.Spacing.S
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { displayMessage } from '@components/Message'
|
||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import RelativeTime from '@components/RelativeTime'
|
import RelativeTime from '@components/RelativeTime'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useNavState } from '@utils/navigation/navigators'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
|
@ -13,42 +15,36 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert
|
||||||
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, useContext, useMemo, useState } from 'react'
|
import React, { useContext, 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 '@tanstack/react-query'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelinePoll: React.FC = () => {
|
const TimelinePoll: React.FC = () => {
|
||||||
const {
|
const { queryKey, status, ownAccount, spoilerHidden, disableDetails, highlighted } =
|
||||||
queryKey,
|
useContext(StatusContext)
|
||||||
rootQueryKey,
|
|
||||||
status,
|
|
||||||
reblogStatus,
|
|
||||||
ownAccount,
|
|
||||||
spoilerHidden,
|
|
||||||
disableDetails
|
|
||||||
} = useContext(StatusContext)
|
|
||||||
if (!queryKey || !status || !status.poll) return null
|
if (!queryKey || !status || !status.poll) return null
|
||||||
const poll = status.poll
|
const poll = status.poll
|
||||||
|
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||||
|
|
||||||
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
|
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
|
||||||
|
|
||||||
|
const navigationState = useNavState()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
const mutation = useTimelineMutation({
|
||||||
onSuccess: ({ body }, params) => {
|
onSuccess: ({ body }, params) => {
|
||||||
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
||||||
queryClient.cancelQueries(queryKey)
|
queryClient.cancelQueries(queryKey)
|
||||||
rootQueryKey && queryClient.cancelQueries(rootQueryKey)
|
|
||||||
|
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
switch (theParams.payload.property) {
|
switch (theParams.payload.type) {
|
||||||
case 'poll':
|
case 'poll':
|
||||||
theParams.payload.data = body as unknown as Mastodon.Poll
|
updateStatusProperty(
|
||||||
updateStatusProperty(theParams)
|
{ ...theParams, poll: body as unknown as Mastodon.Poll },
|
||||||
|
navigationState
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -59,7 +55,7 @@ const TimelinePoll: React.FC = () => {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
function: t(`shared.poll.meta.button.${theParams.payload.type}`)
|
function: t(`componentTimeline:shared.poll.meta.button.${theParams.payload.type}` as any)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -73,7 +69,7 @@ const TimelinePoll: React.FC = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const pollButton = useMemo(() => {
|
const pollButton = () => {
|
||||||
if (!poll.expired) {
|
if (!poll.expired) {
|
||||||
if (!ownAccount && !poll.voted) {
|
if (!ownAccount && !poll.voted) {
|
||||||
return (
|
return (
|
||||||
|
@ -82,62 +78,51 @@ const TimelinePoll: React.FC = () => {
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
type: 'poll',
|
||||||
id: poll.id,
|
action: 'vote',
|
||||||
type: 'vote',
|
|
||||||
options: allOptions
|
options: allOptions
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.vote')}
|
content={t('componentTimeline:shared.poll.meta.button.vote')}
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
disabled={allOptions.filter(o => o !== false).length === 0}
|
disabled={allOptions.filter(o => o !== false).length === 0}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
} else {
|
} else if (highlighted) {
|
||||||
return (
|
return (
|
||||||
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
||||||
<Button
|
<Button
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
status,
|
||||||
rootQueryKey,
|
|
||||||
id: status.id,
|
|
||||||
isReblog: !!reblogStatus,
|
|
||||||
payload: {
|
payload: {
|
||||||
property: 'poll',
|
type: 'poll',
|
||||||
id: poll.id,
|
action: 'refresh'
|
||||||
type: 'refresh'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.refresh')}
|
content={t('componentTimeline:shared.poll.meta.button.refresh')}
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
|
}
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = (index: number): string =>
|
||||||
(index: number): string =>
|
allOptions[index]
|
||||||
allOptions[index]
|
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
: `${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
|
||||||
[allOptions]
|
|
||||||
)
|
|
||||||
|
|
||||||
const pollBodyDisallow = useMemo(() => {
|
const pollBodyDisallow = () => {
|
||||||
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
||||||
|
@ -182,7 +167,7 @@ const TimelinePoll: React.FC = () => {
|
||||||
borderTopRightRadius: 10,
|
borderTopRightRadius: 10,
|
||||||
borderBottomRightRadius: 10,
|
borderBottomRightRadius: 10,
|
||||||
marginTop: StyleConstants.Spacing.XS,
|
marginTop: StyleConstants.Spacing.XS,
|
||||||
marginBottom: StyleConstants.Spacing.S,
|
marginBottom: StyleConstants.Spacing.XS,
|
||||||
width: `${Math.round(
|
width: `${Math.round(
|
||||||
(option.votes_count / (poll.voters_count || poll.votes_count)) * 100
|
(option.votes_count / (poll.voters_count || poll.votes_count)) * 100
|
||||||
)}%`,
|
)}%`,
|
||||||
|
@ -191,8 +176,8 @@ const TimelinePoll: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
}, [theme, poll.options])
|
}
|
||||||
const pollBodyAllow = useMemo(() => {
|
const pollBodyAllow = () => {
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -229,26 +214,30 @@ const TimelinePoll: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))
|
))
|
||||||
}, [theme, allOptions])
|
}
|
||||||
|
|
||||||
const pollVoteCounts = () => {
|
const pollVoteCounts = () => {
|
||||||
if (poll.voters_count !== null) {
|
if (poll.voters_count !== null) {
|
||||||
return t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
|
return t('componentTimeline:shared.poll.meta.count.voters', { count: poll.voters_count })
|
||||||
} else if (poll.votes_count !== null) {
|
} else if (poll.votes_count !== null) {
|
||||||
return t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
|
return t('componentTimeline:shared.poll.meta.count.votes', { count: poll.votes_count })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollExpiration = () => {
|
const pollExpiration = () => {
|
||||||
if (poll.expired) {
|
if (poll.expired) {
|
||||||
return t('shared.poll.meta.expiration.expired')
|
return t('componentTimeline:shared.poll.meta.expiration.expired')
|
||||||
} else {
|
} else {
|
||||||
if (poll.expires_at) {
|
if (poll.expires_at) {
|
||||||
return (
|
return (
|
||||||
<Trans
|
<>
|
||||||
i18nKey='componentTimeline:shared.poll.meta.expiration.until'
|
{' • '}
|
||||||
components={[<RelativeTime time={poll.expires_at} />]}
|
<Trans
|
||||||
/>
|
ns='componentTimeline'
|
||||||
|
i18nKey='shared.poll.meta.expiration.until'
|
||||||
|
components={[<RelativeTime time={poll.expires_at} />]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,7 +247,7 @@ const TimelinePoll: React.FC = () => {
|
||||||
|
|
||||||
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()}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
@ -267,7 +256,7 @@ const TimelinePoll: React.FC = () => {
|
||||||
marginTop: StyleConstants.Spacing.XS
|
marginTop: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pollButton}
|
{pollButton()}
|
||||||
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
||||||
{pollVoteCounts()}
|
{pollVoteCounts()}
|
||||||
{pollExpiration()}
|
{pollExpiration()}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import detectLanguage from '@helpers/detectLanguage'
|
import detectLanguage from '@utils/helpers/detectLanguage'
|
||||||
import getLanguage from '@helpers/getLanguage'
|
import getLanguage from '@utils/helpers/getLanguage'
|
||||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
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'
|
||||||
|
@ -16,7 +16,7 @@ const TimelineTranslate = () => {
|
||||||
const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext)
|
const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext)
|
||||||
if (!status || !highlighted || !rawContent?.current.length) return null
|
if (!status || !highlighted || !rawContent?.current.length) return null
|
||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation(['componentTimeline'])
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const [detected, setDetected] = useState<{
|
const [detected, setDetected] = useState<{
|
||||||
|
@ -101,15 +101,15 @@ const TimelineTranslate = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isError
|
{isError
|
||||||
? t('shared.translate.failed')
|
? t('componentTimeline:shared.translate.failed')
|
||||||
: isSuccess
|
: isSuccess
|
||||||
? typeof data?.error === 'string'
|
? typeof data?.error === 'string'
|
||||||
? t(`shared.translate.${data.error}`)
|
? t(`componentTimeline:shared.translate.${data.error}` as any)
|
||||||
: t('shared.translate.succeed', {
|
: t('componentTimeline:shared.translate.succeed', {
|
||||||
provider: data?.provider,
|
provider: data?.provider,
|
||||||
source: data?.sourceLanguage
|
source: data?.sourceLanguage
|
||||||
})
|
})
|
||||||
: t('shared.translate.default')}
|
: t('componentTimeline:shared.translate.default')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<Circle
|
<Circle
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useScrollToTop } from '@react-navigation/native'
|
||||||
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
|
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
|
import {
|
||||||
|
getAccountStorage,
|
||||||
|
setAccountStorage,
|
||||||
|
useGlobalStorageListener
|
||||||
|
} from '@utils/storage/actions'
|
||||||
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, { RefObject, useCallback, useRef } from 'react'
|
import React, { RefObject, useRef } from 'react'
|
||||||
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
||||||
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
||||||
import { useSelector } from 'react-redux'
|
import TimelineEmpty from './Empty'
|
||||||
import TimelineEmpty from './Timeline/Empty'
|
import TimelineFooter from './Footer'
|
||||||
import TimelineFooter from './Timeline/Footer'
|
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh'
|
||||||
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
|
|
||||||
|
|
||||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<any>)
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
flRef?: RefObject<FlatList<any>>
|
flRef?: RefObject<FlatList<any>>
|
||||||
|
@ -24,7 +29,8 @@ export interface Props {
|
||||||
>
|
>
|
||||||
disableRefresh?: boolean
|
disableRefresh?: boolean
|
||||||
disableInfinity?: boolean
|
disableInfinity?: boolean
|
||||||
customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'>
|
readMarker?: 'read_marker_following'
|
||||||
|
customProps?: Partial<FlatListProps<any>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Timeline: React.FC<Props> = ({
|
const Timeline: React.FC<Props> = ({
|
||||||
|
@ -33,6 +39,7 @@ const Timeline: React.FC<Props> = ({
|
||||||
queryOptions,
|
queryOptions,
|
||||||
disableRefresh = false,
|
disableRefresh = false,
|
||||||
disableInfinity = false,
|
disableInfinity = false,
|
||||||
|
readMarker = undefined,
|
||||||
customProps
|
customProps
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
@ -45,24 +52,12 @@ const Timeline: React.FC<Props> = ({
|
||||||
notifyOnChangeProps: Platform.select({
|
notifyOnChangeProps: Platform.select({
|
||||||
ios: ['dataUpdatedAt', 'isFetching'],
|
ios: ['dataUpdatedAt', 'isFetching'],
|
||||||
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
|
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
|
||||||
}),
|
})
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage?.links?.next && {
|
|
||||||
...(lastPage.links.next.isOffset
|
|
||||||
? { offset: lastPage.links.next.id }
|
|
||||||
: { max_id: lastPage.links.next.id })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
|
||||||
|
|
||||||
const onEndReached = useCallback(
|
|
||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
|
||||||
[isFetchingNextPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
|
const fetchingActive = useRef<boolean>(false)
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const fetchingType = useSharedValue<0 | 1 | 2>(0)
|
const fetchingType = useSharedValue<0 | 1 | 2>(0)
|
||||||
|
@ -85,6 +80,32 @@ const Timeline: React.FC<Props> = ({
|
||||||
[isFetching]
|
[isFetching]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const viewabilityConfigCallbackPairs = useRef<
|
||||||
|
Pick<FlatListProps<any>, 'viewabilityConfigCallbackPairs'>['viewabilityConfigCallbackPairs']
|
||||||
|
>(
|
||||||
|
readMarker
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
viewabilityConfig: {
|
||||||
|
minimumViewTime: 300,
|
||||||
|
itemVisiblePercentThreshold: 80,
|
||||||
|
waitForInteraction: true
|
||||||
|
},
|
||||||
|
onViewableItemsChanged: ({ viewableItems }) => {
|
||||||
|
const marker = readMarker ? getAccountStorage.string(readMarker) : undefined
|
||||||
|
|
||||||
|
const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id
|
||||||
|
if (!fetchingActive.current && firstItemId && firstItemId > (marker || '0')) {
|
||||||
|
setAccountStorage([{ key: readMarker, value: firstItemId }])
|
||||||
|
} else {
|
||||||
|
// setAccountStorage([{ key: readMarker, value: '109519141378761752' }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
|
||||||
const androidRefreshControl = Platform.select({
|
const androidRefreshControl = Platform.select({
|
||||||
android: {
|
android: {
|
||||||
refreshControl: (
|
refreshControl: (
|
||||||
|
@ -93,38 +114,45 @@ const Timeline: React.FC<Props> = ({
|
||||||
colors={[colors.primaryDefault]}
|
colors={[colors.primaryDefault]}
|
||||||
progressBackgroundColor={colors.backgroundDefault}
|
progressBackgroundColor={colors.backgroundDefault}
|
||||||
refreshing={isFetching || isLoading}
|
refreshing={isFetching || isLoading}
|
||||||
onRefresh={() => refetch()}
|
onRefresh={() => {
|
||||||
|
if (readMarker) {
|
||||||
|
setAccountStorage([{ key: readMarker, value: undefined }])
|
||||||
|
}
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useScrollToTop(flRef)
|
useScrollToTop(flRef)
|
||||||
useSelector(getInstanceActive, (prev, next) => {
|
useGlobalStorageListener('account.active', () =>
|
||||||
if (prev !== next) {
|
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
||||||
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
)
|
||||||
}
|
|
||||||
return prev === next
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimelineRefresh
|
<TimelineRefresh
|
||||||
flRef={flRef}
|
flRef={flRef}
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
|
fetchingActive={fetchingActive}
|
||||||
scrollY={scrollY}
|
scrollY={scrollY}
|
||||||
fetchingType={fetchingType}
|
fetchingType={fetchingType}
|
||||||
disableRefresh={disableRefresh}
|
disableRefresh={disableRefresh}
|
||||||
|
readMarker={readMarker}
|
||||||
/>
|
/>
|
||||||
<AnimatedFlatList
|
<AnimatedFlatList
|
||||||
ref={customFLRef || flRef}
|
ref={customFLRef || flRef}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
windowSize={7}
|
windowSize={7}
|
||||||
data={flattenData}
|
data={flattenPages(data)}
|
||||||
|
{...(customProps?.renderItem
|
||||||
|
? { renderItem: customProps.renderItem }
|
||||||
|
: { renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} /> })}
|
||||||
initialNumToRender={6}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={3}
|
maxToRenderPerBatch={3}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
||||||
|
@ -139,13 +167,12 @@ const Timeline: React.FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
maintainVisibleContentPosition={
|
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
|
||||||
isFetching
|
{...(!isLoading && {
|
||||||
? {
|
maintainVisibleContentPosition: {
|
||||||
minIndexForVisible: 0
|
minIndexForVisible: 0
|
||||||
}
|
}
|
||||||
: undefined
|
})}
|
||||||
}
|
|
||||||
{...androidRefreshControl}
|
{...androidRefreshControl}
|
||||||
{...customProps}
|
{...customProps}
|
||||||
/>
|
/>
|
|
@ -2,7 +2,10 @@ import haptics from '@components/haptics'
|
||||||
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'
|
||||||
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
|
import { TabSharedStackParamList, useNavState } from '@utils/navigation/navigators'
|
||||||
|
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
import {
|
import {
|
||||||
QueryKeyRelationship,
|
QueryKeyRelationship,
|
||||||
useRelationshipMutation,
|
useRelationshipMutation,
|
||||||
|
@ -10,39 +13,30 @@ import {
|
||||||
} from '@utils/queryHooks/relationship'
|
} from '@utils/queryHooks/relationship'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateAccountProperty,
|
MutationVarsTimelineUpdateAccountProperty,
|
||||||
QueryKeyTimeline,
|
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { getAccountStorage, getReadableAccounts, useAccountStorage } from '@utils/storage/actions'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Platform } from 'react-native'
|
import { Alert, Platform } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const menuAccount = ({
|
const menuAccount = ({
|
||||||
type,
|
type,
|
||||||
openChange,
|
openChange,
|
||||||
account,
|
account,
|
||||||
queryKey,
|
status
|
||||||
rootQueryKey
|
|
||||||
}: {
|
}: {
|
||||||
type: 'status' | 'account' // Where the action is coming from
|
type: 'status' | 'account' // Where the action is coming from
|
||||||
openChange: boolean
|
openChange: boolean
|
||||||
account?: Pick<Mastodon.Account, 'id' | 'username'>
|
account?: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'>
|
||||||
queryKey?: QueryKeyTimeline
|
status?: Mastodon.Status
|
||||||
rootQueryKey?: QueryKeyTimeline
|
}): ContextMenu => {
|
||||||
}): ContextMenu[][] => {
|
|
||||||
if (!account) return []
|
|
||||||
|
|
||||||
const navigation =
|
const navigation =
|
||||||
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
||||||
const { t } = useTranslation('componentContextMenu')
|
const navState = useNavState()
|
||||||
|
const { t } = useTranslation(['common', 'componentContextMenu', 'componentRelationship'])
|
||||||
|
|
||||||
const menus: ContextMenu[][] = [[]]
|
const menus: ContextMenu = [[]]
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount)
|
|
||||||
const ownAccount = instanceAccount?.id === account.id
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState(openChange)
|
const [enabled, setEnabled] = useState(openChange)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -50,21 +44,32 @@ const menuAccount = ({
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
}
|
}
|
||||||
}, [openChange, enabled])
|
}, [openChange, enabled])
|
||||||
const { data, isFetched } = useRelationshipQuery({ id: account.id, options: { enabled } })
|
const { data: fetchedAccount } = useAccountQuery({ account, _local: true, options: { enabled } })
|
||||||
|
const actualAccount = status?._remote ? fetchedAccount : account
|
||||||
|
const { data, isFetched } = useRelationshipQuery({
|
||||||
|
id: actualAccount?.id,
|
||||||
|
options: { enabled: !!actualAccount?.id && enabled }
|
||||||
|
})
|
||||||
|
|
||||||
|
const ownAccount = useAccountStorage.string('auth.account.id')['0'] === actualAccount?.id
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const timelineMutation = useTimelineMutation({
|
const timelineMutation = useTimelineMutation({
|
||||||
onSuccess: (_, params) => {
|
onSuccess: (_, params) => {
|
||||||
queryClient.refetchQueries(['Relationship', { id: account.id }])
|
queryClient.refetchQueries(['Relationship', { id: actualAccount?.id }])
|
||||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||||
displayMessage({
|
displayMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('common:message.success.message', {
|
message: t('common:message.success.message', {
|
||||||
function: t(`account.${theParams.payload.property}.action`, {
|
function: t(
|
||||||
...(theParams.payload.property !== 'reports' && {
|
`componentContextMenu:account.${theParams.payload.property}.action`,
|
||||||
context: (theParams.payload.currentValue || false).toString()
|
theParams.payload.property !== 'reports'
|
||||||
})
|
? {
|
||||||
})
|
defaultValue: 'false',
|
||||||
|
context: (theParams.payload.currentValue || false).toString()
|
||||||
|
}
|
||||||
|
: { defaultValue: 'false' }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -73,11 +78,15 @@ const menuAccount = ({
|
||||||
displayMessage({
|
displayMessage({
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`account.${theParams.payload.property}.action`, {
|
function: t(
|
||||||
...(theParams.payload.property !== 'reports' && {
|
`componentContextMenu:account.${theParams.payload.property}.action`,
|
||||||
context: (theParams.payload.currentValue || false).toString()
|
theParams.payload.property !== 'reports'
|
||||||
})
|
? {
|
||||||
})
|
defaultValue: 'false',
|
||||||
|
context: (theParams.payload.currentValue || false).toString()
|
||||||
|
}
|
||||||
|
: { defaultValue: 'false' }
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -89,25 +98,28 @@ const menuAccount = ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryKey && queryClient.invalidateQueries(queryKey)
|
for (const key of navState) {
|
||||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
queryClient.invalidateQueries(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }]
|
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }]
|
||||||
const relationshipMutation = useRelationshipMutation({
|
const relationshipMutation = useRelationshipMutation({
|
||||||
onSuccess: (res, { payload: { action } }) => {
|
onSuccess: (res, { payload: { action } }) => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||||
if (action === 'block') {
|
if (action === 'block') {
|
||||||
const queryKey = ['Timeline', { page: 'Following' }]
|
queryClient.invalidateQueries({
|
||||||
queryClient.invalidateQueries({ queryKey, exact: false })
|
queryKey: ['Timeline', { page: 'Following' }],
|
||||||
|
exact: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any, { payload: { action } }) => {
|
onError: (err: any, { payload: { action } }) => {
|
||||||
displayMessage({
|
displayMessage({
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
function: t(`${action}.function`)
|
function: t(`componentContextMenu:${action}.function` as any)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
typeof err.status === 'number' &&
|
typeof err.status === 'number' &&
|
||||||
|
@ -120,14 +132,18 @@ const menuAccount = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!account) return []
|
||||||
|
|
||||||
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
|
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
|
||||||
menus[0].push({
|
menus[0].push({
|
||||||
|
type: 'item',
|
||||||
key: 'account-following',
|
key: 'account-following',
|
||||||
item: {
|
props: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
data &&
|
data &&
|
||||||
|
actualAccount &&
|
||||||
relationshipMutation.mutate({
|
relationshipMutation.mutate({
|
||||||
id: account.id,
|
id: actualAccount.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
payload: { action: 'follow', state: !data?.requested ? data.following : true }
|
payload: { action: 'follow', state: !data?.requested ? data.following : true }
|
||||||
}),
|
}),
|
||||||
|
@ -136,7 +152,8 @@ const menuAccount = ({
|
||||||
hidden: false
|
hidden: false
|
||||||
},
|
},
|
||||||
title: !data?.requested
|
title: !data?.requested
|
||||||
? t('account.following.action', {
|
? t('componentContextMenu:account.following.action', {
|
||||||
|
defaultValue: 'false',
|
||||||
context: (data?.following || false).toString()
|
context: (data?.following || false).toString()
|
||||||
})
|
})
|
||||||
: t('componentRelationship:button.requested'),
|
: t('componentRelationship:button.requested'),
|
||||||
|
@ -147,109 +164,199 @@ const menuAccount = ({
|
||||||
: 'person.badge.minus'
|
: 'person.badge.minus'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ownAccount) {
|
if (!ownAccount) {
|
||||||
menus[0].push({
|
menus[0].push({
|
||||||
|
type: 'item',
|
||||||
key: 'account-list',
|
key: 'account-list',
|
||||||
item: {
|
props: {
|
||||||
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
|
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
|
||||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden: !isFetched || !data?.following
|
hidden: !isFetched || !data?.following
|
||||||
},
|
},
|
||||||
title: t('account.inLists'),
|
title: t('componentContextMenu:account.inLists'),
|
||||||
icon: 'checklist'
|
icon: 'checklist'
|
||||||
})
|
})
|
||||||
menus[0].push({
|
menus[0].push({
|
||||||
key: 'account-mute',
|
type: 'item',
|
||||||
item: {
|
key: 'account-show-boosts',
|
||||||
|
props: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
|
actualAccount &&
|
||||||
|
relationshipMutation.mutate({
|
||||||
|
id: actualAccount.id,
|
||||||
|
type: 'outgoing',
|
||||||
|
payload: { action: 'follow', state: false, reblogs: !data?.showing_reblogs }
|
||||||
|
}),
|
||||||
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
|
destructive: false,
|
||||||
|
hidden: !isFetched || !data?.following
|
||||||
|
},
|
||||||
|
title: t('componentContextMenu:account.showBoosts.action', {
|
||||||
|
defaultValue: 'false',
|
||||||
|
context: (data?.showing_reblogs || false).toString()
|
||||||
|
}),
|
||||||
|
icon: data?.showing_reblogs ? 'rectangle.on.rectangle.slash' : 'rectangle.on.rectangle'
|
||||||
|
})
|
||||||
|
menus[0].push({
|
||||||
|
type: 'item',
|
||||||
|
key: 'account-mute',
|
||||||
|
props: {
|
||||||
|
onSelect: () =>
|
||||||
|
actualAccount &&
|
||||||
timelineMutation.mutate({
|
timelineMutation.mutate({
|
||||||
type: 'updateAccountProperty',
|
type: 'updateAccountProperty',
|
||||||
queryKey,
|
id: actualAccount.id,
|
||||||
id: account.id,
|
|
||||||
payload: { property: 'mute', currentValue: data?.muting }
|
payload: { property: 'mute', currentValue: data?.muting }
|
||||||
}),
|
}),
|
||||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden: false
|
hidden: false
|
||||||
},
|
},
|
||||||
title: t('account.mute.action', {
|
title: t('componentContextMenu:account.mute.action', {
|
||||||
|
defaultValue: 'false',
|
||||||
context: (data?.muting || false).toString()
|
context: (data?.muting || false).toString()
|
||||||
}),
|
}),
|
||||||
icon: data?.muting ? 'eye' : 'eye.slash'
|
icon: data?.muting ? 'eye' : 'eye.slash'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
!ownAccount &&
|
const followAs = () => {
|
||||||
|
if (type !== 'account') return
|
||||||
|
const accounts = getReadableAccounts()
|
||||||
|
menus[0].push({
|
||||||
|
type: 'sub',
|
||||||
|
key: 'account-follow-as',
|
||||||
|
trigger: {
|
||||||
|
key: 'account-follow-as',
|
||||||
|
props: { destructive: false, disabled: false, hidden: !accounts.length },
|
||||||
|
title: t('componentContextMenu:account.followAs.trigger'),
|
||||||
|
icon: 'person.badge.plus'
|
||||||
|
},
|
||||||
|
items: accounts.map(a => ({
|
||||||
|
key: `account-${a.key}`,
|
||||||
|
props: {
|
||||||
|
onSelect: async () => {
|
||||||
|
const lookup = await apiInstance<Mastodon.Account>({
|
||||||
|
account: a.key,
|
||||||
|
method: 'get',
|
||||||
|
url: 'accounts/lookup',
|
||||||
|
params: {
|
||||||
|
acct:
|
||||||
|
account.acct === account.username
|
||||||
|
? `${account.acct}@${getAccountStorage.string('auth.account.domain')}`
|
||||||
|
: account.acct
|
||||||
|
}
|
||||||
|
}).then(res => res.body)
|
||||||
|
await apiInstance({
|
||||||
|
account: a.key,
|
||||||
|
method: 'post',
|
||||||
|
url: `accounts/${lookup.id}/follow`
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
displayMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: t('componentContextMenu:account.followAs.succeed', {
|
||||||
|
context: account.locked ? 'locked' : 'default',
|
||||||
|
defaultValue: 'default',
|
||||||
|
target: account.acct,
|
||||||
|
source: a.acct
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(err =>
|
||||||
|
displayMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: t('common:message.error.message', {
|
||||||
|
function: t('componentContextMenu:account.followAs.failed')
|
||||||
|
}),
|
||||||
|
...(err.status &&
|
||||||
|
typeof err.status === 'number' &&
|
||||||
|
err.data &&
|
||||||
|
err.data.error &&
|
||||||
|
typeof err.data.error === 'string' && {
|
||||||
|
description: err.data.error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
destructive: false,
|
||||||
|
hidden: a.active
|
||||||
|
},
|
||||||
|
title: a.acct
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
followAs()
|
||||||
|
|
||||||
menus.push([
|
menus.push([
|
||||||
{
|
{
|
||||||
key: 'account-block',
|
type: 'sub',
|
||||||
item: {
|
key: 'account-block-report',
|
||||||
onSelect: () =>
|
trigger: {
|
||||||
Alert.alert(t('account.block.alert.title', { username: account.username }), undefined, [
|
key: 'account-block-report',
|
||||||
{
|
props: { destructive: true, disabled: false, hidden: false },
|
||||||
text: t('common:buttons.confirm'),
|
title: t('componentContextMenu:account.blockReport'),
|
||||||
style: 'destructive',
|
icon: 'hand.raised'
|
||||||
onPress: () =>
|
|
||||||
timelineMutation.mutate({
|
|
||||||
type: 'updateAccountProperty',
|
|
||||||
queryKey,
|
|
||||||
id: account.id,
|
|
||||||
payload: { property: 'block', currentValue: data?.blocking }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('common:buttons.cancel')
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
|
||||||
destructive: !data?.blocking,
|
|
||||||
hidden: false
|
|
||||||
},
|
},
|
||||||
title: t('account.block.action', {
|
items: [
|
||||||
context: (data?.blocking || false).toString()
|
{
|
||||||
}),
|
key: 'account-block',
|
||||||
icon: data?.blocking ? 'checkmark.circle' : 'xmark.circle'
|
props: {
|
||||||
},
|
onSelect: () =>
|
||||||
{
|
Alert.alert(
|
||||||
key: 'account-reports',
|
t('componentContextMenu:account.block.alert.title', {
|
||||||
item: {
|
username: actualAccount?.username
|
||||||
onSelect: () =>
|
}),
|
||||||
Alert.alert(
|
undefined,
|
||||||
t('account.reports.alert.title', { username: account.username }),
|
[
|
||||||
undefined,
|
{
|
||||||
[
|
text: t('common:buttons.confirm'),
|
||||||
{
|
style: 'destructive',
|
||||||
text: t('common:buttons.confirm'),
|
onPress: () =>
|
||||||
style: 'destructive',
|
actualAccount &&
|
||||||
onPress: () => {
|
timelineMutation.mutate({
|
||||||
timelineMutation.mutate({
|
type: 'updateAccountProperty',
|
||||||
type: 'updateAccountProperty',
|
id: actualAccount.id,
|
||||||
queryKey,
|
payload: { property: 'block', currentValue: data?.blocking }
|
||||||
id: account.id,
|
})
|
||||||
payload: { property: 'reports' }
|
},
|
||||||
})
|
{
|
||||||
timelineMutation.mutate({
|
text: t('common:buttons.cancel')
|
||||||
type: 'updateAccountProperty',
|
}
|
||||||
queryKey,
|
]
|
||||||
id: account.id,
|
),
|
||||||
payload: { property: 'block', currentValue: false }
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
})
|
destructive: !data?.blocking,
|
||||||
}
|
hidden: false
|
||||||
},
|
},
|
||||||
{
|
title: t('componentContextMenu:account.block.action', {
|
||||||
text: t('common:buttons.cancel')
|
defaultValue: 'false',
|
||||||
}
|
context: (data?.blocking || false).toString()
|
||||||
]
|
}),
|
||||||
),
|
icon: data?.blocking ? 'checkmark.circle' : 'xmark.circle'
|
||||||
disabled: false,
|
},
|
||||||
destructive: true,
|
{
|
||||||
hidden: false
|
key: 'account-reports',
|
||||||
},
|
props: {
|
||||||
title: t('account.reports.action'),
|
onSelect: () =>
|
||||||
icon: 'flag'
|
actualAccount &&
|
||||||
|
navigation.navigate('Tab-Shared-Report', {
|
||||||
|
account: actualAccount,
|
||||||
|
status
|
||||||
|
}),
|
||||||
|
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||||
|
destructive: true,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
title: t('componentContextMenu:account.reports.action'),
|
||||||
|
icon: 'flag'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
}
|
||||||
|
|
||||||
return menus
|
return menus
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,53 @@
|
||||||
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'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList, useNavState } from '@utils/navigation/navigators'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const menuAt = ({ account }: { account: Mastodon.Account }): ContextMenu[][] => {
|
const menuAt = ({ account }: { account: Mastodon.Account }): ContextMenu => {
|
||||||
const { t } = useTranslation('componentContextMenu')
|
const { t } = useTranslation('componentContextMenu')
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||||
|
const navigationState = useNavState()
|
||||||
|
|
||||||
const menus: ContextMenu[][] = []
|
return [
|
||||||
|
[
|
||||||
menus.push([
|
{
|
||||||
{
|
type: 'item',
|
||||||
key: 'at-direct',
|
key: 'at-direct',
|
||||||
item: {
|
props: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
navigation.navigate('Screen-Compose', {
|
navigation.navigate('Screen-Compose', {
|
||||||
type: 'conversation',
|
type: 'conversation',
|
||||||
accts: [account.acct],
|
accts: [account.acct],
|
||||||
visibility: 'direct'
|
visibility: 'direct',
|
||||||
}),
|
navigationState
|
||||||
disabled: false,
|
}),
|
||||||
destructive: false,
|
disabled: false,
|
||||||
hidden: false
|
destructive: false,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
title: t('at.direct'),
|
||||||
|
icon: 'envelope'
|
||||||
},
|
},
|
||||||
title: t('at.direct'),
|
{
|
||||||
icon: 'envelope'
|
type: 'item',
|
||||||
},
|
key: 'at-public',
|
||||||
{
|
props: {
|
||||||
key: 'at-public',
|
onSelect: () =>
|
||||||
item: {
|
navigation.navigate('Screen-Compose', {
|
||||||
onSelect: () =>
|
type: 'conversation',
|
||||||
navigation.navigate('Screen-Compose', {
|
accts: [account.acct],
|
||||||
type: 'conversation',
|
visibility: 'public',
|
||||||
accts: [account.acct],
|
navigationState
|
||||||
visibility: 'public'
|
}),
|
||||||
}),
|
disabled: false,
|
||||||
disabled: false,
|
destructive: false,
|
||||||
destructive: false,
|
hidden: false
|
||||||
hidden: false
|
},
|
||||||
},
|
title: t('at.public'),
|
||||||
title: t('at.public'),
|
icon: 'at'
|
||||||
icon: 'at'
|
}
|
||||||
}
|
]
|
||||||
])
|
]
|
||||||
|
|
||||||
return menus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default menuAt
|
export default menuAt
|
||||||
|
|
|
@ -1,6 +1,53 @@
|
||||||
type ContextMenu = {
|
// type ContextMenu = (
|
||||||
|
// | {
|
||||||
|
// type: 'group'
|
||||||
|
// key: string
|
||||||
|
// items: ContextMenuItem[]
|
||||||
|
// }
|
||||||
|
// | {
|
||||||
|
// type: 'sub'
|
||||||
|
// key: string
|
||||||
|
// trigger: {
|
||||||
|
// key: string
|
||||||
|
// props: {
|
||||||
|
// disabled: boolean
|
||||||
|
// destructive: boolean
|
||||||
|
// hidden: boolean
|
||||||
|
// }
|
||||||
|
// title: string
|
||||||
|
// icon?: string
|
||||||
|
// }
|
||||||
|
// items: ContextMenuItem[]
|
||||||
|
// }
|
||||||
|
// )[]
|
||||||
|
|
||||||
|
type ContextMenu = (ContextMenuItem | ContextMenuSub)[][]
|
||||||
|
|
||||||
|
type ContextMenuItem = {
|
||||||
|
type: 'item'
|
||||||
key: string
|
key: string
|
||||||
item: { onSelect: () => void; disabled: boolean; destructive: boolean; hidden: boolean }
|
props: {
|
||||||
|
onSelect: () => void
|
||||||
|
disabled: boolean
|
||||||
|
destructive: boolean
|
||||||
|
hidden: boolean
|
||||||
|
}
|
||||||
title: string
|
title: string
|
||||||
icon: string
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextMenuSub = {
|
||||||
|
type: 'sub'
|
||||||
|
key: string
|
||||||
|
trigger: {
|
||||||
|
key: string
|
||||||
|
props: {
|
||||||
|
disabled: boolean
|
||||||
|
destructive: boolean
|
||||||
|
hidden: boolean
|
||||||
|
}
|
||||||
|
title: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
items: Omit<ContextMenuItem, 'type'>[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import parse from 'url-parse'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const menuInstance = ({
|
const menuInstance = ({
|
||||||
status,
|
status,
|
||||||
queryKey,
|
queryKey
|
||||||
rootQueryKey
|
|
||||||
}: {
|
}: {
|
||||||
status?: Mastodon.Status
|
status?: Mastodon.Status
|
||||||
queryKey?: QueryKeyTimeline
|
queryKey?: QueryKeyTimeline
|
||||||
rootQueryKey?: QueryKeyTimeline
|
}): ContextMenu => {
|
||||||
}): ContextMenu[][] => {
|
|
||||||
if (!status || !queryKey) return []
|
if (!status || !queryKey) return []
|
||||||
|
|
||||||
const { t } = useTranslation('componentContextMenu')
|
const { t } = useTranslation(['common', 'componentContextMenu'])
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
const mutation = useTimelineMutation({
|
||||||
|
@ -25,38 +23,33 @@ const menuInstance = ({
|
||||||
displayMessage({
|
displayMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('common:message.success.message', {
|
message: t('common:message.success.message', {
|
||||||
function: t(`instance.block.action`, { instance })
|
function: t(`componentContextMenu:instance.block.action`, { instance })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const menus: ContextMenu[][] = []
|
const menus: ContextMenu = []
|
||||||
|
|
||||||
const currentInstance = useSelector(getInstanceUrl)
|
const instance = parse(status.uri).hostname
|
||||||
const instance = status.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
|
||||||
|
|
||||||
if (currentInstance !== instance && instance) {
|
if (instance !== getAccountStorage.string('auth.domain')) {
|
||||||
menus.push([
|
menus.push([
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
key: 'instance-block',
|
key: 'instance-block',
|
||||||
item: {
|
props: {
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('instance.block.alert.title', { instance }),
|
t('componentContextMenu:instance.block.alert.title', { instance }),
|
||||||
t('instance.block.alert.message'),
|
t('componentContextMenu:instance.block.alert.message'),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: t('common:buttons.confirm'),
|
text: t('common:buttons.confirm'),
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
mutation.mutate({
|
mutation.mutate({ type: 'domainBlock', domain: instance })
|
||||||
type: 'domainBlock',
|
|
||||||
queryKey,
|
|
||||||
domain: instance
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -68,7 +61,7 @@ const menuInstance = ({
|
||||||
destructive: true,
|
destructive: true,
|
||||||
hidden: false
|
hidden: false
|
||||||
},
|
},
|
||||||
title: t('instance.block.action', { instance }),
|
title: t('componentContextMenu:instance.block.action', { instance }),
|
||||||
icon: ''
|
icon: ''
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue