diff --git a/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch b/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch
deleted file mode 100644
index f42c82e7..00000000
--- a/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch
+++ /dev/null
@@ -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,
diff --git a/README.md b/README.md
index 7d29999d..34d1f97c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](LICENSE)   [](https://crowdin.tooot.app/project/tooot)
- 
+ 
## Contribute to translation
@@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati
## Special thanks
-[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
-
-[@forenta](https://github.com/forenta) for German translation
-
-[@pat](https://piaille.fr/@pat) for French translation
-
-[@andrigamerita](https://github.com/andrigamerita) for Italian translation
-
-[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
-
-[@hellojaccc](https://github.com/hellojaccc) for Korean translation
-
-[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
-
-[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
-
-[@janlindblom](https://github.com/janlindblom) for Swedish
-
-[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
-
-[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
-
-[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
-
-[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
+- [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation
+- [@forenta](https://github.com/forenta) for German translation
+- [@pat](https://piaille.fr/@pat) for French translation
+- [@andrigamerita](https://github.com/andrigamerita) for Italian translation
+- [@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation
+- [@hellojaccc](https://github.com/hellojaccc) for Korean translation
+- [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation
+- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
+- [@janlindblom](https://github.com/janlindblom) for Swedish
+- [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian
+- [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
+- [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation
+- [@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound
diff --git a/babel.config.js b/babel.config.js
index 1ffee43c..0d5803fa 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -8,11 +8,8 @@ module.exports = function (api) {
{
root: ['./'],
alias: {
- '@assets': './assets',
- '@root': './src',
- '@api': './src/api',
- '@helpers': './src/helpers',
'@components': './src/components',
+ '@i18n': './src/i18n',
'@screens': './src/screens',
'@utils': './src/utils'
}
diff --git a/demo/screenshots/Tab-Shared-Report.png b/demo/screenshots/Tab-Shared-Report.png
new file mode 100644
index 00000000..48eb76bf
Binary files /dev/null and b/demo/screenshots/Tab-Shared-Report.png differ
diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt
index 731c525f..62516967 100644
--- a/fastlane/metadata/en-US/release_notes.txt
+++ b/fastlane/metadata/en-US/release_notes.txt
@@ -1,4 +1,5 @@
Enjoy toooting! This version includes following improvements and fixes:
- Allowing adding more context of reports
- Option to disable autoplay gif
-- Hide boosts from users
\ No newline at end of file
+- Hide boosts from users
+- Followed hashtags are underlined
\ No newline at end of file
diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt
index af941da8..8225007b 100644
--- a/fastlane/metadata/zh-Hans/release_notes.txt
+++ b/fastlane/metadata/zh-Hans/release_notes.txt
@@ -1,4 +1,5 @@
toooting愉快!此版本包括以下改进和修复:
- 可添加举报细节
- 新增暂停自动播放gif动画选项
-- 隐藏用户的转嘟
\ No newline at end of file
+- 隐藏用户的转嘟
+- 下划线高亮正在关注的话题标签
\ No newline at end of file
diff --git a/index.js b/index.js
index 40a9d636..d292a7a1 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
import { registerRootComponent } from 'expo'
-import App from '@root/App'
+import App from './src/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,
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index e148c49d..478da943 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -65,6 +65,9 @@ PODS:
- libwebp/mux (1.2.4):
- libwebp/demux
- libwebp/webp (1.2.4)
+ - MMKV (1.2.14):
+ - MMKVCore (~> 1.2.14)
+ - MMKVCore (1.2.14)
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
@@ -305,6 +308,9 @@ PODS:
- React
- react-native-menu (0.7.2):
- React
+ - react-native-mmkv (2.5.1):
+ - MMKV (>= 1.2.13)
+ - React-Core
- react-native-netinfo (9.3.7):
- React-Core
- react-native-pager-view (6.1.2):
@@ -494,6 +500,7 @@ DEPENDENCIES:
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
- react-native-language-detection (from `../node_modules/react-native-language-detection`)
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
+ - react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
@@ -527,6 +534,8 @@ SPEC REPOS:
- fmt
- libevent
- libwebp
+ - MMKV
+ - MMKVCore
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@@ -629,6 +638,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-language-detection"
react-native-menu:
:path: "../node_modules/@react-native-menu/menu"
+ react-native-mmkv:
+ :path: "../node_modules/react-native-mmkv"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pager-view:
@@ -714,6 +725,8 @@ SPEC CHECKSUMS:
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
+ MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
+ MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
@@ -736,6 +749,7 @@ SPEC CHECKSUMS:
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
+ react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
diff --git a/package.json b/package.json
index aba133e8..714441c9 100644
--- a/package.json
+++ b/package.json
@@ -32,12 +32,11 @@
"@react-native-community/blur": "^4.3.0",
"@react-native-community/netinfo": "9.3.7",
"@react-native-community/segmented-control": "^2.2.2",
- "@react-native-menu/menu": "^0.7.2",
+ "@react-native-menu/menu": "^0.7.3",
"@react-navigation/bottom-tabs": "^6.5.2",
"@react-navigation/native": "^6.1.1",
"@react-navigation/native-stack": "^6.9.7",
"@react-navigation/stack": "^6.3.10",
- "@reduxjs/toolkit": "^1.9.1",
"@sentry/react-native": "4.12.0",
"@sharcoux/slider": "^6.1.1",
"@tanstack/react-query": "^4.20.4",
@@ -60,6 +59,7 @@
"expo-store-review": "^6.0.0",
"expo-video-thumbnails": "^7.0.0",
"expo-web-browser": "~12.0.0",
+ "htmlparser2": "^8.0.1",
"i18next": "^22.4.6",
"linkify-it": "^4.0.1",
"lodash": "^4.17.21",
@@ -75,10 +75,10 @@
"react-native-feather": "^1.1.2",
"react-native-flash-message": "^0.3.1",
"react-native-gesture-handler": "~2.8.0",
- "react-native-htmlview": "^0.16.0",
"react-native-image-picker": "^4.10.3",
"react-native-ios-context-menu": "^1.15.1",
"react-native-language-detection": "^0.2.2",
+ "react-native-mmkv": "^2.5.1",
"react-native-pager-view": "^6.1.2",
"react-native-reanimated": "^2.13.0",
"react-native-reanimated-zoom": "^0.3.3",
@@ -89,7 +89,6 @@
"react-native-swipe-list-view": "^3.2.9",
"react-native-tab-view": "^3.3.4",
"react-redux": "^8.0.5",
- "redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3",
"rtl-detect": "^1.0.4",
"valid-url": "^1.0.9",
@@ -120,7 +119,6 @@
"resolutions": {
"react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch",
"expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch",
- "react-native-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",
"@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"
}
diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts
index 80b3e477..d2a83751 100644
--- a/src/@types/mastodon.d.ts
+++ b/src/@types/mastodon.d.ts
@@ -264,14 +264,6 @@ declare namespace Mastodon {
}
type Filter = T extends 'v2' ? Filter_V2 : Filter_V1
- type Filter_V1 = {
- id: string
- phrase: string
- context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
- expires_at?: string
- irreversible: boolean
- whole_word: boolean
- }
type Filter_V2 = {
id: string
title: string
@@ -281,6 +273,14 @@ declare namespace Mastodon {
keywords: FilterKeyword[]
statuses: FilterStatus[]
}
+ type Filter_V1 = {
+ id: string
+ phrase: string
+ context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
+ expires_at?: string
+ irreversible: boolean
+ whole_word: boolean
+ }
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
@@ -298,7 +298,45 @@ declare namespace Mastodon {
replies_policy: 'none' | 'list' | 'followed'
}
- type Instance = {
+ type Instance = 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
uri: string
title: string
diff --git a/src/@types/untyped.d.ts b/src/@types/untyped.d.ts
index ad27df89..e00bdb1d 100644
--- a/src/@types/untyped.d.ts
+++ b/src/@types/untyped.d.ts
@@ -1,7 +1,5 @@
declare module 'gl-react-blurhash'
-declare module 'htmlparser2-without-node-native'
declare module 'react-native-feather'
-declare module 'react-native-htmlview'
declare module 'react-native-toast-message'
declare module 'rtl-detect'
diff --git a/src/App.tsx b/src/App.tsx
index a3f00f75..1e872150 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,30 +1,34 @@
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 { QueryClientProvider } from '@tanstack/react-query'
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
-import { changeLanguage } from '@utils/slices/settingsSlice'
+import getLanguage from '@utils/helpers/getLanguage'
+import queryClient from '@utils/queryHooks'
+import audio from '@utils/startup/audio'
+import log from '@utils/startup/log'
+import netInfo from '@utils/startup/netInfo'
+import push from '@utils/startup/push'
+import sentry from '@utils/startup/sentry'
+import timezone from '@utils/startup/timezone'
+import { storage } from '@utils/storage'
+import {
+ getGlobalStorage,
+ removeAccount,
+ setAccount,
+ setGlobalStorage
+} from '@utils/storage/actions'
+import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
import ThemeManager from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
-import { IntlProvider } from 'react-intl'
import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
+import { MMKV } from 'react-native-mmkv'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens'
-import { QueryClientProvider } from '@tanstack/react-query'
-import { Provider } from 'react-redux'
-import { PersistGate } from 'redux-persist/integration/react'
+import i18n from './i18n'
+import Screens from './screens'
Platform.select({
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
@@ -36,83 +40,95 @@ push()
timezone()
enableFreeze(true)
+log('log', 'App', 'delay splash')
+SplashScreen.preventAutoHideAsync()
+
const App: React.FC = () => {
log('log', 'App', 'rendering App')
+ const [appIsReady, setAppIsReady] = useState(false)
const [localCorrupt, setLocalCorrupt] = useState()
+ const [hasMigrated, setHasMigrated] = useState(versionStorageGlobal !== undefined)
+
useEffect(() => {
- const delaySplash = async () => {
- log('log', 'App', 'delay splash')
- try {
- await SplashScreen.preventAutoHideAsync()
- } catch (e) {
- console.warn(e)
+ const prepare = async () => {
+ if (!hasMigrated) {
+ try {
+ await migrateFromAsyncStorage()
+ setHasMigrated(true)
+ } catch {}
+ } else {
+ log('log', 'App', 'loading from MMKV')
+ const account = getGlobalStorage.string('account.active')
+ if (account) {
+ const storageAccount = new MMKV({ id: account })
+ const token = storageAccount.getString('auth.token')
+ if (token) {
+ log('log', 'App', `Binding storage of ${account}`)
+ storage.account = storageAccount
+ } else {
+ log('log', 'App', `Token not found for ${account}`)
+ removeAccount(account)
+ }
+ } else {
+ log('log', 'App', 'No active account available')
+ const accounts = getGlobalStorage.object('accounts')
+ if (accounts?.length) {
+ log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`)
+ setAccount(accounts[accounts.length - 1])
+ } else {
+ setGlobalStorage('account.active', undefined)
+ }
+ }
}
+
+ let netInfoRes = undefined
+ try {
+ netInfoRes = await netInfo()
+ } catch {}
+
+ if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
+ setLocalCorrupt(netInfoRes.corrupted)
+ }
+
+ log('log', 'App', `locale: ${Localization.locale}`)
+ const language = getLanguage()
+ if (!language) {
+ if (Platform.OS !== 'ios') {
+ setGlobalStorage('app.language', 'en')
+ }
+ i18n.changeLanguage('en')
+ } else {
+ i18n.changeLanguage(language)
+ }
+
+ setAppIsReady(true)
}
- delaySplash()
+
+ prepare()
}, [])
-
- const onBeforeLift = useCallback(async () => {
- let netInfoRes = undefined
- try {
- netInfoRes = await netInfo()
- } catch {}
-
- if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
- setLocalCorrupt(netInfoRes.corrupted)
- }
-
- log('log', 'App', 'hide splash')
- try {
+ const onLayoutRootView = useCallback(async () => {
+ if (appIsReady) {
+ log('log', 'App', 'hide splash')
await SplashScreen.hideAsync()
- return Promise.resolve()
- } catch (e) {
- console.warn(e)
- return Promise.reject()
}
- }, [])
+ }, [appIsReady])
+ if (!appIsReady) {
+ return null
+ }
return (
-
+
-
- {
- log('log', 'App', 'bootstrapped')
- if (bootstrapped) {
- log('log', 'App', 'loading actual app :)')
- log('log', 'App', `Locale: ${Localization.locale}`)
- const language = getLanguage()
- if (!language) {
- if (Platform.OS !== 'ios') {
- store.dispatch(changeLanguage('en'))
- }
- i18n.changeLanguage('en')
- } else {
- i18n.changeLanguage(language)
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- } else {
- return null
- }
- }}
- />
-
+
+
+
+
+
+
+
+
+
)
diff --git a/src/components/AccountButton.tsx b/src/components/AccountButton.tsx
index 21ef8288..d36f6bb4 100644
--- a/src/components/AccountButton.tsx
+++ b/src/components/AccountButton.tsx
@@ -1,19 +1,24 @@
import { useNavigation } from '@react-navigation/native'
-import initQuery from '@utils/initQuery'
-import { InstanceLatest } from '@utils/migrations/instances/migration'
+import { generateAccountKey, getAccountDetails, setAccount } from '@utils/storage/actions'
+import { StorageGlobal } from '@utils/storage/global'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import Button from './Button'
import haptics from './haptics'
interface Props {
- instance: InstanceLatest
+ account: NonNullable[number]
selected?: boolean
additionalActions?: () => void
}
-const AccountButton: React.FC = ({ instance, selected = false, additionalActions }) => {
+const AccountButton: React.FC = ({ account, selected = false, additionalActions }) => {
const navigation = useNavigation()
+ const accountDetails = getAccountDetails(
+ ['auth.account.acct', 'auth.domain', 'auth.account.id'],
+ account
+ )
+ if (!accountDetails) return null
return (