mirror of https://github.com/tooot-app/app
commit
36e61e9f95
|
@ -9,22 +9,18 @@ jobs:
|
|||
build-ios:
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: -- Step 0 -- Extract branch name
|
||||
uses: tj-actions/branch-names@v6
|
||||
- uses: tj-actions/branch-names@v6
|
||||
id: branch
|
||||
- name: -- Step 1 -- Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: -- Step 2 -- Setup node
|
||||
uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: -- Step 3 -- Install node dependencies
|
||||
run: yarn install
|
||||
- name: -- Step 4 -- Install ruby dependencies
|
||||
run: bundle install
|
||||
- name: -- Step 5 -- Run fastlane
|
||||
- run: corepack enable
|
||||
- run: yarn install
|
||||
- run: bundle install
|
||||
- run: yarn app:build ios
|
||||
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 }}
|
||||
SENTRY_ENVIRONMENT: ${{ steps.branch.outputs.current_branch }}
|
||||
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_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: yarn app:build ios
|
||||
|
||||
build-android:
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: -- Step 0 -- Extract branch name
|
||||
uses: tj-actions/branch-names@v6
|
||||
- uses: tj-actions/branch-names@v6
|
||||
id: branch
|
||||
- name: -- Step 1 -- Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: -- Step 2 -- Setup node
|
||||
uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: -- Step 3 -- Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
- name: -- Step 4 -- Install node dependencies
|
||||
run: yarn install
|
||||
- name: -- Step 5 -- Install ruby dependencies
|
||||
run: bundle install
|
||||
- name: -- Step 6 -- Run fastlane
|
||||
- run: corepack enable
|
||||
- run: yarn install
|
||||
- run: bundle install
|
||||
- run: yarn app:build android
|
||||
env:
|
||||
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 }}
|
||||
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
|
||||
GH_PAT_GET_RELEASE: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: yarn app:build android
|
||||
|
||||
create-release:
|
||||
runs-on: macos-12
|
||||
needs: [build-ios, build-android]
|
||||
steps:
|
||||
- name: -- Step 0 -- Extract branch name
|
||||
uses: tj-actions/branch-names@v6
|
||||
- uses: tj-actions/branch-names@v6
|
||||
id: branch
|
||||
- name: -- Step 1 -- Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: -- Step 2 -- Setup node
|
||||
uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: -- Step 3 -- Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
- name: -- Step 4 -- Install node dependencies
|
||||
run: yarn install
|
||||
- name: -- Step 5 -- Install ruby dependencies
|
||||
run: bundle install
|
||||
- name: -- Step 6 -- Run fastlane
|
||||
- run: corepack enable
|
||||
- run: yarn install
|
||||
- run: bundle install
|
||||
- run: yarn app:build release
|
||||
env:
|
||||
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_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
||||
FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: yarn app:build release
|
||||
|
|
|
@ -66,4 +66,13 @@ buck-out/
|
|||
web-build/
|
||||
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
|
||||
index f52822c..ee98565 100755
|
||||
--- 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 f52822c8bed928f387baf90fdb7342c7416a775a..6d9d480d18342832c4b07af2b10f4a63ff538e7b 100755
|
||||
--- a/index.d.ts
|
||||
+++ b/index.d.ts
|
||||
@@ -5,11 +5,9 @@
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// Minimum TypeScript Version: 3.7
|
||||
|
@ -17,12 +17,18 @@ index f52822c..ee98565 100755
|
|||
|
||||
export type ShareCallback = (share?: ShareData) => void;
|
||||
|
||||
@@ -28,7 +26,7 @@ interface ShareMenuReactView {
|
||||
dismissExtension(error?: string): void;
|
||||
openApp(): void;
|
||||
continueInApp(extraData?: object): void;
|
||||
@@ -25,10 +23,10 @@ interface ShareMenu {
|
||||
}
|
||||
|
||||
interface ShareMenuReactView {
|
||||
- dismissExtension(error?: string): void;
|
||||
- openApp(): void;
|
||||
- continueInApp(extraData?: object): void;
|
||||
- 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;
|
|
@ -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
|
||||
index 9557fdb..ebdeb6f 100644
|
||||
--- 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 9557fdbf2fbf97b7f7aeaf7ce86d301a8ced213d..ebdeb6f4de7846d3241101001755595c52a4b05e 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -1,12 +1,12 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
|
@ -19,10 +19,10 @@ index 9557fdb..ebdeb6f 100644
|
|||
versionCode 1
|
||||
versionName "1.0"
|
||||
ndk {
|
||||
diff --git a/node_modules/react-native-share-menu/ios/ReactShareViewController.swift b/node_modules/react-native-share-menu/ios/ReactShareViewController.swift
|
||||
index f42bce6..ee36062 100644
|
||||
--- 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 f42bce6ce7e3f48a7ddc83f3366b68fd0664b1a0..ee360622b1d03cc9661c78c6f210b84c3b19a725 100644
|
||||
--- a/ios/ReactShareViewController.swift
|
||||
+++ b/ios/ReactShareViewController.swift
|
||||
@@ -13,7 +13,7 @@ class ReactShareViewController: ShareViewController, RCTBridgeDelegate, ReactSha
|
||||
func sourceURL(for bridge: RCTBridge!) -> URL! {
|
||||
#if DEBUG
|
||||
|
@ -32,10 +32,10 @@ index f42bce6..ee36062 100644
|
|||
#else
|
||||
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
diff --git a/node_modules/react-native-share-menu/ios/ShareViewController.swift b/node_modules/react-native-share-menu/ios/ShareViewController.swift
|
||||
index 12d8c92..64aa72b 100644
|
||||
--- 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 12d8c92dda20fabd9e7b55fec57b3d867414063c..8a1db0de285b18a9358a10b2ca8293a8c7d56a8e 100644
|
||||
--- a/ios/ShareViewController.swift
|
||||
+++ b/ios/ShareViewController.swift
|
||||
@@ -19,8 +19,8 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
var hostAppUrlScheme: String?
|
||||
var sharedItems: [Any] = []
|
||||
|
@ -78,7 +78,7 @@ index 12d8c92..64aa72b 100644
|
|||
override func configurationItems() -> [Any]! {
|
||||
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
|
||||
return []
|
||||
@@ -238,11 +235,10 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
@@ -238,7 +235,7 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
|
||||
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.
|
||||
|
@ -87,7 +87,3 @@ index 12d8c92..64aa72b 100644
|
|||
}
|
||||
|
||||
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)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.653.0)
|
||||
aws-sdk-core (3.166.0)
|
||||
aws-partitions (1.687.0)
|
||||
aws-sdk-core (3.168.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
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-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.117.1)
|
||||
aws-sdk-s3 (1.117.2)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
|
@ -86,7 +86,7 @@ GEM
|
|||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
excon (0.93.1)
|
||||
excon (0.95.0)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
|
@ -116,7 +116,7 @@ GEM
|
|||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.210.1)
|
||||
fastlane (2.211.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
|
@ -156,7 +156,7 @@ GEM
|
|||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-json (1.1.0)
|
||||
fastlane-plugin-sentry (1.14.0)
|
||||
fastlane-plugin-sentry (1.15.0)
|
||||
os (~> 1.1, >= 1.1.4)
|
||||
fastlane-plugin-versioning_android (0.1.0)
|
||||
fastlane-plugin-yarn (1.2)
|
||||
|
@ -164,9 +164,9 @@ GEM
|
|||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.29.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-core (0.9.1)
|
||||
google-apis-androidpublisher_v3 (0.32.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.9.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
|
@ -175,8 +175,8 @@ GEM
|
|||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.15.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.16.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.12.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
|
@ -187,7 +187,7 @@ GEM
|
|||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.0)
|
||||
google-cloud-storage (1.43.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
|
@ -208,11 +208,11 @@ GEM
|
|||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.6.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.16.3)
|
||||
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)
|
||||
|
||||
![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
|
||||
|
||||
|
@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati
|
|||
|
||||
## Special thanks
|
||||
|
||||
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
||||
|
||||
[@forenta](https://github.com/forenta) for German translation
|
||||
|
||||
[@pat](https://piaille.fr/@pat) for French translation
|
||||
|
||||
[@andrigamerita](https://github.com/andrigamerita) for Italian translation
|
||||
|
||||
[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
|
||||
|
||||
[@hellojaccc](https://github.com/hellojaccc) for Korean translation
|
||||
|
||||
[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
||||
|
||||
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
||||
|
||||
[@janlindblom](https://github.com/janlindblom) for Swedish
|
||||
|
||||
[@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
|
||||
- [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
||||
- [@forenta](https://github.com/forenta) for German translation
|
||||
- [@pat](https://piaille.fr/@pat) for French translation
|
||||
- [@andrigamerita](https://github.com/andrigamerita) for Italian translation
|
||||
- [@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
|
||||
- [@hellojaccc](https://github.com/hellojaccc) for Korean translation
|
||||
- [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
||||
- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
||||
- [@janlindblom](https://github.com/janlindblom) for Swedish
|
||||
- [@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.google.gms.google-services'
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
@ -231,6 +232,7 @@ android {
|
|||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
namespace 'com.xmflsct.app.tooot'
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
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"
|
||||
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.SYSTEM_ALERT_WINDOW"/>
|
||||
<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.WRITE_EXTERNAL_STORAGE"/>
|
||||
<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">
|
||||
<!-- [Custom] Expo Notifications -->
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/ic_stat_notifications" />
|
||||
|
|
|
@ -22,9 +22,10 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
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("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
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
@ -26,7 +26,7 @@ android.useAndroidX=true
|
|||
android.enableJetifier=true
|
||||
|
||||
# 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.
|
||||
# You can also override it from the CLI using
|
||||
|
|
|
@ -15,8 +15,9 @@ export default (): ExpoConfig => ({
|
|||
},
|
||||
android: {
|
||||
package: 'com.xmflsct.app.tooot',
|
||||
permissions: ['CAMERA', 'VIBRATE'],
|
||||
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
|
||||
permissions: ['NOTIFICATIONS', 'CAMERA', 'VIBRATE'],
|
||||
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT'],
|
||||
googleServicesFile: './android/app/google-services.json'
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true)
|
||||
api.cache(false)
|
||||
|
||||
const plugins = [
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
|
@ -8,11 +8,8 @@ module.exports = function (api) {
|
|||
{
|
||||
root: ['./'],
|
||||
alias: {
|
||||
'@assets': './assets',
|
||||
'@root': './src',
|
||||
'@api': './src/api',
|
||||
'@helpers': './src/helpers',
|
||||
'@components': './src/components',
|
||||
'@i18n': './src/i18n',
|
||||
'@screens': './src/screens',
|
||||
'@utils': './src/utils'
|
||||
}
|
||||
|
@ -21,27 +18,9 @@ module.exports = function (api) {
|
|||
'react-native-reanimated/plugin'
|
||||
]
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.BABEL_ENV === 'production'
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'production' || process.env.BABEL_ENV === 'production') {
|
||||
plugins.push('transform-remove-console')
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
}
|
||||
return { presets: ['babel-preset-expo'], plugins }
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 251 KiB |
|
@ -36,7 +36,7 @@ private_lane :build_ios do
|
|||
export_method: "app-store",
|
||||
include_symbols: true,
|
||||
output_directory: BUILD_DIRECTORY,
|
||||
silent: false
|
||||
silent: true
|
||||
)
|
||||
|
||||
case ENVIRONMENT
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
Enjoy toooting! This version includes following improvements and fixes:
|
||||
- Fixed wrongly update notification
|
||||
- Fix some random crashes
|
||||
- Auto fetch remote content in conversations!
|
||||
- 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愉快!此版本包括以下改进和修复:
|
||||
- 修复错误的升级通知
|
||||
- 修复部分应用崩溃
|
||||
- 主动获取对话的远程内容
|
||||
- 自动加载上次我的关注的阅读位置
|
||||
- 用其它已登陆的账户关注用户
|
||||
- 可添加举报细节
|
||||
- 新增暂停自动播放gif动画选项
|
||||
- 隐藏用户的转嘟
|
||||
- 下划线高亮正在关注的话题标签
|
||||
- 支持GoToSocial
|
6
index.js
6
index.js
|
@ -1,8 +1,4 @@
|
|||
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)
|
||||
|
|
|
@ -19,6 +19,7 @@ target 'tooot' do
|
|||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => flags[:fabric_enabled],
|
||||
# :flipper_configuration => FlipperConfiguration.enabled(["Debug"], { 'Flipper' => '0.159.0' }),
|
||||
:flipper_configuration => FlipperConfiguration.disabled,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
|
|
|
@ -3,10 +3,10 @@ PODS:
|
|||
- DoubleConversion (1.1.6)
|
||||
- EXApplication (5.0.1):
|
||||
- ExpoModulesCore
|
||||
- EXAV (13.0.2):
|
||||
- EXAV (13.1.0):
|
||||
- ExpoModulesCore
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXConstants (14.0.2):
|
||||
- EXConstants (14.1.0):
|
||||
- ExpoModulesCore
|
||||
- EXErrorRecovery (4.0.1):
|
||||
- ExpoModulesCore
|
||||
|
@ -16,34 +16,37 @@ PODS:
|
|||
- ExpoModulesCore
|
||||
- EXNotifications (0.17.0):
|
||||
- ExpoModulesCore
|
||||
- Expo (47.0.8):
|
||||
- Expo (47.0.12):
|
||||
- ExpoModulesCore
|
||||
- ExpoCrypto (12.0.0):
|
||||
- ExpoCrypto (12.1.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoHaptics (12.0.1):
|
||||
- ExpoHaptics (12.1.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoKeepAwake (11.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoLocalization (14.0.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (1.0.3):
|
||||
- ExpoModulesCore (1.1.1):
|
||||
- React-Core
|
||||
- ReactCommon/turbomodule/core
|
||||
- ExpoRandom (13.0.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoStoreReview (6.0.0):
|
||||
- ExpoStoreReview (6.1.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoVideoThumbnails (7.1.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoWebBrowser (12.0.0):
|
||||
- ExpoModulesCore
|
||||
- EXScreenCapture (5.0.0):
|
||||
- ExpoModulesCore
|
||||
- EXScreenOrientation (5.0.1):
|
||||
- ExpoModulesCore
|
||||
- React-Core
|
||||
- EXSecureStore (12.0.0):
|
||||
- ExpoModulesCore
|
||||
- EXSplashScreen (0.17.5):
|
||||
- ExpoModulesCore
|
||||
- React-Core
|
||||
- EXVideoThumbnails (7.0.0):
|
||||
- ExpoModulesCore
|
||||
- FBLazyVector (0.70.6)
|
||||
- FBReactNativeSpec (0.70.6):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
|
@ -65,6 +68,9 @@ PODS:
|
|||
- libwebp/mux (1.2.4):
|
||||
- libwebp/demux
|
||||
- libwebp/webp (1.2.4)
|
||||
- MMKV (1.2.14):
|
||||
- MMKVCore (~> 1.2.14)
|
||||
- MMKVCore (1.2.14)
|
||||
- RCT-Folly (2021.07.22.00):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
|
@ -295,16 +301,19 @@ PODS:
|
|||
- React-Core
|
||||
- react-native-blurhash (1.1.10):
|
||||
- React-Core
|
||||
- react-native-cameraroll (5.2.0):
|
||||
- react-native-cameraroll (5.2.1):
|
||||
- React-Core
|
||||
- react-native-image-picker (4.10.2):
|
||||
- react-native-image-picker (4.10.3):
|
||||
- React-Core
|
||||
- react-native-ios-context-menu (1.15.1):
|
||||
- React-Core
|
||||
- react-native-language-detection (0.2.2):
|
||||
- React
|
||||
- react-native-menu (0.7.2):
|
||||
- react-native-menu (0.7.3):
|
||||
- React
|
||||
- react-native-mmkv (2.5.1):
|
||||
- MMKV (>= 1.2.13)
|
||||
- React-Core
|
||||
- react-native-netinfo (9.3.7):
|
||||
- React-Core
|
||||
- react-native-pager-view (6.1.2):
|
||||
|
@ -392,7 +401,7 @@ PODS:
|
|||
- React-Core
|
||||
- RNFastImage (8.6.3):
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.14.2)
|
||||
- SDWebImage (~> 5.14.3)
|
||||
- SDWebImageWebPCoder (~> 0.9.1)
|
||||
- RNGestureHandler (2.8.0):
|
||||
- React-Core
|
||||
|
@ -433,9 +442,9 @@ PODS:
|
|||
- React
|
||||
- RNSVG (13.6.0):
|
||||
- React-Core
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
- SDWebImage (5.14.3):
|
||||
- SDWebImage/Core (= 5.14.3)
|
||||
- SDWebImage/Core (5.14.3)
|
||||
- SDWebImageWebPCoder (0.9.1):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.13)
|
||||
|
@ -461,11 +470,12 @@ DEPENDENCIES:
|
|||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||
- ExpoRandom (from `../node_modules/expo-random/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`)
|
||||
- EXScreenCapture (from `../node_modules/expo-screen-capture/ios`)
|
||||
- EXScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
|
||||
- EXSecureStore (from `../node_modules/expo-secure-store/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`)
|
||||
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
|
||||
- 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-language-detection (from `../node_modules/react-native-language-detection`)
|
||||
- "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-pager-view (from `../node_modules/react-native-pager-view`)
|
||||
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
|
||||
|
@ -527,6 +538,8 @@ SPEC REPOS:
|
|||
- fmt
|
||||
- libevent
|
||||
- libwebp
|
||||
- MMKV
|
||||
- MMKVCore
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
|
@ -567,16 +580,18 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/expo-random/ios"
|
||||
ExpoStoreReview:
|
||||
:path: "../node_modules/expo-store-review/ios"
|
||||
ExpoVideoThumbnails:
|
||||
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||
ExpoWebBrowser:
|
||||
:path: "../node_modules/expo-web-browser/ios"
|
||||
EXScreenCapture:
|
||||
:path: "../node_modules/expo-screen-capture/ios"
|
||||
EXScreenOrientation:
|
||||
:path: "../node_modules/expo-screen-orientation/ios"
|
||||
EXSecureStore:
|
||||
:path: "../node_modules/expo-secure-store/ios"
|
||||
EXSplashScreen:
|
||||
:path: "../node_modules/expo-splash-screen/ios"
|
||||
EXVideoThumbnails:
|
||||
:path: "../node_modules/expo-video-thumbnails/ios"
|
||||
FBLazyVector:
|
||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
||||
FBReactNativeSpec:
|
||||
|
@ -629,6 +644,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-language-detection"
|
||||
react-native-menu:
|
||||
:path: "../node_modules/@react-native-menu/menu"
|
||||
react-native-mmkv:
|
||||
:path: "../node_modules/react-native-mmkv"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-pager-view:
|
||||
|
@ -688,25 +705,26 @@ SPEC CHECKSUMS:
|
|||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
|
||||
EXAV: 9a45d37772c5329294c054a041dcc39931fc5032
|
||||
EXConstants: 3c86653c422dd77e40d10cbbabb3025003977415
|
||||
EXAV: 4b92292fb107520a25956bea940a94a3bb4911ca
|
||||
EXConstants: 44f7d347d0432a66f469d0ce1dc4e3a0ca1b8b2d
|
||||
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
|
||||
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
|
||||
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
|
||||
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
|
||||
Expo: 36b5f625d36728adbdd1934d4d57182f319ab832
|
||||
ExpoCrypto: 51e7662c7f5bfeab25b7909b8a5d545ec15d4877
|
||||
ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f
|
||||
Expo: f48d305fda3e4e501d686e6bad7d8c8373828279
|
||||
ExpoCrypto: 6eb2a5ede7d95b7359a5f0391ee0c5d2ecd144b3
|
||||
ExpoHaptics: 129d3f8d44c2205adcdf8db760602818463d5437
|
||||
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
|
||||
ExpoLocalization: e202d1e2a4950df17ac8d0889d65a1ffd7532d7e
|
||||
ExpoModulesCore: b5d21c8880afda6fb6ee95469f9ac2ec9b98e995
|
||||
ExpoModulesCore: 485dff3a59b036a33b6050c0a5aea3cf1037fdd1
|
||||
ExpoRandom: 58b7e0a5fe1adf1cb6dc1cbe503a6fe9524f36ce
|
||||
ExpoStoreReview: ff6d631f2949eb7e4b2d14146ef6af25a16d770d
|
||||
ExpoStoreReview: 713336ff504db3a6983475bf7c67519cc5efc86f
|
||||
ExpoVideoThumbnails: 424db02cedfbbe2d498bcb2712ea4ba8a9dcb453
|
||||
ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda
|
||||
EXScreenCapture: d9f1ec31042dfef109290d06c2b4789b7444d16d
|
||||
EXScreenOrientation: 07e5aeff07bce09a2b214981e612d87fd7719997
|
||||
EXSecureStore: daec0117c922a67c658cb229152a9e252e5c1750
|
||||
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
|
||||
EXVideoThumbnails: 8b3e48f3716679dd0cbf949217a31eab5c555799
|
||||
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
|
||||
FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
|
@ -714,6 +732,8 @@ SPEC CHECKSUMS:
|
|||
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
|
||||
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
|
||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
||||
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
||||
|
@ -731,11 +751,12 @@ SPEC CHECKSUMS:
|
|||
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
||||
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
||||
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
|
||||
react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
|
||||
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
|
||||
react-native-cameraroll: f94bf9f46c998963ecd2bb6e9a3f9cca59b6d9f1
|
||||
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
|
||||
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
||||
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
||||
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
||||
react-native-menu: 9d7d6f819cc7fa14a15cf86888c53f3240d86f1b
|
||||
react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3
|
||||
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
||||
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
|
||||
|
@ -755,19 +776,19 @@ SPEC CHECKSUMS:
|
|||
ReactCommon: 349be31adeecffc7986a0de875d7fb0dcf4e251c
|
||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||
RNFastImage: c5dd1b551779c5826fe43b7d36788385da2021e2
|
||||
RNFastImage: 756ab178acb5e3f11d8b0a931956fbd9da8d6e54
|
||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
|
||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||
RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d
|
||||
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
||||
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
SDWebImage: 9c36e66c8ce4620b41a7407698dda44211a96764
|
||||
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
|
||||
Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf
|
||||
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||
|
||||
PODFILE CHECKSUM: 05bf71d31ba782dfda5a6b47d38e98a6f6bc079a
|
||||
PODFILE CHECKSUM: 08742f25aa1cdb93d6d5d5efeafd8803ba02b689
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -86,5 +86,7 @@
|
|||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
86
package.json
86
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tooot",
|
||||
"version": "4.7.2",
|
||||
"version": "4.8.0",
|
||||
"description": "tooot for Mastodon",
|
||||
"author": "xmflsct <me@xmflsct.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
@ -14,8 +14,7 @@
|
|||
"iphone": "react-native run-ios --simulator 'iPhone 14 Pro'",
|
||||
"ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (4th generation)'",
|
||||
"app:build": "bundle exec fastlane",
|
||||
"clean": "react-native-clean-project",
|
||||
"postinstall": "patch-package"
|
||||
"clean": "react-native-clean-project"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/react-native-action-sheet": "^4.0.1",
|
||||
|
@ -28,58 +27,59 @@
|
|||
"@mattermost/react-native-paste-input": "^0.5.2",
|
||||
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
|
||||
"@react-native-async-storage/async-storage": "~1.17.11",
|
||||
"@react-native-camera-roll/camera-roll": "^5.2.0",
|
||||
"@react-native-camera-roll/camera-roll": "^5.2.1",
|
||||
"@react-native-clipboard/clipboard": "^1.11.1",
|
||||
"@react-native-community/blur": "^4.3.0",
|
||||
"@react-native-community/netinfo": "9.3.7",
|
||||
"@react-native-community/segmented-control": "^2.2.2",
|
||||
"@react-native-menu/menu": "^0.7.2",
|
||||
"@react-navigation/bottom-tabs": "^6.5.1",
|
||||
"@react-native-firebase/app": "^16.5.0",
|
||||
"@react-native-menu/menu": "^0.7.3",
|
||||
"@react-navigation/bottom-tabs": "^6.5.2",
|
||||
"@react-navigation/native": "^6.1.1",
|
||||
"@react-navigation/native-stack": "^6.9.6",
|
||||
"@react-navigation/stack": "^6.3.9",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@react-navigation/native-stack": "^6.9.7",
|
||||
"@react-navigation/stack": "^6.3.10",
|
||||
"@sentry/react-native": "4.12.0",
|
||||
"@sharcoux/slider": "^6.1.1",
|
||||
"@tanstack/react-query": "^4.20.4",
|
||||
"axios": "^1.2.1",
|
||||
"@tanstack/react-query": "^4.20.9",
|
||||
"axios": "^1.2.2",
|
||||
"diff": "^5.1.0",
|
||||
"expo": "^47.0.8",
|
||||
"expo-auth-session": "^3.7.3",
|
||||
"expo-av": "^13.0.2",
|
||||
"expo-constants": "^14.0.2",
|
||||
"expo-crypto": "^12.0.0",
|
||||
"expo": "^47.0.12",
|
||||
"expo-auth-session": "^3.8.0",
|
||||
"expo-av": "^13.1.0",
|
||||
"expo-constants": "^14.1.0",
|
||||
"expo-crypto": "^12.1.0",
|
||||
"expo-file-system": "^15.1.1",
|
||||
"expo-haptics": "^12.0.1",
|
||||
"expo-linking": "^3.2.3",
|
||||
"expo-haptics": "^12.1.0",
|
||||
"expo-linking": "^3.3.0",
|
||||
"expo-localization": "^14.0.0",
|
||||
"expo-notifications": "^0.17.0",
|
||||
"expo-random": "^13.0.0",
|
||||
"expo-screen-capture": "^5.0.0",
|
||||
"expo-screen-orientation": "^5.0.1",
|
||||
"expo-secure-store": "^12.0.0",
|
||||
"expo-splash-screen": "^0.17.5",
|
||||
"expo-store-review": "^6.0.0",
|
||||
"expo-video-thumbnails": "^7.0.0",
|
||||
"expo-store-review": "^6.1.0",
|
||||
"expo-video-thumbnails": "^7.1.0",
|
||||
"expo-web-browser": "~12.0.0",
|
||||
"i18next": "^22.4.5",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"i18next": "^22.4.8",
|
||||
"linkify-it": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-i18next": "^12.1.4",
|
||||
"react-intl": "^6.2.5",
|
||||
"react-native": "0.70.6",
|
||||
"react-native": "^0.70.6",
|
||||
"react-native-animated-spinkit": "^1.5.2",
|
||||
"react-native-base64": "^0.2.1",
|
||||
"react-native-blurhash": "^1.1.10",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"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-htmlview": "^0.16.0",
|
||||
"react-native-image-picker": "^4.10.2",
|
||||
"react-native-image-picker": "^4.10.3",
|
||||
"react-native-ios-context-menu": "^1.15.1",
|
||||
"react-native-language-detection": "^0.2.2",
|
||||
"react-native-mmkv": "^2.5.1",
|
||||
"react-native-pager-view": "^6.1.2",
|
||||
"react-native-reanimated": "^2.13.0",
|
||||
"react-native-reanimated-zoom": "^0.3.3",
|
||||
|
@ -90,37 +90,35 @@
|
|||
"react-native-swipe-list-view": "^3.2.9",
|
||||
"react-native-tab-view": "^3.3.4",
|
||||
"react-redux": "^8.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rn-placeholder": "^3.0.3",
|
||||
"rtl-detect": "^1.0.4",
|
||||
"valid-url": "^1.0.9",
|
||||
"zeego": "^0.5.0"
|
||||
"url-parse": "^1.5.10",
|
||||
"zeego": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@expo/config": "^7.0.3",
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/linkify-it": "^3.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/react": "~18.0.26",
|
||||
"@types/react-dom": "~18.0.9",
|
||||
"@types/react-native": "~0.70.8",
|
||||
"@types/react-native-base64": "^0.2.0",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-native": "^0.70.8",
|
||||
"@types/react-native-share-menu": "^5.0.2",
|
||||
"@types/react-timeago": "^4.1.3",
|
||||
"@types/valid-url": "^1.0.3",
|
||||
"@welldone-software/why-did-you-render": "^7.0.1",
|
||||
"@types/url-parse": "^1.4.8",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"chalk": "^4.1.2",
|
||||
"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",
|
||||
"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 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' {
|
||||
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
|
||||
returnEmptyString: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,9 @@ declare namespace Mastodon {
|
|||
source?: Source
|
||||
suspended?: boolean
|
||||
role?: Role
|
||||
|
||||
// Internal
|
||||
_remote?: boolean
|
||||
}
|
||||
|
||||
type Announcement = {
|
||||
|
@ -264,14 +267,6 @@ declare namespace Mastodon {
|
|||
}
|
||||
|
||||
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 = {
|
||||
id: string
|
||||
title: string
|
||||
|
@ -281,6 +276,14 @@ declare namespace Mastodon {
|
|||
keywords: FilterKeyword[]
|
||||
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 }
|
||||
|
||||
|
@ -298,7 +301,48 @@ declare namespace Mastodon {
|
|||
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
|
||||
uri: string
|
||||
title: string
|
||||
|
@ -343,6 +387,9 @@ declare namespace Mastodon {
|
|||
max_expiration: number
|
||||
}
|
||||
}
|
||||
|
||||
// Gotosocial
|
||||
account_domain?: string
|
||||
}
|
||||
|
||||
type Mention = {
|
||||
|
@ -351,6 +398,9 @@ declare namespace Mastodon {
|
|||
username: string
|
||||
acct: string
|
||||
url: string
|
||||
|
||||
// Internal
|
||||
_remote?: boolean
|
||||
}
|
||||
|
||||
type Notification =
|
||||
|
@ -470,6 +520,11 @@ declare namespace Mastodon {
|
|||
updated_at: string
|
||||
}
|
||||
|
||||
type Rule = {
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
|
||||
type Status = {
|
||||
// Base
|
||||
id: string
|
||||
|
@ -509,6 +564,10 @@ declare namespace Mastodon {
|
|||
language?: string
|
||||
text?: string
|
||||
filtered?: FilterResult[]
|
||||
|
||||
// Internal
|
||||
_level?: number
|
||||
_remote?: boolean
|
||||
}
|
||||
|
||||
type StatusHistory = {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
declare module 'gl-react-blurhash'
|
||||
declare module 'htmlparser2-without-node-native'
|
||||
declare module 'react-native-feather'
|
||||
declare module 'react-native-htmlview'
|
||||
declare module 'react-native-toast-message'
|
||||
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 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 { QueryClientProvider } from '@tanstack/react-query'
|
||||
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 * as Localization from 'expo-localization'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { IntlProvider } from 'react-intl'
|
||||
import { LogBox, Platform } from 'react-native'
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||
import { MMKV } from 'react-native-mmkv'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { enableFreeze } from 'react-native-screens'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import i18n from './i18n'
|
||||
import Screens from './screens'
|
||||
|
||||
Platform.select({
|
||||
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||
|
@ -38,83 +42,95 @@ push()
|
|||
timezone()
|
||||
enableFreeze(true)
|
||||
|
||||
log('log', 'App', 'delay splash')
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
const App: React.FC = () => {
|
||||
log('log', 'App', 'rendering App')
|
||||
const [appIsReady, setAppIsReady] = useState(false)
|
||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||
|
||||
const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const delaySplash = async () => {
|
||||
log('log', 'App', 'delay splash')
|
||||
try {
|
||||
await SplashScreen.preventAutoHideAsync()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
const prepare = async () => {
|
||||
if (!hasMigrated) {
|
||||
try {
|
||||
await migrateFromAsyncStorage()
|
||||
setHasMigrated(true)
|
||||
} 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 onBeforeLift = useCallback(async () => {
|
||||
let netInfoRes = undefined
|
||||
try {
|
||||
netInfoRes = await netInfo()
|
||||
} catch {}
|
||||
|
||||
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
||||
setLocalCorrupt(netInfoRes.corrupted)
|
||||
}
|
||||
|
||||
log('log', 'App', 'hide splash')
|
||||
try {
|
||||
const onLayoutRootView = useCallback(async () => {
|
||||
if (appIsReady) {
|
||||
log('log', 'App', 'hide splash')
|
||||
await SplashScreen.hideAsync()
|
||||
return Promise.resolve()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
return Promise.reject()
|
||||
}
|
||||
}, [])
|
||||
}, [appIsReady])
|
||||
if (!appIsReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GestureHandlerRootView style={{ flex: 1 }} onLayout={onLayoutRootView}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<PersistGate
|
||||
persistor={persistor}
|
||||
onBeforeLift={onBeforeLift}
|
||||
children={bootstrapped => {
|
||||
log('log', 'App', 'bootstrapped')
|
||||
if (bootstrapped) {
|
||||
log('log', 'App', 'loading actual app :)')
|
||||
log('log', 'App', `Locale: ${Localization.locale}`)
|
||||
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>
|
||||
<SafeAreaProvider>
|
||||
<ActionSheetProvider>
|
||||
<AccessibilityManager>
|
||||
<ThemeManager>
|
||||
<Screens localCorrupt={localCorrupt} />
|
||||
</ThemeManager>
|
||||
</AccessibilityManager>
|
||||
</ActionSheetProvider>
|
||||
</SafeAreaProvider>
|
||||
</QueryClientProvider>
|
||||
</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'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'>
|
||||
props?: PressableProps
|
||||
}
|
||||
|
||||
|
@ -42,11 +42,11 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
|
|||
style={{
|
||||
width: StyleConstants.Avatar.S,
|
||||
height: StyleConstants.Avatar.S,
|
||||
borderRadius: 6,
|
||||
borderRadius: 8,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<CustomText numberOfLines={1}>
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
import { useNavigation } from '@react-navigation/native'
|
||||
import initQuery from '@utils/initQuery'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { ReadableAccountType, setAccount } from '@utils/storage/actions'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React from 'react'
|
||||
import Button from './Button'
|
||||
import haptics from './haptics'
|
||||
|
||||
interface Props {
|
||||
instance: InstanceLatest
|
||||
selected?: boolean
|
||||
account: ReadableAccountType
|
||||
additionalActions?: () => void
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
|
||||
const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
selected={account.active}
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}}
|
||||
content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`}
|
||||
content={account.acct}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
initQuery({ instance })
|
||||
setAccount(account.key)
|
||||
navigation.goBack()
|
||||
if (additionalActions) {
|
||||
additionalActions()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
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 { Flow } from 'react-native-animated-spinkit'
|
||||
import CustomText from './Text'
|
||||
|
@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({
|
|||
overlay = false,
|
||||
onPress
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
const loadingSpinkit = () =>
|
||||
loading ? (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
||||
</View>
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
) : null
|
||||
|
||||
const mainColor = useMemo(() => {
|
||||
const mainColor = () => {
|
||||
if (selected) {
|
||||
return colors.blue
|
||||
} else if (overlay) {
|
||||
|
@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({
|
|||
return colors.primaryDefault
|
||||
}
|
||||
}
|
||||
}, [theme, disabled, loading, selected])
|
||||
}
|
||||
|
||||
const colorBackground = useMemo(() => {
|
||||
if (overlay) {
|
||||
return colors.backgroundOverlayInvert
|
||||
} else {
|
||||
return colors.backgroundDefault
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
const children = useMemo(() => {
|
||||
const children = () => {
|
||||
switch (type) {
|
||||
case 'icon':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name={content}
|
||||
color={mainColor}
|
||||
color={mainColor()}
|
||||
strokeWidth={strokeWidth}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
||||
/>
|
||||
{loading ? loadingSpinkit : null}
|
||||
{loadingSpinkit()}
|
||||
</>
|
||||
)
|
||||
case 'text':
|
||||
|
@ -103,19 +93,19 @@ const Button: React.FC<Props> = ({
|
|||
<>
|
||||
<CustomText
|
||||
style={{
|
||||
color: mainColor,
|
||||
color: mainColor(),
|
||||
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
||||
opacity: loading ? 0 : 1
|
||||
}}
|
||||
fontWeight={fontBold ? 'Bold' : 'Normal'}
|
||||
fontWeight={fontBold || selected ? 'Bold' : 'Normal'}
|
||||
children={content}
|
||||
testID='text'
|
||||
/>
|
||||
{loading ? loadingSpinkit : null}
|
||||
{loadingSpinkit()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [theme, content, loading, disabled])
|
||||
}
|
||||
|
||||
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
||||
|
||||
|
@ -135,9 +125,9 @@ const Button: React.FC<Props> = ({
|
|||
borderRadius: 100,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: overlay ? 0 : 1,
|
||||
borderColor: mainColor,
|
||||
backgroundColor: colorBackground,
|
||||
borderWidth: overlay ? 0 : selected ? 1.5 : 1,
|
||||
borderColor: mainColor(),
|
||||
backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault,
|
||||
paddingVertical: StyleConstants.Spacing[spacing],
|
||||
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
||||
width: round && layoutHeight ? layoutHeight : undefined
|
||||
|
@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({
|
|||
})}
|
||||
testID='base'
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
children={children()}
|
||||
disabled={selected || disabled || loading}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { Keyboard, Pressable, View } from 'react-native'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
import EmojisContext from './Context'
|
||||
|
||||
const EmojisButton: React.FC = () => {
|
||||
const { colors } = useTheme()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { emojis } from '@components/Emojis'
|
||||
import Icon from '@components/Icon'
|
||||
import CustomText from '@components/Text'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
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 layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
@ -19,16 +19,14 @@ import {
|
|||
View
|
||||
} from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import validUrl from 'valid-url'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
import EmojisContext from './Context'
|
||||
|
||||
const EmojisList = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation(['common', 'screenCompose'])
|
||||
|
||||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||
const { colors, mode } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const addEmoji = (shortcode: string) => {
|
||||
if (emojisState.targetIndex === -1) {
|
||||
|
@ -69,31 +67,77 @@ const EmojisList = () => {
|
|||
>
|
||||
{item.map(emoji => {
|
||||
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
if (validUrl.isHttpsUri(uri)) {
|
||||
return (
|
||||
<Pressable
|
||||
key={emoji.shortcode}
|
||||
onPress={() => {
|
||||
addEmoji(`:${emoji.shortcode}:`)
|
||||
dispatch(countInstanceEmoji(emoji))
|
||||
}}
|
||||
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>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Pressable
|
||||
key={emoji.shortcode}
|
||||
onPress={() => {
|
||||
addEmoji(`:${emoji.shortcode}:`)
|
||||
|
||||
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
|
||||
const calculateScore = (
|
||||
emoji: StorageAccount['emojis_frequent'][number]
|
||||
): number => {
|
||||
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
|
||||
var score = emoji.count + 1
|
||||
var order = Math.log(Math.max(score, 1)) / Math.LN10
|
||||
var sign = score > 0 ? 1 : score === 0 ? 0 : -1
|
||||
return (sign * order + seconds / HALF_LIFE) * 10
|
||||
}
|
||||
|
||||
const currentEmojis = getAccountStorage.object('emojis_frequent')
|
||||
const foundEmojiIndex = currentEmojis?.findIndex(
|
||||
e => e.emoji.shortcode === emoji.shortcode && e.emoji.url === emoji.url
|
||||
)
|
||||
|
||||
let newEmojisSort: StorageAccount['emojis_frequent']
|
||||
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>
|
||||
)
|
||||
|
@ -158,7 +202,6 @@ const EmojisList = () => {
|
|||
onChangeText={setSearch}
|
||||
autoCapitalize='none'
|
||||
clearButtonMode='always'
|
||||
keyboardAppearance={mode}
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
|
|
@ -2,15 +2,13 @@ import EmojisButton from '@components/Emojis/Button'
|
|||
import EmojisList from '@components/Emojis/List'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
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 React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 { useSelector } from 'react-redux'
|
||||
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Emojis/helpers/EmojisContext'
|
||||
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Context'
|
||||
|
||||
export type Props = {
|
||||
inputProps: EmojisState['inputProps']
|
||||
|
@ -35,9 +33,9 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
|
|||
emojisDispatch({ type: 'input', payload: inputProps })
|
||||
}, [inputProps])
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation(['componentEmojis'])
|
||||
const { data } = useEmojisQuery({})
|
||||
const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
|
||||
const [frequentEmojis] = useAccountStorage.object('emojis_frequent')
|
||||
useEffect(() => {
|
||||
if (data && data.length) {
|
||||
let sortedEmojis: NonNullable<Emojis['current']> = []
|
|
@ -1,6 +1,6 @@
|
|||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
Image,
|
||||
|
@ -10,8 +10,8 @@ import {
|
|||
View,
|
||||
ViewStyle
|
||||
} from 'react-native'
|
||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||
|
||||
// blurhas -> if blurhash, show before any loading succeed
|
||||
// original -> load original
|
||||
|
@ -55,17 +55,12 @@ const GracefullyImage = ({
|
|||
const { colors } = useTheme()
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
|
||||
const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote)
|
||||
const source = {
|
||||
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
|
||||
}
|
||||
const onLoad = () => {
|
||||
setImageLoaded(true)
|
||||
if (setImageDimensions && source.uri) {
|
||||
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
|
||||
}
|
||||
uri: reduceMotionEnabled && uri.static ? uri.static : currentUri
|
||||
}
|
||||
|
||||
const blurhashView = useMemo(() => {
|
||||
const blurhashView = () => {
|
||||
if (hidden || !imageLoaded) {
|
||||
if (blurhash) {
|
||||
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
||||
|
@ -75,7 +70,7 @@ const GracefullyImage = ({
|
|||
} else {
|
||||
return null
|
||||
}
|
||||
}, [hidden, imageLoaded])
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
@ -92,13 +87,21 @@ const GracefullyImage = ({
|
|||
/>
|
||||
) : null}
|
||||
<FastImage
|
||||
source={{
|
||||
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
|
||||
}}
|
||||
source={source}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
|||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { PropsWithChildren, useCallback, useState } from 'react'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Dimensions, Pressable, View } from 'react-native'
|
||||
import Sparkline from './Sparkline'
|
||||
import CustomText from './Text'
|
||||
|
@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
|
|||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
const onPress = () => {
|
||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
||||
}, [])
|
||||
}
|
||||
|
||||
const padding = StyleConstants.Spacing.Global.PagePadding
|
||||
const width = Dimensions.get('window').width / 4
|
||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
|
@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({
|
|||
background = false,
|
||||
onPress
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const children = useMemo(() => {
|
||||
const children = () => {
|
||||
switch (type) {
|
||||
case 'icon':
|
||||
return (
|
||||
|
@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({
|
|||
)
|
||||
case 'text':
|
||||
return (
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{ color: colors.primaryDefault }}
|
||||
children={content}
|
||||
/>
|
||||
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} />
|
||||
)
|
||||
}
|
||||
}, [theme])
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
children={children()}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: background
|
||||
? colors.backgroundOverlayDefault
|
||||
: undefined,
|
||||
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
marginLeft: native
|
||||
? -StyleConstants.Spacing.S
|
||||
: StyleConstants.Spacing.S,
|
||||
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||
...(type === 'icon' && {
|
||||
borderRadius: 100
|
||||
}),
|
||||
|
|
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { AccessibilityProps, Pressable, View } from 'react-native'
|
||||
import { Flow } from 'react-native-animated-spinkit'
|
||||
|
||||
|
@ -18,6 +18,7 @@ export interface Props {
|
|||
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
destructive?: boolean
|
||||
|
||||
onPress: () => void
|
||||
}
|
||||
|
@ -34,23 +35,19 @@ const HeaderRight: React.FC<Props> = ({
|
|||
background = false,
|
||||
loading,
|
||||
disabled,
|
||||
destructive = false,
|
||||
onPress
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
const loadingSpinkit = () =>
|
||||
loading ? (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
<Flow
|
||||
size={StyleConstants.Font.Size.M * 1.25}
|
||||
color={colors.secondary}
|
||||
/>
|
||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||
</View>
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
) : null
|
||||
|
||||
const children = useMemo(() => {
|
||||
const children = () => {
|
||||
switch (type) {
|
||||
case 'icon':
|
||||
return (
|
||||
|
@ -59,9 +56,9 @@ const HeaderRight: React.FC<Props> = ({
|
|||
name={content}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
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':
|
||||
|
@ -69,17 +66,22 @@ const HeaderRight: React.FC<Props> = ({
|
|||
<>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
fontWeight={destructive ? 'Bold' : 'Normal'}
|
||||
style={{
|
||||
color: disabled ? colors.secondary : colors.primaryDefault,
|
||||
color: disabled
|
||||
? colors.secondary
|
||||
: destructive
|
||||
? colors.red
|
||||
: colors.primaryDefault,
|
||||
opacity: loading ? 0 : 1
|
||||
}}
|
||||
children={content}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
{loadingSpinkit()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [theme, loading, disabled])
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
@ -88,20 +90,16 @@ const HeaderRight: React.FC<Props> = ({
|
|||
accessibilityRole='button'
|
||||
accessibilityState={accessibilityState}
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
children={children()}
|
||||
disabled={disabled || loading}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: background
|
||||
? colors.backgroundOverlayDefault
|
||||
: undefined,
|
||||
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
marginRight: native
|
||||
? -StyleConstants.Spacing.S
|
||||
: StyleConstants.Spacing.S,
|
||||
marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||
...(type === 'icon' && {
|
||||
borderRadius: 100
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import HeaderLeft from '@components/Header/Left'
|
||||
import HeaderCenter from '@components/Header/Center'
|
||||
import HeaderLeft from '@components/Header/Left'
|
||||
import HeaderRight from '@components/Header/Right'
|
||||
|
||||
export { HeaderLeft, HeaderCenter, HeaderRight }
|
|
@ -3,7 +3,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import React, { forwardRef, RefObject } from 'react'
|
||||
import { Platform, TextInput, TextInputProps, View } from 'react-native'
|
||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||
import { EmojisState } from './Emojis/helpers/EmojisContext'
|
||||
import { EmojisState } from './Emojis/Context'
|
||||
import CustomText from './Text'
|
||||
|
||||
export type Props = {
|
||||
|
@ -85,7 +85,6 @@ const ComponentInput = forwardRef(
|
|||
multiline,
|
||||
numberOfLines: Platform.OS === 'android' ? 5 : undefined
|
||||
})}
|
||||
keyboardAppearance={mode}
|
||||
textAlignVertical='top'
|
||||
{...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 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 { 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 { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Placeholder } from 'rn-placeholder'
|
||||
import validUrl from 'valid-url'
|
||||
import InstanceInfo from './Info'
|
||||
import parse from 'url-parse'
|
||||
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 {
|
||||
scrollViewRef?: RefObject<ScrollView>
|
||||
|
@ -35,7 +39,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
disableHeaderImage,
|
||||
goBack = false
|
||||
}) => {
|
||||
const { t } = useTranslation('componentInstance')
|
||||
const { t } = useTranslation(['common', 'componentInstance'])
|
||||
const { colors, mode } = useTheme()
|
||||
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||
|
||||
|
@ -44,11 +48,9 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
const whitelisted: boolean =
|
||||
!!domain.length &&
|
||||
!!errorCode &&
|
||||
!!validUrl.isHttpsUri(`https://${domain}`) &&
|
||||
!!(parse(`https://${domain}/`).hostname === domain) &&
|
||||
errorCode === 401
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const instanceQuery = useInstanceQuery({
|
||||
domain,
|
||||
options: {
|
||||
|
@ -62,8 +64,6 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
}
|
||||
})
|
||||
|
||||
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
||||
|
||||
const appsMutation = useAppsMutation({
|
||||
retry: false,
|
||||
onSuccess: async (data, variables) => {
|
||||
|
@ -75,56 +75,145 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
const request = new AuthSession.AuthRequest({
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes: deprecateAuthFollow
|
||||
? ['read', 'write', 'push']
|
||||
: ['read', 'write', 'follow', 'push'],
|
||||
scopes: variables.scopes,
|
||||
redirectUri
|
||||
})
|
||||
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(
|
||||
{
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes: ['read', 'write', 'follow', 'push'],
|
||||
scopes: variables.scopes,
|
||||
redirectUri,
|
||||
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` }
|
||||
)
|
||||
queryClient.clear()
|
||||
dispatch(
|
||||
addInstance({
|
||||
domain,
|
||||
token: accessToken,
|
||||
instance: instanceQuery.data!,
|
||||
appData: { clientId, clientSecret }
|
||||
})
|
||||
|
||||
const {
|
||||
body: { id, acct, avatar_static }
|
||||
} = await apiGeneral<Mastodon.Account>({
|
||||
method: 'get',
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const scopes = featureCheck('deprecate_auth_follow')
|
||||
? ['read', 'write', 'push']
|
||||
: ['read', 'write', 'follow', 'push']
|
||||
const processUpdate = useCallback(() => {
|
||||
if (domain) {
|
||||
if (instances && instances.filter(instance => instance.url === domain).length) {
|
||||
Alert.alert(t('update.alert.title'), t('update.alert.message'), [
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'cancel'
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.continue'),
|
||||
onPress: () => appsMutation.mutate({ domain })
|
||||
}
|
||||
])
|
||||
const accounts = getGlobalStorage.object('accounts')
|
||||
if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) {
|
||||
Alert.alert(
|
||||
t('componentInstance:update.alert.title'),
|
||||
t('componentInstance:update.alert.message'),
|
||||
[
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'cancel'
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.continue'),
|
||||
onPress: () => appsMutation.mutate({ domain, scopes })
|
||||
}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
appsMutation.mutate({ domain })
|
||||
appsMutation.mutate({ domain, scopes })
|
||||
}
|
||||
}
|
||||
}, [domain])
|
||||
|
@ -204,12 +293,13 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
text === domain &&
|
||||
instanceQuery.isSuccess &&
|
||||
instanceQuery.data &&
|
||||
instanceQuery.data.uri
|
||||
// @ts-ignore
|
||||
(instanceQuery.data.domain || instanceQuery.data.uri)
|
||||
) {
|
||||
processUpdate()
|
||||
}
|
||||
}}
|
||||
placeholder={' ' + t('server.textInput.placeholder')}
|
||||
placeholder={' ' + t('componentInstance:server.textInput.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
returnKeyType='go'
|
||||
keyboardAppearance={mode}
|
||||
|
@ -222,9 +312,10 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
/>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('server.button')}
|
||||
content={t('componentInstance:server.button')}
|
||||
onPress={processUpdate}
|
||||
disabled={!instanceQuery.data?.uri && !whitelisted}
|
||||
// @ts-ignore
|
||||
disabled={!(instanceQuery.data?.domain || instanceQuery.data?.uri) && !whitelisted}
|
||||
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
||||
/>
|
||||
</View>
|
||||
|
@ -239,37 +330,9 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
paddingTop: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
{t('server.whitelisted')}
|
||||
{t('componentInstance:server.whitelisted')}
|
||||
</CustomText>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
|
@ -287,7 +350,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
}}
|
||||
/>
|
||||
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
|
||||
{t('server.disclaimer.base')}
|
||||
{t('componentInstance:server.disclaimer.base')}
|
||||
</CustomText>
|
||||
</View>
|
||||
<View
|
||||
|
@ -312,7 +375,8 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
accessibilityRole='link'
|
||||
>
|
||||
<Trans
|
||||
i18nKey='componentInstance:server.terms.base'
|
||||
ns='componentInstance'
|
||||
i18nKey='server.terms.base'
|
||||
components={[
|
||||
<CustomText
|
||||
accessible
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import CustomText from '@components/Text'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
heading: string
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { ColorDefinitions } from '@utils/styles/themes'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { Flow } from 'react-native-animated-spinkit'
|
||||
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
||||
|
@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({
|
|||
const { colors, theme } = useTheme()
|
||||
const { screenReaderEnabled } = useAccessibility()
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||
</View>
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ minHeight: 50 }}
|
||||
|
@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({
|
|||
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
{loading ? (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import React, { RefObject } from 'react'
|
||||
import { AccessibilityInfo } from 'react-native'
|
||||
import FlashMessage, { MessageType, showMessage } from 'react-native-flash-message'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import haptics from './haptics'
|
||||
|
||||
const displayMessage = ({
|
||||
|
@ -53,7 +52,6 @@ const displayMessage = ({
|
|||
|
||||
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
enum iconMapping {
|
||||
success = 'CheckCircle',
|
||||
|
@ -96,8 +94,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
|||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
|
||||
shadowRadius: 4,
|
||||
paddingRight: StyleConstants.Spacing.M * 2,
|
||||
marginTop: ref ? undefined : insets.top
|
||||
paddingRight: StyleConstants.Spacing.M * 2
|
||||
}}
|
||||
titleStyle={{
|
||||
color: colors.primaryDefault,
|
||||
|
@ -109,7 +106,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
|||
...StyleConstants.FontStyle.S
|
||||
}}
|
||||
// @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 { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
||||
import { useGlobalStorage } from '@utils/storage/actions'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { adaptiveScale } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Platform, TextStyle } from 'react-native'
|
||||
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_]+:)/)
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
content?: string
|
||||
emojis?: Mastodon.Emoji[]
|
||||
size?: 'S' | 'M' | 'L'
|
||||
adaptiveSize?: boolean
|
||||
|
@ -21,79 +19,81 @@ export interface Props {
|
|||
style?: TextStyle
|
||||
}
|
||||
|
||||
const ParseEmojis = React.memo(
|
||||
({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
const ParseEmojis: React.FC<Props> = ({
|
||||
content,
|
||||
emojis,
|
||||
size = 'M',
|
||||
adaptiveSize = false,
|
||||
fontBold = false,
|
||||
style
|
||||
}) => {
|
||||
if (!content) return null
|
||||
|
||||
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 { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
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 (
|
||||
<CustomText
|
||||
style={[
|
||||
{
|
||||
color: colors.primaryDefault,
|
||||
fontSize: adaptedFontsize,
|
||||
lineHeight: adaptedLineheight
|
||||
},
|
||||
style
|
||||
]}
|
||||
fontWeight={fontBold ? 'Bold' : undefined}
|
||||
>
|
||||
{emojis ? (
|
||||
content
|
||||
.split(regexEmoji)
|
||||
.filter(f => f)
|
||||
.map((str, i) => {
|
||||
if (str.match(regexEmoji)) {
|
||||
const emojiShortcode = str.split(regexEmoji)[1]
|
||||
const emojiIndex = emojis.findIndex(emoji => {
|
||||
return emojiShortcode === `:${emoji.shortcode}:`
|
||||
})
|
||||
if (emojiIndex === -1) {
|
||||
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
|
||||
} else {
|
||||
const uri = reduceMotionEnabled
|
||||
? 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
|
||||
}
|
||||
}
|
||||
const { colors, theme } = useTheme()
|
||||
|
||||
return (
|
||||
<CustomText
|
||||
style={[
|
||||
{
|
||||
color: colors.primaryDefault,
|
||||
fontSize: adaptedFontsize,
|
||||
lineHeight: adaptedLineheight
|
||||
},
|
||||
style
|
||||
]}
|
||||
fontWeight={fontBold ? 'Bold' : undefined}
|
||||
>
|
||||
{emojis ? (
|
||||
content
|
||||
.split(regexEmoji)
|
||||
.filter(f => f)
|
||||
.map((str, i) => {
|
||||
if (str.match(regexEmoji)) {
|
||||
const emojiShortcode = str.split(regexEmoji)[1]
|
||||
const emojiIndex = emojis.findIndex(emoji => {
|
||||
return emojiShortcode === `:${emoji.shortcode}:`
|
||||
})
|
||||
if (emojiIndex === -1) {
|
||||
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
|
||||
} 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>
|
||||
)
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<CustomText>{content}</CustomText>
|
||||
)}
|
||||
</CustomText>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.content === next.content
|
||||
)
|
||||
} else {
|
||||
return <CustomText key={i}>{str}</CustomText>
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<CustomText>{content}</CustomText>
|
||||
)}
|
||||
</CustomText>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParseEmojis
|
||||
|
|
|
@ -1,315 +1,302 @@
|
|||
import Icon from '@components/Icon'
|
||||
import openLink from '@components/openLink'
|
||||
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 { StackNavigationProp } from '@react-navigation/stack'
|
||||
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 layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { adaptiveScale } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { isEqual } from 'lodash'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { ChildNode } from 'domhandler'
|
||||
import { ElementType, parseDocument } from 'htmlparser2'
|
||||
import i18next from 'i18next'
|
||||
import React, { useContext, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Pressable, TextStyleIOS, 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
|
||||
}
|
||||
}
|
||||
import { Platform, Pressable, Text, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
size?: 'S' | 'M' | 'L'
|
||||
textStyles?: TextStyleIOS
|
||||
adaptiveSize?: boolean
|
||||
emojis?: Mastodon.Emoji[]
|
||||
mentions?: Mastodon.Mention[]
|
||||
tags?: Mastodon.Tag[]
|
||||
showFullLink?: boolean
|
||||
numberOfLines?: number
|
||||
expandHint?: string
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
selectable?: boolean
|
||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
emojis?: Mastodon.Emoji[]
|
||||
mentions?: Mastodon.Mention[]
|
||||
}
|
||||
|
||||
const ParseHTML = React.memo(
|
||||
({
|
||||
content,
|
||||
size = 'M',
|
||||
textStyles,
|
||||
adaptiveSize = false,
|
||||
emojis,
|
||||
mentions,
|
||||
tags,
|
||||
showFullLink = false,
|
||||
numberOfLines = 10,
|
||||
expandHint,
|
||||
highlighted = false,
|
||||
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 ParseHTML: React.FC<Props> = ({
|
||||
content,
|
||||
size = 'M',
|
||||
adaptiveSize = false,
|
||||
showFullLink = false,
|
||||
numberOfLines = 10,
|
||||
expandHint,
|
||||
selectable = false,
|
||||
setSpoilerExpanded,
|
||||
emojis,
|
||||
mentions
|
||||
}) => {
|
||||
const { status, highlighted, disableDetails, excludeMentions } = useContext(StatusContext)
|
||||
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const route = useRoute()
|
||||
const { colors, theme } = useTheme()
|
||||
const { t } = useTranslation('componentParse')
|
||||
if (!expandHint) {
|
||||
expandHint = t('HTML.defaultHint')
|
||||
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||
const adaptedFontsize = adaptiveScale(
|
||||
StyleConstants.Font.Size[size],
|
||||
adaptiveSize ? adaptiveFontsize : 0
|
||||
)
|
||||
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 (
|
||||
<ParseEmojis
|
||||
content={children?.toString()}
|
||||
emojis={emojis}
|
||||
key={index}
|
||||
content={content}
|
||||
emojis={status?.emojis || emojis}
|
||||
size={size}
|
||||
adaptiveSize={adaptiveSize}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
const rootComponent = useCallback(
|
||||
({ children }: any) => {
|
||||
const { t } = useTranslation('componentParse')
|
||||
case ElementType.Tag:
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
const classes = node.attribs.class
|
||||
const href = node.attribs.href
|
||||
if (classes) {
|
||||
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 [expanded, setExpanded] = useState(highlighted)
|
||||
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
|
||||
?.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 (
|
||||
<View style={{ overflow: 'hidden' }}>
|
||||
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
||||
<Pressable
|
||||
accessibilityLabel={t('HTML.accessibilityHint')}
|
||||
onPress={() => {
|
||||
layoutAnimation()
|
||||
setExpanded(!expanded)
|
||||
if (setSpoilerExpanded) {
|
||||
setSpoilerExpanded(!expanded)
|
||||
const content = node.children.map(child => unwrapNode(child)).join('')
|
||||
const shouldBeTag = status?.tags?.find(tag => `#${tag.name}` === content)
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
style={{ color: colors.blue }}
|
||||
onPress={async () => {
|
||||
if (!disableDetails) {
|
||||
if (shouldBeTag) {
|
||||
navigation.push('Tab-Shared-Hashtag', {
|
||||
hashtag: content.substring(1)
|
||||
})
|
||||
} else {
|
||||
await openLink(href, navigation)
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 44,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
children={content}
|
||||
/>
|
||||
)
|
||||
break
|
||||
case 'br':
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
|
||||
>
|
||||
<CustomText
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
...StyleConstants.FontStyle.S,
|
||||
color: colors.primaryDefault,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
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}
|
||||
<CustomText
|
||||
children={children}
|
||||
onTextLayout={({ nativeEvent }) => {
|
||||
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
|
||||
setTotalLines(nativeEvent.lines.length)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...textStyles,
|
||||
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
||||
}}
|
||||
numberOfLines={
|
||||
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
|
||||
}
|
||||
selectable={selectable}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
[theme]
|
||||
)
|
||||
|
||||
return (
|
||||
<HTMLView
|
||||
value={content}
|
||||
TextComponent={textComponent}
|
||||
RootComponent={rootComponent}
|
||||
renderNode={renderNodeCallback}
|
||||
{'\n'}
|
||||
</Text>
|
||||
)
|
||||
case 'p':
|
||||
if (index < document.children.length - 1) {
|
||||
return (
|
||||
<Text key={index}>
|
||||
{node.children.map((c, i) => renderNode(c, i))}
|
||||
<Text
|
||||
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
|
||||
>
|
||||
{'\n\n'}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||
}
|
||||
default:
|
||||
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<View style={{ overflow: 'hidden' }}>
|
||||
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
||||
<Pressable
|
||||
accessibilityLabel={t('HTML.accessibilityHint')}
|
||||
onPress={() => {
|
||||
layoutAnimation()
|
||||
setExpanded(!expanded)
|
||||
if (setSpoilerExpanded) {
|
||||
setSpoilerExpanded(!expanded)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 44,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
...StyleConstants.FontStyle.S,
|
||||
color: colors.primaryDefault,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
|
||||
)
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
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 haptics from '@components/haptics'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
@ -8,7 +9,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
export interface Props {
|
||||
id: Mastodon.Account['id']
|
||||
|
@ -16,7 +16,7 @@ export interface Props {
|
|||
|
||||
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation(['common', 'componentRelationship'])
|
||||
|
||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
||||
const queryKeyNotification: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||
|
@ -33,7 +33,7 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
|||
type: 'error',
|
||||
theme,
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`relationship:${type}.function`)
|
||||
function: t(`componentRelationship:${type}.function` as any)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||
import {
|
||||
QueryKeyRelationship,
|
||||
useRelationshipMutation,
|
||||
useRelationshipQuery
|
||||
} from '@utils/queryHooks/relationship'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
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 { useRoute } from '@react-navigation/native'
|
||||
|
||||
export interface Props {
|
||||
id: Mastodon.Account['id']
|
||||
|
@ -22,9 +21,9 @@ export interface Props {
|
|||
|
||||
const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||
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 })
|
||||
|
||||
|
@ -44,7 +43,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||
theme,
|
||||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`${action}.function`)
|
||||
function: t(`componentRelationship:${action}.function` as any)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -61,15 +60,15 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||
let onPress: () => void
|
||||
|
||||
if (query.isError) {
|
||||
content = t('button.error')
|
||||
content = t('componentRelationship:button.error')
|
||||
onPress = () => {}
|
||||
} else {
|
||||
if (query.data?.blocked_by) {
|
||||
content = t('button.blocked_by')
|
||||
content = t('componentRelationship:button.blocked_by')
|
||||
onPress = () => {}
|
||||
} else {
|
||||
if (query.data?.blocking) {
|
||||
content = t('button.blocking')
|
||||
content = t('componentRelationship:button.blocking')
|
||||
onPress = () => {
|
||||
mutation.mutate({
|
||||
id,
|
||||
|
@ -82,7 +81,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||
}
|
||||
} else {
|
||||
if (query.data?.following) {
|
||||
content = t('button.following')
|
||||
content = t('componentRelationship:button.following')
|
||||
onPress = () => {
|
||||
mutation.mutate({
|
||||
id,
|
||||
|
@ -95,7 +94,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||
}
|
||||
} else {
|
||||
if (query.data?.requested) {
|
||||
content = t('button.requested')
|
||||
content = t('componentRelationship:button.requested')
|
||||
onPress = () => {
|
||||
mutation.mutate({
|
||||
id,
|
||||
|
@ -107,7 +106,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||
})
|
||||
}
|
||||
} else {
|
||||
content = t('button.default')
|
||||
content = t('componentRelationship:button.default')
|
||||
onPress = () => {
|
||||
mutation.mutate({
|
||||
id,
|
||||
|
|
|
@ -11,9 +11,15 @@ export interface Props {
|
|||
multiple?: boolean
|
||||
options: { 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 isSelected = (index: number): string =>
|
||||
|
@ -22,10 +28,11 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
|
|||
: `${multiple ? 'Square' : 'Circle'}`
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
{options.map((option, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
|
||||
onPress={() => {
|
||||
if (multiple) {
|
||||
|
@ -56,15 +63,18 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
|
|||
}}
|
||||
name={isSelected(index)}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.primaryDefault}
|
||||
color={disabled ? colors.disabled : colors.primaryDefault}
|
||||
/>
|
||||
<CustomText style={{ flex: 1 }}>
|
||||
<ParseEmojis content={option.content} />
|
||||
<ParseEmojis
|
||||
content={option.content}
|
||||
style={{ color: disabled ? colors.disabled : colors.primaryDefault }}
|
||||
/>
|
||||
</CustomText>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
||||
import { StyleProp, View, ViewStyle } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
extraMarginLeft?: number
|
||||
|
@ -23,7 +23,7 @@ const ComponentSeparator: React.FC<Props> = ({
|
|||
{
|
||||
backgroundColor: colors.backgroundDefault,
|
||||
borderTopColor: colors.border,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopWidth: 1,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
||||
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import apiInstance from '@api/instance'
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
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 { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import TimelineActions from './Shared/Actions'
|
||||
import TimelineContent from './Shared/Content'
|
||||
import StatusContext from './Shared/Context'
|
||||
|
@ -41,10 +41,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
|||
const onPress = useCallback(() => {
|
||||
if (conversation.last_status) {
|
||||
conversation.unread && mutate()
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: conversation.last_status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
navigation.push('Tab-Shared-Toot', { toot: conversation.last_status })
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -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 TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
|
||||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||
import removeHTML from '@helpers/removeHTML'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
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 { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||
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 { 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 { useSelector } from 'react-redux'
|
||||
import * as ContextMenu from 'zeego/context-menu'
|
||||
import StatusContext from './Shared/Context'
|
||||
import TimelineFeedback from './Shared/Feedback'
|
||||
|
@ -31,7 +32,6 @@ import TimelineTranslate from './Shared/Translate'
|
|||
export interface Props {
|
||||
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
disableOnPress?: boolean
|
||||
|
@ -42,7 +42,6 @@ export interface Props {
|
|||
const TimelineDefault: React.FC<Props> = ({
|
||||
item,
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
highlighted = false,
|
||||
disableDetails = false,
|
||||
disableOnPress = false,
|
||||
|
@ -50,7 +49,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
}) => {
|
||||
const status = item.reblog ? item.reblog : item
|
||||
const rawContent = useRef<string[]>([])
|
||||
if (highlighted) {
|
||||
if (highlighted || isConversation) {
|
||||
rawContent.current = [
|
||||
removeHTML(status.content),
|
||||
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
|
||||
|
@ -60,16 +59,18 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
const { colors } = useTheme()
|
||||
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(
|
||||
instanceAccount?.preferences?.['reading:expand:spoilers'] || false
|
||||
preferences?.['reading:expand:spoilers'] || false
|
||||
)
|
||||
const spoilerHidden = status.spoiler_text?.length
|
||||
? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||
: false
|
||||
const detectedLanguage = useRef<string>(status.language || '')
|
||||
const excludeMentions = useRef<Mastodon.Mention[]>([])
|
||||
|
||||
const mainStyle: StyleProp<ViewStyle> = {
|
||||
flex: 1,
|
||||
|
@ -82,9 +83,9 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
const main = () => (
|
||||
<>
|
||||
{item.reblog ? (
|
||||
<TimelineActioned action='reblog' />
|
||||
<TimelineActioned action='reblog' rootStatus={item} />
|
||||
) : item._pinned ? (
|
||||
<TimelineActioned action='pinned' />
|
||||
<TimelineActioned action='pinned' rootStatus={item} />
|
||||
) : null}
|
||||
|
||||
<View
|
||||
|
@ -128,13 +129,13 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
url: status.url || status.uri,
|
||||
rawContent
|
||||
})
|
||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
||||
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
const mInstance = menuInstance({ status, queryKey })
|
||||
|
||||
if (!ownAccount) {
|
||||
let filterResults: FilteredProps['filterResults'] = []
|
||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
||||
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||
if (hasFilterServerSide) {
|
||||
if (status.filtered?.length) {
|
||||
filterResults = status.filtered?.map(filter => filter.filter)
|
||||
|
@ -160,18 +161,18 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
<StatusContext.Provider
|
||||
value={{
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
reblogStatus: item.reblog ? item : undefined,
|
||||
ownAccount,
|
||||
spoilerHidden,
|
||||
rawContent,
|
||||
detectedLanguage,
|
||||
excludeMentions,
|
||||
highlighted,
|
||||
inThread: queryKey?.[1].page === 'Toot',
|
||||
disableDetails,
|
||||
disableOnPress,
|
||||
isConversation
|
||||
isConversation,
|
||||
isRemote: item._remote
|
||||
}}
|
||||
>
|
||||
{disableOnPress ? (
|
||||
|
@ -184,49 +185,58 @@ const TimelineDefault: React.FC<Props> = ({
|
|||
accessible={highlighted ? false : true}
|
||||
style={mainStyle}
|
||||
disabled={highlighted}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}
|
||||
onPress={() => navigation.push('Tab-Shared-Toot', { toot: status })}
|
||||
onLongPress={() => {}}
|
||||
children={main()}
|
||||
/>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
{[mShare, mStatus, mInstance].map((menu, i) => (
|
||||
<Fragment key={i}>
|
||||
{menu.map((group, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{group.map(item => {
|
||||
switch (item.type) {
|
||||
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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
</Fragment>
|
||||
))}
|
||||
</ContextMenu.Content>
|
||||
</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
|
||||
}
|
||||
|
||||
const TimelineEmpty = React.memo(
|
||||
({ queryKey }: Props) => {
|
||||
const { status, refetch } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: { notifyOnChangeProps: ['status'] }
|
||||
})
|
||||
const TimelineEmpty: React.FC<Props> = ({ queryKey }) => {
|
||||
const { status, refetch } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: { notifyOnChangeProps: ['status'] }
|
||||
})
|
||||
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const children = () => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||
case 'error':
|
||||
return (
|
||||
<>
|
||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('empty.error.message')}
|
||||
</CustomText>
|
||||
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Smartphone'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={colors.primaryDefault}
|
||||
/>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
color: colors.secondary
|
||||
}}
|
||||
>
|
||||
{t('empty.success.message')}
|
||||
</CustomText>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const children = () => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||
case 'error':
|
||||
return (
|
||||
<>
|
||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('empty.error.message')}
|
||||
</CustomText>
|
||||
<Button type='text' content={t('empty.error.button')} onPress={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Smartphone'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={colors.primaryDefault}
|
||||
/>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
color: colors.secondary
|
||||
}}
|
||||
>
|
||||
{t('empty.success.message')}
|
||||
</CustomText>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
{children()}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
{children()}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineEmpty
|
||||
|
|
|
@ -13,49 +13,38 @@ export interface Props {
|
|||
disableInfinity: boolean
|
||||
}
|
||||
|
||||
const TimelineFooter = React.memo(
|
||||
({ queryKey, disableInfinity }: Props) => {
|
||||
const { hasNextPage } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
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 TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
|
||||
const { hasNextPage } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: { enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'] }
|
||||
})
|
||||
|
||||
const { colors } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{!disableInfinity && hasNextPage ? (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||
) : (
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
<Trans
|
||||
i18nKey='componentTimeline:end.message'
|
||||
components={[
|
||||
<Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
|
||||
]}
|
||||
/>
|
||||
</CustomText>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{!disableInfinity && hasNextPage ? (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||
) : (
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
<Trans
|
||||
ns='componentTimeline'
|
||||
i18nKey='end.message'
|
||||
components={[
|
||||
<Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
|
||||
]}
|
||||
/>
|
||||
</CustomText>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineFooter
|
||||
|
|
|
@ -11,14 +11,15 @@ import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotifi
|
|||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||
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 { 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 { useSelector } from 'react-redux'
|
||||
import * as ContextMenu from 'zeego/context-menu'
|
||||
import StatusContext from './Shared/Context'
|
||||
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
|
||||
|
@ -31,7 +32,8 @@ export interface Props {
|
|||
}
|
||||
|
||||
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 account =
|
||||
|
@ -40,25 +42,17 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||
: notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
const ownAccount = notification.account?.id === instanceAccount?.id
|
||||
const ownAccount = notification.account?.id === accountId
|
||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||
instanceAccount.preferences?.['reading:expand:spoilers'] || false
|
||||
preferences?.['reading:expand:spoilers'] || false
|
||||
)
|
||||
const spoilerHidden = notification.status?.spoiler_text?.length
|
||||
? !instanceAccount.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||
: false
|
||||
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
notification.status &&
|
||||
navigation.push('Tab-Shared-Toot', {
|
||||
toot: notification.status,
|
||||
rootQueryKey: queryKey
|
||||
})
|
||||
}, [])
|
||||
|
||||
const main = () => {
|
||||
return (
|
||||
<>
|
||||
|
@ -67,6 +61,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||
action={notification.type}
|
||||
isNotification
|
||||
account={notification.account}
|
||||
rootStatus={notification.status}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -117,7 +112,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||
if (!ownAccount) {
|
||||
let filterResults: FilteredProps['filterResults'] = []
|
||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
||||
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||
if (notification.status) {
|
||||
if (hasFilterServerSide) {
|
||||
if (notification.status.filtered?.length) {
|
||||
|
@ -131,7 +126,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||
}
|
||||
|
||||
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)}>
|
||||
<TimelineFiltered filterResults={filterResults} />
|
||||
</Pressable>
|
||||
|
@ -157,44 +152,56 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||
backgroundColor: colors.backgroundDefault,
|
||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
onPress={onPress}
|
||||
onPress={() =>
|
||||
notification.status &&
|
||||
navigation.push('Tab-Shared-Toot', { toot: notification.status })
|
||||
}
|
||||
onLongPress={() => {}}
|
||||
children={main()}
|
||||
/>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<ContextMenu.Item key={menu.key} {...menu.item}>
|
||||
<ContextMenu.ItemTitle children={menu.title} />
|
||||
<ContextMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</ContextMenu.Item>
|
||||
{[mShare, mStatus, mInstance].map((menu, i) => (
|
||||
<Fragment key={i}>
|
||||
{menu.map((group, index) => (
|
||||
<ContextMenu.Group key={index}>
|
||||
{group.map(item => {
|
||||
switch (item.type) {
|
||||
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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
</Fragment>
|
||||
))}
|
||||
</ContextMenu.Content>
|
||||
</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 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 { 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 { FlatList, LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import { FlatList, Platform, Text, View } from 'react-native'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated'
|
||||
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
export interface Props {
|
||||
flRef: RefObject<FlatList<any>>
|
||||
queryKey: QueryKeyTimeline
|
||||
fetchingActive: React.MutableRefObject<boolean>
|
||||
scrollY: Animated.SharedValue<number>
|
||||
fetchingType: Animated.SharedValue<0 | 1 | 2>
|
||||
disableRefresh?: boolean
|
||||
readMarker?: 'read_marker_following'
|
||||
}
|
||||
|
||||
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> = ({
|
||||
flRef,
|
||||
queryKey,
|
||||
fetchingActive,
|
||||
scrollY,
|
||||
fetchingType,
|
||||
disableRefresh = false
|
||||
disableRefresh = false,
|
||||
readMarker
|
||||
}) => {
|
||||
if (Platform.OS !== 'ios') {
|
||||
return null
|
||||
|
@ -44,86 +54,25 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
return null
|
||||
}
|
||||
|
||||
const fetchingLatestIndex = useRef(0)
|
||||
const refetchActive = useRef(false)
|
||||
const PREV_PER_BATCH = 1
|
||||
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 } =
|
||||
useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage?.links?.prev && {
|
||||
...(firstPage.links.prev.isOffset
|
||||
? { offset: firstPage.links.prev.id }
|
||||
: { min_id: firstPage.links.prev.id }),
|
||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
||||
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 queryClient = useQueryClient()
|
||||
const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] })
|
||||
|
||||
useDerivedValue(() => {
|
||||
if (prevActive.current || isRefetching) {
|
||||
fetchingActive.current = true
|
||||
} else {
|
||||
fetchingActive.current = false
|
||||
}
|
||||
}, [prevActive.current, isRefetching])
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
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 arrowY = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
|
@ -145,17 +94,9 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
}))
|
||||
|
||||
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(
|
||||
() => {
|
||||
if (isFetching) {
|
||||
if (fetchingActive.current) {
|
||||
return false
|
||||
}
|
||||
switch (arrowStage.value) {
|
||||
|
@ -188,10 +129,96 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
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(
|
||||
|
@ -202,95 +229,76 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
fetchingType.value = 0
|
||||
switch (data) {
|
||||
case 1:
|
||||
runOnJS(wrapperStartLatest)()
|
||||
runOnJS(clearFirstPage)()
|
||||
runOnJS(fetchPreviousPage)()
|
||||
break
|
||||
runOnJS(runFetchPrevious)()
|
||||
return
|
||||
case 2:
|
||||
runOnJS(prepareRefetch)()
|
||||
runOnJS(callRefetch)()
|
||||
break
|
||||
runOnJS(runFetchLatest)()
|
||||
return
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const headerPadding = useAnimatedStyle(
|
||||
() => ({
|
||||
paddingTop:
|
||||
fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage)
|
||||
? withTiming(StyleConstants.Spacing.M * 2.5)
|
||||
: withTiming(0)
|
||||
}),
|
||||
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
|
||||
)
|
||||
|
||||
return (
|
||||
<Animated.View style={headerPadding}>
|
||||
<View style={styles.base}>
|
||||
{isFetching ? (
|
||||
<View style={styles.container2}>
|
||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.container1}>
|
||||
<Text
|
||||
style={[styles.explanation, { color: colors.primaryDefault }]}
|
||||
onLayout={onLayout}
|
||||
children={t('refresh.fetchPreviousPage')}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
left: textRight + StyleConstants.Spacing.S
|
||||
},
|
||||
arrowY,
|
||||
arrowTop
|
||||
]}
|
||||
children={
|
||||
<Icon
|
||||
name='ArrowLeft'
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.primaryDefault}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container2}>
|
||||
<Text
|
||||
style={[styles.explanation, { color: colors.primaryDefault }]}
|
||||
onLayout={onLayout}
|
||||
children={t('refresh.refetch')}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: CONTAINER_HEIGHT * 2,
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
|
||||
<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.fetchPreviousPage')}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
left: textRight + StyleConstants.Spacing.S
|
||||
},
|
||||
arrowY,
|
||||
arrowTop
|
||||
]}
|
||||
children={
|
||||
<Icon
|
||||
name='ArrowLeft'
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.primaryDefault}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
</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
|
||||
|
|
|
@ -14,11 +14,12 @@ export interface Props {
|
|||
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
|
||||
isNotification?: boolean
|
||||
account?: Mastodon.Account // For notification
|
||||
rootStatus?: Mastodon.Status
|
||||
}
|
||||
|
||||
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||
const { status, reblogStatus } = useContext(StatusContext)
|
||||
const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account)
|
||||
const { status } = useContext(StatusContext)
|
||||
const account = rest.account || (rest.rootStatus || status)?.account
|
||||
if (!account) return null
|
||||
|
||||
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 = () => {
|
||||
switch (action) {
|
||||
|
|
|
@ -2,34 +2,33 @@ import Icon from '@components/Icon'
|
|||
import { displayMessage } from '@components/Message'
|
||||
import CustomText from '@components/Text'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
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 {
|
||||
MutationVarsTimelineUpdateStatusProperty,
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { useAccountStorage } from '@utils/storage/actions'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { uniqBy } from 'lodash'
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import StatusContext from './Context'
|
||||
|
||||
const TimelineActions: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, reblogStatus, ownAccount, highlighted, disableDetails } =
|
||||
useContext(StatusContext)
|
||||
const { queryKey, status, ownAccount, highlighted, disableDetails } = useContext(StatusContext)
|
||||
if (!queryKey || !status || disableDetails) return null
|
||||
|
||||
const navigationState = useNavState()
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors, theme } = useTheme()
|
||||
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||
const { colors } = useTheme()
|
||||
const iconColor = colors.secondary
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
@ -39,28 +38,29 @@ const TimelineActions: React.FC = () => {
|
|||
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
||||
if (
|
||||
// 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
|
||||
(queryKey[1].page === 'Favourites' && theParams.payload.property === 'favourited')
|
||||
(queryKey[1].page === 'Favourites' && theParams.payload.type === 'favourited')
|
||||
) {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
} else if (theParams.payload.property === 'favourited') {
|
||||
} else if (theParams.payload.type === 'favourited') {
|
||||
// When favourited, update favourited page
|
||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||
queryClient.invalidateQueries(tempQueryKey)
|
||||
} else if (theParams.payload.property === 'bookmarked') {
|
||||
} else if (theParams.payload.type === 'bookmarked') {
|
||||
// When bookmarked, update bookmark page
|
||||
const tempQueryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||
queryClient.invalidateQueries(tempQueryKey)
|
||||
}
|
||||
},
|
||||
onError: (err: any, params, oldData) => {
|
||||
onError: (err: any, params) => {
|
||||
const correctParam = params as MutationVarsTimelineUpdateStatusProperty
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
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 &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -74,30 +74,30 @@ const TimelineActions: React.FC = () => {
|
|||
}
|
||||
})
|
||||
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
const onPressReply = useCallback(() => {
|
||||
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||
const onPressReply = () => {
|
||||
const accts = uniqBy(
|
||||
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||
.concat(status.mentions)
|
||||
.filter(d => d?.id !== instanceAccount?.id),
|
||||
.filter(d => d?.id !== accountId),
|
||||
d => d?.id
|
||||
).map(d => d?.acct)
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'reply',
|
||||
incomingStatus: status,
|
||||
accts,
|
||||
queryKey
|
||||
navigationState
|
||||
})
|
||||
}, [status.replies_count])
|
||||
}
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const onPressReblog = useCallback(() => {
|
||||
const onPressReblog = () => {
|
||||
if (!status.reblogged) {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
title: t('shared.actions.reblogged.options.title'),
|
||||
title: t('componentTimeline:shared.actions.reblogged.options.title'),
|
||||
options: [
|
||||
t('shared.actions.reblogged.options.public'),
|
||||
t('shared.actions.reblogged.options.unlisted'),
|
||||
t('componentTimeline:shared.actions.reblogged.options.public'),
|
||||
t('componentTimeline:shared.actions.reblogged.options.unlisted'),
|
||||
t('common:buttons.cancel')
|
||||
],
|
||||
cancelButtonIndex: 2,
|
||||
|
@ -108,32 +108,22 @@ const TimelineActions: React.FC = () => {
|
|||
case 0:
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
propertyCount: 'reblogs_count',
|
||||
countValue: status.reblogs_count,
|
||||
visibility: 'public'
|
||||
type: 'reblogged',
|
||||
visibility: 'public',
|
||||
to: true
|
||||
}
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
propertyCount: 'reblogs_count',
|
||||
countValue: status.reblogs_count,
|
||||
visibility: 'unlisted'
|
||||
type: 'reblogged',
|
||||
visibility: 'unlisted',
|
||||
to: true
|
||||
}
|
||||
})
|
||||
break
|
||||
|
@ -143,91 +133,72 @@ const TimelineActions: React.FC = () => {
|
|||
} else {
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'reblogged',
|
||||
currentValue: status.reblogged,
|
||||
propertyCount: 'reblogs_count',
|
||||
countValue: status.reblogs_count,
|
||||
visibility: 'public'
|
||||
type: 'reblogged',
|
||||
visibility: 'public',
|
||||
to: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [status.reblogged, status.reblogs_count])
|
||||
const onPressFavourite = useCallback(() => {
|
||||
}
|
||||
const onPressFavourite = () => {
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'favourited',
|
||||
currentValue: status.favourited,
|
||||
propertyCount: 'favourites_count',
|
||||
countValue: status.favourites_count
|
||||
type: 'favourited',
|
||||
to: !status.favourited
|
||||
}
|
||||
})
|
||||
}, [status.favourited, status.favourites_count])
|
||||
const onPressBookmark = useCallback(() => {
|
||||
}
|
||||
const onPressBookmark = () => {
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'bookmarked',
|
||||
currentValue: status.bookmarked,
|
||||
propertyCount: undefined,
|
||||
countValue: undefined
|
||||
type: 'bookmarked',
|
||||
to: !status.bookmarked
|
||||
}
|
||||
})
|
||||
}, [status.bookmarked])
|
||||
}
|
||||
|
||||
const childrenReply = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||
{status.replies_count > 0 ? (
|
||||
<CustomText
|
||||
style={{
|
||||
color: colors.secondary,
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
{status.replies_count}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[status.replies_count]
|
||||
const childrenReply = () => (
|
||||
<>
|
||||
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||
{status.replies_count > 0 ? (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
color: colors.secondary,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
{status.replies_count}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
const childrenReblog = useMemo(() => {
|
||||
const childrenReblog = () => {
|
||||
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
||||
const disabled =
|
||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Repeat'
|
||||
color={
|
||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||
? colors.disabled
|
||||
: color(status.reblogged)
|
||||
}
|
||||
color={disabled ? colors.disabled : color(status.reblogged)}
|
||||
crossOut={disabled}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
{status.reblogs_count > 0 ? (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
color:
|
||||
status.visibility === 'private' && !ownAccount
|
||||
? colors.disabled
|
||||
: color(status.reblogged),
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
|
@ -236,58 +207,56 @@ const TimelineActions: React.FC = () => {
|
|||
) : null}
|
||||
</>
|
||||
)
|
||||
}, [status.reblogged, status.reblogs_count])
|
||||
const childrenFavourite = useMemo(() => {
|
||||
}
|
||||
const childrenFavourite = () => {
|
||||
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
||||
return (
|
||||
<>
|
||||
<Icon name='Heart' color={color(status.favourited)} size={StyleConstants.Font.Size.L} />
|
||||
{status.favourites_count > 0 ? (
|
||||
<CustomText
|
||||
style={{
|
||||
color: color(status.favourited),
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
marginLeft: StyleConstants.Spacing.XS,
|
||||
marginTop: 0
|
||||
}}
|
||||
fontStyle='S'
|
||||
style={{ color: color(status.favourited), marginLeft: StyleConstants.Spacing.XS }}
|
||||
>
|
||||
{status.favourites_count}
|
||||
</CustomText>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}, [status.favourited, status.favourites_count])
|
||||
const childrenBookmark = useMemo(() => {
|
||||
}
|
||||
const childrenBookmark = () => {
|
||||
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
||||
return (
|
||||
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
||||
)
|
||||
}, [status.bookmarked])
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Pressable
|
||||
{...(highlighted
|
||||
? {
|
||||
accessibilityLabel: t('shared.actions.reply.accessibilityLabel'),
|
||||
accessibilityLabel: t('componentTimeline:shared.actions.reply.accessibilityLabel'),
|
||||
accessibilityRole: 'button'
|
||||
}
|
||||
: { accessibilityLabel: '' })}
|
||||
style={styles.action}
|
||||
onPress={onPressReply}
|
||||
children={childrenReply}
|
||||
children={childrenReply()}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
{...(highlighted
|
||||
? {
|
||||
accessibilityLabel: t('shared.actions.reblogged.accessibilityLabel'),
|
||||
accessibilityLabel: t(
|
||||
'componentTimeline:shared.actions.reblogged.accessibilityLabel'
|
||||
),
|
||||
accessibilityRole: 'button'
|
||||
}
|
||||
: { accessibilityLabel: '' })}
|
||||
style={styles.action}
|
||||
onPress={onPressReblog}
|
||||
children={childrenReblog}
|
||||
children={childrenReblog()}
|
||||
disabled={
|
||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||
}
|
||||
|
@ -296,25 +265,29 @@ const TimelineActions: React.FC = () => {
|
|||
<Pressable
|
||||
{...(highlighted
|
||||
? {
|
||||
accessibilityLabel: t('shared.actions.favourited.accessibilityLabel'),
|
||||
accessibilityLabel: t(
|
||||
'componentTimeline:shared.actions.favourited.accessibilityLabel'
|
||||
),
|
||||
accessibilityRole: 'button'
|
||||
}
|
||||
: { accessibilityLabel: '' })}
|
||||
style={styles.action}
|
||||
onPress={onPressFavourite}
|
||||
children={childrenFavourite}
|
||||
children={childrenFavourite()}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
{...(highlighted
|
||||
? {
|
||||
accessibilityLabel: t('shared.actions.bookmarked.accessibilityLabel'),
|
||||
accessibilityLabel: t(
|
||||
'componentTimeline:shared.actions.bookmarked.accessibilityLabel'
|
||||
),
|
||||
accessibilityRole: 'button'
|
||||
}
|
||||
: { accessibilityLabel: '' })}
|
||||
style={styles.action}
|
||||
onPress={onPressBookmark}
|
||||
children={childrenBookmark}
|
||||
children={childrenBookmark()}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
@ -326,8 +299,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
|
||||
marginHorizontal: StyleConstants.Spacing.S
|
||||
paddingVertical: StyleConstants.Spacing.S * 1.5
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -7,13 +7,12 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
|
|||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
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 layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import StatusContext from './Context'
|
||||
|
||||
const TimelineAttachment = () => {
|
||||
|
@ -28,13 +27,10 @@ const TimelineAttachment = () => {
|
|||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const account = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) =>
|
||||
prev.preferences?.['reading:expand:media'] === next.preferences?.['reading:expand:media']
|
||||
)
|
||||
const { data: preferences } = usePreferencesQuery()
|
||||
|
||||
const defaultSensitive = () => {
|
||||
switch (account.preferences?.['reading:expand:media']) {
|
||||
switch (preferences?.['reading:expand:media']) {
|
||||
case 'show_all':
|
||||
return false
|
||||
case 'hide_all':
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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 { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
sensitiveShown: boolean
|
||||
|
@ -14,7 +13,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
return !sensitiveShown ? (
|
||||
<Button
|
||||
|
@ -28,7 +27,7 @@ const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
|
|||
type='text'
|
||||
content='ALT'
|
||||
fontBold
|
||||
onPress={() => navigation.navigate('Screen-Actions', { type: 'alt_text', text })}
|
||||
onPress={() => Alert.alert(t('shared.attachment.altText'), text)}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import Button from '@components/Button'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { useGlobalStorage } from '@utils/storage/actions'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
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 { Pressable, View } from 'react-native'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import AttachmentAltText from './AltText'
|
||||
import { Platform } from 'expo-modules-core'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { aspectRatio } from './dimensions'
|
||||
|
||||
export interface Props {
|
||||
|
@ -25,6 +27,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
gifv = false
|
||||
}) => {
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
const [autoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv')
|
||||
|
||||
const videoPlayer = useRef<Video>(null)
|
||||
const [videoLoading, setVideoLoading] = useState(false)
|
||||
|
@ -60,7 +63,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
resizeMode={videoResizeMode}
|
||||
{...(gifv
|
||||
? {
|
||||
shouldPlay: reduceMotionEnabled ? false : true,
|
||||
shouldPlay: reduceMotionEnabled || !autoplayGifv ? false : true,
|
||||
isMuted: true,
|
||||
isLooping: true,
|
||||
source: { uri: video.url }
|
||||
|
@ -70,14 +73,21 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
posterStyle: { resizeMode: ResizeMode.COVER }
|
||||
})}
|
||||
useNativeControls={false}
|
||||
onFullscreenUpdate={event => {
|
||||
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
|
||||
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
|
||||
if (gifv && !reduceMotionEnabled) {
|
||||
videoPlayer.current?.playAsync()
|
||||
} else {
|
||||
videoPlayer.current?.pauseAsync()
|
||||
}
|
||||
onFullscreenUpdate={async ({ fullscreenUpdate }) => {
|
||||
switch (fullscreenUpdate) {
|
||||
case VideoFullscreenUpdate.PLAYER_DID_PRESENT:
|
||||
Platform.OS === 'android' && (await ScreenOrientation.unlockAsync())
|
||||
break
|
||||
case VideoFullscreenUpdate.PLAYER_WILL_DISMISS:
|
||||
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 => {
|
||||
|
@ -106,7 +116,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
video.blurhash ? (
|
||||
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
|
||||
) : null
|
||||
) : !gifv || (gifv && reduceMotionEnabled) ? (
|
||||
) : !gifv || (gifv && (reduceMotionEnabled || !autoplayGifv)) ? (
|
||||
<Button
|
||||
round
|
||||
overlay
|
||||
|
@ -119,6 +129,21 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
) : null}
|
||||
<AttachmentAltText sensitiveShown={sensitiveShown} text={video.description} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
|||
})
|
||||
})}
|
||||
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 }}
|
||||
dimension={
|
||||
|
|
|
@ -2,15 +2,16 @@ import ComponentAccount from '@components/Account'
|
|||
import GracefullyImage from '@components/GracefullyImage'
|
||||
import openLink from '@components/openLink'
|
||||
import CustomText from '@components/Text'
|
||||
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
||||
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 { useSearchQuery } from '@utils/queryHooks/search'
|
||||
import { useStatusQuery } from '@utils/queryHooks/status'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { 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 TimelineDefault from '../Default'
|
||||
import StatusContext from './Context'
|
||||
|
@ -20,96 +21,44 @@ const TimelineCard: React.FC = () => {
|
|||
if (!status || !status.card) return null
|
||||
|
||||
const { colors } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const isStatus = matchStatus(status.card.url)
|
||||
const match = urlMatcher(status.card.url)
|
||||
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
|
||||
const isAccount = matchAccount(status.card.url)
|
||||
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({
|
||||
id: isStatus?.id || '',
|
||||
options: { enabled: false }
|
||||
status: match?.status ? { ...match.status, uri: status.card.url } : undefined,
|
||||
options: { enabled: false, retry: false }
|
||||
})
|
||||
useEffect(() => {
|
||||
if (isStatus) {
|
||||
if (match?.status) {
|
||||
setLoading(true)
|
||||
if (isStatus.sameInstance) {
|
||||
statusQuery
|
||||
.refetch()
|
||||
.then(res => {
|
||||
res.data && setFoundStatus(res.data)
|
||||
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))
|
||||
}
|
||||
statusQuery
|
||||
.refetch()
|
||||
.then(res => {
|
||||
res.data && setFoundStatus(res.data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => setLoading(false))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const accountQuery = useAccountQuery({
|
||||
id: isAccount?.style === 'default' ? isAccount.id : '',
|
||||
options: { enabled: false }
|
||||
account: match?.account ? { ...match?.account, url: status.card.url } : undefined,
|
||||
options: { enabled: false, retry: false }
|
||||
})
|
||||
useEffect(() => {
|
||||
if (isAccount) {
|
||||
if (match?.account) {
|
||||
setLoading(true)
|
||||
if (isAccount.sameInstance && isAccount.style === 'default') {
|
||||
accountQuery
|
||||
.refetch()
|
||||
.then(res => {
|
||||
res.data && setFoundAccount(res.data)
|
||||
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))
|
||||
}
|
||||
accountQuery
|
||||
.refetch()
|
||||
.then(res => {
|
||||
res.data && setFoundAccount(res.data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => setLoading(false))
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -128,10 +77,10 @@ const TimelineCard: React.FC = () => {
|
|||
</View>
|
||||
)
|
||||
}
|
||||
if (isStatus && foundStatus) {
|
||||
if (match?.status && foundStatus) {
|
||||
return <TimelineDefault item={foundStatus} disableDetails disableOnPress />
|
||||
}
|
||||
if (isAccount && foundAccount) {
|
||||
if (match?.account && foundAccount) {
|
||||
return <ComponentAccount account={foundAccount} />
|
||||
}
|
||||
return (
|
||||
|
@ -192,12 +141,23 @@ const TimelineCard: React.FC = () => {
|
|||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
overflow: 'hidden',
|
||||
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()}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { ParseHTML } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, StyleSheet, View } from 'react-native'
|
||||
import { Path, Svg } from 'react-native-svg'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { isRtlLang } from 'rtl-detect'
|
||||
import { View } from 'react-native'
|
||||
import StatusContext from './Context'
|
||||
|
||||
export interface Props {
|
||||
|
@ -17,35 +14,37 @@ export interface Props {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
|
||||
const { data: preferences } = usePreferencesQuery()
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
{/* <CustomText
|
||||
children={excludeMentions?.current.map(mention => mention.username).join(' - ')}
|
||||
style={{ color: colors.secondary }}
|
||||
/> */}
|
||||
{status.spoiler_text?.length ? (
|
||||
<>
|
||||
<ParseHTML
|
||||
content={status.spoiler_text}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
textStyles={
|
||||
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
|
||||
? { writingDirection: 'rtl' }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{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')}
|
||||
</CustomText>
|
||||
) : null}
|
||||
|
@ -53,11 +52,8 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={
|
||||
instanceAccount.preferences?.['reading:expand:spoilers'] || inThread
|
||||
preferences?.['reading:expand:spoilers'] || inThread
|
||||
? notificationOwnToot
|
||||
? 2
|
||||
: 999
|
||||
|
@ -65,13 +61,6 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||
}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
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}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
adaptiveSize
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
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 = {
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
|
||||
status?: Mastodon.Status
|
||||
|
||||
reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
|
||||
ownAccount?: boolean
|
||||
spoilerHidden?: boolean
|
||||
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
||||
detectedLanguage?: React.MutableRefObject<string>
|
||||
excludeMentions?: React.MutableRefObject<Mastodon.Mention[]>
|
||||
|
||||
highlighted?: boolean
|
||||
inThread?: boolean
|
||||
disableDetails?: boolean
|
||||
disableOnPress?: boolean
|
||||
isConversation?: boolean
|
||||
isRemote?: boolean
|
||||
}
|
||||
const StatusContext = createContext<StatusContextType>({} as StatusContextType)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ const TimelineFeedback = () => {
|
|||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
|
||||
const { data } = useStatusHistory({
|
||||
id: status.id,
|
||||
status,
|
||||
options: { enabled: status.edited_at !== undefined }
|
||||
})
|
||||
|
||||
|
@ -82,7 +82,7 @@ const TimelineFeedback = () => {
|
|||
style={[styles.text, { marginRight: 0, color: colors.blue }]}
|
||||
onPress={() =>
|
||||
navigation.push('Tab-Shared-History', {
|
||||
id: status.id,
|
||||
status,
|
||||
detectedLanguage: detectedLanguage?.current || status.language || ''
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import CustomText from '@components/Text'
|
||||
import removeHTML from '@helpers/removeHTML'
|
||||
import { store } from '@root/store'
|
||||
import removeHTML from '@utils/helpers/removeHTML'
|
||||
import { queryClient } from '@utils/queryHooks'
|
||||
import { QueryKeyFilters } from '@utils/queryHooks/filters'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstance } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
|
@ -15,7 +15,7 @@ export interface FilteredProps {
|
|||
|
||||
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||
|
||||
const main = () => {
|
||||
if (!filterResults?.length) {
|
||||
|
@ -23,18 +23,27 @@ const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
|||
}
|
||||
switch (typeof filterResults[0]) {
|
||||
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:
|
||||
return (
|
||||
<>
|
||||
{t('shared.filtered.match', {
|
||||
{t('componentTimeline:shared.filtered.match', {
|
||||
defaultValue: 'v2',
|
||||
context: 'v2',
|
||||
count: filterResults.length,
|
||||
filters: filterResults.map(result => result.title).join(t('common:separator'))
|
||||
})}
|
||||
<CustomText
|
||||
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'>
|
||||
}): FilteredProps['filterResults'] | undefined => {
|
||||
const page = queryKey[1]
|
||||
const instance = getInstance(store.getState())
|
||||
|
||||
let returnFilter: FilteredProps['filterResults'] | undefined
|
||||
|
||||
|
@ -91,7 +99,8 @@ export const shouldFilter = ({
|
|||
break
|
||||
}
|
||||
}
|
||||
instance?.filters?.forEach(filter => {
|
||||
const queryKeyFilters: QueryKeyFilters = ['Filters']
|
||||
queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => {
|
||||
if (returnFilter) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,14 +4,13 @@ import menuStatus from '@components/contextMenu/status'
|
|||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { Fragment, useContext, useState } from 'react'
|
||||
import { Platform, View } from 'react-native'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
|
||||
const TimelineHeaderAndroid: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress, rawContent } =
|
||||
useContext(StatusContext)
|
||||
const { queryKey, status, disableDetails, disableOnPress, rawContent } = useContext(StatusContext)
|
||||
|
||||
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
|
||||
|
||||
|
@ -28,9 +27,9 @@ const TimelineHeaderAndroid: React.FC = () => {
|
|||
type: 'status',
|
||||
openChange,
|
||||
account: status.account,
|
||||
queryKey
|
||||
...(status && { status })
|
||||
})
|
||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
|
||||
return (
|
||||
<View style={{ position: 'absolute', top: 0, right: 0 }}>
|
||||
|
@ -52,34 +51,51 @@ const TimelineHeaderAndroid: React.FC = () => {
|
|||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
||||
<DropdownMenu.ItemTitle children={menu.title} />
|
||||
</DropdownMenu.Item>
|
||||
{[mShare, mAccount, mStatus].map((menu, i) => (
|
||||
<Fragment key={i}>
|
||||
{menu.map((group, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{group.map(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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
</Fragment>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
|
|
@ -2,13 +2,13 @@ import Icon from '@components/Icon'
|
|||
import { displayMessage } from '@components/Message'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||
|
@ -22,7 +22,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
|||
if (!queryKey) return null
|
||||
|
||||
const { colors, theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { t } = useTranslation(['common', 'componentTimeline'])
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
|
@ -32,7 +32,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
|||
theme,
|
||||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`shared.header.conversation.delete.function`)
|
||||
function: t(`componentTimeline:shared.header.conversation.delete.function`)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -53,7 +53,7 @@ const HeaderConversation = ({ conversation }: Props) => {
|
|||
numberOfLines={1}
|
||||
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) => (
|
||||
<CustomText key={account.id} numberOfLines={1}>
|
||||
{index !== 0 ? t('common:separator') : undefined}
|
||||
|
@ -73,13 +73,8 @@ const HeaderConversation = ({ conversation }: Props) => {
|
|||
marginBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{conversation.last_status?.created_at ? (
|
||||
<HeaderSharedCreated
|
||||
created_at={conversation.last_status?.created_at}
|
||||
edited_at={conversation.last_status?.edited_at}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={conversation.last_status?.muted} />
|
||||
{conversation.last_status?.created_at ? <HeaderSharedCreated /> : null}
|
||||
<HeaderSharedMuted />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
@ -89,7 +84,6 @@ const HeaderConversation = ({ conversation }: Props) => {
|
|||
mutation.mutate({
|
||||
type: 'deleteItem',
|
||||
source: 'conversations',
|
||||
queryKey,
|
||||
id: conversation.id
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status'
|
|||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { Fragment, useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Pressable, View } from 'react-native'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
|
@ -13,11 +13,11 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
|||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||
import HeaderSharedReplies from './HeaderShared/Replies'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
|
||||
const TimelineHeaderDefault: React.FC = () => {
|
||||
const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } =
|
||||
useContext(StatusContext)
|
||||
const { queryKey, status, disableDetails, rawContent, isRemote } = useContext(StatusContext)
|
||||
if (!status) return null
|
||||
|
||||
const { colors } = useTheme()
|
||||
|
@ -34,9 +34,9 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||
type: 'status',
|
||||
openChange,
|
||||
account: status.account,
|
||||
queryKey
|
||||
...(status && { status })
|
||||
})
|
||||
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||
|
@ -56,14 +56,19 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||
: { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S })
|
||||
}}
|
||||
>
|
||||
<HeaderSharedCreated
|
||||
created_at={status.created_at}
|
||||
edited_at={status.edited_at}
|
||||
highlighted={highlighted}
|
||||
/>
|
||||
<HeaderSharedVisibility visibility={status.visibility} />
|
||||
<HeaderSharedMuted muted={status.muted} />
|
||||
<HeaderSharedApplication application={status.application} />
|
||||
{isRemote ? (
|
||||
<Icon
|
||||
name='Wifi'
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.secondary}
|
||||
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedCreated />
|
||||
<HeaderSharedVisibility />
|
||||
<HeaderSharedMuted />
|
||||
<HeaderSharedReplies />
|
||||
<HeaderSharedApplication />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
@ -82,37 +87,51 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
||||
<DropdownMenu.ItemTitle children={menu.title} />
|
||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</DropdownMenu.Item>
|
||||
{[mShare, mAccount, mStatus].map((menu, i) => (
|
||||
<Fragment key={i}>
|
||||
{menu.map((group, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{group.map(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}>
|
||||
<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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
</Fragment>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
|
|
@ -5,15 +5,14 @@ import menuShare from '@components/contextMenu/share'
|
|||
import menuStatus from '@components/contextMenu/status'
|
||||
import Icon from '@components/Icon'
|
||||
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
||||
import browserPackage from '@helpers/browserPackage'
|
||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||
import browserPackage from '@utils/helpers/browserPackage'
|
||||
import { getAccountStorage } from '@utils/storage/actions'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 { Platform, Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
|
@ -42,13 +41,11 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||
type: 'status',
|
||||
openChange,
|
||||
account: status?.account,
|
||||
queryKey
|
||||
...(status && { status })
|
||||
})
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
const mInstance = menuInstance({ status, queryKey })
|
||||
|
||||
const url = useSelector(getInstanceUrl)
|
||||
|
||||
const actions = () => {
|
||||
switch (notification.type) {
|
||||
case 'follow':
|
||||
|
@ -62,7 +59,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||
content={t('shared.actions.openReport')}
|
||||
onPress={async () =>
|
||||
WebBrowser.openAuthSessionAsync(
|
||||
`https://${url}/admin/reports/${notification.report.id}`,
|
||||
`https://${getAccountStorage.string('auth.domain')}/admin/reports/${
|
||||
notification.report.id
|
||||
}`,
|
||||
'tooot://tooot',
|
||||
{
|
||||
...(await browserPackage()),
|
||||
|
@ -74,7 +73,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||
/>
|
||||
)
|
||||
default:
|
||||
if (status) {
|
||||
if (status && Platform.OS !== 'android') {
|
||||
return (
|
||||
<Pressable
|
||||
style={{ flex: 1, alignItems: 'center' }}
|
||||
|
@ -89,48 +88,53 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{mShare.map((mGroup, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{mGroup.map(menu => (
|
||||
<DropdownMenu.Item key={menu.key} {...menu.item}>
|
||||
<DropdownMenu.ItemTitle children={menu.title} />
|
||||
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
|
||||
</DropdownMenu.Item>
|
||||
{[mShare, mStatus, mAccount, mInstance].map((menu, i) => (
|
||||
<Fragment key={i}>
|
||||
{menu.map((group, index) => (
|
||||
<DropdownMenu.Group key={index}>
|
||||
{group.map(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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
</Fragment>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
@ -175,31 +179,24 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||
marginBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<HeaderSharedCreated
|
||||
created_at={notification.status?.created_at || notification.created_at}
|
||||
edited_at={notification.status?.edited_at}
|
||||
/>
|
||||
{notification.status?.visibility ? (
|
||||
<HeaderSharedVisibility visibility={notification.status.visibility} />
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={notification.status?.muted} />
|
||||
<HeaderSharedApplication application={notification.status?.application} />
|
||||
<HeaderSharedCreated />
|
||||
{notification.status?.visibility ? <HeaderSharedVisibility /> : null}
|
||||
<HeaderSharedMuted />
|
||||
<HeaderSharedApplication />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{Platform.OS !== 'android' ? (
|
||||
<View
|
||||
style={[
|
||||
{ marginLeft: StyleConstants.Spacing.M },
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'admin.report'
|
||||
? { flexShrink: 1 }
|
||||
: { flex: 1 }
|
||||
]}
|
||||
children={actions()}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={[
|
||||
{ marginLeft: StyleConstants.Spacing.M },
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'admin.report'
|
||||
? { flexShrink: 1 }
|
||||
: { flex: 1 }
|
||||
]}
|
||||
children={actions()}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ParseEmojis } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import { ParseEmojis } from '@root/components/Parse'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
|
|
|
@ -2,38 +2,34 @@ import openLink from '@components/openLink'
|
|||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusContext from '../Context'
|
||||
|
||||
export interface Props {
|
||||
application?: Mastodon.Application
|
||||
const HeaderSharedApplication: React.FC = () => {
|
||||
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
|
||||
|
|
|
@ -3,53 +3,46 @@ import RelativeTime from '@components/RelativeTime'
|
|||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import StatusContext from '../Context'
|
||||
|
||||
export interface Props {
|
||||
created_at: Mastodon.Status['created_at'] | number
|
||||
edited_at?: Mastodon.Status['edited_at']
|
||||
highlighted?: boolean
|
||||
created_at?: Mastodon.Status['created_at'] | number
|
||||
}
|
||||
|
||||
const HeaderSharedCreated = React.memo(
|
||||
({ created_at, edited_at, highlighted = false }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
||||
const { status, highlighted } = useContext(StatusContext)
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
const actualTime = edited_at || created_at
|
||||
if (!status) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
{highlighted ? (
|
||||
<>
|
||||
<FormattedDate
|
||||
value={new Date(actualTime)}
|
||||
dateStyle='medium'
|
||||
timeStyle='short'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<RelativeTime time={actualTime} />
|
||||
)}
|
||||
</CustomText>
|
||||
{edited_at ? (
|
||||
<Icon
|
||||
accessibilityLabel={t(
|
||||
'shared.header.shared.edited.accessibilityLabel'
|
||||
)}
|
||||
name='Edit'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={colors.secondary}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.edited_at === next.edited_at
|
||||
)
|
||||
const actualTime = created_at || status.edited_at || status.created_at
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
{highlighted ? (
|
||||
<>
|
||||
<FormattedDate value={new Date(actualTime)} dateStyle='medium' timeStyle='short' />
|
||||
</>
|
||||
) : (
|
||||
<RelativeTime time={actualTime} />
|
||||
)}
|
||||
</CustomText>
|
||||
{status.edited_at && !highlighted ? (
|
||||
<Icon
|
||||
accessibilityLabel={t('shared.header.shared.edited.accessibilityLabel')}
|
||||
name='Edit'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={colors.secondary}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderSharedCreated
|
||||
|
|
|
@ -1,36 +1,24 @@
|
|||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import StatusContext from '../Context'
|
||||
|
||||
export interface Props {
|
||||
muted?: Mastodon.Status['muted']
|
||||
const HeaderSharedMuted: React.FC = () => {
|
||||
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
|
||||
|
|
|
@ -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 { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import StatusContext from '../Context'
|
||||
|
||||
export interface Props {
|
||||
visibility: Mastodon.Status['visibility']
|
||||
const HeaderSharedVisibility: React.FC = () => {
|
||||
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({
|
||||
visibility: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
|
|
|
@ -5,6 +5,8 @@ import { displayMessage } from '@components/Message'
|
|||
import { ParseEmojis } from '@components/Parse'
|
||||
import RelativeTime from '@components/RelativeTime'
|
||||
import CustomText from '@components/Text'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavState } from '@utils/navigation/navigators'
|
||||
import {
|
||||
MutationVarsTimelineUpdateStatusProperty,
|
||||
useTimelineMutation
|
||||
|
@ -13,42 +15,36 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 { Pressable, View } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import StatusContext from './Context'
|
||||
|
||||
const TimelinePoll: React.FC = () => {
|
||||
const {
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
reblogStatus,
|
||||
ownAccount,
|
||||
spoilerHidden,
|
||||
disableDetails
|
||||
} = useContext(StatusContext)
|
||||
const { queryKey, status, ownAccount, spoilerHidden, disableDetails, highlighted } =
|
||||
useContext(StatusContext)
|
||||
if (!queryKey || !status || !status.poll) return null
|
||||
const poll = status.poll
|
||||
|
||||
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 navigationState = useNavState()
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
onSuccess: ({ body }, params) => {
|
||||
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
||||
queryClient.cancelQueries(queryKey)
|
||||
rootQueryKey && queryClient.cancelQueries(rootQueryKey)
|
||||
|
||||
haptics('Success')
|
||||
switch (theParams.payload.property) {
|
||||
switch (theParams.payload.type) {
|
||||
case 'poll':
|
||||
theParams.payload.data = body as unknown as Mastodon.Poll
|
||||
updateStatusProperty(theParams)
|
||||
updateStatusProperty(
|
||||
{ ...theParams, poll: body as unknown as Mastodon.Poll },
|
||||
navigationState
|
||||
)
|
||||
break
|
||||
}
|
||||
},
|
||||
|
@ -59,7 +55,7 @@ const TimelinePoll: React.FC = () => {
|
|||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
// @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 &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -73,7 +69,7 @@ const TimelinePoll: React.FC = () => {
|
|||
}
|
||||
})
|
||||
|
||||
const pollButton = useMemo(() => {
|
||||
const pollButton = () => {
|
||||
if (!poll.expired) {
|
||||
if (!ownAccount && !poll.voted) {
|
||||
return (
|
||||
|
@ -82,62 +78,51 @@ const TimelinePoll: React.FC = () => {
|
|||
onPress={() =>
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'poll',
|
||||
id: poll.id,
|
||||
type: 'vote',
|
||||
type: 'poll',
|
||||
action: 'vote',
|
||||
options: allOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
type='text'
|
||||
content={t('shared.poll.meta.button.vote')}
|
||||
content={t('componentTimeline:shared.poll.meta.button.vote')}
|
||||
loading={mutation.isLoading}
|
||||
disabled={allOptions.filter(o => o !== false).length === 0}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
} else if (highlighted) {
|
||||
return (
|
||||
<View style={{ marginRight: StyleConstants.Spacing.S }}>
|
||||
<Button
|
||||
onPress={() =>
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
isReblog: !!reblogStatus,
|
||||
status,
|
||||
payload: {
|
||||
property: 'poll',
|
||||
id: poll.id,
|
||||
type: 'refresh'
|
||||
type: 'poll',
|
||||
action: 'refresh'
|
||||
}
|
||||
})
|
||||
}
|
||||
type='text'
|
||||
content={t('shared.poll.meta.button.refresh')}
|
||||
content={t('componentTimeline:shared.poll.meta.button.refresh')}
|
||||
loading={mutation.isLoading}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
|
||||
}
|
||||
|
||||
const isSelected = useCallback(
|
||||
(index: number): string =>
|
||||
allOptions[index]
|
||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
||||
[allOptions]
|
||||
)
|
||||
const isSelected = (index: number): string =>
|
||||
allOptions[index]
|
||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||
: `${poll.multiple ? 'Square' : 'Circle'}`
|
||||
|
||||
const pollBodyDisallow = useMemo(() => {
|
||||
const pollBodyDisallow = () => {
|
||||
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
||||
return poll.options.map((option, index) => (
|
||||
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
||||
|
@ -182,7 +167,7 @@ const TimelinePoll: React.FC = () => {
|
|||
borderTopRightRadius: 10,
|
||||
borderBottomRightRadius: 10,
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
marginBottom: StyleConstants.Spacing.XS,
|
||||
width: `${Math.round(
|
||||
(option.votes_count / (poll.voters_count || poll.votes_count)) * 100
|
||||
)}%`,
|
||||
|
@ -191,8 +176,8 @@ const TimelinePoll: React.FC = () => {
|
|||
/>
|
||||
</View>
|
||||
))
|
||||
}, [theme, poll.options])
|
||||
const pollBodyAllow = useMemo(() => {
|
||||
}
|
||||
const pollBodyAllow = () => {
|
||||
return poll.options.map((option, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
|
@ -229,26 +214,30 @@ const TimelinePoll: React.FC = () => {
|
|||
</View>
|
||||
</Pressable>
|
||||
))
|
||||
}, [theme, allOptions])
|
||||
}
|
||||
|
||||
const pollVoteCounts = () => {
|
||||
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) {
|
||||
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 = () => {
|
||||
if (poll.expired) {
|
||||
return t('shared.poll.meta.expiration.expired')
|
||||
return t('componentTimeline:shared.poll.meta.expiration.expired')
|
||||
} else {
|
||||
if (poll.expires_at) {
|
||||
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 (
|
||||
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
||||
{poll.expired || poll.voted ? pollBodyDisallow() : pollBodyAllow()}
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
@ -267,7 +256,7 @@ const TimelinePoll: React.FC = () => {
|
|||
marginTop: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
{pollButton}
|
||||
{pollButton()}
|
||||
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
||||
{pollVoteCounts()}
|
||||
{pollExpiration()}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ParseHTML } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import detectLanguage from '@helpers/detectLanguage'
|
||||
import getLanguage from '@helpers/getLanguage'
|
||||
import detectLanguage from '@utils/helpers/detectLanguage'
|
||||
import getLanguage from '@utils/helpers/getLanguage'
|
||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
@ -16,7 +16,7 @@ const TimelineTranslate = () => {
|
|||
const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext)
|
||||
if (!status || !highlighted || !rawContent?.current.length) return null
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { t } = useTranslation(['componentTimeline'])
|
||||
const { colors } = useTheme()
|
||||
|
||||
const [detected, setDetected] = useState<{
|
||||
|
@ -101,15 +101,15 @@ const TimelineTranslate = () => {
|
|||
}}
|
||||
>
|
||||
{isError
|
||||
? t('shared.translate.failed')
|
||||
? t('componentTimeline:shared.translate.failed')
|
||||
: isSuccess
|
||||
? typeof data?.error === 'string'
|
||||
? t(`shared.translate.${data.error}`)
|
||||
: t('shared.translate.succeed', {
|
||||
? t(`componentTimeline:shared.translate.${data.error}` as any)
|
||||
: t('componentTimeline:shared.translate.succeed', {
|
||||
provider: data?.provider,
|
||||
source: data?.sourceLanguage
|
||||
})
|
||||
: t('shared.translate.default')}
|
||||
: t('componentTimeline:shared.translate.default')}
|
||||
</CustomText>
|
||||
{isFetching ? (
|
||||
<Circle
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import ComponentSeparator from '@components/Separator'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
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 { 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 Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
||||
import { useSelector } from 'react-redux'
|
||||
import TimelineEmpty from './Timeline/Empty'
|
||||
import TimelineFooter from './Timeline/Footer'
|
||||
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
|
||||
import TimelineEmpty from './Empty'
|
||||
import TimelineFooter from './Footer'
|
||||
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh'
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<any>)
|
||||
|
||||
export interface Props {
|
||||
flRef?: RefObject<FlatList<any>>
|
||||
|
@ -24,7 +29,8 @@ export interface Props {
|
|||
>
|
||||
disableRefresh?: boolean
|
||||
disableInfinity?: boolean
|
||||
customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'>
|
||||
readMarker?: 'read_marker_following'
|
||||
customProps?: Partial<FlatListProps<any>>
|
||||
}
|
||||
|
||||
const Timeline: React.FC<Props> = ({
|
||||
|
@ -33,6 +39,7 @@ const Timeline: React.FC<Props> = ({
|
|||
queryOptions,
|
||||
disableRefresh = false,
|
||||
disableInfinity = false,
|
||||
readMarker = undefined,
|
||||
customProps
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
|
@ -45,24 +52,12 @@ const Timeline: React.FC<Props> = ({
|
|||
notifyOnChangeProps: Platform.select({
|
||||
ios: ['dataUpdatedAt', 'isFetching'],
|
||||
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 fetchingActive = useRef<boolean>(false)
|
||||
|
||||
const scrollY = useSharedValue(0)
|
||||
const fetchingType = useSharedValue<0 | 1 | 2>(0)
|
||||
|
@ -85,6 +80,32 @@ const Timeline: React.FC<Props> = ({
|
|||
[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({
|
||||
android: {
|
||||
refreshControl: (
|
||||
|
@ -93,38 +114,45 @@ const Timeline: React.FC<Props> = ({
|
|||
colors={[colors.primaryDefault]}
|
||||
progressBackgroundColor={colors.backgroundDefault}
|
||||
refreshing={isFetching || isLoading}
|
||||
onRefresh={() => refetch()}
|
||||
onRefresh={() => {
|
||||
if (readMarker) {
|
||||
setAccountStorage([{ key: readMarker, value: undefined }])
|
||||
}
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
useScrollToTop(flRef)
|
||||
useSelector(getInstanceActive, (prev, next) => {
|
||||
if (prev !== next) {
|
||||
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
||||
}
|
||||
return prev === next
|
||||
})
|
||||
useGlobalStorageListener('account.active', () =>
|
||||
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineRefresh
|
||||
flRef={flRef}
|
||||
queryKey={queryKey}
|
||||
fetchingActive={fetchingActive}
|
||||
scrollY={scrollY}
|
||||
fetchingType={fetchingType}
|
||||
disableRefresh={disableRefresh}
|
||||
readMarker={readMarker}
|
||||
/>
|
||||
<AnimatedFlatList
|
||||
ref={customFLRef || flRef}
|
||||
scrollEventThrottle={16}
|
||||
onScroll={onScroll}
|
||||
windowSize={7}
|
||||
data={flattenData}
|
||||
data={flattenPages(data)}
|
||||
{...(customProps?.renderItem
|
||||
? { renderItem: customProps.renderItem }
|
||||
: { renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} /> })}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={3}
|
||||
onEndReached={onEndReached}
|
||||
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.75}
|
||||
ListFooterComponent={
|
||||
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
||||
|
@ -139,13 +167,12 @@ const Timeline: React.FC<Props> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
maintainVisibleContentPosition={
|
||||
isFetching
|
||||
? {
|
||||
minIndexForVisible: 0
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
|
||||
{...(!isLoading && {
|
||||
maintainVisibleContentPosition: {
|
||||
minIndexForVisible: 0
|
||||
}
|
||||
})}
|
||||
{...androidRefreshControl}
|
||||
{...customProps}
|
||||
/>
|
|
@ -2,7 +2,10 @@ import haptics from '@components/haptics'
|
|||
import { displayMessage } from '@components/Message'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
||||
import { 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 {
|
||||
QueryKeyRelationship,
|
||||
useRelationshipMutation,
|
||||
|
@ -10,39 +13,30 @@ import {
|
|||
} from '@utils/queryHooks/relationship'
|
||||
import {
|
||||
MutationVarsTimelineUpdateAccountProperty,
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { getAccountStorage, getReadableAccounts, useAccountStorage } from '@utils/storage/actions'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Platform } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const menuAccount = ({
|
||||
type,
|
||||
openChange,
|
||||
account,
|
||||
queryKey,
|
||||
rootQueryKey
|
||||
status
|
||||
}: {
|
||||
type: 'status' | 'account' // Where the action is coming from
|
||||
openChange: boolean
|
||||
account?: Pick<Mastodon.Account, 'id' | 'username'>
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
}): ContextMenu[][] => {
|
||||
if (!account) return []
|
||||
|
||||
account?: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'>
|
||||
status?: Mastodon.Status
|
||||
}): ContextMenu => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
const navState = useNavState()
|
||||
const { t } = useTranslation(['common', 'componentContextMenu', 'componentRelationship'])
|
||||
|
||||
const menus: ContextMenu[][] = [[]]
|
||||
|
||||
const instanceAccount = useSelector(getInstanceAccount)
|
||||
const ownAccount = instanceAccount?.id === account.id
|
||||
const menus: ContextMenu = [[]]
|
||||
|
||||
const [enabled, setEnabled] = useState(openChange)
|
||||
useEffect(() => {
|
||||
|
@ -50,21 +44,32 @@ const menuAccount = ({
|
|||
setEnabled(true)
|
||||
}
|
||||
}, [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 timelineMutation = useTimelineMutation({
|
||||
onSuccess: (_, params) => {
|
||||
queryClient.refetchQueries(['Relationship', { id: account.id }])
|
||||
queryClient.refetchQueries(['Relationship', { id: actualAccount?.id }])
|
||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||
displayMessage({
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(`account.${theParams.payload.property}.action`, {
|
||||
...(theParams.payload.property !== 'reports' && {
|
||||
context: (theParams.payload.currentValue || false).toString()
|
||||
})
|
||||
})
|
||||
function: t(
|
||||
`componentContextMenu:account.${theParams.payload.property}.action`,
|
||||
theParams.payload.property !== 'reports'
|
||||
? {
|
||||
defaultValue: 'false',
|
||||
context: (theParams.payload.currentValue || false).toString()
|
||||
}
|
||||
: { defaultValue: 'false' }
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -73,11 +78,15 @@ const menuAccount = ({
|
|||
displayMessage({
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`account.${theParams.payload.property}.action`, {
|
||||
...(theParams.payload.property !== 'reports' && {
|
||||
context: (theParams.payload.currentValue || false).toString()
|
||||
})
|
||||
})
|
||||
function: t(
|
||||
`componentContextMenu:account.${theParams.payload.property}.action`,
|
||||
theParams.payload.property !== 'reports'
|
||||
? {
|
||||
defaultValue: 'false',
|
||||
context: (theParams.payload.currentValue || false).toString()
|
||||
}
|
||||
: { defaultValue: 'false' }
|
||||
)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -89,25 +98,28 @@ const menuAccount = ({
|
|||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryKey && queryClient.invalidateQueries(queryKey)
|
||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
||||
for (const key of navState) {
|
||||
queryClient.invalidateQueries(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }]
|
||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }]
|
||||
const relationshipMutation = useRelationshipMutation({
|
||||
onSuccess: (res, { payload: { action } }) => {
|
||||
haptics('Success')
|
||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||
if (action === 'block') {
|
||||
const queryKey = ['Timeline', { page: 'Following' }]
|
||||
queryClient.invalidateQueries({ queryKey, exact: false })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['Timeline', { page: 'Following' }],
|
||||
exact: false
|
||||
})
|
||||
}
|
||||
},
|
||||
onError: (err: any, { payload: { action } }) => {
|
||||
displayMessage({
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`${action}.function`)
|
||||
function: t(`componentContextMenu:${action}.function` as any)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -120,14 +132,18 @@ const menuAccount = ({
|
|||
}
|
||||
})
|
||||
|
||||
if (!account) return []
|
||||
|
||||
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
|
||||
menus[0].push({
|
||||
type: 'item',
|
||||
key: 'account-following',
|
||||
item: {
|
||||
props: {
|
||||
onSelect: () =>
|
||||
data &&
|
||||
actualAccount &&
|
||||
relationshipMutation.mutate({
|
||||
id: account.id,
|
||||
id: actualAccount.id,
|
||||
type: 'outgoing',
|
||||
payload: { action: 'follow', state: !data?.requested ? data.following : true }
|
||||
}),
|
||||
|
@ -136,7 +152,8 @@ const menuAccount = ({
|
|||
hidden: false
|
||||
},
|
||||
title: !data?.requested
|
||||
? t('account.following.action', {
|
||||
? t('componentContextMenu:account.following.action', {
|
||||
defaultValue: 'false',
|
||||
context: (data?.following || false).toString()
|
||||
})
|
||||
: t('componentRelationship:button.requested'),
|
||||
|
@ -147,109 +164,199 @@ const menuAccount = ({
|
|||
: 'person.badge.minus'
|
||||
})
|
||||
}
|
||||
|
||||
if (!ownAccount) {
|
||||
menus[0].push({
|
||||
type: 'item',
|
||||
key: 'account-list',
|
||||
item: {
|
||||
props: {
|
||||
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
|
||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||
destructive: false,
|
||||
hidden: !isFetched || !data?.following
|
||||
},
|
||||
title: t('account.inLists'),
|
||||
title: t('componentContextMenu:account.inLists'),
|
||||
icon: 'checklist'
|
||||
})
|
||||
menus[0].push({
|
||||
key: 'account-mute',
|
||||
item: {
|
||||
type: 'item',
|
||||
key: 'account-show-boosts',
|
||||
props: {
|
||||
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({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
id: actualAccount.id,
|
||||
payload: { property: 'mute', currentValue: data?.muting }
|
||||
}),
|
||||
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
|
||||
destructive: false,
|
||||
hidden: false
|
||||
},
|
||||
title: t('account.mute.action', {
|
||||
title: t('componentContextMenu:account.mute.action', {
|
||||
defaultValue: 'false',
|
||||
context: (data?.muting || false).toString()
|
||||
}),
|
||||
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([
|
||||
{
|
||||
key: 'account-block',
|
||||
item: {
|
||||
onSelect: () =>
|
||||
Alert.alert(t('account.block.alert.title', { username: account.username }), undefined, [
|
||||
{
|
||||
text: t('common:buttons.confirm'),
|
||||
style: 'destructive',
|
||||
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
|
||||
type: 'sub',
|
||||
key: 'account-block-report',
|
||||
trigger: {
|
||||
key: 'account-block-report',
|
||||
props: { destructive: true, disabled: false, hidden: false },
|
||||
title: t('componentContextMenu:account.blockReport'),
|
||||
icon: 'hand.raised'
|
||||
},
|
||||
title: t('account.block.action', {
|
||||
context: (data?.blocking || false).toString()
|
||||
}),
|
||||
icon: data?.blocking ? 'checkmark.circle' : 'xmark.circle'
|
||||
},
|
||||
{
|
||||
key: 'account-reports',
|
||||
item: {
|
||||
onSelect: () =>
|
||||
Alert.alert(
|
||||
t('account.reports.alert.title', { username: account.username }),
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
text: t('common:buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
timelineMutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
payload: { property: 'reports' }
|
||||
})
|
||||
timelineMutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
payload: { property: 'block', currentValue: false }
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel')
|
||||
}
|
||||
]
|
||||
),
|
||||
disabled: false,
|
||||
destructive: true,
|
||||
hidden: false
|
||||
},
|
||||
title: t('account.reports.action'),
|
||||
icon: 'flag'
|
||||
items: [
|
||||
{
|
||||
key: 'account-block',
|
||||
props: {
|
||||
onSelect: () =>
|
||||
Alert.alert(
|
||||
t('componentContextMenu:account.block.alert.title', {
|
||||
username: actualAccount?.username
|
||||
}),
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
text: t('common:buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () =>
|
||||
actualAccount &&
|
||||
timelineMutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
id: actualAccount.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('componentContextMenu:account.block.action', {
|
||||
defaultValue: 'false',
|
||||
context: (data?.blocking || false).toString()
|
||||
}),
|
||||
icon: data?.blocking ? 'checkmark.circle' : 'xmark.circle'
|
||||
},
|
||||
{
|
||||
key: 'account-reports',
|
||||
props: {
|
||||
onSelect: () =>
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,50 +1,53 @@
|
|||
import { useNavigation } from '@react-navigation/native'
|
||||
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'
|
||||
|
||||
const menuAt = ({ account }: { account: Mastodon.Account }): ContextMenu[][] => {
|
||||
const menuAt = ({ account }: { account: Mastodon.Account }): ContextMenu => {
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||
const navigationState = useNavState()
|
||||
|
||||
const menus: ContextMenu[][] = []
|
||||
|
||||
menus.push([
|
||||
{
|
||||
key: 'at-direct',
|
||||
item: {
|
||||
onSelect: () =>
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'conversation',
|
||||
accts: [account.acct],
|
||||
visibility: 'direct'
|
||||
}),
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
hidden: false
|
||||
return [
|
||||
[
|
||||
{
|
||||
type: 'item',
|
||||
key: 'at-direct',
|
||||
props: {
|
||||
onSelect: () =>
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'conversation',
|
||||
accts: [account.acct],
|
||||
visibility: 'direct',
|
||||
navigationState
|
||||
}),
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
hidden: false
|
||||
},
|
||||
title: t('at.direct'),
|
||||
icon: 'envelope'
|
||||
},
|
||||
title: t('at.direct'),
|
||||
icon: 'envelope'
|
||||
},
|
||||
{
|
||||
key: 'at-public',
|
||||
item: {
|
||||
onSelect: () =>
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'conversation',
|
||||
accts: [account.acct],
|
||||
visibility: 'public'
|
||||
}),
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
hidden: false
|
||||
},
|
||||
title: t('at.public'),
|
||||
icon: 'at'
|
||||
}
|
||||
])
|
||||
|
||||
return menus
|
||||
{
|
||||
type: 'item',
|
||||
key: 'at-public',
|
||||
props: {
|
||||
onSelect: () =>
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'conversation',
|
||||
accts: [account.acct],
|
||||
visibility: 'public',
|
||||
navigationState
|
||||
}),
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
hidden: false
|
||||
},
|
||||
title: t('at.public'),
|
||||
icon: 'at'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
item: { onSelect: () => void; disabled: boolean; destructive: boolean; hidden: boolean }
|
||||
props: {
|
||||
onSelect: () => void
|
||||
disabled: boolean
|
||||
destructive: boolean
|
||||
hidden: boolean
|
||||
}
|
||||
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 { useQueryClient } from '@tanstack/react-query'
|
||||
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 { Alert } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import parse from 'url-parse'
|
||||
|
||||
const menuInstance = ({
|
||||
status,
|
||||
queryKey,
|
||||
rootQueryKey
|
||||
queryKey
|
||||
}: {
|
||||
status?: Mastodon.Status
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
}): ContextMenu[][] => {
|
||||
}): ContextMenu => {
|
||||
if (!status || !queryKey) return []
|
||||
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
const { t } = useTranslation(['common', 'componentContextMenu'])
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
|
@ -25,38 +23,33 @@ const menuInstance = ({
|
|||
displayMessage({
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(`instance.block.action`, { instance })
|
||||
function: t(`componentContextMenu:instance.block.action`, { instance })
|
||||
})
|
||||
})
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
||||
}
|
||||
})
|
||||
|
||||
const menus: ContextMenu[][] = []
|
||||
const menus: ContextMenu = []
|
||||
|
||||
const currentInstance = useSelector(getInstanceUrl)
|
||||
const instance = status.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
||||
const instance = parse(status.uri).hostname
|
||||
|
||||
if (currentInstance !== instance && instance) {
|
||||
if (instance !== getAccountStorage.string('auth.domain')) {
|
||||
menus.push([
|
||||
{
|
||||
type: 'item',
|
||||
key: 'instance-block',
|
||||
item: {
|
||||
props: {
|
||||
onSelect: () =>
|
||||
Alert.alert(
|
||||
t('instance.block.alert.title', { instance }),
|
||||
t('instance.block.alert.message'),
|
||||
t('componentContextMenu:instance.block.alert.title', { instance }),
|
||||
t('componentContextMenu:instance.block.alert.message'),
|
||||
[
|
||||
{
|
||||
text: t('common:buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
mutation.mutate({
|
||||
type: 'domainBlock',
|
||||
queryKey,
|
||||
domain: instance
|
||||
})
|
||||
mutation.mutate({ type: 'domainBlock', domain: instance })
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -68,7 +61,7 @@ const menuInstance = ({
|
|||
destructive: true,
|
||||
hidden: false
|
||||
},
|
||||
title: t('instance.block.action', { instance }),
|
||||
title: t('componentContextMenu:instance.block.action', { instance }),
|
||||
icon: ''
|
||||
}
|
||||
])
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue