mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Merge branch 'main' into candidate
This commit is contained in:
@@ -1,22 +0,0 @@
|
|||||||
diff --git a/HTMLView.js b/HTMLView.js
|
|
||||||
index 43f8b7eb552d9a44b5feef74ec2ae7d7ddbc2fca..334d144f1fb96067726bca199ff37267c2fa0fb2 100644
|
|
||||||
--- a/HTMLView.js
|
|
||||||
+++ b/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.object,
|
|
||||||
stylesheet: PropTypes.object,
|
|
||||||
TextComponent: PropTypes.func,
|
|
||||||
textComponentProps: PropTypes.object,
|
|
40
README.md
40
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](LICENSE)   [](https://crowdin.tooot.app/project/tooot)
|
[](LICENSE)   [](https://crowdin.tooot.app/project/tooot)
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
## Contribute to translation
|
## Contribute to translation
|
||||||
|
|
||||||
@@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati
|
|||||||
|
|
||||||
## Special thanks
|
## Special thanks
|
||||||
|
|
||||||
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
- [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
|
||||||
|
- [@forenta](https://github.com/forenta) for German translation
|
||||||
[@forenta](https://github.com/forenta) for German translation
|
- [@pat](https://piaille.fr/@pat) for French translation
|
||||||
|
- [@andrigamerita](https://github.com/andrigamerita) for Italian translation
|
||||||
[@pat](https://piaille.fr/@pat) for French translation
|
- [@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
|
||||||
|
- [@hellojaccc](https://github.com/hellojaccc) for Korean translation
|
||||||
[@andrigamerita](https://github.com/andrigamerita) for Italian translation
|
- [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
||||||
|
- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
||||||
[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
|
- [@janlindblom](https://github.com/janlindblom) for Swedish
|
||||||
|
- [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
|
||||||
[@hellojaccc](https://github.com/hellojaccc) for Korean translation
|
- [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
|
||||||
|
- [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
|
||||||
[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
|
- [@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
|
||||||
|
|
||||||
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
|
|
||||||
|
|
||||||
[@janlindblom](https://github.com/janlindblom) for Swedish
|
|
||||||
|
|
||||||
[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
|
|
||||||
|
|
||||||
[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
|
|
||||||
|
|
||||||
[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
|
|
||||||
|
|
||||||
[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
|
|
||||||
|
@@ -8,11 +8,8 @@ module.exports = function (api) {
|
|||||||
{
|
{
|
||||||
root: ['./'],
|
root: ['./'],
|
||||||
alias: {
|
alias: {
|
||||||
'@assets': './assets',
|
|
||||||
'@root': './src',
|
|
||||||
'@api': './src/api',
|
|
||||||
'@helpers': './src/helpers',
|
|
||||||
'@components': './src/components',
|
'@components': './src/components',
|
||||||
|
'@i18n': './src/i18n',
|
||||||
'@screens': './src/screens',
|
'@screens': './src/screens',
|
||||||
'@utils': './src/utils'
|
'@utils': './src/utils'
|
||||||
}
|
}
|
||||||
|
BIN
demo/screenshots/Tab-Shared-Report.png
Normal file
BIN
demo/screenshots/Tab-Shared-Report.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 251 KiB |
@@ -1,4 +1,5 @@
|
|||||||
Enjoy toooting! This version includes following improvements and fixes:
|
Enjoy toooting! This version includes following improvements and fixes:
|
||||||
- Allowing adding more context of reports
|
- Allowing adding more context of reports
|
||||||
- Option to disable autoplay gif
|
- Option to disable autoplay gif
|
||||||
- Hide boosts from users
|
- Hide boosts from users
|
||||||
|
- Followed hashtags are underlined
|
@@ -1,4 +1,5 @@
|
|||||||
toooting愉快!此版本包括以下改进和修复:
|
toooting愉快!此版本包括以下改进和修复:
|
||||||
- 可添加举报细节
|
- 可添加举报细节
|
||||||
- 新增暂停自动播放gif动画选项
|
- 新增暂停自动播放gif动画选项
|
||||||
- 隐藏用户的转嘟
|
- 隐藏用户的转嘟
|
||||||
|
- 下划线高亮正在关注的话题标签
|
2
index.js
2
index.js
@@ -1,6 +1,6 @@
|
|||||||
import { registerRootComponent } from 'expo'
|
import { registerRootComponent } from 'expo'
|
||||||
|
|
||||||
import App from '@root/App'
|
import App from './src/App'
|
||||||
|
|
||||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => 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,
|
// It also ensures that whether you load the app in the Expo client or in a native build,
|
||||||
|
@@ -65,6 +65,9 @@ PODS:
|
|||||||
- libwebp/mux (1.2.4):
|
- libwebp/mux (1.2.4):
|
||||||
- libwebp/demux
|
- libwebp/demux
|
||||||
- libwebp/webp (1.2.4)
|
- libwebp/webp (1.2.4)
|
||||||
|
- MMKV (1.2.14):
|
||||||
|
- MMKVCore (~> 1.2.14)
|
||||||
|
- MMKVCore (1.2.14)
|
||||||
- RCT-Folly (2021.07.22.00):
|
- RCT-Folly (2021.07.22.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -305,6 +308,9 @@ PODS:
|
|||||||
- React
|
- React
|
||||||
- react-native-menu (0.7.2):
|
- react-native-menu (0.7.2):
|
||||||
- React
|
- React
|
||||||
|
- react-native-mmkv (2.5.1):
|
||||||
|
- MMKV (>= 1.2.13)
|
||||||
|
- React-Core
|
||||||
- react-native-netinfo (9.3.7):
|
- react-native-netinfo (9.3.7):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-pager-view (6.1.2):
|
- react-native-pager-view (6.1.2):
|
||||||
@@ -494,6 +500,7 @@ DEPENDENCIES:
|
|||||||
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
|
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
|
||||||
- react-native-language-detection (from `../node_modules/react-native-language-detection`)
|
- react-native-language-detection (from `../node_modules/react-native-language-detection`)
|
||||||
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
|
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
|
||||||
|
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
|
||||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||||
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||||
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
|
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
|
||||||
@@ -527,6 +534,8 @@ SPEC REPOS:
|
|||||||
- fmt
|
- fmt
|
||||||
- libevent
|
- libevent
|
||||||
- libwebp
|
- libwebp
|
||||||
|
- MMKV
|
||||||
|
- MMKVCore
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageWebPCoder
|
- SDWebImageWebPCoder
|
||||||
- Sentry
|
- Sentry
|
||||||
@@ -629,6 +638,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-language-detection"
|
:path: "../node_modules/react-native-language-detection"
|
||||||
react-native-menu:
|
react-native-menu:
|
||||||
:path: "../node_modules/@react-native-menu/menu"
|
:path: "../node_modules/@react-native-menu/menu"
|
||||||
|
react-native-mmkv:
|
||||||
|
:path: "../node_modules/react-native-mmkv"
|
||||||
react-native-netinfo:
|
react-native-netinfo:
|
||||||
:path: "../node_modules/@react-native-community/netinfo"
|
:path: "../node_modules/@react-native-community/netinfo"
|
||||||
react-native-pager-view:
|
react-native-pager-view:
|
||||||
@@ -714,6 +725,8 @@ SPEC CHECKSUMS:
|
|||||||
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
|
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
|
||||||
|
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
|
||||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||||
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
||||||
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
||||||
@@ -736,6 +749,7 @@ SPEC CHECKSUMS:
|
|||||||
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
||||||
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
||||||
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
||||||
|
react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3
|
||||||
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||||
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
||||||
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
|
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
|
||||||
|
@@ -32,12 +32,11 @@
|
|||||||
"@react-native-community/blur": "^4.3.0",
|
"@react-native-community/blur": "^4.3.0",
|
||||||
"@react-native-community/netinfo": "9.3.7",
|
"@react-native-community/netinfo": "9.3.7",
|
||||||
"@react-native-community/segmented-control": "^2.2.2",
|
"@react-native-community/segmented-control": "^2.2.2",
|
||||||
"@react-native-menu/menu": "^0.7.2",
|
"@react-native-menu/menu": "^0.7.3",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.2",
|
"@react-navigation/bottom-tabs": "^6.5.2",
|
||||||
"@react-navigation/native": "^6.1.1",
|
"@react-navigation/native": "^6.1.1",
|
||||||
"@react-navigation/native-stack": "^6.9.7",
|
"@react-navigation/native-stack": "^6.9.7",
|
||||||
"@react-navigation/stack": "^6.3.10",
|
"@react-navigation/stack": "^6.3.10",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
|
||||||
"@sentry/react-native": "4.12.0",
|
"@sentry/react-native": "4.12.0",
|
||||||
"@sharcoux/slider": "^6.1.1",
|
"@sharcoux/slider": "^6.1.1",
|
||||||
"@tanstack/react-query": "^4.20.4",
|
"@tanstack/react-query": "^4.20.4",
|
||||||
@@ -60,6 +59,7 @@
|
|||||||
"expo-store-review": "^6.0.0",
|
"expo-store-review": "^6.0.0",
|
||||||
"expo-video-thumbnails": "^7.0.0",
|
"expo-video-thumbnails": "^7.0.0",
|
||||||
"expo-web-browser": "~12.0.0",
|
"expo-web-browser": "~12.0.0",
|
||||||
|
"htmlparser2": "^8.0.1",
|
||||||
"i18next": "^22.4.6",
|
"i18next": "^22.4.6",
|
||||||
"linkify-it": "^4.0.1",
|
"linkify-it": "^4.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -75,10 +75,10 @@
|
|||||||
"react-native-feather": "^1.1.2",
|
"react-native-feather": "^1.1.2",
|
||||||
"react-native-flash-message": "^0.3.1",
|
"react-native-flash-message": "^0.3.1",
|
||||||
"react-native-gesture-handler": "~2.8.0",
|
"react-native-gesture-handler": "~2.8.0",
|
||||||
"react-native-htmlview": "^0.16.0",
|
|
||||||
"react-native-image-picker": "^4.10.3",
|
"react-native-image-picker": "^4.10.3",
|
||||||
"react-native-ios-context-menu": "^1.15.1",
|
"react-native-ios-context-menu": "^1.15.1",
|
||||||
"react-native-language-detection": "^0.2.2",
|
"react-native-language-detection": "^0.2.2",
|
||||||
|
"react-native-mmkv": "^2.5.1",
|
||||||
"react-native-pager-view": "^6.1.2",
|
"react-native-pager-view": "^6.1.2",
|
||||||
"react-native-reanimated": "^2.13.0",
|
"react-native-reanimated": "^2.13.0",
|
||||||
"react-native-reanimated-zoom": "^0.3.3",
|
"react-native-reanimated-zoom": "^0.3.3",
|
||||||
@@ -89,7 +89,6 @@
|
|||||||
"react-native-swipe-list-view": "^3.2.9",
|
"react-native-swipe-list-view": "^3.2.9",
|
||||||
"react-native-tab-view": "^3.3.4",
|
"react-native-tab-view": "^3.3.4",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
|
||||||
"rn-placeholder": "^3.0.3",
|
"rn-placeholder": "^3.0.3",
|
||||||
"rtl-detect": "^1.0.4",
|
"rtl-detect": "^1.0.4",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
@@ -120,7 +119,6 @@
|
|||||||
"resolutions": {
|
"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",
|
"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",
|
"expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch",
|
||||||
"react-native-htmlview@^0.16.0": "patch:react-native-htmlview@npm%3A0.16.0#./.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.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",
|
"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"
|
"@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"
|
||||||
}
|
}
|
||||||
|
56
src/@types/mastodon.d.ts
vendored
56
src/@types/mastodon.d.ts
vendored
@@ -264,14 +264,6 @@ declare namespace Mastodon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
|
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
|
||||||
type Filter_V1 = {
|
|
||||||
id: string
|
|
||||||
phrase: string
|
|
||||||
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
|
|
||||||
expires_at?: string
|
|
||||||
irreversible: boolean
|
|
||||||
whole_word: boolean
|
|
||||||
}
|
|
||||||
type Filter_V2 = {
|
type Filter_V2 = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -281,6 +273,14 @@ declare namespace Mastodon {
|
|||||||
keywords: FilterKeyword[]
|
keywords: FilterKeyword[]
|
||||||
statuses: FilterStatus[]
|
statuses: FilterStatus[]
|
||||||
}
|
}
|
||||||
|
type Filter_V1 = {
|
||||||
|
id: string
|
||||||
|
phrase: string
|
||||||
|
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
|
||||||
|
expires_at?: string
|
||||||
|
irreversible: boolean
|
||||||
|
whole_word: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
|
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
|
||||||
|
|
||||||
@@ -298,7 +298,45 @@ declare namespace Mastodon {
|
|||||||
replies_policy: 'none' | 'list' | 'followed'
|
replies_policy: 'none' | 'list' | 'followed'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Instance = {
|
type Instance<T extends 'v1' | 'v2'> = T extends 'v2' ? Instance_V2 : Instance_V1
|
||||||
|
type Instance_V2 = {
|
||||||
|
domain: string
|
||||||
|
title: string
|
||||||
|
version: string
|
||||||
|
source_url: string
|
||||||
|
description: string
|
||||||
|
usage: { users: { active_month: number } }
|
||||||
|
thumbnail: { url: string; blurhash?: string; versions?: { '@1x'?: string; '@2x'?: string } }
|
||||||
|
languages: string[]
|
||||||
|
configuration: {
|
||||||
|
urls: { streaming_api: string }
|
||||||
|
accounts: { max_featured_tags: number }
|
||||||
|
statuses: {
|
||||||
|
max_characters: number
|
||||||
|
max_media_attachments: number
|
||||||
|
characters_reserved_per_url: number
|
||||||
|
}
|
||||||
|
media_attachments: {
|
||||||
|
supported_mime_types: string[]
|
||||||
|
image_size_limit: number
|
||||||
|
image_matrix_limit: number
|
||||||
|
video_size_limit: number
|
||||||
|
video_frame_rate_limit: number
|
||||||
|
video_matrix_limit: number
|
||||||
|
}
|
||||||
|
polls: {
|
||||||
|
max_options: number
|
||||||
|
max_characters_per_option: number
|
||||||
|
min_expiration: number
|
||||||
|
max_expiration: number
|
||||||
|
}
|
||||||
|
translation: { enabled: boolean }
|
||||||
|
registrations: { enabled: boolean; approval_required: boolean; message?: string }
|
||||||
|
contact: { email: string; account: Account }
|
||||||
|
rules: Rule[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type Instance_V1 = {
|
||||||
// Base
|
// Base
|
||||||
uri: string
|
uri: string
|
||||||
title: string
|
title: string
|
||||||
|
2
src/@types/untyped.d.ts
vendored
2
src/@types/untyped.d.ts
vendored
@@ -1,7 +1,5 @@
|
|||||||
declare module 'gl-react-blurhash'
|
declare module 'gl-react-blurhash'
|
||||||
declare module 'htmlparser2-without-node-native'
|
|
||||||
declare module 'react-native-feather'
|
declare module 'react-native-feather'
|
||||||
declare module 'react-native-htmlview'
|
|
||||||
declare module 'react-native-toast-message'
|
declare module 'react-native-toast-message'
|
||||||
declare module 'rtl-detect'
|
declare module 'rtl-detect'
|
||||||
|
|
||||||
|
176
src/App.tsx
176
src/App.tsx
@@ -1,30 +1,34 @@
|
|||||||
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||||
import getLanguage from '@helpers/getLanguage'
|
|
||||||
import queryClient from '@helpers/queryClient'
|
|
||||||
import i18n from '@root/i18n/i18n'
|
|
||||||
import Screens from '@root/Screens'
|
|
||||||
import audio from '@root/startup/audio'
|
|
||||||
import log from '@root/startup/log'
|
|
||||||
import netInfo from '@root/startup/netInfo'
|
|
||||||
import push from '@root/startup/push'
|
|
||||||
import sentry from '@root/startup/sentry'
|
|
||||||
import timezone from '@root/startup/timezone'
|
|
||||||
import { persistor, store } from '@root/store'
|
|
||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
|
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
|
||||||
import { changeLanguage } from '@utils/slices/settingsSlice'
|
import getLanguage from '@utils/helpers/getLanguage'
|
||||||
|
import queryClient from '@utils/queryHooks'
|
||||||
|
import audio from '@utils/startup/audio'
|
||||||
|
import log from '@utils/startup/log'
|
||||||
|
import netInfo from '@utils/startup/netInfo'
|
||||||
|
import push from '@utils/startup/push'
|
||||||
|
import sentry from '@utils/startup/sentry'
|
||||||
|
import timezone from '@utils/startup/timezone'
|
||||||
|
import { storage } from '@utils/storage'
|
||||||
|
import {
|
||||||
|
getGlobalStorage,
|
||||||
|
removeAccount,
|
||||||
|
setAccount,
|
||||||
|
setGlobalStorage
|
||||||
|
} from '@utils/storage/actions'
|
||||||
|
import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
|
||||||
import ThemeManager from '@utils/styles/ThemeManager'
|
import ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import * as Localization from 'expo-localization'
|
import * as Localization from 'expo-localization'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { IntlProvider } from 'react-intl'
|
|
||||||
import { LogBox, Platform } from 'react-native'
|
import { LogBox, Platform } from 'react-native'
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||||
|
import { MMKV } from 'react-native-mmkv'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
import { enableFreeze } from 'react-native-screens'
|
import { enableFreeze } from 'react-native-screens'
|
||||||
import { QueryClientProvider } from '@tanstack/react-query'
|
import i18n from './i18n'
|
||||||
import { Provider } from 'react-redux'
|
import Screens from './screens'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
|
||||||
|
|
||||||
Platform.select({
|
Platform.select({
|
||||||
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||||
@@ -36,83 +40,95 @@ push()
|
|||||||
timezone()
|
timezone()
|
||||||
enableFreeze(true)
|
enableFreeze(true)
|
||||||
|
|
||||||
|
log('log', 'App', 'delay splash')
|
||||||
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
log('log', 'App', 'rendering App')
|
log('log', 'App', 'rendering App')
|
||||||
|
const [appIsReady, setAppIsReady] = useState(false)
|
||||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||||
|
|
||||||
|
const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delaySplash = async () => {
|
const prepare = async () => {
|
||||||
log('log', 'App', 'delay splash')
|
if (!hasMigrated) {
|
||||||
try {
|
try {
|
||||||
await SplashScreen.preventAutoHideAsync()
|
await migrateFromAsyncStorage()
|
||||||
} catch (e) {
|
setHasMigrated(true)
|
||||||
console.warn(e)
|
} catch {}
|
||||||
|
} else {
|
||||||
|
log('log', 'App', 'loading from MMKV')
|
||||||
|
const account = getGlobalStorage.string('account.active')
|
||||||
|
if (account) {
|
||||||
|
const storageAccount = new MMKV({ id: account })
|
||||||
|
const token = storageAccount.getString('auth.token')
|
||||||
|
if (token) {
|
||||||
|
log('log', 'App', `Binding storage of ${account}`)
|
||||||
|
storage.account = storageAccount
|
||||||
|
} else {
|
||||||
|
log('log', 'App', `Token not found for ${account}`)
|
||||||
|
removeAccount(account)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('log', 'App', 'No active account available')
|
||||||
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
if (accounts?.length) {
|
||||||
|
log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`)
|
||||||
|
setAccount(accounts[accounts.length - 1])
|
||||||
|
} else {
|
||||||
|
setGlobalStorage('account.active', undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let netInfoRes = undefined
|
||||||
|
try {
|
||||||
|
netInfoRes = await netInfo()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
||||||
|
setLocalCorrupt(netInfoRes.corrupted)
|
||||||
|
}
|
||||||
|
|
||||||
|
log('log', 'App', `locale: ${Localization.locale}`)
|
||||||
|
const language = getLanguage()
|
||||||
|
if (!language) {
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
setGlobalStorage('app.language', 'en')
|
||||||
|
}
|
||||||
|
i18n.changeLanguage('en')
|
||||||
|
} else {
|
||||||
|
i18n.changeLanguage(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppIsReady(true)
|
||||||
}
|
}
|
||||||
delaySplash()
|
|
||||||
|
prepare()
|
||||||
}, [])
|
}, [])
|
||||||
|
const onLayoutRootView = useCallback(async () => {
|
||||||
const onBeforeLift = useCallback(async () => {
|
if (appIsReady) {
|
||||||
let netInfoRes = undefined
|
log('log', 'App', 'hide splash')
|
||||||
try {
|
|
||||||
netInfoRes = await netInfo()
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
|
||||||
setLocalCorrupt(netInfoRes.corrupted)
|
|
||||||
}
|
|
||||||
|
|
||||||
log('log', 'App', 'hide splash')
|
|
||||||
try {
|
|
||||||
await SplashScreen.hideAsync()
|
await SplashScreen.hideAsync()
|
||||||
return Promise.resolve()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [appIsReady])
|
||||||
|
if (!appIsReady) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }} onLayout={onLayoutRootView}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Provider store={store}>
|
<SafeAreaProvider>
|
||||||
<PersistGate
|
<ActionSheetProvider>
|
||||||
persistor={persistor}
|
<AccessibilityManager>
|
||||||
onBeforeLift={onBeforeLift}
|
<ThemeManager>
|
||||||
children={bootstrapped => {
|
<Screens localCorrupt={localCorrupt} />
|
||||||
log('log', 'App', 'bootstrapped')
|
</ThemeManager>
|
||||||
if (bootstrapped) {
|
</AccessibilityManager>
|
||||||
log('log', 'App', 'loading actual app :)')
|
</ActionSheetProvider>
|
||||||
log('log', 'App', `Locale: ${Localization.locale}`)
|
</SafeAreaProvider>
|
||||||
const language = getLanguage()
|
|
||||||
if (!language) {
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
store.dispatch(changeLanguage('en'))
|
|
||||||
}
|
|
||||||
i18n.changeLanguage('en')
|
|
||||||
} else {
|
|
||||||
i18n.changeLanguage(language)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={language}>
|
|
||||||
<SafeAreaProvider>
|
|
||||||
<ActionSheetProvider>
|
|
||||||
<AccessibilityManager>
|
|
||||||
<ThemeManager>
|
|
||||||
<Screens localCorrupt={localCorrupt} />
|
|
||||||
</ThemeManager>
|
|
||||||
</AccessibilityManager>
|
|
||||||
</ActionSheetProvider>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
</IntlProvider>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
)
|
)
|
||||||
|
@@ -1,19 +1,24 @@
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import initQuery from '@utils/initQuery'
|
import { generateAccountKey, getAccountDetails, setAccount } from '@utils/storage/actions'
|
||||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
import { StorageGlobal } from '@utils/storage/global'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import haptics from './haptics'
|
import haptics from './haptics'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instance: InstanceLatest
|
account: NonNullable<StorageGlobal['accounts']>[number]
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
additionalActions?: () => void
|
additionalActions?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
|
const AccountButton: React.FC<Props> = ({ account, selected = false, additionalActions }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
const accountDetails = getAccountDetails(
|
||||||
|
['auth.account.acct', 'auth.domain', 'auth.account.id'],
|
||||||
|
account
|
||||||
|
)
|
||||||
|
if (!accountDetails) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -23,10 +28,17 @@ const AccountButton: React.FC<Props> = ({ instance, selected = false, additional
|
|||||||
marginBottom: StyleConstants.Spacing.M,
|
marginBottom: StyleConstants.Spacing.M,
|
||||||
marginRight: StyleConstants.Spacing.M
|
marginRight: StyleConstants.Spacing.M
|
||||||
}}
|
}}
|
||||||
content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`}
|
content={`@${accountDetails['auth.account.acct']}@${accountDetails['auth.domain']}${
|
||||||
|
selected ? ' ✓' : ''
|
||||||
|
}`}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
initQuery({ instance })
|
setAccount(
|
||||||
|
generateAccountKey({
|
||||||
|
domain: accountDetails['auth.domain'],
|
||||||
|
id: accountDetails['auth.account.id']
|
||||||
|
})
|
||||||
|
)
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
if (additionalActions) {
|
if (additionalActions) {
|
||||||
additionalActions()
|
additionalActions()
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
@@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({
|
|||||||
overlay = false,
|
overlay = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<View style={{ position: 'absolute' }}>
|
||||||
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const mainColor = useMemo(() => {
|
const mainColor = () => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return colors.blue
|
return colors.blue
|
||||||
} else if (overlay) {
|
} else if (overlay) {
|
||||||
@@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({
|
|||||||
return colors.primaryDefault
|
return colors.primaryDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, disabled, loading, selected])
|
}
|
||||||
|
|
||||||
const colorBackground = useMemo(() => {
|
const children = () => {
|
||||||
if (overlay) {
|
|
||||||
return colors.backgroundOverlayInvert
|
|
||||||
} else {
|
|
||||||
return colors.backgroundDefault
|
|
||||||
}
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name={content}
|
name={content}
|
||||||
color={mainColor}
|
color={mainColor()}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
style={{ opacity: loading ? 0 : 1 }}
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -103,7 +93,7 @@ const Button: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<CustomText
|
<CustomText
|
||||||
style={{
|
style={{
|
||||||
color: mainColor,
|
color: mainColor(),
|
||||||
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
||||||
opacity: loading ? 0 : 1
|
opacity: loading ? 0 : 1
|
||||||
}}
|
}}
|
||||||
@@ -111,11 +101,11 @@ const Button: React.FC<Props> = ({
|
|||||||
children={content}
|
children={content}
|
||||||
testID='text'
|
testID='text'
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, content, loading, disabled])
|
}
|
||||||
|
|
||||||
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
||||||
|
|
||||||
@@ -136,8 +126,8 @@ const Button: React.FC<Props> = ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: overlay ? 0 : 1,
|
borderWidth: overlay ? 0 : 1,
|
||||||
borderColor: mainColor,
|
borderColor: mainColor(),
|
||||||
backgroundColor: colorBackground,
|
backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault,
|
||||||
paddingVertical: StyleConstants.Spacing[spacing],
|
paddingVertical: StyleConstants.Spacing[spacing],
|
||||||
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
||||||
width: round && layoutHeight ? layoutHeight : undefined
|
width: round && layoutHeight ? layoutHeight : undefined
|
||||||
@@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
testID='base'
|
testID='base'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={selected || disabled || loading}
|
disabled={selected || disabled || loading}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Keyboard, Pressable, View } from 'react-native'
|
import { Keyboard, Pressable, View } from 'react-native'
|
||||||
import EmojisContext from './helpers/EmojisContext'
|
import EmojisContext from './Context'
|
||||||
|
|
||||||
const EmojisButton: React.FC = () => {
|
const EmojisButton: React.FC = () => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { emojis } from '@components/Emojis'
|
import { emojis } from '@components/Emojis'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { countInstanceEmoji } from '@utils/slices/instancesSlice'
|
import { StorageAccount } from '@utils/storage/account'
|
||||||
|
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
@@ -20,10 +20,9 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
import EmojisContext from './helpers/EmojisContext'
|
import EmojisContext from './Context'
|
||||||
|
|
||||||
const EmojisList = () => {
|
const EmojisList = () => {
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
|
|
||||||
@@ -75,7 +74,59 @@ const EmojisList = () => {
|
|||||||
key={emoji.shortcode}
|
key={emoji.shortcode}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
addEmoji(`:${emoji.shortcode}:`)
|
addEmoji(`:${emoji.shortcode}:`)
|
||||||
dispatch(countInstanceEmoji(emoji))
|
|
||||||
|
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
|
||||||
|
const calculateScore = (
|
||||||
|
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 }}
|
style={{ padding: StyleConstants.Spacing.S }}
|
||||||
>
|
>
|
||||||
|
@@ -2,14 +2,13 @@ import EmojisButton from '@components/Emojis/Button'
|
|||||||
import EmojisList from '@components/Emojis/List'
|
import EmojisList from '@components/Emojis/List'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
||||||
import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
|
import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Keyboard, KeyboardAvoidingView, View } from 'react-native'
|
import { Keyboard, KeyboardAvoidingView, View } from 'react-native'
|
||||||
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useSelector } from 'react-redux'
|
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Context'
|
||||||
import EmojisContext, { Emojis, emojisReducer, EmojisState } from './Emojis/helpers/EmojisContext'
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
inputProps: EmojisState['inputProps']
|
inputProps: EmojisState['inputProps']
|
||||||
@@ -36,7 +35,7 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
|
|||||||
|
|
||||||
const { t } = useTranslation(['componentEmojis'])
|
const { t } = useTranslation(['componentEmojis'])
|
||||||
const { data } = useEmojisQuery({})
|
const { data } = useEmojisQuery({})
|
||||||
const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
|
const [frequentEmojis] = useAccountStorage.object('emojis_frequent')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data.length) {
|
if (data && data.length) {
|
||||||
let sortedEmojis: NonNullable<Emojis['current']> = []
|
let sortedEmojis: NonNullable<Emojis['current']> = []
|
@@ -1,6 +1,6 @@
|
|||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
AccessibilityProps,
|
||||||
Image,
|
Image,
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle
|
ViewStyle
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
|
||||||
import { Blurhash } from 'react-native-blurhash'
|
import { Blurhash } from 'react-native-blurhash'
|
||||||
|
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||||
|
|
||||||
// blurhas -> if blurhash, show before any loading succeed
|
// blurhas -> if blurhash, show before any loading succeed
|
||||||
// original -> load original
|
// original -> load original
|
||||||
@@ -65,7 +65,7 @@ const GracefullyImage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blurhashView = useMemo(() => {
|
const blurhashView = () => {
|
||||||
if (hidden || !imageLoaded) {
|
if (hidden || !imageLoaded) {
|
||||||
if (blurhash) {
|
if (blurhash) {
|
||||||
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
||||||
@@ -75,7 +75,7 @@ const GracefullyImage = ({
|
|||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}, [hidden, imageLoaded])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -98,7 +98,7 @@ const GracefullyImage = ({
|
|||||||
style={[{ flex: 1 }, imageStyle]}
|
style={[{ flex: 1 }, imageStyle]}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
/>
|
/>
|
||||||
{blurhashView}
|
{blurhashView()}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
|||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { PropsWithChildren, useCallback, useState } from 'react'
|
import React, { PropsWithChildren, useState } from 'react'
|
||||||
import { Dimensions, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable, View } from 'react-native'
|
||||||
import Sparkline from './Sparkline'
|
import Sparkline from './Sparkline'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
@@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = () => {
|
||||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const padding = StyleConstants.Spacing.Global.PagePadding
|
const padding = StyleConstants.Spacing.Global.PagePadding
|
||||||
const width = Dimensions.get('window').width / 4
|
const width = Dimensions.get('window').width / 4
|
||||||
|
@@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { Pressable } from 'react-native'
|
import { Pressable } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({
|
|||||||
background = false,
|
background = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
@@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<CustomText
|
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} />
|
||||||
fontStyle='M'
|
|
||||||
style={{ color: colors.primaryDefault }}
|
|
||||||
children={content}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: background
|
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||||
? colors.backgroundOverlayDefault
|
|
||||||
: undefined,
|
|
||||||
minHeight: 44,
|
minHeight: 44,
|
||||||
minWidth: 44,
|
minWidth: 44,
|
||||||
marginLeft: native
|
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||||
? -StyleConstants.Spacing.S
|
|
||||||
: StyleConstants.Spacing.S,
|
|
||||||
...(type === 'icon' && {
|
...(type === 'icon' && {
|
||||||
borderRadius: 100
|
borderRadius: 100
|
||||||
}),
|
}),
|
||||||
|
@@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { AccessibilityProps, Pressable, View } from 'react-native'
|
import { AccessibilityProps, Pressable, View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
|
|
||||||
@@ -40,16 +40,14 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<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>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
@@ -60,7 +58,7 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
size={StyleConstants.Spacing.M * 1.25}
|
size={StyleConstants.Spacing.M * 1.25}
|
||||||
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
|
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -79,11 +77,11 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
children={content}
|
children={content}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, loading, disabled])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -92,7 +90,7 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityState={accessibilityState}
|
accessibilityState={accessibilityState}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import HeaderLeft from '@components/Header/Left'
|
|
||||||
import HeaderCenter from '@components/Header/Center'
|
import HeaderCenter from '@components/Header/Center'
|
||||||
|
import HeaderLeft from '@components/Header/Left'
|
||||||
import HeaderRight from '@components/Header/Right'
|
import HeaderRight from '@components/Header/Right'
|
||||||
|
|
||||||
export { HeaderLeft, HeaderCenter, HeaderRight }
|
export { HeaderLeft, HeaderCenter, HeaderRight }
|
@@ -3,7 +3,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||||||
import React, { forwardRef, RefObject } from 'react'
|
import React, { forwardRef, RefObject } from 'react'
|
||||||
import { Platform, TextInput, TextInputProps, View } from 'react-native'
|
import { Platform, TextInput, TextInputProps, View } from 'react-native'
|
||||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||||
import { EmojisState } from './Emojis/helpers/EmojisContext'
|
import { EmojisState } from './Emojis/Context'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
@@ -1,55 +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.FC<Props> = ({ style, header, content, potentialWidth }) => {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InstanceInfo
|
|
@@ -1,28 +1,35 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import apiGeneral from '@utils/api/general'
|
||||||
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
|
import queryClient from '@utils/queryHooks'
|
||||||
|
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
||||||
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
||||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
import { checkInstanceFeature, getInstances } from '@utils/slices/instancesSlice'
|
import { StorageAccount } from '@utils/storage/account'
|
||||||
|
import {
|
||||||
|
generateAccountKey,
|
||||||
|
getGlobalStorage,
|
||||||
|
setAccountStorage,
|
||||||
|
setGlobalStorage
|
||||||
|
} from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as AuthSession from 'expo-auth-session'
|
import * as AuthSession from 'expo-auth-session'
|
||||||
|
import * as Random from 'expo-random'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { RefObject, useCallback, useState } from 'react'
|
import React, { RefObject, useCallback, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||||
|
import base64 from 'react-native-base64'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { Placeholder } from 'rn-placeholder'
|
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
import InstanceInfo from './Info'
|
|
||||||
import CustomText from '../Text'
|
import CustomText from '../Text'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { storage } from '@utils/storage'
|
||||||
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
import { MMKV } from 'react-native-mmkv'
|
||||||
import queryClient from '@helpers/queryClient'
|
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import addInstance from '@utils/slices/instances/add'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollViewRef?: RefObject<ScrollView>
|
scrollViewRef?: RefObject<ScrollView>
|
||||||
@@ -47,8 +54,6 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
!!validUrl.isHttpsUri(`https://${domain}`) &&
|
!!validUrl.isHttpsUri(`https://${domain}`) &&
|
||||||
errorCode === 401
|
errorCode === 401
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const instances = useSelector(getInstances, () => true)
|
|
||||||
const instanceQuery = useInstanceQuery({
|
const instanceQuery = useInstanceQuery({
|
||||||
domain,
|
domain,
|
||||||
options: {
|
options: {
|
||||||
@@ -62,7 +67,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
const deprecateAuthFollow = featureCheck('deprecate_auth_follow')
|
||||||
|
|
||||||
const appsMutation = useAppsMutation({
|
const appsMutation = useAppsMutation({
|
||||||
retry: false,
|
retry: false,
|
||||||
@@ -97,14 +102,86 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
|
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
|
||||||
)
|
)
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
dispatch(
|
|
||||||
addInstance({
|
const {
|
||||||
domain,
|
body: { id, acct, avatar_static }
|
||||||
token: accessToken,
|
} = await apiGeneral<Mastodon.Account>({
|
||||||
instance: instanceQuery.data!,
|
method: 'get',
|
||||||
appData: { clientId, clientSecret }
|
domain,
|
||||||
})
|
url: `api/v1/accounts/verify_credentials`,
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
const accountKey = generateAccountKey({ domain, id })
|
||||||
|
const account = accounts?.find(account => account === accountKey)
|
||||||
|
|
||||||
|
const accountDetails: StorageAccount = {
|
||||||
|
'auth.clientId': clientId,
|
||||||
|
'auth.clientSecret': clientSecret,
|
||||||
|
'auth.token': accessToken,
|
||||||
|
'auth.domain': domain,
|
||||||
|
'auth.account.id': id,
|
||||||
|
'auth.account.acct': acct,
|
||||||
|
'auth.account.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: base64.encodeFromByteArray(Random.getRandomBytes(16))
|
||||||
|
},
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
storage.account = new MMKV({ id: accountKey })
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
setGlobalStorage('accounts', accounts?.concat([accountKey]))
|
||||||
|
}
|
||||||
|
setGlobalStorage('account.active', accountKey)
|
||||||
|
|
||||||
goBack && navigation.goBack()
|
goBack && navigation.goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +189,8 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
|
|
||||||
const processUpdate = useCallback(() => {
|
const processUpdate = useCallback(() => {
|
||||||
if (domain) {
|
if (domain) {
|
||||||
if (instances && instances.filter(instance => instance.url === domain).length) {
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
if (accounts && accounts.filter(account => account.startsWith(`${domain}/`)).length) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('componentInstance:update.alert.title'),
|
t('componentInstance:update.alert.title'),
|
||||||
t('componentInstance:update.alert.message'),
|
t('componentInstance:update.alert.message'),
|
||||||
@@ -208,7 +286,8 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
text === domain &&
|
text === domain &&
|
||||||
instanceQuery.isSuccess &&
|
instanceQuery.isSuccess &&
|
||||||
instanceQuery.data &&
|
instanceQuery.data &&
|
||||||
instanceQuery.data.uri
|
// @ts-ignore
|
||||||
|
(instanceQuery.data.domain || instanceQuery.data.uri)
|
||||||
) {
|
) {
|
||||||
processUpdate()
|
processUpdate()
|
||||||
}
|
}
|
||||||
@@ -228,7 +307,8 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
type='text'
|
type='text'
|
||||||
content={t('componentInstance:server.button')}
|
content={t('componentInstance:server.button')}
|
||||||
onPress={processUpdate}
|
onPress={processUpdate}
|
||||||
disabled={!instanceQuery.data?.uri && !whitelisted}
|
// @ts-ignore
|
||||||
|
disabled={!(instanceQuery.data?.domain || instanceQuery.data?.uri) && !whitelisted}
|
||||||
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -245,35 +325,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{t('componentInstance:server.whitelisted')}
|
{t('componentInstance:server.whitelisted')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
) : (
|
) : null}
|
||||||
<Placeholder>
|
|
||||||
<InstanceInfo
|
|
||||||
header={t('componentInstance:server.information.name')}
|
|
||||||
content={instanceQuery.data?.title || undefined}
|
|
||||||
potentialWidth={2}
|
|
||||||
/>
|
|
||||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'flex-start' }}
|
|
||||||
header={t('componentInstance:server.information.accounts')}
|
|
||||||
content={instanceQuery.data?.stats?.user_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'center' }}
|
|
||||||
header={t('componentInstance:server.information.statuses')}
|
|
||||||
content={instanceQuery.data?.stats?.status_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
<InstanceInfo
|
|
||||||
style={{ alignItems: 'flex-end' }}
|
|
||||||
header={t('componentInstance:server.information.domains')}
|
|
||||||
content={instanceQuery.data?.stats?.domain_count?.toString() || undefined}
|
|
||||||
potentialWidth={4}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Placeholder>
|
|
||||||
)}
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import CustomText from '@components/Text'
|
||||||
import { View } from 'react-native'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import CustomText from '@components/Text'
|
import React from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
heading: string
|
heading: string
|
||||||
|
@@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { ColorDefinitions } from '@utils/styles/themes'
|
import { ColorDefinitions } from '@utils/styles/themes'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
||||||
@@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { screenReaderEnabled } = useAccessibility()
|
const { screenReaderEnabled } = useAccessibility()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
|
||||||
() => (
|
|
||||||
<View style={{ position: 'absolute' }}>
|
|
||||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{ minHeight: 50 }}
|
style={{ minHeight: 50 }}
|
||||||
@@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{loading && loadingSpinkit}
|
{loading ? (
|
||||||
|
<View style={{ position: 'absolute' }}>
|
||||||
|
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
import ParseEmojis from './Parse/Emojis'
|
|
||||||
import ParseHTML from './Parse/HTML'
|
|
||||||
|
|
||||||
export { ParseEmojis, ParseHTML }
|
|
@@ -1,13 +1,12 @@
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Platform, TextStyle } from 'react-native'
|
import { Platform, TextStyle } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
|
|
||||||
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
|
||||||
@@ -21,81 +20,85 @@ export interface Props {
|
|||||||
style?: TextStyle
|
style?: TextStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseEmojis = React.memo(
|
const ParseEmojis: React.FC<Props> = ({
|
||||||
({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
|
content,
|
||||||
if (!content) return null
|
emojis,
|
||||||
|
size = 'M',
|
||||||
|
adaptiveSize = false,
|
||||||
|
fontBold = false,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
if (!content) return null
|
||||||
|
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
|
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const adaptedFontsize = adaptiveScale(
|
const adaptedFontsize = adaptiveScale(
|
||||||
StyleConstants.Font.Size[size],
|
StyleConstants.Font.Size[size],
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
)
|
)
|
||||||
const adaptedLineheight = adaptiveScale(
|
const adaptedLineheight = adaptiveScale(
|
||||||
StyleConstants.Font.LineHeight[size],
|
StyleConstants.Font.LineHeight[size],
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomText
|
<CustomText
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
color: colors.primaryDefault,
|
color: colors.primaryDefault,
|
||||||
fontSize: adaptedFontsize,
|
fontSize: adaptedFontsize,
|
||||||
lineHeight: adaptedLineheight
|
lineHeight: adaptedLineheight
|
||||||
},
|
},
|
||||||
style
|
style
|
||||||
]}
|
]}
|
||||||
fontWeight={fontBold ? 'Bold' : undefined}
|
fontWeight={fontBold ? 'Bold' : undefined}
|
||||||
>
|
>
|
||||||
{emojis ? (
|
{emojis ? (
|
||||||
content
|
content
|
||||||
.split(regexEmoji)
|
.split(regexEmoji)
|
||||||
.filter(f => f)
|
.filter(f => f)
|
||||||
.map((str, i) => {
|
.map((str, i) => {
|
||||||
if (str.match(regexEmoji)) {
|
if (str.match(regexEmoji)) {
|
||||||
const emojiShortcode = str.split(regexEmoji)[1]
|
const emojiShortcode = str.split(regexEmoji)[1]
|
||||||
const emojiIndex = emojis.findIndex(emoji => {
|
const emojiIndex = emojis.findIndex(emoji => {
|
||||||
return emojiShortcode === `:${emoji.shortcode}:`
|
return emojiShortcode === `:${emoji.shortcode}:`
|
||||||
})
|
})
|
||||||
if (emojiIndex === -1) {
|
if (emojiIndex === -1) {
|
||||||
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return <CustomText key={i}>{str}</CustomText>
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
) : (
|
return <CustomText key={i}>{str}</CustomText>
|
||||||
<CustomText>{content}</CustomText>
|
}
|
||||||
)}
|
})
|
||||||
</CustomText>
|
) : (
|
||||||
)
|
<CustomText>{content}</CustomText>
|
||||||
},
|
)}
|
||||||
(prev, next) => prev.content === next.content && prev.style?.color === next.style?.color
|
</CustomText>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ParseEmojis
|
export default ParseEmojis
|
||||||
|
@@ -1,146 +1,19 @@
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import openLink from '@components/openLink'
|
import openLink from '@components/openLink'
|
||||||
import ParseEmojis from '@components/Parse/Emojis'
|
import ParseEmojis from '@components/Parse/Emojis'
|
||||||
import CustomText from '@components/Text'
|
|
||||||
import { getHost } from '@helpers/urlMatcher'
|
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native'
|
import { useNavigation, useRoute } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
import { useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { isEqual } from 'lodash'
|
import { ChildNode } from 'domhandler'
|
||||||
import React, { useCallback, useState } from 'react'
|
import { ElementType, parseDocument } from 'htmlparser2'
|
||||||
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, TextStyleIOS, View } from 'react-native'
|
import { Pressable, Text, 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 host = getHost(href)
|
|
||||||
// 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 : host}
|
|
||||||
{!shouldBeTag ? '/...' : null}
|
|
||||||
</CustomText>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'p':
|
|
||||||
if (!node.children.length) {
|
|
||||||
return <View key={index} /> // bug when the tag is empty
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
content: string
|
content: string
|
||||||
@@ -159,158 +32,233 @@ export interface Props {
|
|||||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseHTML = React.memo(
|
const ParseHTML: React.FC<Props> = ({
|
||||||
({
|
content,
|
||||||
content,
|
size = 'M',
|
||||||
size = 'M',
|
textStyles,
|
||||||
textStyles,
|
adaptiveSize = false,
|
||||||
adaptiveSize = false,
|
emojis,
|
||||||
emojis,
|
mentions,
|
||||||
mentions,
|
tags,
|
||||||
tags,
|
showFullLink = false,
|
||||||
showFullLink = false,
|
numberOfLines = 10,
|
||||||
numberOfLines = 10,
|
expandHint,
|
||||||
expandHint,
|
highlighted = false,
|
||||||
highlighted = false,
|
disableDetails = false,
|
||||||
disableDetails = false,
|
selectable = false,
|
||||||
selectable = false,
|
setSpoilerExpanded
|
||||||
setSpoilerExpanded
|
}) => {
|
||||||
}: Props) => {
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
const adaptedFontsize = adaptiveScale(
|
||||||
const adaptedFontsize = adaptiveScale(
|
StyleConstants.Font.Size[size],
|
||||||
StyleConstants.Font.Size[size],
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
)
|
||||||
)
|
const adaptedLineheight = adaptiveScale(
|
||||||
const adaptedLineheight = adaptiveScale(
|
StyleConstants.Font.LineHeight[size],
|
||||||
StyleConstants.Font.LineHeight[size],
|
adaptiveSize ? adaptiveFontsize : 0
|
||||||
adaptiveSize ? adaptiveFontsize : 0
|
)
|
||||||
)
|
|
||||||
|
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
const route = useRoute()
|
const { params } = useRoute()
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('componentParse')
|
const { t } = useTranslation('componentParse')
|
||||||
if (!expandHint) {
|
if (!expandHint) {
|
||||||
expandHint = t('HTML.defaultHint')
|
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')) return ''
|
||||||
|
if (node.attribs.class?.includes('ellipsis'))
|
||||||
|
return node.children.map(child => unwrapNode(child)).join('') + '...'
|
||||||
|
}
|
||||||
|
return node.children.map(child => unwrapNode(child)).join('')
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (disableDetails) {
|
const renderNode = (node: ChildNode, index: number) => {
|
||||||
numberOfLines = 4
|
switch (node.type) {
|
||||||
}
|
case ElementType.Text:
|
||||||
|
|
||||||
const renderNodeCallback = useCallback(
|
|
||||||
(node: any, index: any) =>
|
|
||||||
renderNode({
|
|
||||||
routeParams: route.params,
|
|
||||||
colors,
|
|
||||||
node,
|
|
||||||
index,
|
|
||||||
adaptedFontsize,
|
|
||||||
adaptedLineheight,
|
|
||||||
navigation,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
showFullLink,
|
|
||||||
disableDetails
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const textComponent = useCallback(({ children }: any) => {
|
|
||||||
if (children) {
|
|
||||||
return (
|
return (
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
content={children?.toString()}
|
key={index}
|
||||||
|
content={node.data}
|
||||||
emojis={emojis}
|
emojis={emojis}
|
||||||
size={size}
|
size={size}
|
||||||
adaptiveSize={adaptiveSize}
|
adaptiveSize={adaptiveSize}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
case ElementType.Tag:
|
||||||
return null
|
switch (node.name) {
|
||||||
}
|
case 'a':
|
||||||
}, [])
|
const classes = node.attribs.class
|
||||||
const rootComponent = useCallback(
|
const href = node.attribs.href
|
||||||
({ children }: any) => {
|
if (classes) {
|
||||||
const { t } = useTranslation('componentParse')
|
if (classes.includes('hashtag')) {
|
||||||
|
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1].toLowerCase()
|
||||||
|
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={node.children.map(unwrapNode).join('')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (classes.includes('mention') && mentions?.length) {
|
||||||
|
const mentionIndex = mentions.findIndex(mention => mention.url === href)
|
||||||
|
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
|
||||||
|
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{ color: mentionIndex > -1 ? colors.blue : undefined }}
|
||||||
|
onPress={() =>
|
||||||
|
mentionIndex > -1 &&
|
||||||
|
!disableDetails &&
|
||||||
|
!sameAccount &&
|
||||||
|
navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] })
|
||||||
|
}
|
||||||
|
children={node.children.map(unwrapNode).join('')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [totalLines, setTotalLines] = useState<number>()
|
const content = node.children.map(child => unwrapNode(child)).join('')
|
||||||
const [expanded, setExpanded] = useState(highlighted)
|
const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content)
|
||||||
|
return (
|
||||||
return (
|
<Text
|
||||||
<View style={{ overflow: 'hidden' }}>
|
key={index}
|
||||||
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
style={{ color: colors.blue }}
|
||||||
<Pressable
|
onPress={async () => {
|
||||||
accessibilityLabel={t('HTML.accessibilityHint')}
|
if (!disableDetails) {
|
||||||
onPress={() => {
|
if (shouldBeTag) {
|
||||||
layoutAnimation()
|
navigation.push('Tab-Shared-Hashtag', {
|
||||||
setExpanded(!expanded)
|
hashtag: content.substring(1)
|
||||||
if (setSpoilerExpanded) {
|
})
|
||||||
setSpoilerExpanded(!expanded)
|
} else {
|
||||||
|
await openLink(href, navigation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
children={content !== href ? content : showFullLink ? href : content}
|
||||||
flexDirection: 'row',
|
/>
|
||||||
justifyContent: 'center',
|
)
|
||||||
alignItems: 'center',
|
break
|
||||||
minHeight: 44,
|
case 'p':
|
||||||
backgroundColor: colors.backgroundDefault
|
if (index < document.children.length - 1) {
|
||||||
}}
|
return (
|
||||||
>
|
<Text key={index}>
|
||||||
<CustomText
|
{node.children.map((c, i) => renderNode(c, i))}
|
||||||
style={{
|
<Text style={{ lineHeight: adaptedLineheight / 2 }}>{'\n\n'}</Text>
|
||||||
textAlign: 'center',
|
</Text>
|
||||||
...StyleConstants.FontStyle.S,
|
)
|
||||||
color: colors.primaryDefault,
|
} else {
|
||||||
marginRight: StyleConstants.Spacing.S
|
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||||
}}
|
}
|
||||||
children={t('HTML.expanded', {
|
default:
|
||||||
hint: expandHint,
|
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
|
||||||
moreLines:
|
}
|
||||||
numberOfLines > 1 && typeof totalLines === 'number'
|
}
|
||||||
? t('HTML.moreLines', { count: totalLines - numberOfLines })
|
return null
|
||||||
: ''
|
}
|
||||||
})}
|
return (
|
||||||
/>
|
<View style={{ overflow: 'hidden' }}>
|
||||||
<Icon
|
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
|
||||||
name={expanded ? 'Minimize2' : 'Maximize2'}
|
<Pressable
|
||||||
color={colors.primaryDefault}
|
accessibilityLabel={t('HTML.accessibilityHint')}
|
||||||
strokeWidth={2}
|
onPress={() => {
|
||||||
size={StyleConstants.Font.Size[size]}
|
layoutAnimation()
|
||||||
/>
|
setExpanded(!expanded)
|
||||||
</Pressable>
|
if (setSpoilerExpanded) {
|
||||||
) : null}
|
setSpoilerExpanded(!expanded)
|
||||||
<CustomText
|
}
|
||||||
children={children}
|
}}
|
||||||
onTextLayout={({ nativeEvent }) => {
|
style={{
|
||||||
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
|
flexDirection: 'row',
|
||||||
setTotalLines(nativeEvent.lines.length)
|
justifyContent: 'center',
|
||||||
}
|
alignItems: 'center',
|
||||||
}}
|
minHeight: 44,
|
||||||
style={{
|
backgroundColor: colors.backgroundDefault
|
||||||
...textStyles,
|
}}
|
||||||
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
>
|
||||||
}}
|
<Text
|
||||||
numberOfLines={
|
style={{
|
||||||
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
|
textAlign: 'center',
|
||||||
}
|
...StyleConstants.FontStyle.S,
|
||||||
selectable={selectable}
|
color: colors.primaryDefault,
|
||||||
/>
|
marginRight: StyleConstants.Spacing.S
|
||||||
</View>
|
}}
|
||||||
)
|
children={t('HTML.expanded', {
|
||||||
},
|
hint: expandHint,
|
||||||
[theme]
|
moreLines:
|
||||||
)
|
numberOfLines > 1 && typeof totalLines === 'number'
|
||||||
|
? t('HTML.moreLines', { count: totalLines - numberOfLines })
|
||||||
return (
|
: ''
|
||||||
<HTMLView
|
})}
|
||||||
value={content}
|
/>
|
||||||
TextComponent={textComponent}
|
<Icon
|
||||||
RootComponent={rootComponent}
|
name={expanded ? 'Minimize2' : 'Maximize2'}
|
||||||
renderNode={renderNodeCallback}
|
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,
|
||||||
|
...textStyles,
|
||||||
|
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
||||||
|
}}
|
||||||
|
numberOfLines={
|
||||||
|
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
|
||||||
|
}
|
||||||
|
selectable={selectable}
|
||||||
/>
|
/>
|
||||||
)
|
</View>
|
||||||
},
|
)
|
||||||
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default ParseHTML
|
export default ParseHTML
|
||||||
|
4
src/components/Parse/index.tsx
Normal file
4
src/components/Parse/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import ParseEmojis from './Emojis'
|
||||||
|
import ParseHTML from './HTML'
|
||||||
|
|
||||||
|
export { ParseEmojis, ParseHTML }
|
@@ -1,6 +1,7 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
@@ -8,7 +9,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
|
import { useRoute } from '@react-navigation/native'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import {
|
import {
|
||||||
QueryKeyRelationship,
|
QueryKeyRelationship,
|
||||||
useRelationshipMutation,
|
useRelationshipMutation,
|
||||||
useRelationshipQuery
|
useRelationshipQuery
|
||||||
} from '@utils/queryHooks/relationship'
|
} from '@utils/queryHooks/relationship'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { useRoute } from '@react-navigation/native'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
@@ -24,7 +23,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { t } = useTranslation(['common', 'componentRelationship'])
|
const { t } = useTranslation(['common', 'componentRelationship'])
|
||||||
|
|
||||||
const canFollowNotify = useSelector(checkInstanceFeature('account_follow_notify'))
|
const canFollowNotify = featureCheck('account_follow_notify')
|
||||||
|
|
||||||
const query = useRelationshipQuery({ id })
|
const query = useRelationshipQuery({ id })
|
||||||
|
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
import apiInstance from '@api/instance'
|
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
||||||
import TimelineActions from './Shared/Actions'
|
import TimelineActions from './Shared/Actions'
|
||||||
import TimelineContent from './Shared/Content'
|
import TimelineContent from './Shared/Content'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
@@ -115,4 +115,4 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineConversation
|
export default React.memo(TimelineConversation, () => true)
|
||||||
|
@@ -9,17 +9,18 @@ import TimelineCard from '@components/Timeline/Shared/Card'
|
|||||||
import TimelineContent from '@components/Timeline/Shared/Content'
|
import TimelineContent from '@components/Timeline/Shared/Content'
|
||||||
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
|
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
|
||||||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||||
import removeHTML from '@helpers/removeHTML'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
|
import removeHTML from '@utils/helpers/removeHTML'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { Fragment, useRef, useState } from 'react'
|
||||||
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as ContextMenu from 'zeego/context-menu'
|
import * as ContextMenu from 'zeego/context-menu'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
import TimelineFeedback from './Shared/Feedback'
|
import TimelineFeedback from './Shared/Feedback'
|
||||||
@@ -60,14 +61,15 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
const ownAccount = status.account?.id === instanceAccount?.id
|
const ownAccount = status.account?.id === accountId
|
||||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||||
instanceAccount?.preferences?.['reading:expand:spoilers'] || false
|
preferences?.['reading:expand:spoilers'] || false
|
||||||
)
|
)
|
||||||
const spoilerHidden = status.spoiler_text?.length
|
const spoilerHidden = status.spoiler_text?.length
|
||||||
? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||||
: false
|
: false
|
||||||
const detectedLanguage = useRef<string>(status.language || '')
|
const detectedLanguage = useRef<string>(status.language || '')
|
||||||
|
|
||||||
@@ -134,7 +136,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
if (!ownAccount) {
|
if (!ownAccount) {
|
||||||
let filterResults: FilteredProps['filterResults'] = []
|
let filterResults: FilteredProps['filterResults'] = []
|
||||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||||
if (hasFilterServerSide) {
|
if (hasFilterServerSide) {
|
||||||
if (status.filtered?.length) {
|
if (status.filtered?.length) {
|
||||||
filterResults = status.filtered?.map(filter => filter.filter)
|
filterResults = status.filtered?.map(filter => filter.filter)
|
||||||
@@ -196,8 +198,8 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
{[mShare, mStatus, mInstance].map(type => (
|
{[mShare, mStatus, mInstance].map((type, i) => (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{type.map((mGroup, index) => (
|
{type.map((mGroup, index) => (
|
||||||
<ContextMenu.Group key={index}>
|
<ContextMenu.Group key={index}>
|
||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
@@ -208,7 +210,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</ContextMenu.Group>
|
</ContextMenu.Group>
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
@@ -219,4 +221,4 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineDefault
|
export default React.memo(TimelineDefault, () => true)
|
||||||
|
@@ -11,14 +11,15 @@ import TimelineHeaderNotification from '@components/Timeline/Shared/HeaderNotifi
|
|||||||
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timeline/Shared/Poll'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as ContextMenu from 'zeego/context-menu'
|
import * as ContextMenu from 'zeego/context-menu'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
|
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
|
||||||
@@ -31,7 +32,8 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
const status = notification.status?.reblog ? notification.status.reblog : notification.status
|
const status = notification.status?.reblog ? notification.status.reblog : notification.status
|
||||||
const account =
|
const account =
|
||||||
@@ -40,25 +42,17 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
: notification.status
|
: notification.status
|
||||||
? notification.status.account
|
? notification.status.account
|
||||||
: notification.account
|
: notification.account
|
||||||
const ownAccount = notification.account?.id === instanceAccount?.id
|
const ownAccount = notification.account?.id === accountId
|
||||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||||
instanceAccount.preferences?.['reading:expand:spoilers'] || false
|
preferences?.['reading:expand:spoilers'] || false
|
||||||
)
|
)
|
||||||
const spoilerHidden = notification.status?.spoiler_text?.length
|
const spoilerHidden = notification.status?.spoiler_text?.length
|
||||||
? !instanceAccount.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||||
: false
|
: false
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
notification.status &&
|
|
||||||
navigation.push('Tab-Shared-Toot', {
|
|
||||||
toot: notification.status,
|
|
||||||
rootQueryKey: queryKey
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -117,7 +111,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
if (!ownAccount) {
|
if (!ownAccount) {
|
||||||
let filterResults: FilteredProps['filterResults'] = []
|
let filterResults: FilteredProps['filterResults'] = []
|
||||||
const [filterRevealed, setFilterRevealed] = useState(false)
|
const [filterRevealed, setFilterRevealed] = useState(false)
|
||||||
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
|
const hasFilterServerSide = featureCheck('filter_server_side')
|
||||||
if (notification.status) {
|
if (notification.status) {
|
||||||
if (hasFilterServerSide) {
|
if (hasFilterServerSide) {
|
||||||
if (notification.status.filtered?.length) {
|
if (notification.status.filtered?.length) {
|
||||||
@@ -157,15 +151,21 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
onPress={onPress}
|
onPress={() =>
|
||||||
|
notification.status &&
|
||||||
|
navigation.push('Tab-Shared-Toot', {
|
||||||
|
toot: notification.status,
|
||||||
|
rootQueryKey: queryKey
|
||||||
|
})
|
||||||
|
}
|
||||||
onLongPress={() => {}}
|
onLongPress={() => {}}
|
||||||
children={main()}
|
children={main()}
|
||||||
/>
|
/>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
{[mShare, mStatus, mInstance].map(type => (
|
{[mShare, mStatus, mInstance].map((type, i) => (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{type.map((mGroup, index) => (
|
{type.map((mGroup, index) => (
|
||||||
<ContextMenu.Group key={index}>
|
<ContextMenu.Group key={index}>
|
||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
@@ -176,7 +176,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
))}
|
))}
|
||||||
</ContextMenu.Group>
|
</ContextMenu.Group>
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
@@ -185,4 +185,4 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineNotifications
|
export default React.memo(TimelineNotifications, () => true)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
||||||
import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
@@ -16,7 +17,6 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming
|
withTiming
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
flRef: RefObject<FlatList<any>>
|
flRef: RefObject<FlatList<any>>
|
||||||
|
@@ -2,24 +2,23 @@ import Icon from '@components/Icon'
|
|||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
QueryKeyTimeline,
|
QueryKeyTimeline,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineActions: React.FC = () => {
|
const TimelineActions: React.FC = () => {
|
||||||
@@ -76,12 +75,12 @@ const TimelineActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
const onPressReply = useCallback(() => {
|
const onPressReply = () => {
|
||||||
const accts = uniqBy(
|
const accts = uniqBy(
|
||||||
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||||
.concat(status.mentions)
|
.concat(status.mentions)
|
||||||
.filter(d => d?.id !== instanceAccount?.id),
|
.filter(d => d?.id !== accountId),
|
||||||
d => d?.id
|
d => d?.id
|
||||||
).map(d => d?.acct)
|
).map(d => d?.acct)
|
||||||
navigation.navigate('Screen-Compose', {
|
navigation.navigate('Screen-Compose', {
|
||||||
@@ -90,9 +89,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
accts,
|
accts,
|
||||||
queryKey
|
queryKey
|
||||||
})
|
})
|
||||||
}, [status.replies_count])
|
}
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const onPressReblog = useCallback(() => {
|
const onPressReblog = () => {
|
||||||
if (!status.reblogged) {
|
if (!status.reblogged) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@@ -158,8 +157,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const onPressFavourite = useCallback(() => {
|
const onPressFavourite = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -173,8 +172,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
countValue: status.favourites_count
|
countValue: status.favourites_count
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const onPressBookmark = useCallback(() => {
|
const onPressBookmark = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -188,28 +187,25 @@ const TimelineActions: React.FC = () => {
|
|||||||
countValue: undefined
|
countValue: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
const childrenReply = useMemo(
|
const childrenReply = () => (
|
||||||
() => (
|
<>
|
||||||
<>
|
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||||
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
{status.replies_count > 0 ? (
|
||||||
{status.replies_count > 0 ? (
|
<CustomText
|
||||||
<CustomText
|
style={{
|
||||||
style={{
|
color: colors.secondary,
|
||||||
color: colors.secondary,
|
fontSize: StyleConstants.Font.Size.M,
|
||||||
fontSize: StyleConstants.Font.Size.M,
|
marginLeft: StyleConstants.Spacing.XS
|
||||||
marginLeft: StyleConstants.Spacing.XS
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{status.replies_count}
|
||||||
{status.replies_count}
|
</CustomText>
|
||||||
</CustomText>
|
) : null}
|
||||||
) : null}
|
</>
|
||||||
</>
|
|
||||||
),
|
|
||||||
[status.replies_count]
|
|
||||||
)
|
)
|
||||||
const childrenReblog = useMemo(() => {
|
const childrenReblog = () => {
|
||||||
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
||||||
const disabled =
|
const disabled =
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
@@ -237,8 +233,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const childrenFavourite = useMemo(() => {
|
const childrenFavourite = () => {
|
||||||
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -257,13 +253,13 @@ const TimelineActions: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const childrenBookmark = useMemo(() => {
|
const childrenBookmark = () => {
|
||||||
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
||||||
)
|
)
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row' }}>
|
<View style={{ flexDirection: 'row' }}>
|
||||||
@@ -276,7 +272,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReply}
|
onPress={onPressReply}
|
||||||
children={childrenReply}
|
children={childrenReply()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -290,7 +286,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReblog}
|
onPress={onPressReblog}
|
||||||
children={childrenReblog}
|
children={childrenReblog()}
|
||||||
disabled={
|
disabled={
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
}
|
}
|
||||||
@@ -307,7 +303,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressFavourite}
|
onPress={onPressFavourite}
|
||||||
children={childrenFavourite}
|
children={childrenFavourite()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -321,7 +317,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressBookmark}
|
onPress={onPressBookmark}
|
||||||
children={childrenBookmark}
|
children={childrenBookmark()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@@ -7,13 +7,12 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelineAttachment = () => {
|
const TimelineAttachment = () => {
|
||||||
@@ -28,13 +27,10 @@ const TimelineAttachment = () => {
|
|||||||
|
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const account = useSelector(
|
const { data: preferences } = usePreferencesQuery()
|
||||||
getInstanceAccount,
|
|
||||||
(prev, next) =>
|
|
||||||
prev.preferences?.['reading:expand:media'] === next.preferences?.['reading:expand:media']
|
|
||||||
)
|
|
||||||
const defaultSensitive = () => {
|
const defaultSensitive = () => {
|
||||||
switch (account.preferences?.['reading:expand:media']) {
|
switch (preferences?.['reading:expand:media']) {
|
||||||
case 'show_all':
|
case 'show_all':
|
||||||
return false
|
return false
|
||||||
case 'hide_all':
|
case 'hide_all':
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
|
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
|
||||||
|
import { Platform } from 'expo-modules-core'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { Blurhash } from 'react-native-blurhash'
|
import { Blurhash } from 'react-native-blurhash'
|
||||||
import AttachmentAltText from './AltText'
|
import AttachmentAltText from './AltText'
|
||||||
import { Platform } from 'expo-modules-core'
|
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
|
||||||
import { aspectRatio } from './dimensions'
|
import { aspectRatio } from './dimensions'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { getSettingsAutoplayGifv } from '@utils/slices/settingsSlice'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
total: number
|
total: number
|
||||||
@@ -27,7 +26,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||||||
gifv = false
|
gifv = false
|
||||||
}) => {
|
}) => {
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const autoplayGifv = useSelector(getSettingsAutoplayGifv)
|
const [autoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv')
|
||||||
|
|
||||||
const videoPlayer = useRef<Video>(null)
|
const videoPlayer = useRef<Video>(null)
|
||||||
const [videoLoading, setVideoLoading] = useState(false)
|
const [videoLoading, setVideoLoading] = useState(false)
|
||||||
|
@@ -2,8 +2,8 @@ import ComponentAccount from '@components/Account'
|
|||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import openLink from '@components/openLink'
|
import openLink from '@components/openLink'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { matchAccount, matchStatus } from '@utils/helpers/urlMatcher'
|
||||||
import { useAccountQuery } from '@utils/queryHooks/account'
|
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
import { useStatusQuery } from '@utils/queryHooks/status'
|
import { useStatusQuery } from '@utils/queryHooks/status'
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform } from 'react-native'
|
import { Platform, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { isRtlLang } from 'rtl-detect'
|
import { isRtlLang } from 'rtl-detect'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
@@ -21,10 +20,11 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('componentTimeline')
|
const { t } = useTranslation('componentTimeline')
|
||||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View>
|
||||||
{status.spoiler_text?.length ? (
|
{status.spoiler_text?.length ? (
|
||||||
<>
|
<>
|
||||||
<ParseHTML
|
<ParseHTML
|
||||||
@@ -63,7 +63,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
mentions={status.mentions}
|
mentions={status.mentions}
|
||||||
tags={status.tags}
|
tags={status.tags}
|
||||||
numberOfLines={
|
numberOfLines={
|
||||||
instanceAccount.preferences?.['reading:expand:spoilers'] || inThread
|
preferences?.['reading:expand:spoilers'] || inThread
|
||||||
? notificationOwnToot
|
? notificationOwnToot
|
||||||
? 2
|
? 2
|
||||||
: 999
|
: 999
|
||||||
@@ -97,7 +97,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import removeHTML from '@helpers/removeHTML'
|
import queryClient from '@utils/queryHooks'
|
||||||
import { store } from '@root/store'
|
import removeHTML from '@utils/helpers/removeHTML'
|
||||||
|
import { QueryKeyFilters } from '@utils/queryHooks/filters'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstance } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -75,7 +75,6 @@ export const shouldFilter = ({
|
|||||||
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
|
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
|
||||||
}): FilteredProps['filterResults'] | undefined => {
|
}): FilteredProps['filterResults'] | undefined => {
|
||||||
const page = queryKey[1]
|
const page = queryKey[1]
|
||||||
const instance = getInstance(store.getState())
|
|
||||||
|
|
||||||
let returnFilter: FilteredProps['filterResults'] | undefined
|
let returnFilter: FilteredProps['filterResults'] | undefined
|
||||||
|
|
||||||
@@ -100,7 +99,8 @@ export const shouldFilter = ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instance?.filters?.forEach(filter => {
|
const queryKeyFilters: QueryKeyFilters = ['Filters']
|
||||||
|
queryClient.getQueryData<Mastodon.Filter<'v1'>[]>(queryKeyFilters)?.forEach(filter => {
|
||||||
if (returnFilter) {
|
if (returnFilter) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status'
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { Platform, View } from 'react-native'
|
import { Platform, View } from 'react-native'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
@@ -53,8 +53,8 @@ const TimelineHeaderAndroid: React.FC = () => {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{[mShare, mAccount, mStatus].map(type => (
|
{[mShare, mAccount, mStatus].map((type, i) => (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{type.map((mGroup, index) => (
|
{type.map((mGroup, index) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<DropdownMenu.Group key={index}>
|
||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
@@ -65,7 +65,7 @@ const TimelineHeaderAndroid: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
@@ -2,13 +2,13 @@ import Icon from '@components/Icon'
|
|||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||||
|
@@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status'
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Pressable, View } from 'react-native'
|
import { Platform, Pressable, View } from 'react-native'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
@@ -83,8 +83,8 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{[mShare, mAccount, mStatus].map(type => (
|
{[mShare, mAccount, mStatus].map((type, i) => (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{type.map((mGroup, index) => (
|
{type.map((mGroup, index) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<DropdownMenu.Group key={index}>
|
||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
@@ -95,7 +95,7 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
@@ -5,15 +5,14 @@ import menuShare from '@components/contextMenu/share'
|
|||||||
import menuStatus from '@components/contextMenu/status'
|
import menuStatus from '@components/contextMenu/status'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { Fragment, useContext, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Pressable, View } from 'react-native'
|
import { Platform, Pressable, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
import HeaderSharedAccount from './HeaderShared/Account'
|
import HeaderSharedAccount from './HeaderShared/Account'
|
||||||
@@ -48,8 +47,6 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
const mStatus = menuStatus({ status, queryKey })
|
const mStatus = menuStatus({ status, queryKey })
|
||||||
const mInstance = menuInstance({ status, queryKey })
|
const mInstance = menuInstance({ status, queryKey })
|
||||||
|
|
||||||
const url = useSelector(getInstanceUrl)
|
|
||||||
|
|
||||||
const actions = () => {
|
const actions = () => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
@@ -63,7 +60,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
content={t('shared.actions.openReport')}
|
content={t('shared.actions.openReport')}
|
||||||
onPress={async () =>
|
onPress={async () =>
|
||||||
WebBrowser.openAuthSessionAsync(
|
WebBrowser.openAuthSessionAsync(
|
||||||
`https://${url}/admin/reports/${notification.report.id}`,
|
`https://${getAccountStorage.string('auth.domain')}/admin/reports/${
|
||||||
|
notification.report.id
|
||||||
|
}`,
|
||||||
'tooot://tooot',
|
'tooot://tooot',
|
||||||
{
|
{
|
||||||
...(await browserPackage()),
|
...(await browserPackage()),
|
||||||
@@ -90,8 +89,8 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{[mShare, mStatus, mAccount, mInstance].map(type => (
|
{[mShare, mStatus, mAccount, mInstance].map((type, i) => (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{type.map((mGroup, index) => (
|
{type.map((mGroup, index) => (
|
||||||
<DropdownMenu.Group key={index}>
|
<DropdownMenu.Group key={index}>
|
||||||
{mGroup.map(menu => (
|
{mGroup.map(menu => (
|
||||||
@@ -102,7 +101,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
))}
|
))}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { ParseEmojis } from '@root/components/Parse'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@@ -5,6 +5,7 @@ import { displayMessage } from '@components/Message'
|
|||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import RelativeTime from '@components/RelativeTime'
|
import RelativeTime from '@components/RelativeTime'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
@@ -13,10 +14,9 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { maxBy } from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
const TimelinePoll: React.FC = () => {
|
const TimelinePoll: React.FC = () => {
|
||||||
@@ -58,6 +58,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
theme,
|
theme,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:message.error.message', {
|
message: t('common:message.error.message', {
|
||||||
|
// @ts-ignore
|
||||||
function: t(`componentTimeline:shared.poll.meta.button.${theParams.payload.type}` as any)
|
function: t(`componentTimeline:shared.poll.meta.button.${theParams.payload.type}` as any)
|
||||||
}),
|
}),
|
||||||
...(err.status &&
|
...(err.status &&
|
||||||
@@ -72,7 +73,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const pollButton = useMemo(() => {
|
const pollButton = () => {
|
||||||
if (!poll.expired) {
|
if (!poll.expired) {
|
||||||
if (!ownAccount && !poll.voted) {
|
if (!ownAccount && !poll.voted) {
|
||||||
return (
|
return (
|
||||||
@@ -126,17 +127,14 @@ const TimelinePoll: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
|
}
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = (index: number): string =>
|
||||||
(index: number): string =>
|
allOptions[index]
|
||||||
allOptions[index]
|
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
: `${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
|
||||||
[allOptions]
|
|
||||||
)
|
|
||||||
|
|
||||||
const pollBodyDisallow = useMemo(() => {
|
const pollBodyDisallow = () => {
|
||||||
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
||||||
@@ -190,8 +188,8 @@ const TimelinePoll: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
}, [theme, poll.options])
|
}
|
||||||
const pollBodyAllow = useMemo(() => {
|
const pollBodyAllow = () => {
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
@@ -228,7 +226,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))
|
))
|
||||||
}, [theme, allOptions])
|
}
|
||||||
|
|
||||||
const pollVoteCounts = () => {
|
const pollVoteCounts = () => {
|
||||||
if (poll.voters_count !== null) {
|
if (poll.voters_count !== null) {
|
||||||
@@ -262,7 +260,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
||||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
{poll.expired || poll.voted ? pollBodyDisallow() : pollBodyAllow()}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -271,7 +269,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
marginTop: StyleConstants.Spacing.XS
|
marginTop: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pollButton}
|
{pollButton()}
|
||||||
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
||||||
{pollVoteCounts()}
|
{pollVoteCounts()}
|
||||||
{pollExpiration()}
|
{pollExpiration()}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import detectLanguage from '@helpers/detectLanguage'
|
import detectLanguage from '@utils/helpers/detectLanguage'
|
||||||
import getLanguage from '@helpers/getLanguage'
|
import getLanguage from '@utils/helpers/getLanguage'
|
||||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
@@ -2,16 +2,15 @@ import ComponentSeparator from '@components/Separator'
|
|||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useScrollToTop } from '@react-navigation/native'
|
||||||
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
|
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import { useGlobalStorageListener } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject, useCallback, useRef } from 'react'
|
import React, { RefObject, useRef } from 'react'
|
||||||
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
||||||
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
||||||
import { useSelector } from 'react-redux'
|
import TimelineEmpty from './Empty'
|
||||||
import TimelineEmpty from './Timeline/Empty'
|
import TimelineFooter from './Footer'
|
||||||
import TimelineFooter from './Timeline/Footer'
|
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh'
|
||||||
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
|
|
||||||
|
|
||||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
||||||
|
|
||||||
@@ -57,11 +56,6 @@ const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
||||||
|
|
||||||
const onEndReached = useCallback(
|
|
||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
|
||||||
[isFetchingNextPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
@@ -100,12 +94,9 @@ const Timeline: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useScrollToTop(flRef)
|
useScrollToTop(flRef)
|
||||||
useSelector(getInstanceActive, (prev, next) => {
|
useGlobalStorageListener('account.active', () =>
|
||||||
if (prev !== next) {
|
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
||||||
flRef.current?.scrollToOffset({ offset: 0, animated: false })
|
)
|
||||||
}
|
|
||||||
return prev === next
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -124,7 +115,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
data={flattenData}
|
data={flattenData}
|
||||||
initialNumToRender={6}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={3}
|
maxToRenderPerBatch={3}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
@@ -2,6 +2,7 @@ import haptics from '@components/haptics'
|
|||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
import { TabSharedStackParamList } from '@utils/navigation/navigators'
|
||||||
import {
|
import {
|
||||||
QueryKeyRelationship,
|
QueryKeyRelationship,
|
||||||
@@ -13,12 +14,10 @@ import {
|
|||||||
QueryKeyTimeline,
|
QueryKeyTimeline,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Platform } from 'react-native'
|
import { Alert, Platform } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const menuAccount = ({
|
const menuAccount = ({
|
||||||
type,
|
type,
|
||||||
@@ -43,8 +42,7 @@ const menuAccount = ({
|
|||||||
|
|
||||||
const menus: ContextMenu[][] = [[]]
|
const menus: ContextMenu[][] = [[]]
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount)
|
const ownAccount = useAccountStorage.string('auth.account.id')['0'] === account.id
|
||||||
const ownAccount = instanceAccount?.id === account.id
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState(openChange)
|
const [enabled, setEnabled] = useState(openChange)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { getHost } from '@utils/helpers/urlMatcher'
|
||||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const menuInstance = ({
|
const menuInstance = ({
|
||||||
status,
|
status,
|
||||||
@@ -35,10 +35,9 @@ const menuInstance = ({
|
|||||||
|
|
||||||
const menus: ContextMenu[][] = []
|
const menus: ContextMenu[][] = []
|
||||||
|
|
||||||
const currentInstance = useSelector(getInstanceUrl)
|
const instance = getHost(status.uri)
|
||||||
const instance = status.uri && status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
|
||||||
|
|
||||||
if (currentInstance !== instance && instance) {
|
if (instance === getAccountStorage.string('auth.domain')) {
|
||||||
menus.push([
|
menus.push([
|
||||||
{
|
{
|
||||||
key: 'instance-block',
|
key: 'instance-block',
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
import apiInstance from '@api/instance'
|
|
||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import {
|
import {
|
||||||
MutationVarsTimelineUpdateStatusProperty,
|
MutationVarsTimelineUpdateStatusProperty,
|
||||||
QueryKeyTimeline,
|
QueryKeyTimeline,
|
||||||
useTimelineMutation
|
useTimelineMutation
|
||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const menuStatus = ({
|
const menuStatus = ({
|
||||||
status,
|
status,
|
||||||
@@ -57,10 +57,10 @@ const menuStatus = ({
|
|||||||
|
|
||||||
const menus: ContextMenu[][] = []
|
const menus: ContextMenu[][] = []
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id)
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
const ownAccount = instanceAccount?.id === status.account?.id
|
const ownAccount = accountId === status.account?.id
|
||||||
|
|
||||||
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
|
const canEditPost = featureCheck('edit_post')
|
||||||
|
|
||||||
menus.push([
|
menus.push([
|
||||||
{
|
{
|
||||||
@@ -203,9 +203,7 @@ const menuStatus = ({
|
|||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden:
|
hidden: !ownAccount && !status.mentions.filter(mention => mention.id === accountId).length
|
||||||
!ownAccount &&
|
|
||||||
!status.mentions.filter(mention => mention.id === instanceAccount.id).length
|
|
||||||
},
|
},
|
||||||
title: t('componentContextMenu:status.mute.action', {
|
title: t('componentContextMenu:status.mute.action', {
|
||||||
defaultValue: 'false',
|
defaultValue: 'false',
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
import { store } from '@root/store'
|
import queryClient from '@utils/queryHooks'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
import { QueryKeyInstance } from '@utils/queryHooks/instance'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { Asset, launchImageLibrary } from 'react-native-image-picker'
|
import { Asset, launchImageLibrary } from 'react-native-image-picker'
|
||||||
|
|
||||||
|
const queryKeyInstance: QueryKeyInstance = ['Instance']
|
||||||
|
export const MAX_MEDIA_ATTACHMENTS: number =
|
||||||
|
queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration?.statuses
|
||||||
|
.max_media_attachments || 4
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
mediaType?: 'photo' | 'video'
|
mediaType?: 'photo' | 'video'
|
||||||
resize?: { width?: number; height?: number }
|
resize?: { width?: number; height?: number }
|
||||||
@@ -22,7 +27,7 @@ const mediaSelector = async ({
|
|||||||
indicateMaximum = false,
|
indicateMaximum = false,
|
||||||
showActionSheetWithOptions
|
showActionSheetWithOptions
|
||||||
}: Props): Promise<Asset[]> => {
|
}: Props): Promise<Asset[]> => {
|
||||||
const _maximum = maximum || getInstanceConfigurationStatusMaxAttachments(store.getState()) || 4
|
const _maximum = maximum || MAX_MEDIA_ATTACHMENTS
|
||||||
|
|
||||||
const options = () => {
|
const options = () => {
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import apiInstance from '@api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
import navigationRef from '@helpers/navigationRef'
|
import { matchAccount, matchStatus } from '@utils/helpers/urlMatcher'
|
||||||
import { matchAccount, matchStatus } from '@helpers/urlMatcher'
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
import { store } from '@root/store'
|
|
||||||
import { SearchResult } from '@utils/queryHooks/search'
|
import { SearchResult } from '@utils/queryHooks/search'
|
||||||
import { getSettingsBrowser } from '@utils/slices/settingsSlice'
|
import { getGlobalStorage } from '@utils/storage/actions'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
@@ -89,7 +88,7 @@ const openLink = async (url: string, navigation?: any) => {
|
|||||||
loadingLink = false
|
loadingLink = false
|
||||||
const validatedUrl = validUrl.isWebUri(url)
|
const validatedUrl = validUrl.isWebUri(url)
|
||||||
if (validatedUrl) {
|
if (validatedUrl) {
|
||||||
switch (getSettingsBrowser(store.getState())) {
|
switch (getGlobalStorage.string('app.browser')) {
|
||||||
// Some links might end with an empty space at the end that triggers an error
|
// Some links might end with an empty space at the end that triggers an error
|
||||||
case 'internal':
|
case 'internal':
|
||||||
await WebBrowser.openBrowserAsync(validatedUrl, {
|
await WebBrowser.openBrowserAsync(validatedUrl, {
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import { store } from '@root/store'
|
|
||||||
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
|
|
||||||
import * as Localization from 'expo-localization'
|
|
||||||
import { Platform } from "react-native"
|
|
||||||
|
|
||||||
const getLanguage = (): string => {
|
|
||||||
return Platform.OS === 'ios'
|
|
||||||
? Localization.locale
|
|
||||||
: getSettingsLanguage(store.getState())
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getLanguage
|
|
@@ -1,21 +1,21 @@
|
|||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import { initReactI18next } from 'react-i18next'
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
import ca from '@root/i18n/ca'
|
import ca from './ca'
|
||||||
import de from '@root/i18n/de'
|
import de from './de'
|
||||||
import en from '@root/i18n/en'
|
import en from './en'
|
||||||
import es from '@root/i18n/es'
|
import es from './es'
|
||||||
import fr from '@root/i18n/fr'
|
import fr from './fr'
|
||||||
import it from '@root/i18n/it'
|
import it from './it'
|
||||||
import ja from '@root/i18n/ja'
|
import ja from './ja'
|
||||||
import ko from '@root/i18n/ko'
|
import ko from './ko'
|
||||||
import nl from '@root/i18n/nl'
|
import nl from './nl'
|
||||||
import pt_BR from '@root/i18n/pt_BR'
|
import pt_BR from './pt_BR'
|
||||||
import sv from '@root/i18n/sv'
|
import sv from './sv'
|
||||||
import uk from '@root/i18n/uk'
|
import uk from './uk'
|
||||||
import vi from '@root/i18n/vi'
|
import vi from './vi'
|
||||||
import zh_Hans from '@root/i18n/zh-Hans'
|
import zh_Hans from './zh-Hans'
|
||||||
import zh_Hant from '@root/i18n/zh-Hant'
|
import zh_Hant from './zh-Hant'
|
||||||
|
|
||||||
import '@formatjs/intl-getcanonicallocales/polyfill'
|
import '@formatjs/intl-getcanonicallocales/polyfill'
|
||||||
import '@formatjs/intl-locale/polyfill'
|
import '@formatjs/intl-locale/polyfill'
|
||||||
@@ -54,6 +54,7 @@ import '@formatjs/intl-numberformat/locale-data/zh-Hans'
|
|||||||
import '@formatjs/intl-numberformat/locale-data/zh-Hant'
|
import '@formatjs/intl-numberformat/locale-data/zh-Hant'
|
||||||
|
|
||||||
import '@formatjs/intl-datetimeformat/polyfill'
|
import '@formatjs/intl-datetimeformat/polyfill'
|
||||||
|
import '@formatjs/intl-datetimeformat/add-all-tz'
|
||||||
import '@formatjs/intl-datetimeformat/locale-data/ca'
|
import '@formatjs/intl-datetimeformat/locale-data/ca'
|
||||||
import '@formatjs/intl-datetimeformat/locale-data/de'
|
import '@formatjs/intl-datetimeformat/locale-data/de'
|
||||||
import '@formatjs/intl-datetimeformat/locale-data/en'
|
import '@formatjs/intl-datetimeformat/locale-data/en'
|
||||||
@@ -69,7 +70,6 @@ import '@formatjs/intl-datetimeformat/locale-data/uk'
|
|||||||
import '@formatjs/intl-datetimeformat/locale-data/vi'
|
import '@formatjs/intl-datetimeformat/locale-data/vi'
|
||||||
import '@formatjs/intl-datetimeformat/locale-data/zh-Hans'
|
import '@formatjs/intl-datetimeformat/locale-data/zh-Hans'
|
||||||
import '@formatjs/intl-datetimeformat/locale-data/zh-Hant'
|
import '@formatjs/intl-datetimeformat/locale-data/zh-Hant'
|
||||||
import '@formatjs/intl-datetimeformat/add-all-tz'
|
|
||||||
|
|
||||||
import '@formatjs/intl-relativetimeformat/polyfill'
|
import '@formatjs/intl-relativetimeformat/polyfill'
|
||||||
import '@formatjs/intl-relativetimeformat/locale-data/ca'
|
import '@formatjs/intl-relativetimeformat/locale-data/ca'
|
@@ -1,15 +1,14 @@
|
|||||||
import AccountButton from '@components/AccountButton'
|
import AccountButton from '@components/AccountButton'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import navigationRef from '@helpers/navigationRef'
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { getInstances } from '@utils/slices/instancesSlice'
|
import { getGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, Image, ScrollView, View } from 'react-native'
|
import { FlatList, Image, ScrollView, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const Share = ({
|
const Share = ({
|
||||||
text,
|
text,
|
||||||
@@ -93,7 +92,7 @@ const ScreenAccountSelection = ({
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('screenAccountSelection')
|
const { t } = useTranslation('screenAccountSelection')
|
||||||
|
|
||||||
const instances = useSelector(getInstances, () => true)
|
const accounts = getGlobalStorage.object('accounts')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -126,27 +125,24 @@ const ScreenAccountSelection = ({
|
|||||||
marginTop: StyleConstants.Spacing.M
|
marginTop: StyleConstants.Spacing.M
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{instances.length
|
{accounts &&
|
||||||
? instances
|
accounts
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) =>
|
.sort((a, b) => a.localeCompare(b))
|
||||||
`${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
|
.map((account, index) => {
|
||||||
|
return (
|
||||||
|
<AccountButton
|
||||||
|
key={index}
|
||||||
|
account={account}
|
||||||
|
additionalActions={() =>
|
||||||
|
navigationRef.navigate('Screen-Compose', {
|
||||||
|
type: 'share',
|
||||||
|
...share
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
.map((instance, index) => {
|
})}
|
||||||
return (
|
|
||||||
<AccountButton
|
|
||||||
key={index}
|
|
||||||
instance={instance}
|
|
||||||
additionalActions={() => {
|
|
||||||
navigationRef.navigate('Screen-Compose', {
|
|
||||||
type: 'share',
|
|
||||||
...share
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -34,9 +34,8 @@ const ScreenActions = ({
|
|||||||
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const dismiss = useCallback(() => {
|
const dismiss = () => navigation.goBack()
|
||||||
navigation.goBack()
|
|
||||||
}, [])
|
|
||||||
const onGestureEvent = useAnimatedGestureHandler({
|
const onGestureEvent = useAnimatedGestureHandler({
|
||||||
onActive: ({ translationY }) => {
|
onActive: ({ translationY }) => {
|
||||||
panY.value = translationY
|
panY.value = translationY
|
||||||
|
@@ -9,7 +9,7 @@ import { RootStackScreenProps } from '@utils/navigation/navigators'
|
|||||||
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@@ -56,148 +56,140 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
|||||||
}
|
}
|
||||||
}, [query.data])
|
}, [query.data])
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = ({ item, index }: { item: Mastodon.Announcement; index: number }) => (
|
||||||
({ item, index }: { item: Mastodon.Announcement; index: number }) => (
|
<View
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
width: Dimensions.get('window').width,
|
||||||
|
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginVertical: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => navigation.goBack()} />
|
||||||
<View
|
<View
|
||||||
key={index}
|
|
||||||
style={{
|
style={{
|
||||||
width: Dimensions.get('window').width,
|
flexShrink: 1,
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||||
marginVertical: StyleConstants.Spacing.Global.PagePadding,
|
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||||
justifyContent: 'center'
|
borderWidth: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderColor: colors.primaryDefault,
|
||||||
|
backgroundColor: colors.backgroundDefault
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => navigation.goBack()} />
|
<CustomText
|
||||||
<View
|
fontStyle='S'
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 1,
|
marginBottom: StyleConstants.Spacing.S,
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
color: colors.secondary
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
borderColor: colors.primaryDefault,
|
|
||||||
backgroundColor: colors.backgroundDefault
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CustomText
|
<Trans
|
||||||
fontStyle='S'
|
ns='screenAnnouncements'
|
||||||
style={{
|
i18nKey='content.published'
|
||||||
marginBottom: StyleConstants.Spacing.S,
|
components={[<RelativeTime time={item.published_at} />]}
|
||||||
color: colors.secondary
|
/>
|
||||||
}}
|
</CustomText>
|
||||||
>
|
<ScrollView
|
||||||
<Trans
|
style={{
|
||||||
ns='screenAnnouncements'
|
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2
|
||||||
i18nKey='content.published'
|
}}
|
||||||
components={[<RelativeTime time={item.published_at} />]}
|
showsVerticalScrollIndicator
|
||||||
/>
|
>
|
||||||
</CustomText>
|
<ParseHTML
|
||||||
<ScrollView
|
content={item.content}
|
||||||
|
size='M'
|
||||||
|
emojis={item.emojis}
|
||||||
|
mentions={item.mentions}
|
||||||
|
numberOfLines={999}
|
||||||
|
selectable
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
{item.reactions?.length ? (
|
||||||
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2
|
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2
|
||||||
}}
|
}}
|
||||||
showsVerticalScrollIndicator
|
|
||||||
>
|
>
|
||||||
<ParseHTML
|
{item.reactions?.map(reaction => (
|
||||||
content={item.content}
|
<Pressable
|
||||||
size='M'
|
key={reaction.name}
|
||||||
emojis={item.emojis}
|
style={{
|
||||||
mentions={item.mentions}
|
borderWidth: 1,
|
||||||
numberOfLines={999}
|
padding: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||||
selectable
|
marginTop: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||||
/>
|
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||||
</ScrollView>
|
marginRight: StyleConstants.Spacing.M,
|
||||||
{item.reactions?.length ? (
|
borderRadius: 6,
|
||||||
<View
|
flexDirection: 'row',
|
||||||
style={{
|
borderColor: reaction.me ? colors.disabled : colors.primaryDefault,
|
||||||
flexDirection: 'row',
|
backgroundColor: reaction.me ? colors.disabled : colors.backgroundDefault
|
||||||
flexWrap: 'wrap',
|
}}
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2
|
onPress={() =>
|
||||||
}}
|
mutation.mutate({
|
||||||
>
|
id: item.id,
|
||||||
{item.reactions?.map(reaction => (
|
type: 'reaction',
|
||||||
<Pressable
|
name: reaction.name,
|
||||||
key={reaction.name}
|
me: reaction.me
|
||||||
style={{
|
})
|
||||||
borderWidth: 1,
|
}
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding / 2,
|
>
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding / 2,
|
{reaction.url ? (
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2,
|
<FastImage
|
||||||
marginRight: StyleConstants.Spacing.M,
|
source={{
|
||||||
borderRadius: 6,
|
uri: reduceMotionEnabled ? reaction.static_url : reaction.url
|
||||||
flexDirection: 'row',
|
}}
|
||||||
borderColor: reaction.me ? colors.disabled : colors.primaryDefault,
|
style={{
|
||||||
backgroundColor: reaction.me ? colors.disabled : colors.backgroundDefault
|
width: StyleConstants.Font.LineHeight.M + 3,
|
||||||
}}
|
height: StyleConstants.Font.LineHeight.M
|
||||||
onPress={() =>
|
}}
|
||||||
mutation.mutate({
|
/>
|
||||||
id: item.id,
|
) : (
|
||||||
type: 'reaction',
|
<CustomText fontStyle='M'>{reaction.name}</CustomText>
|
||||||
name: reaction.name,
|
)}
|
||||||
me: reaction.me
|
{reaction.count ? (
|
||||||
})
|
<CustomText
|
||||||
}
|
fontStyle='S'
|
||||||
>
|
style={{
|
||||||
{reaction.url ? (
|
marginLeft: StyleConstants.Spacing.S,
|
||||||
<FastImage
|
color: colors.primaryDefault
|
||||||
source={{
|
}}
|
||||||
uri: reduceMotionEnabled ? reaction.static_url : reaction.url
|
>
|
||||||
}}
|
{reaction.count}
|
||||||
style={{
|
</CustomText>
|
||||||
width: StyleConstants.Font.LineHeight.M + 3,
|
) : null}
|
||||||
height: StyleConstants.Font.LineHeight.M
|
</Pressable>
|
||||||
}}
|
))}
|
||||||
/>
|
</View>
|
||||||
) : (
|
) : null}
|
||||||
<CustomText fontStyle='M'>{reaction.name}</CustomText>
|
<Button
|
||||||
)}
|
type='text'
|
||||||
{reaction.count ? (
|
content={item.read ? t('content.button.read') : t('content.button.unread')}
|
||||||
<CustomText
|
loading={mutation.isLoading}
|
||||||
fontStyle='S'
|
disabled={item.read}
|
||||||
style={{
|
onPress={() => {
|
||||||
marginLeft: StyleConstants.Spacing.S,
|
!item.read &&
|
||||||
color: colors.primaryDefault
|
mutation.mutate({
|
||||||
}}
|
id: item.id,
|
||||||
>
|
type: 'dismiss'
|
||||||
{reaction.count}
|
})
|
||||||
</CustomText>
|
}}
|
||||||
) : null}
|
/>
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
<Button
|
|
||||||
type='text'
|
|
||||||
content={item.read ? t('content.button.read') : t('content.button.unread')}
|
|
||||||
loading={mutation.isLoading}
|
|
||||||
disabled={item.read}
|
|
||||||
onPress={() => {
|
|
||||||
!item.read &&
|
|
||||||
mutation.mutate({
|
|
||||||
id: item.id,
|
|
||||||
type: 'dismiss'
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
),
|
</View>
|
||||||
[mode]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onMomentumScrollEnd = useCallback(
|
const onMomentumScrollEnd = ({
|
||||||
({
|
nativeEvent: {
|
||||||
nativeEvent: {
|
contentOffset: { x },
|
||||||
contentOffset: { x },
|
layoutMeasurement: { width }
|
||||||
layoutMeasurement: { width }
|
}
|
||||||
}
|
}: NativeSyntheticEvent<NativeScrollEvent>) => setIndex(Math.floor(x / width))
|
||||||
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
||||||
setIndex(Math.floor(x / width))
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const ListEmptyComponent = useCallback(() => {
|
const ListEmptyComponent = () => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -209,7 +201,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
|||||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
return Platform.OS === 'ios' ? (
|
return Platform.OS === 'ios' ? (
|
||||||
<BlurView
|
<BlurView
|
||||||
|
@@ -1,419 +0,0 @@
|
|||||||
import { handleError } from '@api/helpers'
|
|
||||||
import { ComponentEmojis } from '@components/Emojis'
|
|
||||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|
||||||
import haptics from '@root/components/haptics'
|
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import ComposeRoot from '@screens/Compose/Root'
|
|
||||||
import { formatText } from '@screens/Compose/utils/processText'
|
|
||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
|
||||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
|
||||||
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
|
||||||
import {
|
|
||||||
getInstanceAccount,
|
|
||||||
getInstanceConfigurationStatusMaxChars,
|
|
||||||
removeInstanceDraft,
|
|
||||||
updateInstanceDraft
|
|
||||||
} from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import { filter } from 'lodash'
|
|
||||||
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { Alert, Keyboard, Platform } from 'react-native'
|
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeDraftsList from './Compose/DraftsList'
|
|
||||||
import ComposeEditAttachment from './Compose/EditAttachment'
|
|
||||||
import { uploadAttachment } from './Compose/Root/Footer/addAttachment'
|
|
||||||
import ComposeContext from './Compose/utils/createContext'
|
|
||||||
import composeInitialState from './Compose/utils/initialState'
|
|
||||||
import composeParseState from './Compose/utils/parseState'
|
|
||||||
import composePost from './Compose/utils/post'
|
|
||||||
import composeReducer from './Compose/utils/reducer'
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
|
||||||
|
|
||||||
const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|
||||||
route: { params },
|
|
||||||
navigation
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
|
||||||
const { colors } = useTheme()
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const [hasKeyboard, setHasKeyboard] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const keyboardShown = Keyboard.addListener('keyboardWillShow', () => setHasKeyboard(true))
|
|
||||||
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () => setHasKeyboard(false))
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
keyboardShown.remove()
|
|
||||||
keyboardHidden.remove()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const localAccount = useSelector(getInstanceAccount, (prev, next) =>
|
|
||||||
prev?.preferences && next?.preferences
|
|
||||||
? prev?.preferences['posting:default:visibility'] ===
|
|
||||||
next?.preferences['posting:default:visibility']
|
|
||||||
: true
|
|
||||||
)
|
|
||||||
const initialReducerState = useMemo(() => {
|
|
||||||
if (params) {
|
|
||||||
return composeParseState(params)
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...composeInitialState,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
attachments: {
|
|
||||||
...composeInitialState.attachments,
|
|
||||||
sensitive:
|
|
||||||
localAccount?.preferences && localAccount?.preferences['posting:default:sensitive']
|
|
||||||
? localAccount?.preferences['posting:default:sensitive']
|
|
||||||
: false
|
|
||||||
},
|
|
||||||
visibility:
|
|
||||||
localAccount?.preferences && localAccount.preferences['posting:default:visibility']
|
|
||||||
? localAccount.preferences['posting:default:visibility']
|
|
||||||
: 'public'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [composeState, composeDispatch] = useReducer(composeReducer, initialReducerState)
|
|
||||||
|
|
||||||
const maxTootChars = useSelector(getInstanceConfigurationStatusMaxChars, () => true)
|
|
||||||
const totalTextCount =
|
|
||||||
(composeState.spoiler.active ? composeState.spoiler.count : 0) + composeState.text.count
|
|
||||||
|
|
||||||
// If compose state is dirty, then disallow add back drafts
|
|
||||||
useEffect(() => {
|
|
||||||
composeDispatch({
|
|
||||||
type: 'dirty',
|
|
||||||
payload:
|
|
||||||
totalTextCount !== 0 ||
|
|
||||||
composeState.attachments.uploads.length !== 0 ||
|
|
||||||
(composeState.poll.active === true &&
|
|
||||||
filter(composeState.poll.options, o => {
|
|
||||||
return o !== undefined && o.length > 0
|
|
||||||
}).length > 0)
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
totalTextCount,
|
|
||||||
composeState.attachments.uploads.length,
|
|
||||||
composeState.poll.active,
|
|
||||||
composeState.poll.options
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
switch (params?.type) {
|
|
||||||
case 'share':
|
|
||||||
if (params.text) {
|
|
||||||
formatText({
|
|
||||||
textInput: 'text',
|
|
||||||
composeDispatch,
|
|
||||||
content: params.text,
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (params.media?.length) {
|
|
||||||
for (const m of params.media) {
|
|
||||||
uploadAttachment({
|
|
||||||
composeDispatch,
|
|
||||||
media: { uri: m.uri, fileName: 'temp.jpg', type: m.mime }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'edit':
|
|
||||||
case 'deleteEdit':
|
|
||||||
if (params.incomingStatus.spoiler_text) {
|
|
||||||
formatText({
|
|
||||||
textInput: 'spoiler',
|
|
||||||
composeDispatch,
|
|
||||||
content: params.incomingStatus.spoiler_text,
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
formatText({
|
|
||||||
textInput: 'text',
|
|
||||||
composeDispatch,
|
|
||||||
content: params.incomingStatus.text!,
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'reply':
|
|
||||||
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
|
|
||||||
if (actualStatus.spoiler_text) {
|
|
||||||
formatText({
|
|
||||||
textInput: 'spoiler',
|
|
||||||
composeDispatch,
|
|
||||||
content: actualStatus.spoiler_text,
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
params.accts.length && // When replying to myself only, do not add space or even format text
|
|
||||||
formatText({
|
|
||||||
textInput: 'text',
|
|
||||||
composeDispatch,
|
|
||||||
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'conversation':
|
|
||||||
formatText({
|
|
||||||
textInput: 'text',
|
|
||||||
composeDispatch,
|
|
||||||
content:
|
|
||||||
(params.text ? `${params.text}\n` : '') +
|
|
||||||
params.accts.map(acct => `@${acct}`).join(' ') +
|
|
||||||
' ',
|
|
||||||
disableDebounce: true
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, [params?.type])
|
|
||||||
|
|
||||||
const saveDraft = () => {
|
|
||||||
dispatch(
|
|
||||||
updateInstanceDraft({
|
|
||||||
timestamp: composeState.timestamp,
|
|
||||||
spoiler: composeState.spoiler.raw,
|
|
||||||
text: composeState.text.raw,
|
|
||||||
poll: composeState.poll,
|
|
||||||
attachments: composeState.attachments,
|
|
||||||
visibility: composeState.visibility,
|
|
||||||
visibilityLock: composeState.visibilityLock,
|
|
||||||
replyToStatus: composeState.replyToStatus
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const removeDraft = useCallback(() => {
|
|
||||||
dispatch(removeInstanceDraft(composeState.timestamp))
|
|
||||||
}, [composeState.timestamp])
|
|
||||||
useEffect(() => {
|
|
||||||
const autoSave = composeState.dirty
|
|
||||||
? setInterval(() => {
|
|
||||||
saveDraft()
|
|
||||||
}, 1000)
|
|
||||||
: removeDraft()
|
|
||||||
return () => autoSave && clearInterval(autoSave)
|
|
||||||
}, [composeState])
|
|
||||||
|
|
||||||
const headerLeft = useCallback(
|
|
||||||
() => (
|
|
||||||
<HeaderLeft
|
|
||||||
type='text'
|
|
||||||
content={t('common:buttons.cancel')}
|
|
||||||
onPress={() => {
|
|
||||||
if (!composeState.dirty) {
|
|
||||||
navigation.goBack()
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Alert.alert(t('screenCompose:heading.left.alert.title'), undefined, [
|
|
||||||
{
|
|
||||||
text: t('screenCompose:heading.left.alert.buttons.delete'),
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: () => {
|
|
||||||
removeDraft()
|
|
||||||
navigation.goBack()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('screenCompose:heading.left.alert.buttons.save'),
|
|
||||||
onPress: () => {
|
|
||||||
saveDraft()
|
|
||||||
navigation.goBack()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('common:buttons.cancel'),
|
|
||||||
style: 'cancel'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[composeState]
|
|
||||||
)
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const headerRightDisabled = useMemo(() => {
|
|
||||||
if (totalTextCount > maxTootChars) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}, [totalTextCount, composeState.attachments.uploads, composeState.text.raw])
|
|
||||||
const mutateTimeline = useTimelineMutation({ onMutate: true })
|
|
||||||
const headerRight = useCallback(
|
|
||||||
() => (
|
|
||||||
<HeaderRight
|
|
||||||
type='text'
|
|
||||||
content={t(
|
|
||||||
`screenCompose:heading.right.button.${
|
|
||||||
(params?.type &&
|
|
||||||
(params.type === 'conversation'
|
|
||||||
? params.visibility === 'direct'
|
|
||||||
? params.type
|
|
||||||
: 'default'
|
|
||||||
: params.type)) ||
|
|
||||||
'default'
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
onPress={() => {
|
|
||||||
composeDispatch({ type: 'posting', payload: true })
|
|
||||||
|
|
||||||
composePost(params, composeState)
|
|
||||||
.then(res => {
|
|
||||||
haptics('Success')
|
|
||||||
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
|
|
||||||
// https://github.com/tooot-app/app/issues/59
|
|
||||||
} else {
|
|
||||||
dispatch(updateStoreReview(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (params?.type) {
|
|
||||||
case 'edit':
|
|
||||||
mutateTimeline.mutate({
|
|
||||||
type: 'editItem',
|
|
||||||
queryKey: params.queryKey,
|
|
||||||
rootQueryKey: params.rootQueryKey,
|
|
||||||
status: res
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'deleteEdit':
|
|
||||||
case 'reply':
|
|
||||||
if (params?.queryKey && params.queryKey[1].page === 'Toot') {
|
|
||||||
queryClient.invalidateQueries(params.queryKey)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
removeDraft()
|
|
||||||
navigation.goBack()
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error?.removeReply) {
|
|
||||||
Alert.alert(
|
|
||||||
t('screenCompose:heading.right.alert.removeReply.title'),
|
|
||||||
t('screenCompose:heading.right.alert.removeReply.description'),
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: t('common:buttons.cancel'),
|
|
||||||
onPress: () => {
|
|
||||||
composeDispatch({ type: 'posting', payload: false })
|
|
||||||
},
|
|
||||||
style: 'destructive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('screenCompose:heading.right.alert.removeReply.confirm'),
|
|
||||||
onPress: () => {
|
|
||||||
composeDispatch({ type: 'removeReply' })
|
|
||||||
composeDispatch({ type: 'posting', payload: false })
|
|
||||||
},
|
|
||||||
style: 'default'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
haptics('Error')
|
|
||||||
handleError({ message: 'Posting error', captureResponse: true })
|
|
||||||
composeDispatch({ type: 'posting', payload: false })
|
|
||||||
Alert.alert(t('screenCompose:heading.right.alert.default.title'), undefined, [
|
|
||||||
{ text: t('screenCompose:heading.right.alert.default.button') }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
loading={composeState.posting}
|
|
||||||
disabled={headerRightDisabled}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[totalTextCount, composeState]
|
|
||||||
)
|
|
||||||
|
|
||||||
const headerContent = useMemo(() => {
|
|
||||||
return `${totalTextCount} / ${maxTootChars}`
|
|
||||||
}, [totalTextCount, maxTootChars, composeState.dirty])
|
|
||||||
|
|
||||||
const inputProps: EmojisState['inputProps'] = [
|
|
||||||
{
|
|
||||||
value: [
|
|
||||||
composeState.text.raw,
|
|
||||||
content => {
|
|
||||||
formatText({ textInput: 'text', composeDispatch, content })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
selection: [
|
|
||||||
composeState.text.selection,
|
|
||||||
selection => composeDispatch({ type: 'text', payload: { selection } })
|
|
||||||
],
|
|
||||||
isFocused: composeState.textInputFocus.isFocused.text,
|
|
||||||
maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0),
|
|
||||||
ref: composeState.textInputFocus.refs.text
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: [
|
|
||||||
composeState.spoiler.raw,
|
|
||||||
content => formatText({ textInput: 'spoiler', composeDispatch, content })
|
|
||||||
],
|
|
||||||
selection: [
|
|
||||||
composeState.spoiler.selection,
|
|
||||||
selection => composeDispatch({ type: 'spoiler', payload: { selection } })
|
|
||||||
],
|
|
||||||
isFocused: composeState.textInputFocus.isFocused.spoiler,
|
|
||||||
maxLength: maxTootChars - composeState.text.count,
|
|
||||||
ref: composeState.textInputFocus.refs.spoiler
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComponentEmojis
|
|
||||||
inputProps={inputProps}
|
|
||||||
customButton
|
|
||||||
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
||||||
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
|
||||||
>
|
|
||||||
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
|
||||||
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Compose-Root'
|
|
||||||
component={ComposeRoot}
|
|
||||||
options={{
|
|
||||||
title: headerContent,
|
|
||||||
headerTitleStyle: {
|
|
||||||
fontWeight:
|
|
||||||
totalTextCount > maxTootChars
|
|
||||||
? StyleConstants.Font.Weight.Bold
|
|
||||||
: StyleConstants.Font.Weight.Normal,
|
|
||||||
fontSize: StyleConstants.Font.Size.M
|
|
||||||
},
|
|
||||||
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
|
||||||
headerLeft,
|
|
||||||
headerRight
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Compose-DraftsList'
|
|
||||||
component={ComposeDraftsList}
|
|
||||||
options={{ presentation: 'modal' }}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Compose-EditAttachment'
|
|
||||||
component={ComposeEditAttachment}
|
|
||||||
options={{ presentation: 'modal' }}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
</ComposeContext.Provider>
|
|
||||||
</ComponentEmojis>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScreenCompose
|
|
@@ -1,12 +1,11 @@
|
|||||||
import apiInstance from '@api/instance'
|
|
||||||
import { HeaderLeft } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
|
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
|
||||||
import { useAppDispatch } from '@root/store'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
|
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
|
import { getAccountStorage, setAccountStorage, useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
@@ -15,11 +14,19 @@ import { Dimensions, Modal, Platform, Pressable, View } from 'react-native'
|
|||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { PanGestureHandler } from 'react-native-gesture-handler'
|
import { PanGestureHandler } from 'react-native-gesture-handler'
|
||||||
import { SwipeListView } from 'react-native-swipe-list-view'
|
import { SwipeListView } from 'react-native-swipe-list-view'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
import { formatText } from './utils/processText'
|
import { formatText } from './utils/processText'
|
||||||
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
|
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
|
||||||
|
|
||||||
|
export const removeDraft = (timestamp: number) =>
|
||||||
|
setAccountStorage([
|
||||||
|
{
|
||||||
|
key: 'drafts',
|
||||||
|
value:
|
||||||
|
getAccountStorage.object('drafts')?.filter(draft => draft.timestamp !== timestamp) || []
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>> = ({
|
const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>> = ({
|
||||||
navigation,
|
navigation,
|
||||||
route: {
|
route: {
|
||||||
@@ -39,11 +46,8 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { composeDispatch } = useContext(ComposeContext)
|
const { composeDispatch } = useContext(ComposeContext)
|
||||||
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
|
const [drafts] = useAccountStorage.object('drafts')
|
||||||
draft => draft.timestamp !== timestamp
|
|
||||||
)
|
|
||||||
const [checkingAttachments, setCheckingAttachments] = useState(false)
|
const [checkingAttachments, setCheckingAttachments] = useState(false)
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
|
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||||||
</View>
|
</View>
|
||||||
<PanGestureHandler enabled={Platform.OS === 'ios'}>
|
<PanGestureHandler enabled={Platform.OS === 'ios'}>
|
||||||
<SwipeListView
|
<SwipeListView
|
||||||
data={instanceDrafts}
|
data={drafts.filter(draft => draft.timestamp !== timestamp)}
|
||||||
renderItem={({ item }: { item: ComposeStateDraft }) => {
|
renderItem={({ item }: { item: ComposeStateDraft }) => {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -113,7 +117,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||||||
type: 'loadDraft',
|
type: 'loadDraft',
|
||||||
payload: tempDraft
|
payload: tempDraft
|
||||||
})
|
})
|
||||||
dispatch(removeInstanceDraft(item.timestamp))
|
removeDraft(item.timestamp)
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -172,7 +176,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
backgroundColor: colors.red
|
backgroundColor: colors.red
|
||||||
}}
|
}}
|
||||||
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
|
onPress={() => removeDraft(item.timestamp)}
|
||||||
children={
|
children={
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import apiInstance from '@api/instance'
|
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
|
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Modal, View } from 'react-native'
|
import { Modal, View } from 'react-native'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
|
|
||||||
const ComposePosting = () => {
|
const ComposePosting = () => {
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
import { emojis } from '@components/Emojis'
|
import { emojis } from '@components/Emojis'
|
||||||
import EmojisContext from '@components/Emojis/helpers/EmojisContext'
|
import EmojisContext from '@components/Emojis/Context'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
|
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
import chooseAndUploadAttachment from './Footer/addAttachment'
|
import chooseAndUploadAttachment from './Footer/addAttachment'
|
||||||
|
|
||||||
@@ -18,12 +17,8 @@ const ComposeActions: React.FC = () => {
|
|||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const instanceConfigurationStatusMaxAttachments = useSelector(
|
|
||||||
getInstanceConfigurationStatusMaxAttachments,
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const attachmentColor = useMemo(() => {
|
const attachmentColor = () => {
|
||||||
if (composeState.poll.active) return colors.disabled
|
if (composeState.poll.active) return colors.disabled
|
||||||
|
|
||||||
if (composeState.attachments.uploads.length) {
|
if (composeState.attachments.uploads.length) {
|
||||||
@@ -31,19 +26,16 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
const attachmentOnPress = () => {
|
const attachmentOnPress = () => {
|
||||||
if (composeState.poll.active) return
|
if (composeState.poll.active) return
|
||||||
|
|
||||||
if (composeState.attachments.uploads.length < instanceConfigurationStatusMaxAttachments) {
|
if (composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS) {
|
||||||
return chooseAndUploadAttachment({
|
return chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
|
||||||
composeDispatch,
|
|
||||||
showActionSheetWithOptions
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollColor = useMemo(() => {
|
const pollColor = () => {
|
||||||
if (composeState.attachments.uploads.length) return colors.disabled
|
if (composeState.attachments.uploads.length) return colors.disabled
|
||||||
|
|
||||||
if (composeState.poll.active) {
|
if (composeState.poll.active) {
|
||||||
@@ -51,7 +43,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
const pollOnPress = () => {
|
const pollOnPress = () => {
|
||||||
if (!composeState.attachments.uploads.length) {
|
if (!composeState.attachments.uploads.length) {
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
@@ -65,7 +57,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityIcon = useMemo(() => {
|
const visibilityIcon = () => {
|
||||||
switch (composeState.visibility) {
|
switch (composeState.visibility) {
|
||||||
case 'public':
|
case 'public':
|
||||||
return 'Globe'
|
return 'Globe'
|
||||||
@@ -76,7 +68,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
case 'direct':
|
case 'direct':
|
||||||
return 'Mail'
|
return 'Mail'
|
||||||
}
|
}
|
||||||
}, [composeState.visibility])
|
}
|
||||||
const visibilityOnPress = () => {
|
const visibilityOnPress = () => {
|
||||||
if (!composeState.visibilityLock) {
|
if (!composeState.visibilityLock) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
@@ -124,7 +116,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||||
const emojiColor = useMemo(() => {
|
const emojiColor = () => {
|
||||||
if (!emojis.current?.length) return colors.disabled
|
if (!emojis.current?.length) return colors.disabled
|
||||||
|
|
||||||
if (emojisState.targetIndex !== -1) {
|
if (emojisState.targetIndex !== -1) {
|
||||||
@@ -132,7 +124,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [emojis.current?.length, emojisState.targetIndex])
|
}
|
||||||
const emojiOnPress = () => {
|
const emojiOnPress = () => {
|
||||||
if (emojisState.targetIndex === -1) {
|
if (emojisState.targetIndex === -1) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
@@ -167,7 +159,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={attachmentOnPress}
|
onPress={attachmentOnPress}
|
||||||
children={<Icon name='Camera' size={24} color={attachmentColor} />}
|
children={<Icon name='Camera' size={24} color={attachmentColor()} />}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
@@ -179,7 +171,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={pollOnPress}
|
onPress={pollOnPress}
|
||||||
children={<Icon name='BarChart2' size={24} color={pollColor} />}
|
children={<Icon name='BarChart2' size={24} color={pollColor()} />}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
@@ -191,7 +183,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
onPress={visibilityOnPress}
|
onPress={visibilityOnPress}
|
||||||
children={
|
children={
|
||||||
<Icon
|
<Icon
|
||||||
name={visibilityIcon}
|
name={visibilityIcon()}
|
||||||
size={24}
|
size={24}
|
||||||
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
|
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
|
||||||
/>
|
/>
|
||||||
@@ -221,7 +213,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={emojiOnPress}
|
onPress={emojiOnPress}
|
||||||
children={<Icon name='Smile' size={24} color={emojiColor} />}
|
children={<Icon name='Smile' size={24} color={emojiColor()} />}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { getInstanceDrafts } from '@utils/slices/instancesSlice'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import React, { RefObject, useContext, useEffect } from 'react'
|
import React, { RefObject, useContext, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -17,15 +16,14 @@ const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
|
|||||||
const { t } = useTranslation('screenCompose')
|
const { t } = useTranslation('screenCompose')
|
||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
const { composeState } = useContext(ComposeContext)
|
const { composeState } = useContext(ComposeContext)
|
||||||
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
|
const [drafts] = useAccountStorage.object('drafts')
|
||||||
draft => draft.timestamp !== composeState.timestamp
|
const draftsCount = drafts.filter(draft => draft.timestamp !== composeState.timestamp).length
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
}, [composeState.dirty])
|
}, [composeState.dirty])
|
||||||
|
|
||||||
if (!composeState.dirty && instanceDrafts?.length) {
|
if (!composeState.dirty && draftsCount) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
accessible
|
accessible
|
||||||
@@ -34,9 +32,7 @@ const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
|
|||||||
children={
|
children={
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
content={t('content.root.drafts', {
|
content={t('content.root.drafts', { count: draftsCount })}
|
||||||
count: instanceDrafts.length
|
|
||||||
})}
|
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Screen-Compose-DraftsList', {
|
navigation.navigate('Screen-Compose-DraftsList', {
|
||||||
timestamp: composeState.timestamp
|
timestamp: composeState.timestamp
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
|
import React, { RefObject, useContext, useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
|
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
import { ExtendedAttachment } from '../../utils/types'
|
import { ExtendedAttachment } from '../../utils/types'
|
||||||
import chooseAndUploadAttachment from './addAttachment'
|
import chooseAndUploadAttachment from './addAttachment'
|
||||||
@@ -31,20 +30,15 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
|
|
||||||
const maxAttachments = useSelector(getInstanceConfigurationStatusMaxAttachments, () => true)
|
|
||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null)
|
const flatListRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const sensitiveOnPress = useCallback(
|
const sensitiveOnPress = () =>
|
||||||
() =>
|
composeDispatch({
|
||||||
composeDispatch({
|
type: 'attachments/sensitive',
|
||||||
type: 'attachments/sensitive',
|
payload: { sensitive: !composeState.attachments.sensitive }
|
||||||
payload: { sensitive: !composeState.attachments.sensitive }
|
})
|
||||||
}),
|
|
||||||
[composeState.attachments.sensitive]
|
|
||||||
)
|
|
||||||
|
|
||||||
const calculateWidth = useCallback((item: ExtendedAttachment) => {
|
const calculateWidth = (item: ExtendedAttachment) => {
|
||||||
if (item.local) {
|
if (item.local) {
|
||||||
return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT
|
return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT
|
||||||
} else {
|
} else {
|
||||||
@@ -62,9 +56,9 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
return DEFAULT_HEIGHT
|
return DEFAULT_HEIGHT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const snapToOffsets = useMemo(() => {
|
const snapToOffsets = () => {
|
||||||
const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => {
|
const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => {
|
||||||
let currentOffset = 0
|
let currentOffset = 0
|
||||||
Array.from(Array(index).keys()).map(
|
Array.from(Array(index).keys()).map(
|
||||||
@@ -84,160 +78,116 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
StyleConstants.Spacing.Global.PagePadding
|
StyleConstants.Spacing.Global.PagePadding
|
||||||
]
|
]
|
||||||
: attachmentsOffsets
|
: attachmentsOffsets
|
||||||
}, [composeState.attachments.uploads.length])
|
}
|
||||||
let prevOffsets = useRef<number[]>()
|
let prevOffsets = useRef<number[]>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (snapToOffsets.length > (prevOffsets.current ? prevOffsets.current.length : 0)) {
|
const snap = snapToOffsets()
|
||||||
|
if (snap.length > (prevOffsets.current ? prevOffsets.current.length : 0)) {
|
||||||
flatListRef.current?.scrollToOffset({
|
flatListRef.current?.scrollToOffset({
|
||||||
offset: snapToOffsets[snapToOffsets.length - 2] + snapToOffsets[snapToOffsets.length - 1]
|
offset: snap[snapToOffsets.length - 2] + snap[snapToOffsets.length - 1]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
prevOffsets.current = snapToOffsets
|
prevOffsets.current = snap
|
||||||
}, [snapToOffsets, prevOffsets.current])
|
}, [snapToOffsets, prevOffsets.current])
|
||||||
|
|
||||||
const renderAttachment = useCallback(
|
const renderAttachment = ({ item, index }: { item: ExtendedAttachment; index: number }) => {
|
||||||
({ item, index }: { item: ExtendedAttachment; index: number }) => {
|
return (
|
||||||
return (
|
<View
|
||||||
<View
|
key={index}
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
height: DEFAULT_HEIGHT,
|
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
width: calculateWidth(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
source={{
|
|
||||||
uri: item.local?.thumbnail || item.remote?.preview_url
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{item.remote?.meta?.original?.duration ? (
|
|
||||||
<CustomText
|
|
||||||
fontStyle='S'
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: StyleConstants.Spacing.S,
|
|
||||||
left: StyleConstants.Spacing.S,
|
|
||||||
paddingLeft: StyleConstants.Spacing.S,
|
|
||||||
paddingRight: StyleConstants.Spacing.S,
|
|
||||||
paddingTop: StyleConstants.Spacing.XS,
|
|
||||||
paddingBottom: StyleConstants.Spacing.XS,
|
|
||||||
color: colors.backgroundDefault,
|
|
||||||
backgroundColor: colors.backgroundOverlayInvert
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.remote.meta.original.duration}
|
|
||||||
</CustomText>
|
|
||||||
) : null}
|
|
||||||
{item.uploading ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.backgroundOverlayInvert
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Circle size={StyleConstants.Font.Size.L} color={colors.primaryOverlay} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignContent: 'flex-end',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
padding: StyleConstants.Spacing.S
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
accessibilityLabel={t('content.root.footer.attachments.remove.accessibilityLabel', {
|
|
||||||
attachment: index + 1
|
|
||||||
})}
|
|
||||||
type='icon'
|
|
||||||
content='X'
|
|
||||||
spacing='M'
|
|
||||||
round
|
|
||||||
overlay
|
|
||||||
onPress={() => {
|
|
||||||
layoutAnimation()
|
|
||||||
composeDispatch({
|
|
||||||
type: 'attachment/delete',
|
|
||||||
payload: item.remote!.id
|
|
||||||
})
|
|
||||||
haptics('Success')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!composeState.attachments.disallowEditing ? (
|
|
||||||
<Button
|
|
||||||
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
|
|
||||||
attachment: index + 1
|
|
||||||
})}
|
|
||||||
type='icon'
|
|
||||||
content='Edit'
|
|
||||||
spacing='M'
|
|
||||||
round
|
|
||||||
overlay
|
|
||||||
onPress={() => {
|
|
||||||
navigation.navigate('Screen-Compose-EditAttachment', {
|
|
||||||
index
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const listFooter = useMemo(
|
|
||||||
() => (
|
|
||||||
<Pressable
|
|
||||||
accessible
|
|
||||||
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
|
|
||||||
style={{
|
style={{
|
||||||
height: DEFAULT_HEIGHT,
|
height: DEFAULT_HEIGHT,
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
||||||
width: DEFAULT_HEIGHT,
|
width: calculateWidth(item)
|
||||||
backgroundColor: colors.backgroundOverlayInvert
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
|
||||||
await chooseAndUploadAttachment({
|
|
||||||
composeDispatch,
|
|
||||||
showActionSheetWithOptions
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<FastImage
|
||||||
type='icon'
|
style={{ width: '100%', height: '100%' }}
|
||||||
content='UploadCloud'
|
source={{
|
||||||
spacing='M'
|
uri: item.local?.thumbnail || item.remote?.preview_url
|
||||||
round
|
|
||||||
overlay
|
|
||||||
onPress={async () => {
|
|
||||||
await chooseAndUploadAttachment({
|
|
||||||
composeDispatch,
|
|
||||||
showActionSheetWithOptions
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2,
|
|
||||||
left: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
{item.remote?.meta?.original?.duration ? (
|
||||||
),
|
<CustomText
|
||||||
[]
|
fontStyle='S'
|
||||||
)
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: StyleConstants.Spacing.S,
|
||||||
|
left: StyleConstants.Spacing.S,
|
||||||
|
paddingLeft: StyleConstants.Spacing.S,
|
||||||
|
paddingRight: StyleConstants.Spacing.S,
|
||||||
|
paddingTop: StyleConstants.Spacing.XS,
|
||||||
|
paddingBottom: StyleConstants.Spacing.XS,
|
||||||
|
color: colors.backgroundDefault,
|
||||||
|
backgroundColor: colors.backgroundOverlayInvert
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.remote.meta.original.duration}
|
||||||
|
</CustomText>
|
||||||
|
) : null}
|
||||||
|
{item.uploading ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.backgroundOverlayInvert
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Circle size={StyleConstants.Font.Size.L} color={colors.primaryOverlay} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignContent: 'flex-end',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
padding: StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
accessibilityLabel={t('content.root.footer.attachments.remove.accessibilityLabel', {
|
||||||
|
attachment: index + 1
|
||||||
|
})}
|
||||||
|
type='icon'
|
||||||
|
content='X'
|
||||||
|
spacing='M'
|
||||||
|
round
|
||||||
|
overlay
|
||||||
|
onPress={() => {
|
||||||
|
layoutAnimation()
|
||||||
|
composeDispatch({
|
||||||
|
type: 'attachment/delete',
|
||||||
|
payload: item.remote!.id
|
||||||
|
})
|
||||||
|
haptics('Success')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!composeState.attachments.disallowEditing ? (
|
||||||
|
<Button
|
||||||
|
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
|
||||||
|
attachment: index + 1
|
||||||
|
})}
|
||||||
|
type='icon'
|
||||||
|
content='Edit'
|
||||||
|
spacing='M'
|
||||||
|
round
|
||||||
|
overlay
|
||||||
|
onPress={() => {
|
||||||
|
navigation.navigate('Screen-Compose-EditAttachment', {
|
||||||
|
index
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -279,13 +229,54 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
pagingEnabled={false}
|
pagingEnabled={false}
|
||||||
snapToAlignment='center'
|
snapToAlignment='center'
|
||||||
renderItem={renderAttachment}
|
renderItem={renderAttachment}
|
||||||
snapToOffsets={snapToOffsets}
|
snapToOffsets={snapToOffsets()}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
data={composeState.attachments.uploads}
|
data={composeState.attachments.uploads}
|
||||||
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
|
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
composeState.attachments.uploads.length < maxAttachments ? listFooter : null
|
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? (
|
||||||
|
<Pressable
|
||||||
|
accessible
|
||||||
|
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
|
||||||
|
style={{
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
width: DEFAULT_HEIGHT,
|
||||||
|
backgroundColor: colors.backgroundOverlayInvert
|
||||||
|
}}
|
||||||
|
onPress={async () => {
|
||||||
|
await chooseAndUploadAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
showActionSheetWithOptions
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='icon'
|
||||||
|
content='UploadCloud'
|
||||||
|
spacing='M'
|
||||||
|
round
|
||||||
|
overlay
|
||||||
|
onPress={async () => {
|
||||||
|
await chooseAndUploadAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
showActionSheetWithOptions
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top:
|
||||||
|
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) /
|
||||||
|
2,
|
||||||
|
left:
|
||||||
|
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -3,14 +3,13 @@ import Icon from '@components/Icon'
|
|||||||
import { MenuRow } from '@components/Menu'
|
import { MenuRow } from '@components/Menu'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { getInstanceConfigurationPoll } from '@utils/slices/instancesSlice'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, TextInput, View } from 'react-native'
|
import { StyleSheet, TextInput, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
|
|
||||||
const ComposePoll: React.FC = () => {
|
const ComposePoll: React.FC = () => {
|
||||||
@@ -24,11 +23,11 @@ const ComposePoll: React.FC = () => {
|
|||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
|
|
||||||
const instanceConfigurationPoll = useSelector(getInstanceConfigurationPoll, () => true)
|
const { data } = useInstanceQuery()
|
||||||
const MAX_OPTIONS = instanceConfigurationPoll.max_options
|
const MAX_OPTIONS = data?.configuration?.polls.max_options || 4
|
||||||
const MAX_CHARS_PER_OPTION = instanceConfigurationPoll.max_characters_per_option
|
const MAX_CHARS_PER_OPTION = data?.configuration?.polls.max_characters_per_option
|
||||||
const MIN_EXPIRATION = instanceConfigurationPoll.min_expiration
|
const MIN_EXPIRATION = data?.configuration?.polls.min_expiration || 300
|
||||||
const MAX_EXPIRATION = instanceConfigurationPoll.max_expiration
|
const MAX_EXPIRATION = data?.configuration?.polls.max_expiration || 2629746
|
||||||
|
|
||||||
const [firstRender, setFirstRender] = useState(true)
|
const [firstRender, setFirstRender] = useState(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
|
import mediaSelector from '@components/mediaSelector'
|
||||||
|
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
import * as Crypto from 'expo-crypto'
|
import * as Crypto from 'expo-crypto'
|
||||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||||
|
import i18next from 'i18next'
|
||||||
import { Dispatch } from 'react'
|
import { Dispatch } from 'react'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { ComposeAction } from '../../utils/types'
|
|
||||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
|
||||||
import i18next from 'i18next'
|
|
||||||
import apiInstance from '@api/instance'
|
|
||||||
import mediaSelector from '@components/mediaSelector'
|
|
||||||
import { Asset } from 'react-native-image-picker'
|
import { Asset } from 'react-native-image-picker'
|
||||||
|
import { ComposeAction } from '../../utils/types'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
|
@@ -1,36 +0,0 @@
|
|||||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import React, { useContext } from 'react'
|
|
||||||
import { StyleSheet, View } from 'react-native'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../utils/createContext'
|
|
||||||
import ComposePostingAs from './Header/PostingAs'
|
|
||||||
import ComposeSpoilerInput from './Header/SpoilerInput'
|
|
||||||
import ComposeTextInput from './Header/TextInput'
|
|
||||||
|
|
||||||
const ComposeRootHeader: React.FC = () => {
|
|
||||||
const { composeState } = useContext(ComposeContext)
|
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
|
||||||
const localInstances = useSelector(getInstances, (prev, next) => prev.length === next.length)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{instanceActive !== -1 && localInstances.length > 1 ? (
|
|
||||||
<View style={styles.postingAs}>
|
|
||||||
<ComposePostingAs />
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
|
|
||||||
<ComposeTextInput />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
postingAs: {
|
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginTop: StyleConstants.Spacing.S
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ComposeRootHeader
|
|
@@ -1,24 +1,32 @@
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { getInstanceAccount, getInstanceUri } from '@utils/slices/instancesSlice'
|
import { getAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { View } from 'react-native'
|
||||||
|
|
||||||
const ComposePostingAs = () => {
|
const ComposePostingAs = () => {
|
||||||
|
const accounts = useGlobalStorage.object('accounts')
|
||||||
|
if (!accounts.length) return null
|
||||||
|
|
||||||
const { t } = useTranslation('screenCompose')
|
const { t } = useTranslation('screenCompose')
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
|
|
||||||
const instanceUri = useSelector(getInstanceUri)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
<View
|
||||||
{t('content.root.header.postingAs', {
|
style={{
|
||||||
acct: instanceAccount?.acct,
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
domain: instanceUri
|
marginTop: StyleConstants.Spacing.S
|
||||||
})}
|
}}
|
||||||
</CustomText>
|
>
|
||||||
|
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||||
|
{t('content.root.header.postingAs', {
|
||||||
|
acct: getAccountStorage.string('auth.account.acct'),
|
||||||
|
domain: getAccountStorage.string('auth.domain')
|
||||||
|
})}
|
||||||
|
</CustomText>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { TextInput } from 'react-native'
|
import { TextInput } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
import { formatText } from '../../utils/processText'
|
import { formatText } from '../../utils/processText'
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ const ComposeSpoilerInput: React.FC = () => {
|
|||||||
const { t } = useTranslation('screenCompose')
|
const { t } = useTranslation('screenCompose')
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
|
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
|
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
|
||||||
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)
|
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)
|
||||||
|
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
|
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import PasteInput, { PastedFile } from '@mattermost/react-native-paste-input'
|
import PasteInput, { PastedFile } from '@mattermost/react-native-paste-input'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
import { useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { adaptiveScale } from '@utils/styles/scaling'
|
import { adaptiveScale } from '@utils/styles/scaling'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
import { formatText } from '../../utils/processText'
|
import { formatText } from '../../utils/processText'
|
||||||
import { uploadAttachment } from '../Footer/addAttachment'
|
import { uploadAttachment } from '../Footer/addAttachment'
|
||||||
@@ -18,9 +17,7 @@ const ComposeTextInput: React.FC = () => {
|
|||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
|
|
||||||
const maxAttachments = useSelector(getInstanceConfigurationStatusMaxAttachments, () => true)
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
|
|
||||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
|
||||||
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
|
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
|
||||||
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)
|
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)
|
||||||
|
|
||||||
@@ -72,7 +69,7 @@ const ComposeTextInput: React.FC = () => {
|
|||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
disableCopyPaste={false}
|
disableCopyPaste={false}
|
||||||
onPaste={(error: string | null | undefined, files: PastedFile[]) => {
|
onPaste={(error: string | null | undefined, files: PastedFile[]) => {
|
||||||
if (composeState.attachments.uploads.length + files.length > maxAttachments) {
|
if (composeState.attachments.uploads.length + files.length > MAX_MEDIA_ATTACHMENTS) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'),
|
t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'),
|
||||||
undefined,
|
undefined,
|
||||||
|
20
src/screens/Compose/Root/Header/index.tsx
Normal file
20
src/screens/Compose/Root/Header/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import ComposeContext from '../../utils/createContext'
|
||||||
|
import ComposePostingAs from './PostingAs'
|
||||||
|
import ComposeSpoilerInput from './SpoilerInput'
|
||||||
|
import ComposeTextInput from './TextInput'
|
||||||
|
|
||||||
|
const ComposeRootHeader: React.FC = () => {
|
||||||
|
const { composeState } = useContext(ComposeContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ComposePostingAs />
|
||||||
|
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
|
||||||
|
<ComposeTextInput />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComposeRootHeader
|
@@ -2,29 +2,20 @@ import ComponentSeparator from '@components/Separator'
|
|||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useMemo, useRef } from 'react'
|
import React, { useContext, useEffect, useRef } from 'react'
|
||||||
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import ComposeActions from './Root/Actions'
|
import ComposePosting from '../Posting'
|
||||||
import ComposePosting from './Posting'
|
import ComposeContext from '../utils/createContext'
|
||||||
import ComposeRootFooter from './Root/Footer'
|
import ComposeActions from './Actions'
|
||||||
import ComposeRootHeader from './Root/Header'
|
import ComposeDrafts from './Drafts'
|
||||||
import ComposeRootSuggestion from './Root/Suggestion'
|
import ComposeRootFooter from './Footer'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeRootHeader from './Header'
|
||||||
import ComposeDrafts from './Root/Drafts'
|
import ComposeRootSuggestion from './Suggestion'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { getInstanceConfigurationStatusCharsURL } from '@utils/slices/instancesSlice'
|
|
||||||
|
|
||||||
export let instanceConfigurationStatusCharsURL = 23
|
|
||||||
|
|
||||||
const ComposeRoot = () => {
|
const ComposeRoot = () => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
instanceConfigurationStatusCharsURL = useSelector(
|
|
||||||
getInstanceConfigurationStatusCharsURL,
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const accessibleRefDrafts = useRef(null)
|
const accessibleRefDrafts = useRef(null)
|
||||||
const accessibleRefAttachments = useRef(null)
|
const accessibleRefAttachments = useRef(null)
|
||||||
|
|
||||||
@@ -62,29 +53,22 @@ const ComposeRoot = () => {
|
|||||||
}
|
}
|
||||||
}, [composeState.tag])
|
}, [composeState.tag])
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
|
||||||
if (isFetching) {
|
|
||||||
return (
|
|
||||||
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [isFetching])
|
|
||||||
|
|
||||||
const Footer = useMemo(
|
|
||||||
() => <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />,
|
|
||||||
[accessibleRefAttachments.current]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
|
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={
|
||||||
|
isFetching ? (
|
||||||
|
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
||||||
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
ListHeaderComponent={ComposeRootHeader}
|
ListHeaderComponent={ComposeRootHeader}
|
||||||
ListFooterComponent={Footer}
|
ListFooterComponent={
|
||||||
|
<ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />
|
||||||
|
}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data={data ? data[mapSchemaToType()] : undefined}
|
data={data ? data[mapSchemaToType()] : undefined}
|
417
src/screens/Compose/index.tsx
Normal file
417
src/screens/Compose/index.tsx
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
import { ComponentEmojis } from '@components/Emojis'
|
||||||
|
import { EmojisState } from '@components/Emojis/Context'
|
||||||
|
import haptics from '@components/haptics'
|
||||||
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
|
import ComposeRoot from '@screens/Compose/Root'
|
||||||
|
import { formatText } from '@screens/Compose/utils/processText'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { handleError } from '@utils/api/helpers'
|
||||||
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
|
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||||
|
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
|
import {
|
||||||
|
getAccountStorage,
|
||||||
|
getGlobalStorage,
|
||||||
|
setAccountStorage,
|
||||||
|
setGlobalStorage
|
||||||
|
} from '@utils/storage/actions'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import * as StoreReview from 'expo-store-review'
|
||||||
|
import { filter } from 'lodash'
|
||||||
|
import React, { useEffect, useMemo, useReducer, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Alert, Keyboard, Platform } from 'react-native'
|
||||||
|
import ComposeDraftsList, { removeDraft } from './DraftsList'
|
||||||
|
import ComposeEditAttachment from './EditAttachment'
|
||||||
|
import { uploadAttachment } from './Root/Footer/addAttachment'
|
||||||
|
import ComposeContext from './utils/createContext'
|
||||||
|
import composeInitialState from './utils/initialState'
|
||||||
|
import composeParseState from './utils/parseState'
|
||||||
|
import composePost from './utils/post'
|
||||||
|
import composeReducer from './utils/reducer'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||||
|
route: { params },
|
||||||
|
navigation
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const [hasKeyboard, setHasKeyboard] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const keyboardShown = Keyboard.addListener('keyboardWillShow', () => setHasKeyboard(true))
|
||||||
|
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () => setHasKeyboard(false))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
keyboardShown.remove()
|
||||||
|
keyboardHidden.remove()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
|
const initialReducerState = useMemo(() => {
|
||||||
|
if (params) {
|
||||||
|
return composeParseState(params)
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...composeInitialState,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
attachments: {
|
||||||
|
...composeInitialState.attachments,
|
||||||
|
sensitive:
|
||||||
|
preferences?.['posting:default:sensitive'] !== undefined
|
||||||
|
? preferences['posting:default:sensitive']
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
visibility: preferences?.['posting:default:visibility'] || 'public'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [composeState, composeDispatch] = useReducer(composeReducer, initialReducerState)
|
||||||
|
|
||||||
|
const { data: dataInstance } = useInstanceQuery()
|
||||||
|
const maxTootChars = dataInstance?.configuration?.statuses.max_characters || 500
|
||||||
|
const totalTextCount =
|
||||||
|
(composeState.spoiler.active ? composeState.spoiler.count : 0) + composeState.text.count
|
||||||
|
|
||||||
|
// If compose state is dirty, then disallow add back drafts
|
||||||
|
useEffect(() => {
|
||||||
|
composeDispatch({
|
||||||
|
type: 'dirty',
|
||||||
|
payload:
|
||||||
|
totalTextCount !== 0 ||
|
||||||
|
composeState.attachments.uploads.length !== 0 ||
|
||||||
|
(composeState.poll.active === true &&
|
||||||
|
filter(composeState.poll.options, o => {
|
||||||
|
return o !== undefined && o.length > 0
|
||||||
|
}).length > 0)
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
totalTextCount,
|
||||||
|
composeState.attachments.uploads.length,
|
||||||
|
composeState.poll.active,
|
||||||
|
composeState.poll.options
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (params?.type) {
|
||||||
|
case 'share':
|
||||||
|
if (params.text) {
|
||||||
|
formatText({
|
||||||
|
textInput: 'text',
|
||||||
|
composeDispatch,
|
||||||
|
content: params.text,
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (params.media?.length) {
|
||||||
|
for (const m of params.media) {
|
||||||
|
uploadAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
media: { uri: m.uri, fileName: 'temp.jpg', type: m.mime }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'edit':
|
||||||
|
case 'deleteEdit':
|
||||||
|
if (params.incomingStatus.spoiler_text) {
|
||||||
|
formatText({
|
||||||
|
textInput: 'spoiler',
|
||||||
|
composeDispatch,
|
||||||
|
content: params.incomingStatus.spoiler_text,
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
formatText({
|
||||||
|
textInput: 'text',
|
||||||
|
composeDispatch,
|
||||||
|
content: params.incomingStatus.text!,
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'reply':
|
||||||
|
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
|
||||||
|
if (actualStatus.spoiler_text) {
|
||||||
|
formatText({
|
||||||
|
textInput: 'spoiler',
|
||||||
|
composeDispatch,
|
||||||
|
content: actualStatus.spoiler_text,
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
params.accts.length && // When replying to myself only, do not add space or even format text
|
||||||
|
formatText({
|
||||||
|
textInput: 'text',
|
||||||
|
composeDispatch,
|
||||||
|
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'conversation':
|
||||||
|
formatText({
|
||||||
|
textInput: 'text',
|
||||||
|
composeDispatch,
|
||||||
|
content:
|
||||||
|
(params.text ? `${params.text}\n` : '') +
|
||||||
|
params.accts.map(acct => `@${acct}`).join(' ') +
|
||||||
|
' ',
|
||||||
|
disableDebounce: true
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [params?.type])
|
||||||
|
|
||||||
|
const saveDraft = () => {
|
||||||
|
const payload = {
|
||||||
|
timestamp: composeState.timestamp,
|
||||||
|
spoiler: composeState.spoiler.raw,
|
||||||
|
text: composeState.text.raw,
|
||||||
|
poll: composeState.poll,
|
||||||
|
attachments: composeState.attachments,
|
||||||
|
visibility: composeState.visibility,
|
||||||
|
visibilityLock: composeState.visibilityLock,
|
||||||
|
replyToStatus: composeState.replyToStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDrafts = getAccountStorage.object('drafts') || []
|
||||||
|
const draftIndex = currentDrafts?.findIndex(
|
||||||
|
({ timestamp }) => timestamp === composeState.timestamp
|
||||||
|
)
|
||||||
|
if (draftIndex === -1) {
|
||||||
|
currentDrafts?.unshift(payload)
|
||||||
|
} else {
|
||||||
|
currentDrafts[draftIndex] = payload
|
||||||
|
}
|
||||||
|
setAccountStorage([{ key: 'drafts', value: currentDrafts }])
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
const autoSave = composeState.dirty
|
||||||
|
? setInterval(() => {
|
||||||
|
saveDraft()
|
||||||
|
}, 1000)
|
||||||
|
: removeDraft(composeState.timestamp)
|
||||||
|
return () => (autoSave ? clearInterval(autoSave) : undefined)
|
||||||
|
}, [composeState])
|
||||||
|
|
||||||
|
const headerRightDisabled = () => {
|
||||||
|
if (totalTextCount > maxTootChars) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const mutateTimeline = useTimelineMutation({ onMutate: true })
|
||||||
|
|
||||||
|
const inputProps: EmojisState['inputProps'] = [
|
||||||
|
{
|
||||||
|
value: [
|
||||||
|
composeState.text.raw,
|
||||||
|
content => {
|
||||||
|
formatText({ textInput: 'text', composeDispatch, content })
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selection: [
|
||||||
|
composeState.text.selection,
|
||||||
|
selection => composeDispatch({ type: 'text', payload: { selection } })
|
||||||
|
],
|
||||||
|
isFocused: composeState.textInputFocus.isFocused.text,
|
||||||
|
maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0),
|
||||||
|
ref: composeState.textInputFocus.refs.text
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: [
|
||||||
|
composeState.spoiler.raw,
|
||||||
|
content => formatText({ textInput: 'spoiler', composeDispatch, content })
|
||||||
|
],
|
||||||
|
selection: [
|
||||||
|
composeState.spoiler.selection,
|
||||||
|
selection => composeDispatch({ type: 'spoiler', payload: { selection } })
|
||||||
|
],
|
||||||
|
isFocused: composeState.textInputFocus.isFocused.spoiler,
|
||||||
|
maxLength: maxTootChars - composeState.text.count,
|
||||||
|
ref: composeState.textInputFocus.refs.spoiler
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComponentEmojis
|
||||||
|
inputProps={inputProps}
|
||||||
|
customButton
|
||||||
|
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
||||||
|
>
|
||||||
|
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
||||||
|
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Compose-Root'
|
||||||
|
component={ComposeRoot}
|
||||||
|
options={{
|
||||||
|
title: `${totalTextCount} / ${maxTootChars}`,
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight:
|
||||||
|
totalTextCount > maxTootChars
|
||||||
|
? StyleConstants.Font.Weight.Bold
|
||||||
|
: StyleConstants.Font.Weight.Normal,
|
||||||
|
fontSize: StyleConstants.Font.Size.M
|
||||||
|
},
|
||||||
|
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
||||||
|
headerLeft: () => (
|
||||||
|
<HeaderLeft
|
||||||
|
type='text'
|
||||||
|
content={t('common:buttons.cancel')}
|
||||||
|
onPress={() => {
|
||||||
|
if (!composeState.dirty) {
|
||||||
|
navigation.goBack()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
Alert.alert(t('screenCompose:heading.left.alert.title'), undefined, [
|
||||||
|
{
|
||||||
|
text: t('screenCompose:heading.left.alert.buttons.delete'),
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => {
|
||||||
|
removeDraft(composeState.timestamp)
|
||||||
|
navigation.goBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('screenCompose:heading.left.alert.buttons.save'),
|
||||||
|
onPress: () => {
|
||||||
|
saveDraft()
|
||||||
|
navigation.goBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('common:buttons.cancel'),
|
||||||
|
style: 'cancel'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
headerRight: () => (
|
||||||
|
<HeaderRight
|
||||||
|
type='text'
|
||||||
|
content={t(
|
||||||
|
`screenCompose:heading.right.button.${
|
||||||
|
(params?.type &&
|
||||||
|
(params.type === 'conversation'
|
||||||
|
? params.visibility === 'direct'
|
||||||
|
? params.type
|
||||||
|
: 'default'
|
||||||
|
: params.type)) ||
|
||||||
|
'default'
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
onPress={() => {
|
||||||
|
composeDispatch({ type: 'posting', payload: true })
|
||||||
|
|
||||||
|
composePost(params, composeState)
|
||||||
|
.then(res => {
|
||||||
|
haptics('Success')
|
||||||
|
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
|
||||||
|
// https://github.com/tooot-app/app/issues/59
|
||||||
|
} else {
|
||||||
|
const currentCount = getGlobalStorage.number(
|
||||||
|
'app.count_till_store_review'
|
||||||
|
)
|
||||||
|
if (currentCount === 10) {
|
||||||
|
StoreReview?.isAvailableAsync()
|
||||||
|
.then(() => StoreReview.requestReview())
|
||||||
|
.catch(() => {})
|
||||||
|
} else {
|
||||||
|
setGlobalStorage('app.count_till_store_review', (currentCount || 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (params?.type) {
|
||||||
|
case 'edit':
|
||||||
|
mutateTimeline.mutate({
|
||||||
|
type: 'editItem',
|
||||||
|
queryKey: params.queryKey,
|
||||||
|
rootQueryKey: params.rootQueryKey,
|
||||||
|
status: res
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'deleteEdit':
|
||||||
|
case 'reply':
|
||||||
|
if (params?.queryKey && params.queryKey[1].page === 'Toot') {
|
||||||
|
queryClient.invalidateQueries(params.queryKey)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
removeDraft(composeState.timestamp)
|
||||||
|
navigation.goBack()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error?.removeReply) {
|
||||||
|
Alert.alert(
|
||||||
|
t('screenCompose:heading.right.alert.removeReply.title'),
|
||||||
|
t('screenCompose:heading.right.alert.removeReply.description'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: t('common:buttons.cancel'),
|
||||||
|
onPress: () => {
|
||||||
|
composeDispatch({ type: 'posting', payload: false })
|
||||||
|
},
|
||||||
|
style: 'destructive'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('screenCompose:heading.right.alert.removeReply.confirm'),
|
||||||
|
onPress: () => {
|
||||||
|
composeDispatch({ type: 'removeReply' })
|
||||||
|
composeDispatch({ type: 'posting', payload: false })
|
||||||
|
},
|
||||||
|
style: 'default'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
haptics('Error')
|
||||||
|
handleError({ message: 'Posting error', captureResponse: true })
|
||||||
|
composeDispatch({ type: 'posting', payload: false })
|
||||||
|
Alert.alert(
|
||||||
|
t('screenCompose:heading.right.alert.default.title'),
|
||||||
|
undefined,
|
||||||
|
[{ text: t('screenCompose:heading.right.alert.default.button') }]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
loading={composeState.posting}
|
||||||
|
disabled={headerRightDisabled()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Compose-DraftsList'
|
||||||
|
component={ComposeDraftsList}
|
||||||
|
options={{ presentation: 'modal' }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Compose-EditAttachment'
|
||||||
|
component={ComposeEditAttachment}
|
||||||
|
options={{ presentation: 'modal' }}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</ComposeContext.Provider>
|
||||||
|
</ComponentEmojis>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScreenCompose
|
@@ -1,14 +1,12 @@
|
|||||||
import { store } from '@root/store'
|
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import composeInitialState from './initialState'
|
import composeInitialState from './initialState'
|
||||||
import { ComposeState } from './types'
|
import { ComposeState } from './types'
|
||||||
|
|
||||||
const assignVisibility = (
|
const assignVisibility = (
|
||||||
target: ComposeState['visibility']
|
target: ComposeState['visibility']
|
||||||
): Pick<ComposeState, 'visibility' | 'visibilityLock'> => {
|
): Pick<ComposeState, 'visibility' | 'visibilityLock'> => {
|
||||||
const accountPreference =
|
const preferences = getAccountStorage.object('preferences')
|
||||||
getInstanceAccount(store.getState())?.preferences?.['posting:default:visibility'] || 'public'
|
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case 'direct':
|
case 'direct':
|
||||||
@@ -16,13 +14,13 @@ const assignVisibility = (
|
|||||||
case 'private':
|
case 'private':
|
||||||
return { visibility: 'private', visibilityLock: false }
|
return { visibility: 'private', visibilityLock: false }
|
||||||
case 'unlisted':
|
case 'unlisted':
|
||||||
if (accountPreference === 'private') {
|
if (preferences?.['posting:default:visibility'] === 'private') {
|
||||||
return { visibility: 'private', visibilityLock: false }
|
return { visibility: 'private', visibilityLock: false }
|
||||||
} else {
|
} else {
|
||||||
return { visibility: 'unlisted', visibilityLock: false }
|
return { visibility: 'unlisted', visibilityLock: false }
|
||||||
}
|
}
|
||||||
case 'public':
|
case 'public':
|
||||||
switch (accountPreference) {
|
switch (preferences) {
|
||||||
case 'private':
|
case 'private':
|
||||||
return { visibility: 'private', visibilityLock: false }
|
return { visibility: 'private', visibilityLock: false }
|
||||||
case 'unlisted':
|
case 'unlisted':
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import apiInstance from '@api/instance'
|
|
||||||
import detectLanguage from '@helpers/detectLanguage'
|
|
||||||
import { ComposeState } from '@screens/Compose/utils/types'
|
import { ComposeState } from '@screens/Compose/utils/types'
|
||||||
|
import apiInstance from '@utils/api/instance'
|
||||||
|
import detectLanguage from '@utils/helpers/detectLanguage'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import * as Crypto from 'expo-crypto'
|
import * as Crypto from 'expo-crypto'
|
||||||
import { getPureContent } from './processText'
|
import { getPureContent } from './processText'
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
|
import { emojis } from '@components/Emojis'
|
||||||
|
import CustomText from '@components/Text'
|
||||||
|
import queryClient from '@utils/queryHooks'
|
||||||
|
import { QueryKeyInstance } from '@utils/queryHooks/instance'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import LinkifyIt from 'linkify-it'
|
import LinkifyIt from 'linkify-it'
|
||||||
import { debounce, differenceWith, isEqual } from 'lodash'
|
import { debounce, differenceWith, isEqual } from 'lodash'
|
||||||
import React, { Dispatch } from 'react'
|
import React, { Dispatch } from 'react'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import { ComposeAction, ComposeState } from './types'
|
import { ComposeAction, ComposeState } from './types'
|
||||||
import { instanceConfigurationStatusCharsURL } from '../Root'
|
|
||||||
import CustomText from '@components/Text'
|
|
||||||
import { emojis } from '@components/Emojis'
|
|
||||||
|
|
||||||
export interface Params {
|
export interface Params {
|
||||||
textInput: ComposeState['textInputFocus']['current']
|
textInput: ComposeState['textInputFocus']['current']
|
||||||
@@ -140,7 +141,11 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal
|
|||||||
contentLength = contentLength + main.length
|
contentLength = contentLength + main.length
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
contentLength = contentLength + instanceConfigurationStatusCharsURL
|
const queryKeyInstance: QueryKeyInstance = ['Instance']
|
||||||
|
contentLength =
|
||||||
|
contentLength +
|
||||||
|
(queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration
|
||||||
|
?.statuses.characters_reserved_per_url || 23)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_content = next
|
_content = next
|
||||||
|
96
src/screens/Compose/utils/types.d.ts
vendored
96
src/screens/Compose/utils/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react'
|
||||||
import { Asset } from 'react-native-image-picker'
|
import { Asset } from 'react-native-image-picker'
|
||||||
|
|
||||||
export type ExtendedAttachment = {
|
export type ExtendedAttachment = {
|
||||||
@@ -67,65 +67,65 @@ export type ComposeState = {
|
|||||||
|
|
||||||
export type ComposeAction =
|
export type ComposeAction =
|
||||||
| {
|
| {
|
||||||
type: 'loadDraft'
|
type: 'loadDraft'
|
||||||
payload: ComposeStateDraft
|
payload: ComposeStateDraft
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'dirty'
|
type: 'dirty'
|
||||||
payload: ComposeState['dirty']
|
payload: ComposeState['dirty']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'posting'
|
type: 'posting'
|
||||||
payload: ComposeState['posting']
|
payload: ComposeState['posting']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'spoiler'
|
type: 'spoiler'
|
||||||
payload: Partial<ComposeState['spoiler']>
|
payload: Partial<ComposeState['spoiler']>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'text'
|
type: 'text'
|
||||||
payload: Partial<ComposeState['text']>
|
payload: Partial<ComposeState['text']>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'tag'
|
type: 'tag'
|
||||||
payload: ComposeState['tag']
|
payload: ComposeState['tag']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'poll'
|
type: 'poll'
|
||||||
payload: Partial<ComposeState['poll']>
|
payload: Partial<ComposeState['poll']>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachments/sensitive'
|
type: 'attachments/sensitive'
|
||||||
payload: Pick<ComposeState['attachments'], 'sensitive'>
|
payload: Pick<ComposeState['attachments'], 'sensitive'>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/upload/start'
|
type: 'attachment/upload/start'
|
||||||
payload: Pick<ExtendedAttachment, 'local' | 'uploading'>
|
payload: Pick<ExtendedAttachment, 'local' | 'uploading'>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/upload/end'
|
type: 'attachment/upload/end'
|
||||||
payload: { remote: Mastodon.Attachment; local: Asset }
|
payload: { remote: Mastodon.Attachment; local: Asset }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/upload/fail'
|
type: 'attachment/upload/fail'
|
||||||
payload: ExtendedAttachment['local']['hash']
|
payload: ExtendedAttachment['local']['hash']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/delete'
|
type: 'attachment/delete'
|
||||||
payload: NonNullable<ExtendedAttachment['remote']>['id']
|
payload: NonNullable<ExtendedAttachment['remote']>['id']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/edit'
|
type: 'attachment/edit'
|
||||||
payload: ExtendedAttachment['remote']
|
payload: ExtendedAttachment['remote']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'visibility'
|
type: 'visibility'
|
||||||
payload: ComposeState['visibility']
|
payload: ComposeState['visibility']
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'textInputFocus'
|
type: 'textInputFocus'
|
||||||
payload: Partial<ComposeState['textInputFocus']>
|
payload: Partial<ComposeState['textInputFocus']>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeReply'
|
type: 'removeReply'
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@@ -18,9 +18,9 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gesture-handler'
|
import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gesture-handler'
|
||||||
import { runOnJS, useSharedValue } from 'react-native-reanimated'
|
import { runOnJS, useSharedValue } from 'react-native-reanimated'
|
||||||
import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom'
|
import { createZoomListComponent, Zoom } from 'react-native-reanimated-zoom'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import saveImage from './ImageViewer/save'
|
import saveImage from './save'
|
||||||
|
|
||||||
const ZoomFlatList = createZoomListComponent(FlatList)
|
const ZoomFlatList = createZoomListComponent(FlatList)
|
||||||
|
|
||||||
@@ -40,111 +40,16 @@ const ScreenImagesViewer = ({
|
|||||||
|
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
const { mode, colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation(['common', 'screenImageViewer'])
|
const { t } = useTranslation(['common', 'screenImageViewer'])
|
||||||
|
|
||||||
const initialIndex = imageUrls.findIndex(image => image.id === id)
|
const initialIndex = imageUrls.findIndex(image => image.id === id)
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
||||||
|
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const onPress = useCallback(() => {
|
|
||||||
showActionSheetWithOptions(
|
|
||||||
{
|
|
||||||
options: [
|
|
||||||
t('screenImageViewer:content.options.save'),
|
|
||||||
t('screenImageViewer:content.options.share'),
|
|
||||||
t('common:buttons.cancel')
|
|
||||||
],
|
|
||||||
cancelButtonIndex: 2,
|
|
||||||
...androidActionSheetStyles(colors)
|
|
||||||
},
|
|
||||||
async buttonIndex => {
|
|
||||||
switch (buttonIndex) {
|
|
||||||
case 0:
|
|
||||||
saveImage({ image: imageUrls[currentIndex] })
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
switch (Platform.OS) {
|
|
||||||
case 'ios':
|
|
||||||
await Share.share({ url: imageUrls[currentIndex].url })
|
|
||||||
break
|
|
||||||
case 'android':
|
|
||||||
await Share.share({ message: imageUrls[currentIndex].url })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, [currentIndex])
|
|
||||||
|
|
||||||
const isZoomed = useSharedValue(false)
|
const isZoomed = useSharedValue(false)
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
|
||||||
({
|
|
||||||
item
|
|
||||||
}: {
|
|
||||||
item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
|
|
||||||
}) => {
|
|
||||||
const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT
|
|
||||||
const imageRatio = item.width && item.height ? item.width / item.height : 1
|
|
||||||
const imageWidth = item.width || 100
|
|
||||||
const imageHeight = item.height || 100
|
|
||||||
|
|
||||||
const maxWidthScale = item.width ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
|
|
||||||
const maxHeightScale = item.height ? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
|
|
||||||
const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Zoom
|
|
||||||
onZoomBegin={() => (isZoomed.value = true)}
|
|
||||||
onZoomEnd={() => (isZoomed.value = false)}
|
|
||||||
maximumZoomScale={max > 8 ? 8 : max}
|
|
||||||
simultaneousGesture={Gesture.Fling()
|
|
||||||
.direction(Directions.DOWN)
|
|
||||||
.onStart(() => {
|
|
||||||
if (isZoomed.value === false) {
|
|
||||||
runOnJS(navigation.goBack)()
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
children={
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: WINDOW_WIDTH,
|
|
||||||
height: WINDOW_HEIGHT,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GracefullyImage
|
|
||||||
uri={{ preview: item.preview_url, remote: item.remote_url, original: item.url }}
|
|
||||||
dimension={{
|
|
||||||
width:
|
|
||||||
screenRatio > imageRatio
|
|
||||||
? (WINDOW_HEIGHT / imageHeight) * imageWidth
|
|
||||||
: WINDOW_WIDTH,
|
|
||||||
height:
|
|
||||||
screenRatio > imageRatio
|
|
||||||
? WINDOW_HEIGHT
|
|
||||||
: (WINDOW_WIDTH / imageWidth) * imageHeight
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[isZoomed.value]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onViewableItemsChanged = useCallback(
|
|
||||||
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
|
||||||
setCurrentIndex(viewableItems[0]?.index || 0)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ backgroundColor: 'black' }}>
|
<View style={{ backgroundColor: 'black' }}>
|
||||||
<StatusBar hidden />
|
<StatusBar hidden />
|
||||||
@@ -169,7 +74,36 @@ const ScreenImagesViewer = ({
|
|||||||
content='MoreHorizontal'
|
content='MoreHorizontal'
|
||||||
native={false}
|
native={false}
|
||||||
background
|
background
|
||||||
onPress={onPress}
|
onPress={() =>
|
||||||
|
showActionSheetWithOptions(
|
||||||
|
{
|
||||||
|
options: [
|
||||||
|
t('screenImageViewer:content.options.save'),
|
||||||
|
t('screenImageViewer:content.options.share'),
|
||||||
|
t('common:buttons.cancel')
|
||||||
|
],
|
||||||
|
cancelButtonIndex: 2,
|
||||||
|
...androidActionSheetStyles(colors)
|
||||||
|
},
|
||||||
|
async buttonIndex => {
|
||||||
|
switch (buttonIndex) {
|
||||||
|
case 0:
|
||||||
|
saveImage({ image: imageUrls[currentIndex] })
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
switch (Platform.OS) {
|
||||||
|
case 'ios':
|
||||||
|
await Share.share({ url: imageUrls[currentIndex].url })
|
||||||
|
break
|
||||||
|
case 'android':
|
||||||
|
await Share.share({ message: imageUrls[currentIndex].url })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<LongPressGestureHandler
|
<LongPressGestureHandler
|
||||||
@@ -211,8 +145,71 @@ const ScreenImagesViewer = ({
|
|||||||
pagingEnabled
|
pagingEnabled
|
||||||
horizontal
|
horizontal
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderItem}
|
renderItem={({
|
||||||
onViewableItemsChanged={onViewableItemsChanged}
|
item
|
||||||
|
}: {
|
||||||
|
item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
|
||||||
|
}) => {
|
||||||
|
const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT
|
||||||
|
const imageRatio = item.width && item.height ? item.width / item.height : 1
|
||||||
|
const imageWidth = item.width || 100
|
||||||
|
const imageHeight = item.height || 100
|
||||||
|
|
||||||
|
const maxWidthScale = item.width
|
||||||
|
? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4
|
||||||
|
: 0
|
||||||
|
const maxHeightScale = item.height
|
||||||
|
? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4
|
||||||
|
: 0
|
||||||
|
const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Zoom
|
||||||
|
onZoomBegin={() => (isZoomed.value = true)}
|
||||||
|
onZoomEnd={() => (isZoomed.value = false)}
|
||||||
|
maximumZoomScale={max > 8 ? 8 : max}
|
||||||
|
simultaneousGesture={Gesture.Fling()
|
||||||
|
.direction(Directions.DOWN)
|
||||||
|
.onStart(() => {
|
||||||
|
if (isZoomed.value === false) {
|
||||||
|
runOnJS(navigation.goBack)()
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
children={
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: WINDOW_WIDTH,
|
||||||
|
height: WINDOW_HEIGHT,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GracefullyImage
|
||||||
|
uri={{
|
||||||
|
preview: item.preview_url,
|
||||||
|
remote: item.remote_url,
|
||||||
|
original: item.url
|
||||||
|
}}
|
||||||
|
dimension={{
|
||||||
|
width:
|
||||||
|
screenRatio > imageRatio
|
||||||
|
? (WINDOW_HEIGHT / imageHeight) * imageWidth
|
||||||
|
: WINDOW_WIDTH,
|
||||||
|
height:
|
||||||
|
screenRatio > imageRatio
|
||||||
|
? WINDOW_HEIGHT
|
||||||
|
: (WINDOW_WIDTH / imageWidth) * imageHeight
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onViewableItemsChanged={({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
||||||
|
setCurrentIndex(viewableItems[0]?.index || 0)
|
||||||
|
}}
|
||||||
viewabilityConfig={{
|
viewabilityConfig={{
|
||||||
itemVisiblePercentThreshold: 50
|
itemVisiblePercentThreshold: 50
|
||||||
}}
|
}}
|
@@ -8,13 +8,12 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
|||||||
import usePopToTop from '@utils/navigation/usePopToTop'
|
import usePopToTop from '@utils/navigation/usePopToTop'
|
||||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceFollowingPage, updateInstanceFollowingPage } from '@utils/slices/instancesSlice'
|
import { setAccountStorage, useAccountStorage } from '@utils/storage/actions'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
|
|
||||||
const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({
|
const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({
|
||||||
@@ -25,11 +24,10 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
|
|
||||||
const { data: lists } = useListsQuery()
|
const { data: lists } = useListsQuery()
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const [pageLocal] = useAccountStorage.object('page_local')
|
||||||
const instanceFollowingPage = useSelector(getInstanceFollowingPage)
|
|
||||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Following', ...instanceFollowingPage }
|
{ page: 'Following', ...pageLocal }
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,7 +57,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
: t('tabs.local.name')
|
: t('tabs.local.name')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{page.page === 'Following' && !instanceFollowingPage.showBoosts ? (
|
{page.page === 'Following' && !pageLocal.showBoosts ? (
|
||||||
<Icon
|
<Icon
|
||||||
name='Repeat'
|
name='Repeat'
|
||||||
size={StyleConstants.Font.Size.M}
|
size={StyleConstants.Font.Size.M}
|
||||||
@@ -68,7 +66,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
crossOut
|
crossOut
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{page.page === 'Following' && !instanceFollowingPage.showReplies ? (
|
{page.page === 'Following' && !pageLocal.showReplies ? (
|
||||||
<Icon
|
<Icon
|
||||||
name='MessageCircle'
|
name='MessageCircle'
|
||||||
size={StyleConstants.Font.Size.M}
|
size={StyleConstants.Font.Size.M}
|
||||||
@@ -90,9 +88,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key='default'
|
key='default'
|
||||||
onSelect={() =>
|
onSelect={() => setQueryKey(['Timeline', { page: 'Following', ...pageLocal }])}
|
||||||
setQueryKey(['Timeline', { page: 'Following', ...instanceFollowingPage }])
|
|
||||||
}
|
|
||||||
disabled={page.page === 'Following'}
|
disabled={page.page === 'Following'}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle children={t('tabs.local.name')} />
|
<DropdownMenu.ItemTitle children={t('tabs.local.name')} />
|
||||||
@@ -100,19 +96,22 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key='showBoosts'
|
key='showBoosts'
|
||||||
value={instanceFollowingPage.showBoosts ? 'on' : 'off'}
|
value={pageLocal.showBoosts ? 'on' : 'off'}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
setQueryKey([
|
setQueryKey([
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{
|
{
|
||||||
page: 'Following',
|
page: 'Following',
|
||||||
showBoosts: !instanceFollowingPage.showBoosts,
|
showBoosts: !pageLocal.showBoosts,
|
||||||
showReplies: instanceFollowingPage.showReplies
|
showReplies: pageLocal.showReplies
|
||||||
|
}
|
||||||
|
])
|
||||||
|
setAccountStorage([
|
||||||
|
{
|
||||||
|
key: 'page_local',
|
||||||
|
value: { ...pageLocal, showBoosts: !pageLocal.showBoosts }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
dispatch(
|
|
||||||
updateInstanceFollowingPage({ showBoosts: !instanceFollowingPage.showBoosts })
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
@@ -120,19 +119,22 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
key='showReplies'
|
key='showReplies'
|
||||||
value={instanceFollowingPage.showReplies ? 'on' : 'off'}
|
value={pageLocal.showReplies ? 'on' : 'off'}
|
||||||
onValueChange={() => {
|
onValueChange={() => {
|
||||||
setQueryKey([
|
setQueryKey([
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{
|
{
|
||||||
page: 'Following',
|
page: 'Following',
|
||||||
showBoosts: instanceFollowingPage.showBoosts,
|
showBoosts: pageLocal.showBoosts,
|
||||||
showReplies: !instanceFollowingPage.showReplies
|
showReplies: !pageLocal.showReplies
|
||||||
|
}
|
||||||
|
])
|
||||||
|
setAccountStorage([
|
||||||
|
{
|
||||||
|
key: 'page_local',
|
||||||
|
value: { ...pageLocal, showReplies: !pageLocal.showReplies }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
dispatch(
|
|
||||||
updateInstanceFollowingPage({ showReplies: !instanceFollowingPage.showReplies })
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle children={t('tabs.local.options.showReplies')} />
|
<DropdownMenu.ItemTitle children={t('tabs.local.options.showReplies')} />
|
||||||
@@ -174,7 +176,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, [mode, queryKey[1], instanceFollowingPage, lists])
|
}, [mode, queryKey[1], pageLocal, lists])
|
||||||
|
|
||||||
usePopToTop()
|
usePopToTop()
|
||||||
|
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
import { EmojisState } from '@components/Emojis/Context'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import ComponentInput from '@components/Input'
|
import ComponentInput from '@components/Input'
|
||||||
import { displayMessage, Message } from '@components/Message'
|
import { displayMessage, Message } from '@components/Message'
|
||||||
import Selections from '@components/Selections'
|
import Selections from '@components/Selections'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import { CommonActions } from '@react-navigation/native'
|
import { CommonActions } from '@react-navigation/native'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
|
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Platform, ScrollView, TextInput } from 'react-native'
|
import { Alert, ScrollView, TextInput } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
|
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
|
||||||
navigation,
|
navigation,
|
||||||
|
@@ -2,6 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import { displayMessage } from '@components/Message'
|
import { displayMessage } from '@components/Message'
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
|
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
@@ -9,7 +10,6 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import { menuListAccounts, menuListDelete, menuListEdit } from './menus'
|
import { menuListAccounts, menuListDelete, menuListEdit } from './menus'
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import navigationRef from '@helpers/navigationRef'
|
import { UseMutationResult } from '@tanstack/react-query'
|
||||||
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { UseMutationResult } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({
|
export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({
|
||||||
key: 'list-accounts',
|
key: 'list-accounts',
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentEmojis } from '@components/Emojis'
|
import { ComponentEmojis } from '@components/Emojis'
|
||||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
import { EmojisState } from '@components/Emojis/Context'
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import ComponentInput from '@components/Input'
|
import ComponentInput from '@components/Input'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentEmojis } from '@components/Emojis'
|
import { ComponentEmojis } from '@components/Emojis'
|
||||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
import { EmojisState } from '@components/Emojis/Context'
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import ComponentInput from '@components/Input'
|
import ComponentInput from '@components/Input'
|
||||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentEmojis } from '@components/Emojis'
|
import { ComponentEmojis } from '@components/Emojis'
|
||||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
import { EmojisState } from '@components/Emojis/Context'
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import ComponentInput from '@components/Input'
|
import ComponentInput from '@components/Input'
|
||||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { useAppDispatch } from '@root/store'
|
import queryClient from '@utils/queryHooks'
|
||||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||||
|
import { QueryKeyPreferences } from '@utils/queryHooks/preferences'
|
||||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject } from 'react'
|
import React, { RefObject } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -24,7 +24,11 @@ const TabMeProfileRoot: React.FC<
|
|||||||
|
|
||||||
const { data, isFetching } = useProfileQuery()
|
const { data, isFetching } = useProfileQuery()
|
||||||
const { mutateAsync } = useProfileMutation()
|
const { mutateAsync } = useProfileMutation()
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
const refetchPreferences = () => {
|
||||||
|
const queryKeyPreferences: QueryKeyPreferences = ['Preferences']
|
||||||
|
queryClient.refetchQueries(queryKeyPreferences)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
@@ -117,7 +121,7 @@ const TabMeProfileRoot: React.FC<
|
|||||||
},
|
},
|
||||||
type: 'source[privacy]',
|
type: 'source[privacy]',
|
||||||
data: indexVisibilityMapping[buttonIndex]
|
data: indexVisibilityMapping[buttonIndex]
|
||||||
}).then(() => dispatch(updateAccountPreferences()))
|
}).then(() => refetchPreferences())
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -139,7 +143,7 @@ const TabMeProfileRoot: React.FC<
|
|||||||
},
|
},
|
||||||
type: 'source[sensitive]',
|
type: 'source[sensitive]',
|
||||||
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
|
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
|
||||||
}).then(() => dispatch(updateAccountPreferences()))
|
}).then(() => refetchPreferences())
|
||||||
}
|
}
|
||||||
loading={isFetching}
|
loading={isFetching}
|
||||||
/>
|
/>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import { Message } from '@components/Message'
|
import { Message } from '@components/Message'
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
import { TabMeProfileStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeProfileStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
@@ -6,10 +6,10 @@ import React, { useRef } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { KeyboardAvoidingView, Platform } from 'react-native'
|
import { KeyboardAvoidingView, Platform } from 'react-native'
|
||||||
import FlashMessage from 'react-native-flash-message'
|
import FlashMessage from 'react-native-flash-message'
|
||||||
import TabMeProfileFields from './Profile/Fields'
|
import TabMeProfileFields from './Fields'
|
||||||
import TabMeProfileName from './Profile/Name'
|
import TabMeProfileName from './Name'
|
||||||
import TabMeProfileNote from './Profile/Note'
|
import TabMeProfileNote from './Note'
|
||||||
import TabMeProfileRoot from './Profile/Root'
|
import TabMeProfileRoot from './Root'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<TabMeProfileStackParamList>()
|
const Stack = createNativeStackNavigator<TabMeProfileStackParamList>()
|
||||||
|
|
@@ -1,40 +1,39 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
|
import { displayMessage } from '@components/Message'
|
||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
import * as Sentry from '@sentry/react-native'
|
||||||
import { useAppDispatch } from '@root/store'
|
import apiInstance from '@utils/api/instance'
|
||||||
import { isDevelopment } from '@utils/checkEnvironment'
|
import apiTooot, { TOOOT_API_DOMAIN } from '@utils/api/tooot'
|
||||||
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
|
import { isDevelopment } from '@utils/helpers/checkEnvironment'
|
||||||
|
import { PUSH_ADMIN, PUSH_DEFAULT, setChannels } from '@utils/push/constants'
|
||||||
|
import { updateExpoToken } from '@utils/push/updateExpoToken'
|
||||||
import { useAppsQuery } from '@utils/queryHooks/apps'
|
import { useAppsQuery } from '@utils/queryHooks/apps'
|
||||||
import { useProfileQuery } from '@utils/queryHooks/profile'
|
import { useProfileQuery } from '@utils/queryHooks/profile'
|
||||||
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
|
import { setAccountStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { PUSH_ADMIN, PUSH_DEFAULT, usePushFeatures } from '@utils/slices/instances/push/utils'
|
|
||||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
|
||||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
|
||||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
|
||||||
import { getInstance, getInstancePush } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { AppState, Linking, ScrollView, View } from 'react-native'
|
import { AppState, Linking, Platform, ScrollView, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const TabMePush: React.FC = () => {
|
const TabMePush: React.FC = () => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
|
|
||||||
const instance = useSelector(getInstance)
|
const [expoToken] = useGlobalStorage.string('app.expo_token')
|
||||||
const expoToken = useSelector(getExpoToken)
|
const [push] = useAccountStorage.object('push')
|
||||||
|
const [domain] = useAccountStorage.string('auth.domain')
|
||||||
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
|
const [accountAcct] = useAccountStorage.string('auth.account.acct')
|
||||||
|
|
||||||
const appsQuery = useAppsQuery()
|
const appsQuery = useAppsQuery()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const instancePush = useSelector(getInstancePush)
|
|
||||||
|
|
||||||
const [pushAvailable, setPushAvailable] = useState<boolean>()
|
const [pushAvailable, setPushAvailable] = useState<boolean>()
|
||||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||||
@@ -45,7 +44,7 @@ const TabMePush: React.FC = () => {
|
|||||||
setPushEnabled(permissions.granted)
|
setPushEnabled(permissions.granted)
|
||||||
setPushCanAskAgain(permissions.canAskAgain)
|
setPushCanAskAgain(permissions.canAskAgain)
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
dispatch(retrieveExpoToken())
|
await updateExpoToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appsQuery.data?.vapid_key) {
|
if (appsQuery.data?.vapid_key) {
|
||||||
@@ -54,7 +53,7 @@ const TabMePush: React.FC = () => {
|
|||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
setPushAvailable(true)
|
setPushAvailable(true)
|
||||||
} else {
|
} else {
|
||||||
setPushAvailable(!!expoToken)
|
setPushAvailable(!!expoToken?.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,26 +63,29 @@ const TabMePush: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [appsQuery.data?.vapid_key])
|
}, [appsQuery.data?.vapid_key])
|
||||||
|
|
||||||
const pushFeatures = usePushFeatures()
|
|
||||||
|
|
||||||
const alerts = () =>
|
const alerts = () =>
|
||||||
instancePush?.alerts
|
push?.alerts
|
||||||
? PUSH_DEFAULT(pushFeatures).map(alert => (
|
? PUSH_DEFAULT.map(alert => (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
key={alert}
|
key={alert}
|
||||||
title={t(`me.push.${alert}.heading`)}
|
title={t(`me.push.${alert}.heading`)}
|
||||||
switchDisabled={!pushEnabled || !instancePush.global}
|
switchDisabled={!pushEnabled || !push.global}
|
||||||
switchValue={instancePush?.alerts[alert]}
|
switchValue={push?.alerts[alert]}
|
||||||
switchOnValueChange={() =>
|
switchOnValueChange={async () => {
|
||||||
dispatch(
|
const alerts = { ...push?.alerts, [alert]: !push?.alerts[alert] }
|
||||||
updateInstancePushAlert({
|
const formData = new FormData()
|
||||||
alerts: {
|
for (const [key, value] of Object.entries(alerts)) {
|
||||||
...instancePush?.alerts,
|
formData.append(`data[alerts][${key}]`, value.toString())
|
||||||
[alert]: !instancePush?.alerts[alert]
|
}
|
||||||
}
|
|
||||||
})
|
await apiInstance<Mastodon.PushSubscription>({
|
||||||
)
|
method: 'put',
|
||||||
}
|
url: 'push/subscription',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
setAccountStorage([{ key: 'push', value: { ...push, alerts } }])
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null
|
: null
|
||||||
@@ -91,26 +93,34 @@ const TabMePush: React.FC = () => {
|
|||||||
const profileQuery = useProfileQuery()
|
const profileQuery = useProfileQuery()
|
||||||
const adminAlerts = () =>
|
const adminAlerts = () =>
|
||||||
profileQuery.data?.role?.permissions
|
profileQuery.data?.role?.permissions
|
||||||
? PUSH_ADMIN(pushFeatures, profileQuery.data?.role?.permissions).map(({ type }) => (
|
? PUSH_ADMIN.map(({ type }) => (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
key={type}
|
key={type}
|
||||||
title={t(`me.push.${type}.heading`)}
|
title={t(`me.push.${type}.heading`)}
|
||||||
switchDisabled={!pushEnabled || !instancePush.global}
|
switchDisabled={!pushEnabled || !push.global}
|
||||||
switchValue={instancePush?.alerts[type]}
|
switchValue={push?.alerts[type]}
|
||||||
switchOnValueChange={() =>
|
switchOnValueChange={async () => {
|
||||||
dispatch(
|
const alerts = { ...push?.alerts, [type]: !push?.alerts[type] }
|
||||||
updateInstancePushAlert({
|
const formData = new FormData()
|
||||||
alerts: {
|
for (const [key, value] of Object.entries(alerts)) {
|
||||||
...instancePush?.alerts,
|
formData.append(`data[alerts][${key}]`, value.toString())
|
||||||
[type]: !instancePush?.alerts[type]
|
}
|
||||||
}
|
|
||||||
})
|
await apiInstance<Mastodon.PushSubscription>({
|
||||||
)
|
method: 'put',
|
||||||
}
|
url: 'push/subscription',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
setAccountStorage([{ key: 'push', value: { ...push, alerts } }])
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const pushPath = `${expoToken}/${domain}/${accountId}`
|
||||||
|
const accountFull = `@${accountAcct}@${domain}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
{!!appsQuery.data?.vapid_key ? (
|
{!!appsQuery.data?.vapid_key ? (
|
||||||
@@ -142,24 +152,103 @@ const TabMePush: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('me.push.global.heading', {
|
title={t('me.push.global.heading', { acct: `@${accountAcct}@${domain}` })}
|
||||||
acct: `@${instance.account.acct}@${instance.uri}`
|
|
||||||
})}
|
|
||||||
description={t('me.push.global.description')}
|
description={t('me.push.global.description')}
|
||||||
switchDisabled={!pushEnabled}
|
switchDisabled={!pushEnabled}
|
||||||
switchValue={pushEnabled === false ? false : instancePush?.global}
|
switchValue={pushEnabled === false ? false : push?.global}
|
||||||
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global))}
|
switchOnValueChange={async () => {
|
||||||
|
if (push.global) {
|
||||||
|
// Turning off
|
||||||
|
await apiInstance({
|
||||||
|
method: 'delete',
|
||||||
|
url: 'push/subscription'
|
||||||
|
})
|
||||||
|
await apiTooot({
|
||||||
|
method: 'delete',
|
||||||
|
url: `push/unsubscribe/${pushPath}`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
Notifications.deleteNotificationChannelGroupAsync(accountFull)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountStorage([{ key: 'push', value: { ...push, global: false } }])
|
||||||
|
} else {
|
||||||
|
// Turning on
|
||||||
|
const randomPath = (Math.random() + 1).toString(36).substring(2)
|
||||||
|
|
||||||
|
const endpoint = `https://${TOOOT_API_DOMAIN}/push/send/${pushPath}/${randomPath}`
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('subscription[endpoint]', endpoint)
|
||||||
|
formData.append(
|
||||||
|
'subscription[keys][p256dh]',
|
||||||
|
'BMn2PLpZrMefG981elzG6SB1EY9gU7QZwmtZ/a/J2vUeWG+zXgeskMPwHh4T/bxsD4l7/8QT94F57CbZqYRRfJo='
|
||||||
|
)
|
||||||
|
formData.append('subscription[keys][auth]', push.key)
|
||||||
|
for (const [key, value] of Object.entries(push.alerts)) {
|
||||||
|
formData.append(`data[alerts][${key}]`, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await apiInstance<Mastodon.PushSubscription>({
|
||||||
|
method: 'post',
|
||||||
|
url: 'push/subscription',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.body.server_key?.length) {
|
||||||
|
displayMessage({
|
||||||
|
type: 'danger',
|
||||||
|
duration: 'long',
|
||||||
|
message: t('me.push.missingServerKey.message'),
|
||||||
|
description: t('me.push.missingServerKey.description')
|
||||||
|
})
|
||||||
|
Sentry.setContext('Push server key', {
|
||||||
|
instance: domain,
|
||||||
|
resBody: res.body
|
||||||
|
})
|
||||||
|
Sentry.captureMessage('Push register error')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiTooot({
|
||||||
|
method: 'post',
|
||||||
|
url: `push/subscribe/${pushPath}`,
|
||||||
|
body: {
|
||||||
|
accountFull,
|
||||||
|
serverKey: res.body.server_key,
|
||||||
|
auth: push.decode === false ? null : push.key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
setChannels(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountStorage([{ key: 'push', value: { ...push, global: true } }])
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('me.push.decode.heading')}
|
title={t('me.push.decode.heading')}
|
||||||
description={t('me.push.decode.description')}
|
description={t('me.push.decode.description')}
|
||||||
switchDisabled={!pushEnabled || !instancePush?.global}
|
switchDisabled={!pushEnabled || !push?.global}
|
||||||
switchValue={instancePush?.decode}
|
switchValue={push?.decode}
|
||||||
switchOnValueChange={() =>
|
switchOnValueChange={async () => {
|
||||||
dispatch(updateInstancePushDecode(!instancePush?.decode))
|
await apiTooot({
|
||||||
}
|
method: 'put',
|
||||||
|
url: `push/update-decode/${pushPath}`,
|
||||||
|
body: { auth: push?.decode ? null : push.key }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
setChannels(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountStorage([{ key: 'push', value: { ...push, decode: !push.decode } }])
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('me.push.howitworks')}
|
title={t('me.push.howitworks')}
|
||||||
|
@@ -1,64 +1,37 @@
|
|||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAppDispatch } from '@root/store'
|
|
||||||
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
||||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||||
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
import {
|
|
||||||
checkInstanceFeature,
|
|
||||||
getInstanceMePage,
|
|
||||||
updateInstanceMePage
|
|
||||||
} from '@utils/slices/instancesSlice'
|
|
||||||
import { getInstancePush } from '@utils/slices/instancesSlice'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const Collections: React.FC = () => {
|
const Collections: React.FC = () => {
|
||||||
const { t } = useTranslation(['screenAnnouncements', 'screenTabs'])
|
const { t } = useTranslation(['screenAnnouncements', 'screenTabs'])
|
||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const [pageMe, setPageMe] = useAccountStorage.object('page_me')
|
||||||
const mePage = useSelector(getInstanceMePage)
|
|
||||||
|
|
||||||
const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
|
|
||||||
useFollowedTagsQuery({
|
|
||||||
options: {
|
|
||||||
enabled: canFollowTags,
|
|
||||||
onSuccess: data =>
|
|
||||||
dispatch(
|
|
||||||
updateInstanceMePage({
|
|
||||||
followedTags: { shown: !!data?.pages?.[0].body?.length }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
useListsQuery({
|
useListsQuery({
|
||||||
options: {
|
options: {
|
||||||
onSuccess: data =>
|
onSuccess: data => setPageMe({ ...pageMe, lists: { shown: !!data?.length } })
|
||||||
dispatch(
|
|
||||||
updateInstanceMePage({
|
|
||||||
lists: { shown: !!data?.length }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
useAnnouncementQuery({
|
useAnnouncementQuery({
|
||||||
showAll: true,
|
showAll: true,
|
||||||
options: {
|
options: {
|
||||||
onSuccess: data =>
|
onSuccess: data =>
|
||||||
dispatch(
|
setPageMe({
|
||||||
updateInstanceMePage({
|
...pageMe,
|
||||||
announcements: {
|
announcements: {
|
||||||
shown: !!data?.length ? true : false,
|
shown: !!data?.length ? true : false,
|
||||||
unread: data?.filter(announcement => !announcement.read).length
|
unread: data?.filter(announcement => !announcement.read).length
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global)
|
const [instancePush] = useAccountStorage.object('push')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
@@ -80,7 +53,7 @@ const Collections: React.FC = () => {
|
|||||||
title={t('screenTabs:me.stacks.favourites.name')}
|
title={t('screenTabs:me.stacks.favourites.name')}
|
||||||
onPress={() => navigation.navigate('Tab-Me-Favourites')}
|
onPress={() => navigation.navigate('Tab-Me-Favourites')}
|
||||||
/>
|
/>
|
||||||
{mePage.lists.shown ? (
|
{pageMe.lists.shown ? (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
iconFront='List'
|
iconFront='List'
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
@@ -88,7 +61,7 @@ const Collections: React.FC = () => {
|
|||||||
onPress={() => navigation.navigate('Tab-Me-List-List')}
|
onPress={() => navigation.navigate('Tab-Me-List-List')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{mePage.followedTags.shown ? (
|
{pageMe.followedTags.shown ? (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
iconFront='Hash'
|
iconFront='Hash'
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
@@ -96,15 +69,15 @@ const Collections: React.FC = () => {
|
|||||||
onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
|
onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{mePage.announcements.shown ? (
|
{pageMe.announcements.shown ? (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
iconFront='Clipboard'
|
iconFront='Clipboard'
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
title={t('screenAnnouncements:heading')}
|
title={t('screenAnnouncements:heading')}
|
||||||
content={
|
content={
|
||||||
mePage.announcements.unread
|
pageMe.announcements.unread
|
||||||
? t('screenTabs:me.root.announcements.content.unread', {
|
? t('screenTabs:me.root.announcements.content.unread', {
|
||||||
amount: mePage.announcements.unread
|
amount: pageMe.announcements.unread
|
||||||
})
|
})
|
||||||
: t('screenTabs:me.root.announcements.content.read')
|
: t('screenTabs:me.root.announcements.content.read')
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,15 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@root/components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { useAppDispatch } from '@root/store'
|
import { removeAccount, useGlobalStorage } from '@utils/storage/actions'
|
||||||
import removeInstance from '@utils/slices/instances/remove'
|
|
||||||
import { getInstance } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const Logout: React.FC = () => {
|
const Logout: React.FC = () => {
|
||||||
const { t } = useTranslation(['common', 'screenTabs'])
|
const { t } = useTranslation(['common', 'screenTabs'])
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const queryClient = useQueryClient()
|
const [accountActive] = useGlobalStorage.string('account.active')
|
||||||
const instance = useSelector(getInstance)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -35,10 +30,9 @@ const Logout: React.FC = () => {
|
|||||||
text: t('screenTabs:me.root.logout.alert.buttons.logout'),
|
text: t('screenTabs:me.root.logout.alert.buttons.logout'),
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
if (instance) {
|
if (accountActive) {
|
||||||
haptics('Success')
|
haptics('Light')
|
||||||
queryClient.clear()
|
removeAccount(accountActive)
|
||||||
dispatch(removeInstance(instance))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import browserPackage from '@helpers/browserPackage'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { getInstanceActive, getInstanceUrl } from '@utils/slices/instancesSlice'
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
|
import { getAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
|
||||||
const url = useSelector(getInstanceUrl)
|
const [accountActive] = useGlobalStorage.string('account.active')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
@@ -21,14 +20,14 @@ const Settings: React.FC = () => {
|
|||||||
title={t('me.stacks.settings.name')}
|
title={t('me.stacks.settings.name')}
|
||||||
onPress={() => navigation.navigate('Tab-Me-Settings')}
|
onPress={() => navigation.navigate('Tab-Me-Settings')}
|
||||||
/>
|
/>
|
||||||
{instanceActive !== -1 ? (
|
{accountActive ? (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
iconFront='Sliders'
|
iconFront='Sliders'
|
||||||
iconBack='ExternalLink'
|
iconBack='ExternalLink'
|
||||||
title={t('me.stacks.webSettings.name')}
|
title={t('me.stacks.webSettings.name')}
|
||||||
onPress={async () =>
|
onPress={async () =>
|
||||||
WebBrowser.openAuthSessionAsync(
|
WebBrowser.openAuthSessionAsync(
|
||||||
`https://${url}/settings/preferences`,
|
`https://${getAccountStorage.string('auth.domain')}/settings/preferences`,
|
||||||
'tooot://tooot',
|
'tooot://tooot',
|
||||||
{
|
{
|
||||||
...(await browserPackage()),
|
...(await browserPackage()),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user