1
0
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:
xmflsct
2022-12-29 01:10:58 +01:00
233 changed files with 3568 additions and 5353 deletions

View File

@@ -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,

View File

@@ -2,7 +2,7 @@
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot) [![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
![GitHub Workflow Status (candidate)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release) ![GitHub Workflow Status (candidate)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=candidate&label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=release&label=build%20release)
## Contribute to translation ## Contribute to translation
@@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati
## Special thanks ## Special thanks
[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation - [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
- [@forenta](https://github.com/forenta) for German translation
[@forenta](https://github.com/forenta) for German translation - [@pat](https://piaille.fr/@pat) for French translation
- [@andrigamerita](https://github.com/andrigamerita) for Italian translation
[@pat](https://piaille.fr/@pat) for French translation - [@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
- [@hellojaccc](https://github.com/hellojaccc) for Korean translation
[@andrigamerita](https://github.com/andrigamerita) for Italian translation - [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation - [@janlindblom](https://github.com/janlindblom) for Swedish
- [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
[@hellojaccc](https://github.com/hellojaccc) for Korean translation - [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
- [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation - [@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
[@janlindblom](https://github.com/janlindblom) for Swedish
[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound

View File

@@ -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'
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

View File

@@ -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

View File

@@ -1,4 +1,5 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 可添加举报细节 - 可添加举报细节
- 新增暂停自动播放gif动画选项 - 新增暂停自动播放gif动画选项
- 隐藏用户的转嘟 - 隐藏用户的转嘟
- 下划线高亮正在关注的话题标签

View File

@@ -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,

View File

@@ -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

View File

@@ -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"
} }

View File

@@ -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

View File

@@ -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'

View File

@@ -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>
) )

View File

@@ -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()

View File

@@ -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}
/> />
) )

View File

@@ -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()

View File

@@ -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 }}
> >

View File

@@ -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']> = []

View File

@@ -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>
) )
} }

View File

@@ -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

View File

@@ -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
}), }),

View File

@@ -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',

View File

@@ -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 }

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
import ParseEmojis from './Parse/Emojis'
import ParseHTML from './Parse/HTML'
export { ParseEmojis, ParseHTML }

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
import ParseEmojis from './Emojis'
import ParseHTML from './HTML'
export { ParseEmojis, ParseHTML }

View File

@@ -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']

View File

@@ -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 })

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>>

View File

@@ -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>
) )

View File

@@ -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':

View File

@@ -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)

View File

@@ -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'

View File

@@ -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>
) )
} }

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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()}

View File

@@ -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'

View File

@@ -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} />

View File

@@ -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(() => {

View File

@@ -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',

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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, {

View File

@@ -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

View File

@@ -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'

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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={{

View File

@@ -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'

View File

@@ -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 = () => {

View File

@@ -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>
) )

View File

@@ -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

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
) )
} }

View File

@@ -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)

View File

@@ -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,

View 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

View File

@@ -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}

View 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

View File

@@ -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':

View File

@@ -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'

View File

@@ -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

View File

@@ -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'
} }

View File

@@ -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
}} }}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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'

View File

@@ -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',

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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}
/> />

View File

@@ -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>()

View File

@@ -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')}

View File

@@ -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')
} }

View File

@@ -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))
} }
} }
}, },

View File

@@ -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