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:
4
index.js
4
index.js
@ -1,8 +1,4 @@
|
|||||||
import { registerRootComponent } from 'expo'
|
import { registerRootComponent } from 'expo'
|
||||||
|
|
||||||
import App from './src/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,
|
|
||||||
// the environment is set up appropriately
|
|
||||||
registerRootComponent(App)
|
registerRootComponent(App)
|
||||||
|
@ -69,7 +69,6 @@
|
|||||||
"react-intl": "^6.2.5",
|
"react-intl": "^6.2.5",
|
||||||
"react-native": "^0.70.6",
|
"react-native": "^0.70.6",
|
||||||
"react-native-animated-spinkit": "^1.5.2",
|
"react-native-animated-spinkit": "^1.5.2",
|
||||||
"react-native-base64": "^0.2.1",
|
|
||||||
"react-native-blurhash": "^1.1.10",
|
"react-native-blurhash": "^1.1.10",
|
||||||
"react-native-fast-image": "^8.6.3",
|
"react-native-fast-image": "^8.6.3",
|
||||||
"react-native-feather": "^1.1.2",
|
"react-native-feather": "^1.1.2",
|
||||||
@ -104,7 +103,6 @@
|
|||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/react-native": "^0.70.8",
|
"@types/react-native": "^0.70.8",
|
||||||
"@types/react-native-base64": "^0.2.0",
|
|
||||||
"@types/react-native-share-menu": "^5.0.2",
|
"@types/react-native-share-menu": "^5.0.2",
|
||||||
"@types/valid-url": "^1.0.3",
|
"@types/valid-url": "^1.0.3",
|
||||||
"babel-plugin-module-resolver": "^4.1.0",
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
@ -4,10 +4,11 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import apiGeneral from '@utils/api/general'
|
import apiGeneral from '@utils/api/general'
|
||||||
import browserPackage from '@utils/helpers/browserPackage'
|
import browserPackage from '@utils/helpers/browserPackage'
|
||||||
import { featureCheck } from '@utils/helpers/featureCheck'
|
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||||
import queryClient from '@utils/queryHooks'
|
|
||||||
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
||||||
|
import queryClient from '@utils/queryHooks'
|
||||||
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
||||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||||
|
import { storage } from '@utils/storage'
|
||||||
import { StorageAccount } from '@utils/storage/account'
|
import { StorageAccount } from '@utils/storage/account'
|
||||||
import {
|
import {
|
||||||
generateAccountKey,
|
generateAccountKey,
|
||||||
@ -24,12 +25,10 @@ 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 { MMKV } from 'react-native-mmkv'
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
import CustomText from '../Text'
|
import CustomText from '../Text'
|
||||||
import { storage } from '@utils/storage'
|
|
||||||
import { MMKV } from 'react-native-mmkv'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollViewRef?: RefObject<ScrollView>
|
scrollViewRef?: RefObject<ScrollView>
|
||||||
@ -87,7 +86,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
await request.makeAuthUrlAsync(discovery)
|
await request.makeAuthUrlAsync(discovery)
|
||||||
|
|
||||||
const promptResult = await request.promptAsync(discovery)
|
const promptResult = await request.promptAsync(discovery, await browserPackage())
|
||||||
|
|
||||||
if (promptResult?.type === 'success') {
|
if (promptResult?.type === 'success') {
|
||||||
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
||||||
@ -155,7 +154,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
'admin.sign_up': false,
|
'admin.sign_up': false,
|
||||||
'admin.report': false
|
'admin.report': false
|
||||||
},
|
},
|
||||||
key: base64.encodeFromByteArray(Random.getRandomBytes(16))
|
key: Math.random().toString(36).slice(2, 12)
|
||||||
},
|
},
|
||||||
page_local: {
|
page_local: {
|
||||||
showBoosts: true,
|
showBoosts: true,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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 StatusContext from '@components/Timeline/Shared/Context'
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native'
|
import { useNavigation, useRoute } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
@ -11,43 +12,38 @@ import { adaptiveScale } from '@utils/styles/scaling'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { ChildNode } from 'domhandler'
|
import { ChildNode } from 'domhandler'
|
||||||
import { ElementType, parseDocument } from 'htmlparser2'
|
import { ElementType, parseDocument } from 'htmlparser2'
|
||||||
import React, { useState } from 'react'
|
import i18next from 'i18next'
|
||||||
|
import React, { useContext, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, Text, TextStyleIOS, View } from 'react-native'
|
import { Platform, Pressable, Text, TextStyleIOS, View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
content: string
|
content: string
|
||||||
size?: 'S' | 'M' | 'L'
|
size?: 'S' | 'M' | 'L'
|
||||||
textStyles?: TextStyleIOS
|
|
||||||
adaptiveSize?: boolean
|
adaptiveSize?: boolean
|
||||||
emojis?: Mastodon.Emoji[]
|
|
||||||
mentions?: Mastodon.Mention[]
|
|
||||||
tags?: Mastodon.Tag[]
|
|
||||||
showFullLink?: boolean
|
showFullLink?: boolean
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
expandHint?: string
|
expandHint?: string
|
||||||
highlighted?: boolean
|
|
||||||
disableDetails?: boolean
|
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
emojis?: Mastodon.Emoji[]
|
||||||
|
mentions?: Mastodon.Mention[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseHTML: React.FC<Props> = ({
|
const ParseHTML: React.FC<Props> = ({
|
||||||
content,
|
content,
|
||||||
size = 'M',
|
size = 'M',
|
||||||
textStyles,
|
|
||||||
adaptiveSize = false,
|
adaptiveSize = false,
|
||||||
emojis,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
showFullLink = false,
|
showFullLink = false,
|
||||||
numberOfLines = 10,
|
numberOfLines = 10,
|
||||||
expandHint,
|
expandHint,
|
||||||
highlighted = false,
|
|
||||||
disableDetails = false,
|
|
||||||
selectable = false,
|
selectable = false,
|
||||||
setSpoilerExpanded
|
setSpoilerExpanded,
|
||||||
|
emojis,
|
||||||
|
mentions
|
||||||
}) => {
|
}) => {
|
||||||
|
const { status, highlighted, disableDetails, excludeMentions } = useContext(StatusContext)
|
||||||
|
|
||||||
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const adaptedFontsize = adaptiveScale(
|
const adaptedFontsize = adaptiveScale(
|
||||||
StyleConstants.Font.Size[size],
|
StyleConstants.Font.Size[size],
|
||||||
@ -91,14 +87,16 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const startingOfText = useRef<boolean>(false)
|
||||||
const renderNode = (node: ChildNode, index: number) => {
|
const renderNode = (node: ChildNode, index: number) => {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case ElementType.Text:
|
case ElementType.Text:
|
||||||
|
node.data.trim().length && (startingOfText.current = true) // Removing empty spaces appeared between tags and mentions
|
||||||
return (
|
return (
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
key={index}
|
key={index}
|
||||||
content={node.data}
|
content={node.data.replace(new RegExp(/^\s+/), '')}
|
||||||
emojis={emojis}
|
emojis={status?.emojis || emojis}
|
||||||
size={size}
|
size={size}
|
||||||
adaptiveSize={adaptiveSize}
|
adaptiveSize={adaptiveSize}
|
||||||
/>
|
/>
|
||||||
@ -138,19 +136,28 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (classes.includes('mention') && mentions?.length) {
|
if (classes.includes('mention') && (status?.mentions?.length || mentions?.length)) {
|
||||||
const mentionIndex = mentions.findIndex(mention => mention.url === href)
|
const matchedMention = (status?.mentions || mentions || []).find(
|
||||||
|
mention => mention.url === href
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
matchedMention &&
|
||||||
|
!startingOfText.current &&
|
||||||
|
excludeMentions?.current.find(eM => eM.id === matchedMention.id)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
|
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
|
||||||
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
|
const sameAccount = paramsAccount?.id === matchedMention?.id
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={index}
|
key={index}
|
||||||
style={{ color: mentionIndex > -1 ? colors.blue : undefined }}
|
style={{ color: matchedMention ? colors.blue : undefined }}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
mentionIndex > -1 &&
|
matchedMention &&
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
!sameAccount &&
|
!sameAccount &&
|
||||||
navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] })
|
navigation.push('Tab-Shared-Account', { account: matchedMention })
|
||||||
}
|
}
|
||||||
children={node.children.map(unwrapNode).join('')}
|
children={node.children.map(unwrapNode).join('')}
|
||||||
/>
|
/>
|
||||||
@ -159,7 +166,7 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = node.children.map(child => unwrapNode(child)).join('')
|
const content = node.children.map(child => unwrapNode(child)).join('')
|
||||||
const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content)
|
const shouldBeTag = status?.tags?.find(tag => `#${tag.name}` === content)
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={index}
|
key={index}
|
||||||
@ -249,7 +256,10 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: adaptedFontsize,
|
fontSize: adaptedFontsize,
|
||||||
lineHeight: adaptedLineheight,
|
lineHeight: adaptedLineheight,
|
||||||
...textStyles,
|
...(Platform.OS === 'ios' &&
|
||||||
|
status?.language &&
|
||||||
|
i18next.dir(status.language) === 'rtl' &&
|
||||||
|
({ writingDirection: 'rtl' } as { writingDirection: 'rtl' })),
|
||||||
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
height: numberOfLines === 1 && !expanded ? 0 : undefined
|
||||||
}}
|
}}
|
||||||
numberOfLines={
|
numberOfLines={
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
import { StyleProp, View, ViewStyle } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
extraMarginLeft?: number
|
extraMarginLeft?: number
|
||||||
@ -23,7 +23,7 @@ const ComponentSeparator: React.FC<Props> = ({
|
|||||||
{
|
{
|
||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
borderTopColor: colors.border,
|
borderTopColor: colors.border,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: 1,
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
|
||||||
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const status = item.reblog ? item.reblog : item
|
const status = item.reblog ? item.reblog : item
|
||||||
const rawContent = useRef<string[]>([])
|
const rawContent = useRef<string[]>([])
|
||||||
if (highlighted) {
|
if (highlighted || isConversation) {
|
||||||
rawContent.current = [
|
rawContent.current = [
|
||||||
removeHTML(status.content),
|
removeHTML(status.content),
|
||||||
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
|
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
|
||||||
@ -72,6 +72,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded
|
||||||
: false
|
: false
|
||||||
const detectedLanguage = useRef<string>(status.language || '')
|
const detectedLanguage = useRef<string>(status.language || '')
|
||||||
|
const excludeMentions = useRef<Mastodon.Mention[]>([])
|
||||||
|
|
||||||
const mainStyle: StyleProp<ViewStyle> = {
|
const mainStyle: StyleProp<ViewStyle> = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -169,6 +170,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
spoilerHidden,
|
spoilerHidden,
|
||||||
rawContent,
|
rawContent,
|
||||||
detectedLanguage,
|
detectedLanguage,
|
||||||
|
excludeMentions,
|
||||||
highlighted,
|
highlighted,
|
||||||
inThread: queryKey?.[1].page === 'Toot',
|
inThread: queryKey?.[1].page === 'Toot',
|
||||||
disableDetails,
|
disableDetails,
|
||||||
|
@ -329,8 +329,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
|
paddingVertical: StyleConstants.Spacing.S * 1.5
|
||||||
marginHorizontal: StyleConstants.Spacing.S
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { useStatusQuery } from '@utils/queryHooks/status'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import TimelineDefault from '../Default'
|
import TimelineDefault from '../Default'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
@ -192,7 +192,7 @@ const TimelineCard: React.FC = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: StyleConstants.Spacing.M,
|
marginTop: StyleConstants.Spacing.M,
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: 1,
|
||||||
borderRadius: StyleConstants.Spacing.S,
|
borderRadius: StyleConstants.Spacing.S,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderColor: colors.border
|
borderColor: colors.border
|
||||||
|
@ -3,10 +3,9 @@ import CustomText from '@components/Text'
|
|||||||
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
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 i18next from 'i18next'
|
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -23,11 +22,6 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
|
|
||||||
const { data: preferences } = usePreferencesQuery()
|
const { data: preferences } = usePreferencesQuery()
|
||||||
|
|
||||||
const isRTLiOSTextStyles =
|
|
||||||
Platform.OS === 'ios' && status.language && i18next.dir(status.language) === 'rtl'
|
|
||||||
? ({ writingDirection: 'rtl' } as { writingDirection: 'rtl' })
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{status.spoiler_text?.length ? (
|
{status.spoiler_text?.length ? (
|
||||||
@ -36,13 +30,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
content={status.spoiler_text}
|
content={status.spoiler_text}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={999}
|
numberOfLines={999}
|
||||||
highlighted={highlighted}
|
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={isRTLiOSTextStyles}
|
|
||||||
/>
|
/>
|
||||||
{inThread ? (
|
{inThread ? (
|
||||||
<CustomText
|
<CustomText
|
||||||
@ -60,9 +48,6 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={
|
numberOfLines={
|
||||||
preferences?.['reading:expand:spoilers'] || inThread
|
preferences?.['reading:expand:spoilers'] || inThread
|
||||||
? notificationOwnToot
|
? notificationOwnToot
|
||||||
@ -72,9 +57,6 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
}
|
}
|
||||||
expandHint={t('shared.content.expandHint')}
|
expandHint={t('shared.content.expandHint')}
|
||||||
setSpoilerExpanded={setSpoilerExpanded}
|
setSpoilerExpanded={setSpoilerExpanded}
|
||||||
highlighted={highlighted}
|
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={isRTLiOSTextStyles}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -82,12 +64,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
|
|||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
adaptiveSize
|
adaptiveSize
|
||||||
emojis={status.emojis}
|
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
|
numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
|
||||||
disableDetails={disableDetails}
|
|
||||||
textStyles={isRTLiOSTextStyles}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -14,6 +14,7 @@ type StatusContextType = {
|
|||||||
spoilerHidden?: boolean
|
spoilerHidden?: boolean
|
||||||
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
|
||||||
detectedLanguage?: React.MutableRefObject<string>
|
detectedLanguage?: React.MutableRefObject<string>
|
||||||
|
excludeMentions?: React.MutableRefObject<Mastodon.Mention[]>
|
||||||
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
inThread?: boolean
|
inThread?: boolean
|
||||||
|
@ -13,6 +13,7 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
|||||||
import HeaderSharedApplication from './HeaderShared/Application'
|
import HeaderSharedApplication from './HeaderShared/Application'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedMuted from './HeaderShared/Muted'
|
import HeaderSharedMuted from './HeaderShared/Muted'
|
||||||
|
import HeaderSharedReplies from './HeaderShared/Replies'
|
||||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||||
|
|
||||||
const TimelineHeaderDefault: React.FC = () => {
|
const TimelineHeaderDefault: React.FC = () => {
|
||||||
@ -64,6 +65,7 @@ const TimelineHeaderDefault: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<HeaderSharedVisibility visibility={status.visibility} />
|
<HeaderSharedVisibility visibility={status.visibility} />
|
||||||
<HeaderSharedMuted muted={status.muted} />
|
<HeaderSharedMuted muted={status.muted} />
|
||||||
|
<HeaderSharedReplies />
|
||||||
<HeaderSharedApplication application={status.application} />
|
<HeaderSharedApplication application={status.application} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
52
src/components/Timeline/Shared/HeaderShared/Replies.tsx
Normal file
52
src/components/Timeline/Shared/HeaderShared/Replies.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import CustomText from '@components/Text'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React, { Fragment, useContext } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import StatusContext from '../Context'
|
||||||
|
|
||||||
|
const HeaderSharedReplies: React.FC = () => {
|
||||||
|
const { status, rawContent, excludeMentions, isConversation } = useContext(StatusContext)
|
||||||
|
if (!isConversation) return null
|
||||||
|
|
||||||
|
const navigation = useNavigation<any>()
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
const mentionsBeginning = rawContent?.current?.[0]
|
||||||
|
.match(new RegExp(/^(?:@\S+\s+)+/))?.[0]
|
||||||
|
?.match(new RegExp(/@\S+/, 'g'))
|
||||||
|
excludeMentions &&
|
||||||
|
(excludeMentions.current =
|
||||||
|
mentionsBeginning?.length && status?.mentions
|
||||||
|
? status.mentions.filter(mention => mentionsBeginning.includes(`@${mention.username}`))
|
||||||
|
: [])
|
||||||
|
|
||||||
|
return excludeMentions?.current.length ? (
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
style={{
|
||||||
|
marginLeft: StyleConstants.Spacing.S,
|
||||||
|
flexDirection: 'row',
|
||||||
|
color: colors.secondary
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{t('shared.header.shared.replies')}
|
||||||
|
{excludeMentions.current.map((mention, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{' '}
|
||||||
|
<CustomText
|
||||||
|
style={{ color: colors.blue, paddingLeft: StyleConstants.Spacing.S }}
|
||||||
|
children={`@${mention.username}`}
|
||||||
|
onPress={() => navigation.push('Tab-Shared-Account', { account: mention })}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
</CustomText>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderSharedReplies
|
@ -2,6 +2,7 @@ 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 { flattenPages } from '@utils/queryHooks/utils'
|
||||||
import { useGlobalStorageListener } from '@utils/storage/actions'
|
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'
|
||||||
@ -54,8 +55,6 @@ const Timeline: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
|
||||||
|
|
||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
@ -112,7 +111,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
windowSize={7}
|
windowSize={7}
|
||||||
data={flattenData}
|
data={flattenPages(data)}
|
||||||
initialNumToRender={6}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={3}
|
maxToRenderPerBatch={3}
|
||||||
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
||||||
|
@ -203,7 +203,7 @@ const menuStatus = ({
|
|||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
hidden: !ownAccount && !status.mentions.filter(mention => mention.id === accountId).length
|
hidden: !ownAccount && !status.mentions?.filter(mention => mention.id === accountId).length
|
||||||
},
|
},
|
||||||
title: t('componentContextMenu:status.mute.action', {
|
title: t('componentContextMenu:status.mute.action', {
|
||||||
defaultValue: 'false',
|
defaultValue: 'false',
|
||||||
|
@ -122,6 +122,7 @@
|
|||||||
"muted": {
|
"muted": {
|
||||||
"accessibilityLabel": "Toot muted"
|
"accessibilityLabel": "Toot muted"
|
||||||
},
|
},
|
||||||
|
"replies": "Replies",
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"direct": {
|
"direct": {
|
||||||
"accessibilityLabel": "Toot is a direct message"
|
"accessibilityLabel": "Toot is a direct message"
|
||||||
|
@ -265,8 +265,8 @@
|
|||||||
"darkTheme": {
|
"darkTheme": {
|
||||||
"heading": "Dark theme",
|
"heading": "Dark theme",
|
||||||
"options": {
|
"options": {
|
||||||
"lighter": "Lighter",
|
"lighter": "Default",
|
||||||
"darker": "Darker"
|
"darker": "True black"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
|
@ -2,7 +2,7 @@ import TimelineDefault from '@components/Timeline/Default'
|
|||||||
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 { StyleSheet, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
|
|
||||||
const ComposeReply: React.FC = () => {
|
const ComposeReply: React.FC = () => {
|
||||||
@ -16,7 +16,7 @@ const ComposeReply: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: 1,
|
||||||
borderRadius: StyleConstants.Spacing.S,
|
borderRadius: StyleConstants.Spacing.S,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
|
@ -5,6 +5,7 @@ import { displayMessage } from '@components/Message'
|
|||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
|
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
|
||||||
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
@ -15,7 +16,8 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
|
|||||||
const { t } = useTranslation(['common', 'screenTabs'])
|
const { t } = useTranslation(['common', 'screenTabs'])
|
||||||
|
|
||||||
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
|
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
|
||||||
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
|
const flattenData = flattenPages(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flattenData.length === 0) {
|
if (flattenData.length === 0) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
useListAccountsMutation,
|
useListAccountsMutation,
|
||||||
useListAccountsQuery
|
useListAccountsQuery
|
||||||
} from '@utils/queryHooks/lists'
|
} from '@utils/queryHooks/lists'
|
||||||
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
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'
|
||||||
@ -18,7 +19,7 @@ import { FlatList, View } from 'react-native'
|
|||||||
const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> = ({
|
const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> = ({
|
||||||
route: { params }
|
route: { params }
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation(['common', 'screenTabs'])
|
const { t } = useTranslation(['common', 'screenTabs'])
|
||||||
|
|
||||||
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
|
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
|
||||||
@ -34,8 +35,6 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
|
||||||
|
|
||||||
const mutation = useListAccountsMutation({
|
const mutation = useListAccountsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
@ -53,7 +52,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={flattenData}
|
data={flattenPages(data)}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<ComponentAccount
|
<ComponentAccount
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -89,7 +89,7 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
|
|||||||
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
|
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
|
||||||
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
|
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
|
||||||
color: (fontSize || 0) === size ? colors.primaryDefault : colors.secondary,
|
color: (fontSize || 0) === size ? colors.primaryDefault : colors.secondary,
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: 1,
|
||||||
borderColor: colors.border
|
borderColor: colors.border
|
||||||
}}
|
}}
|
||||||
fontWeight={(fontSize || 0) === size ? 'Bold' : undefined}
|
fontWeight={(fontSize || 0) === size ? 'Bold' : undefined}
|
||||||
|
@ -6,7 +6,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native'
|
import { KeyboardAvoidingView, Platform, View } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
|
|
||||||
const TabMeSwitch: React.FC = () => {
|
const TabMeSwitch: React.FC = () => {
|
||||||
@ -49,7 +49,7 @@ const TabMeSwitch: React.FC = () => {
|
|||||||
marginTop: StyleConstants.Spacing.S,
|
marginTop: StyleConstants.Spacing.S,
|
||||||
paddingTop: StyleConstants.Spacing.M,
|
paddingTop: StyleConstants.Spacing.M,
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: 1,
|
||||||
borderTopColor: colors.border
|
borderTopColor: colors.border
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -4,6 +4,7 @@ import { useNavigation } 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 { useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
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'
|
||||||
@ -32,12 +33,9 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
only_media: true
|
only_media: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = data?.pages
|
const flattenData = flattenPages(data)
|
||||||
? data.pages
|
.filter(status => !(status as Mastodon.Status).sensitive)
|
||||||
.flatMap(d => [...d.body])
|
.splice(0, DISPLAY_AMOUNT)
|
||||||
.filter(status => !(status as Mastodon.Status).sensitive)
|
|
||||||
.splice(0, DISPLAY_AMOUNT)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const styleContainer = useAnimatedStyle(() => {
|
const styleContainer = useAnimatedStyle(() => {
|
||||||
if (flattenData.length) {
|
if (flattenData.length) {
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { HeaderLeft } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
import Timeline from '@components/Timeline'
|
import CustomText from '@components/Text'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTootQuery } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, View } from 'react-native'
|
import { FlatList, View } from 'react-native'
|
||||||
|
import { Path, Svg } from 'react-native-svg'
|
||||||
|
|
||||||
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||||
navigation,
|
navigation,
|
||||||
@ -16,6 +17,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
params: { toot, rootQueryKey }
|
params: { toot, rootQueryKey }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -25,55 +27,23 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
|
|
||||||
|
|
||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const [itemsLength, setItemsLength] = useState(0)
|
|
||||||
const scrolled = useRef(false)
|
const scrolled = useRef(false)
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const observer = new InfiniteQueryObserver(queryClient, {
|
|
||||||
queryKey,
|
|
||||||
enabled: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const replyLevels = useRef<{ id: string; level: number }[]>([])
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
|
||||||
const data = useRef<Mastodon.Status[]>()
|
const { data } = useTootQuery({
|
||||||
const highlightIndex = useRef<number>(0)
|
...queryKey[1],
|
||||||
useEffect(() => {
|
options: {
|
||||||
return observer.subscribe(result => {
|
meta: { toot },
|
||||||
if (result.isSuccess) {
|
onSuccess: data => {
|
||||||
const flattenData = result.data?.pages
|
if (data.body.length < 1) {
|
||||||
? // @ts-ignore
|
|
||||||
result.data.pages.flatMap(d => [...d.body])
|
|
||||||
: []
|
|
||||||
// Auto go back when toot page is empty
|
|
||||||
if (flattenData.length < 1) {
|
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.current = flattenData
|
|
||||||
highlightIndex.current = flattenData.findIndex(({ id }) => id === toot.id)
|
|
||||||
|
|
||||||
for (const [index, status] of flattenData.entries()) {
|
|
||||||
if (status.id === toot.id) continue
|
|
||||||
if (status.in_reply_to_id === toot.id) continue
|
|
||||||
|
|
||||||
if (!replyLevels.current.find(reply => reply.id === status.in_reply_to_id)) {
|
|
||||||
const prevLevel =
|
|
||||||
replyLevels.current.find(reply => reply.id === flattenData[index - 1].in_reply_to_id)
|
|
||||||
?.level || 0
|
|
||||||
replyLevels.current.push({
|
|
||||||
id: status.in_reply_to_id,
|
|
||||||
level: prevLevel + 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setItemsLength(flattenData.length)
|
|
||||||
if (!scrolled.current) {
|
if (!scrolled.current) {
|
||||||
scrolled.current = true
|
scrolled.current = true
|
||||||
const pointer = flattenData.findIndex(({ id }) => id === toot.id)
|
const pointer = data.body.findIndex(({ id }) => id === toot.id)
|
||||||
if (pointer < 1) return
|
if (pointer < 1) return
|
||||||
const length = flRef.current?.props.data?.length
|
const length = flRef.current?.props.data?.length
|
||||||
if (!length) return
|
if (!length) return
|
||||||
@ -91,78 +61,188 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}, [scrolled.current, replyLevels.current])
|
})
|
||||||
|
|
||||||
|
const heights = useRef<(number | undefined)[]>([])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<FlatList
|
||||||
flRef={flRef}
|
ref={flRef}
|
||||||
queryKey={queryKey}
|
scrollEventThrottle={16}
|
||||||
queryOptions={{ staleTime: 0, refetchOnMount: true }}
|
windowSize={7}
|
||||||
customProps={{
|
data={data?.body}
|
||||||
ItemSeparatorComponent: ({ leadingItem }) => {
|
renderItem={({ item, index }) => {
|
||||||
const levels = {
|
const MAX_LEVEL = 10
|
||||||
current:
|
const ARC = StyleConstants.Avatar.XS / 4
|
||||||
replyLevels.current.find(reply => reply.id === leadingItem.in_reply_to_id)?.level || 0
|
|
||||||
}
|
const prev = data?.body[index - 1]?._level || 0
|
||||||
return (
|
const curr = item._level
|
||||||
|
const next = data?.body[index + 1]?._level || 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingLeft:
|
||||||
|
index > (data?.highlightIndex || 0)
|
||||||
|
? Math.min(item._level, MAX_LEVEL) * StyleConstants.Spacing.S
|
||||||
|
: undefined
|
||||||
|
}}
|
||||||
|
onLayout={({
|
||||||
|
nativeEvent: {
|
||||||
|
layout: { height }
|
||||||
|
}
|
||||||
|
}) => (heights.current[index] = height)}
|
||||||
|
>
|
||||||
|
<TimelineDefault
|
||||||
|
item={item}
|
||||||
|
queryKey={queryKey}
|
||||||
|
rootQueryKey={rootQueryKey}
|
||||||
|
highlighted={toot.id === item.id}
|
||||||
|
isConversation={toot.id !== item.id}
|
||||||
|
/>
|
||||||
|
{curr > 1 || next > 1
|
||||||
|
? [...new Array(curr)].map((_, i) => {
|
||||||
|
if (i > MAX_LEVEL) return null
|
||||||
|
|
||||||
|
const lastLine = curr === i + 1
|
||||||
|
if (lastLine) {
|
||||||
|
if (curr === prev + 1 || curr === next - 1) {
|
||||||
|
if (curr > next) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Svg key={i} style={{ position: 'absolute' }}>
|
||||||
|
<Path
|
||||||
|
d={
|
||||||
|
`M ${curr * StyleConstants.Spacing.S + ARC} ${
|
||||||
|
StyleConstants.Spacing.M + StyleConstants.Avatar.XS / 2
|
||||||
|
} ` +
|
||||||
|
`a ${ARC} ${ARC} 0 0 0 -${ARC} ${ARC} ` +
|
||||||
|
`v 999`
|
||||||
|
}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={colors.border}
|
||||||
|
strokeOpacity={0.6}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (i >= curr - 2) return null
|
||||||
|
return (
|
||||||
|
<Svg key={i} style={{ position: 'absolute' }}>
|
||||||
|
<Path
|
||||||
|
d={
|
||||||
|
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
|
||||||
|
`v ${
|
||||||
|
(heights.current[index] || 999) -
|
||||||
|
(StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 -
|
||||||
|
StyleConstants.Avatar.XS / 2
|
||||||
|
} ` +
|
||||||
|
`a ${ARC} ${ARC} 0 0 0 ${ARC} ${ARC}`
|
||||||
|
}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={colors.border}
|
||||||
|
strokeOpacity={0.6}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i >= next - 1) {
|
||||||
|
return (
|
||||||
|
<Svg key={i} style={{ position: 'absolute' }}>
|
||||||
|
<Path
|
||||||
|
d={
|
||||||
|
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
|
||||||
|
`v ${
|
||||||
|
(heights.current[index] || 999) -
|
||||||
|
(StyleConstants.Spacing.S * 1.5 +
|
||||||
|
StyleConstants.Font.Size.L * 1.35) /
|
||||||
|
2
|
||||||
|
} ` +
|
||||||
|
`h ${ARC}`
|
||||||
|
}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={colors.border}
|
||||||
|
strokeOpacity={0.6}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Svg key={i} style={{ position: 'absolute' }}>
|
||||||
|
<Path
|
||||||
|
d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 999`}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={colors.border}
|
||||||
|
strokeOpacity={0.6}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
{/* <CustomText
|
||||||
|
children={data?.body[index - 1]?._level}
|
||||||
|
style={{ position: 'absolute', top: 4, left: 4, color: colors.red }}
|
||||||
|
/>
|
||||||
|
<CustomText
|
||||||
|
children={item._level}
|
||||||
|
style={{ position: 'absolute', top: 20, left: 4, color: colors.yellow }}
|
||||||
|
/>
|
||||||
|
<CustomText
|
||||||
|
children={data?.body[index + 1]?._level}
|
||||||
|
style={{ position: 'absolute', top: 36, left: 4, color: colors.green }}
|
||||||
|
/> */}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
initialNumToRender={6}
|
||||||
|
maxToRenderPerBatch={3}
|
||||||
|
ItemSeparatorComponent={({ leadingItem }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<ComponentSeparator
|
<ComponentSeparator
|
||||||
extraMarginLeft={
|
extraMarginLeft={
|
||||||
toot.id === leadingItem.id
|
toot.id === leadingItem.id
|
||||||
? 0
|
? 0
|
||||||
: StyleConstants.Avatar.XS +
|
: StyleConstants.Avatar.XS +
|
||||||
StyleConstants.Spacing.S +
|
StyleConstants.Spacing.S +
|
||||||
Math.max(0, levels.current - 1) * 8
|
Math.max(0, leadingItem._level - 1) * 8
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
{leadingItem._level > 1
|
||||||
},
|
? [...new Array(leadingItem._level - 1)].map((_, i) => (
|
||||||
renderItem: ({ item, index }) => {
|
<Svg key={i} style={{ position: 'absolute', top: -1 }}>
|
||||||
const levels = {
|
<Path
|
||||||
previous:
|
d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 1`}
|
||||||
replyLevels.current.find(
|
strokeWidth={1}
|
||||||
reply => reply.id === data.current?.[index - 1]?.in_reply_to_id
|
stroke={colors.border}
|
||||||
)?.level || 0,
|
strokeOpacity={0.6}
|
||||||
current:
|
/>
|
||||||
replyLevels.current.find(reply => reply.id === item.in_reply_to_id)?.level || 0,
|
</Svg>
|
||||||
next:
|
))
|
||||||
replyLevels.current.find(
|
: null}
|
||||||
reply => reply.id === data.current?.[index + 1]?.in_reply_to_id
|
</>
|
||||||
)?.level || 0
|
)
|
||||||
}
|
}}
|
||||||
|
onScrollToIndexFailed={error => {
|
||||||
return (
|
const offset = error.averageItemLength * error.index
|
||||||
<View
|
flRef.current?.scrollToOffset({ offset })
|
||||||
style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}
|
try {
|
||||||
>
|
error.index < (data?.body.length || 0) &&
|
||||||
<TimelineDefault
|
setTimeout(
|
||||||
item={item}
|
() =>
|
||||||
queryKey={queryKey}
|
flRef.current?.scrollToIndex({
|
||||||
rootQueryKey={rootQueryKey}
|
index: error.index,
|
||||||
highlighted={toot.id === item.id}
|
viewOffset: 100
|
||||||
isConversation={toot.id !== item.id}
|
}),
|
||||||
/>
|
500
|
||||||
</View>
|
)
|
||||||
)
|
} catch {}
|
||||||
},
|
|
||||||
onScrollToIndexFailed: error => {
|
|
||||||
const offset = error.averageItemLength * error.index
|
|
||||||
flRef.current?.scrollToOffset({ offset })
|
|
||||||
try {
|
|
||||||
error.index < itemsLength &&
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
flRef.current?.scrollToIndex({
|
|
||||||
index: error.index,
|
|
||||||
viewOffset: 100
|
|
||||||
}),
|
|
||||||
500
|
|
||||||
)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disableRefresh
|
|
||||||
disableInfinity
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import apiInstance from '@utils/api/instance'
|
|||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { SearchResult } from '@utils/queryHooks/search'
|
import { SearchResult } from '@utils/queryHooks/search'
|
||||||
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
|
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
|
||||||
|
import { flattenPages } from '@utils/queryHooks/utils'
|
||||||
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, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
@ -39,14 +40,13 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
|
|||||||
getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id }
|
getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
|
|
||||||
|
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
windowSize={7}
|
windowSize={7}
|
||||||
data={flattenData}
|
data={flattenPages(data)}
|
||||||
style={{
|
style={{
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
paddingVertical: StyleConstants.Spacing.Global.PagePadding
|
paddingVertical: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
@ -15,7 +15,7 @@ const removeHTML = (text: string): string => {
|
|||||||
parser.write(text)
|
parser.write(text)
|
||||||
parser.end()
|
parser.end()
|
||||||
|
|
||||||
return raw
|
return raw.replace(new RegExp(/\s$/), '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default removeHTML
|
export default removeHTML
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import {
|
import {
|
||||||
MutationOptions,
|
MutationOptions,
|
||||||
QueryFunctionContext,
|
QueryFunctionContext,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
UseInfiniteQueryOptions,
|
UseInfiniteQueryOptions,
|
||||||
useMutation
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
UseQueryOptions
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
import { PagedResponse } from '@utils/api/helpers'
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
import apiInstance from '@utils/api/instance'
|
import apiInstance from '@utils/api/instance'
|
||||||
@ -17,6 +19,67 @@ import deleteItem from './timeline/deleteItem'
|
|||||||
import editItem from './timeline/editItem'
|
import editItem from './timeline/editItem'
|
||||||
import updateStatusProperty from './timeline/updateStatusProperty'
|
import updateStatusProperty from './timeline/updateStatusProperty'
|
||||||
|
|
||||||
|
const queryFunctionToot = async ({ queryKey, meta }: QueryFunctionContext<QueryKeyTimeline>) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const id = queryKey[1].toot
|
||||||
|
const target =
|
||||||
|
(meta?.toot as Mastodon.Status) ||
|
||||||
|
undefined ||
|
||||||
|
(await apiInstance<Mastodon.Status>({
|
||||||
|
method: 'get',
|
||||||
|
url: `statuses/${id}`
|
||||||
|
}).then(res => res.body))
|
||||||
|
const context = await apiInstance<{
|
||||||
|
ancestors: Mastodon.Status[]
|
||||||
|
descendants: Mastodon.Status[]
|
||||||
|
}>({
|
||||||
|
method: 'get',
|
||||||
|
url: `statuses/${id}/context`
|
||||||
|
})
|
||||||
|
|
||||||
|
const statuses: (Mastodon.Status & { _level?: number })[] = [
|
||||||
|
...context.body.ancestors,
|
||||||
|
target,
|
||||||
|
...context.body.descendants
|
||||||
|
]
|
||||||
|
|
||||||
|
const highlightIndex = context.body.ancestors.length
|
||||||
|
|
||||||
|
for (const [index, status] of statuses.entries()) {
|
||||||
|
if (index < highlightIndex || status.id === id) {
|
||||||
|
statuses[index]._level = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level
|
||||||
|
statuses[index]._level = (repliedLevel || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return { body: statuses, highlightIndex }
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTootQuery = ({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKeyTimeline[1] & {
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
{
|
||||||
|
body: (Mastodon.Status & { _level: number })[]
|
||||||
|
highlightIndex: number
|
||||||
|
},
|
||||||
|
AxiosError
|
||||||
|
>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunctionToot, {
|
||||||
|
staleTime: 0,
|
||||||
|
refetchOnMount: true,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- */
|
||||||
|
|
||||||
export type QueryKeyTimeline = [
|
export type QueryKeyTimeline = [
|
||||||
'Timeline',
|
'Timeline',
|
||||||
(
|
(
|
||||||
@ -36,16 +99,16 @@ export type QueryKeyTimeline = [
|
|||||||
page: 'List'
|
page: 'List'
|
||||||
list: Mastodon.List['id']
|
list: Mastodon.List['id']
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
page: 'Toot'
|
|
||||||
toot: Mastodon.Status['id']
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
page: 'Account'
|
page: 'Account'
|
||||||
account: Mastodon.Account['id']
|
account: Mastodon.Account['id']
|
||||||
exclude_reblogs: boolean
|
exclude_reblogs: boolean
|
||||||
only_media: boolean
|
only_media: boolean
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
page: 'Toot'
|
||||||
|
toot: Mastodon.Status['id']
|
||||||
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -209,22 +272,6 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
|
|||||||
url: `timelines/list/${page.list}`,
|
url: `timelines/list/${page.list}`,
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
|
|
||||||
case 'Toot':
|
|
||||||
const res1_1 = await apiInstance<Mastodon.Status>({
|
|
||||||
method: 'get',
|
|
||||||
url: `statuses/${page.toot}`
|
|
||||||
})
|
|
||||||
const res2_1 = await apiInstance<{
|
|
||||||
ancestors: Mastodon.Status[]
|
|
||||||
descendants: Mastodon.Status[]
|
|
||||||
}>({
|
|
||||||
method: 'get',
|
|
||||||
url: `statuses/${page.toot}/context`
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants]
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
@ -454,4 +501,4 @@ const useTimelineMutation = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useTimelineQuery, useTimelineMutation }
|
export { useTootQuery, useTimelineQuery, useTimelineMutation }
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { InfiniteData } from '@tanstack/react-query'
|
||||||
import { PagedResponse } from '@utils/api/helpers'
|
import { PagedResponse } from '@utils/api/helpers'
|
||||||
|
|
||||||
export const infinitePageParams = {
|
export const infinitePageParams = {
|
||||||
@ -6,3 +7,6 @@ export const infinitePageParams = {
|
|||||||
getNextPageParam: (lastPage: PagedResponse<any>) =>
|
getNextPageParam: (lastPage: PagedResponse<any>) =>
|
||||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
lastPage.links?.next && { max_id: lastPage.links.next }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>
|
||||||
|
data?.pages.map(page => page.body).flat() || []
|
||||||
|
4
src/utils/storage/migrations/legacy/app.ts
Normal file
4
src/utils/storage/migrations/legacy/app.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type LegacyApp = {
|
||||||
|
expoToken?: string
|
||||||
|
versionUpdate: boolean
|
||||||
|
}
|
19
src/utils/storage/migrations/legacy/contexts.ts
Normal file
19
src/utils/storage/migrations/legacy/contexts.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ScreenTabsStackParamList } from '@utils/navigation/navigators'
|
||||||
|
|
||||||
|
export type LegacyContexts = {
|
||||||
|
storeReview: {
|
||||||
|
context: Readonly<number>
|
||||||
|
current: number
|
||||||
|
shown: boolean
|
||||||
|
}
|
||||||
|
publicRemoteNotice: {
|
||||||
|
context: Readonly<number>
|
||||||
|
current: number
|
||||||
|
hidden: boolean
|
||||||
|
}
|
||||||
|
previousTab: Extract<
|
||||||
|
keyof ScreenTabsStackParamList,
|
||||||
|
'Tab-Local' | 'Tab-Public' | 'Tab-Notifications' | 'Tab-Me'
|
||||||
|
>
|
||||||
|
previousSegment?: Extract<App.Pages, 'Local' | 'LocalPublic' | 'Trending'>
|
||||||
|
}
|
67
src/utils/storage/migrations/legacy/instance.ts
Normal file
67
src/utils/storage/migrations/legacy/instance.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ComposeStateDraft } from '@screens/Compose/utils/types'
|
||||||
|
|
||||||
|
export type LegacyInstance = {
|
||||||
|
active: boolean
|
||||||
|
appData: {
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
|
}
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
uri: Mastodon.Instance<'v1'>['uri']
|
||||||
|
urls: Mastodon.Instance<'v1'>['urls']
|
||||||
|
account: {
|
||||||
|
id: Mastodon.Account['id']
|
||||||
|
acct: Mastodon.Account['acct']
|
||||||
|
avatarStatic: Mastodon.Account['avatar_static']
|
||||||
|
preferences?: Mastodon.Preferences
|
||||||
|
}
|
||||||
|
version: string
|
||||||
|
configuration?: Mastodon.Instance<any>['configuration']
|
||||||
|
filters: Mastodon.Filter<any>[]
|
||||||
|
notifications_filter: {
|
||||||
|
follow: boolean
|
||||||
|
follow_request: boolean
|
||||||
|
favourite: boolean
|
||||||
|
reblog: boolean
|
||||||
|
mention: boolean
|
||||||
|
poll: boolean
|
||||||
|
status: boolean
|
||||||
|
update: boolean
|
||||||
|
'admin.sign_up'?: boolean
|
||||||
|
'admin.report'?: boolean
|
||||||
|
}
|
||||||
|
push: {
|
||||||
|
global: { value: boolean } | boolean
|
||||||
|
decode: { value: boolean } | boolean
|
||||||
|
alerts: {
|
||||||
|
follow: { value: boolean } | boolean
|
||||||
|
follow_request: { value: boolean } | boolean
|
||||||
|
favourite: { value: boolean } | boolean
|
||||||
|
reblog: { value: boolean } | boolean
|
||||||
|
mention: { value: boolean } | boolean
|
||||||
|
poll: { value: boolean } | boolean
|
||||||
|
status?: { value: boolean } | boolean
|
||||||
|
update?: { value: boolean } | boolean
|
||||||
|
'admin.sign_up': { value: boolean } | boolean
|
||||||
|
'admin.report': { value: boolean } | boolean
|
||||||
|
}
|
||||||
|
keys: { auth?: string }
|
||||||
|
}
|
||||||
|
followingPage?: {
|
||||||
|
showBoosts: boolean
|
||||||
|
showReplies: boolean
|
||||||
|
}
|
||||||
|
mePage: {
|
||||||
|
followedTags?: { shown: boolean }
|
||||||
|
lists: { shown: boolean }
|
||||||
|
announcements: { shown: boolean; unread: number }
|
||||||
|
}
|
||||||
|
drafts: ComposeStateDraft[]
|
||||||
|
frequentEmojis: {
|
||||||
|
emoji: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>
|
||||||
|
score: number
|
||||||
|
count: number
|
||||||
|
lastUsed: number
|
||||||
|
}[]
|
||||||
|
}
|
8
src/utils/storage/migrations/legacy/settings.ts
Normal file
8
src/utils/storage/migrations/legacy/settings.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type LegacySettings = {
|
||||||
|
fontsize: -1 | 0 | 1 | 2 | 3 | string
|
||||||
|
language: string
|
||||||
|
theme: 'light' | 'dark' | 'auto'
|
||||||
|
darkTheme: 'lighter' | 'darker'
|
||||||
|
browser: 'internal' | 'external'
|
||||||
|
staticEmoji: boolean
|
||||||
|
}
|
@ -2,6 +2,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
|
|||||||
import log from '@utils/startup/log'
|
import log from '@utils/startup/log'
|
||||||
import { secureStorage, storage } from '@utils/storage'
|
import { secureStorage, storage } from '@utils/storage'
|
||||||
import { MMKV } from 'react-native-mmkv'
|
import { MMKV } from 'react-native-mmkv'
|
||||||
|
import { LegacyApp } from './legacy/app'
|
||||||
|
import { LegacyContexts } from './legacy/contexts'
|
||||||
|
import { LegacyInstance } from './legacy/instance'
|
||||||
|
import { LegacySettings } from './legacy/settings'
|
||||||
|
|
||||||
export const versionStorageGlobal = storage.global.getNumber('version.global')
|
export const versionStorageGlobal = storage.global.getNumber('version.global')
|
||||||
|
|
||||||
@ -9,6 +13,9 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
log('log', 'Migration', 'Migrating...')
|
log('log', 'Migration', 'Migrating...')
|
||||||
const start = global.performance.now()
|
const start = global.performance.now()
|
||||||
|
|
||||||
|
const unwrapPushData = (setting: { value: boolean } | boolean | undefined): boolean =>
|
||||||
|
typeof setting === 'object' ? setting.value : typeof setting === 'boolean' ? setting : true
|
||||||
|
|
||||||
const keys = ['persist:app', 'persist:contexts', 'persist:settings'] as [
|
const keys = ['persist:app', 'persist:contexts', 'persist:settings'] as [
|
||||||
'persist:app',
|
'persist:app',
|
||||||
'persist:contexts',
|
'persist:contexts',
|
||||||
@ -21,13 +28,13 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'persist:app':
|
case 'persist:app':
|
||||||
const storeApp = JSON.parse(value)
|
const storeApp: LegacyApp = JSON.parse(value)
|
||||||
if (storeApp.expoToken?.length) {
|
if (storeApp.expoToken?.length) {
|
||||||
storage.global.set('app.expo_token', storeApp.expoToken.replaceAll(`\"`, ``))
|
storage.global.set('app.expo_token', storeApp.expoToken.replaceAll(`\"`, ``))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'persist:contexts':
|
case 'persist:contexts':
|
||||||
const storeContexts = JSON.parse(value)
|
const storeContexts: LegacyContexts = JSON.parse(value)
|
||||||
if (storeContexts.storeReview.current) {
|
if (storeContexts.storeReview.current) {
|
||||||
storage.global.set(
|
storage.global.set(
|
||||||
'app.count_till_store_review',
|
'app.count_till_store_review',
|
||||||
@ -37,17 +44,22 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
storage.global.set('app.prev_tab', storeContexts.previousTab.replaceAll(`\"`, ``))
|
storage.global.set('app.prev_tab', storeContexts.previousTab.replaceAll(`\"`, ``))
|
||||||
storage.global.set(
|
storage.global.set(
|
||||||
'app.prev_public_segment',
|
'app.prev_public_segment',
|
||||||
storeContexts.previousSegment.replaceAll(`\"`, ``)
|
(storeContexts.previousSegment || 'Local').replaceAll(`\"`, ``)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'persist:settings':
|
case 'persist:settings':
|
||||||
const storeSettings = JSON.parse(value)
|
const storeSettings: LegacySettings = JSON.parse(value)
|
||||||
storage.global.set('app.font_size', storeSettings.fontsize || 0)
|
storage.global.set(
|
||||||
|
'app.font_size',
|
||||||
|
(typeof storeSettings.fontsize === 'string'
|
||||||
|
? storeSettings.fontsize.replaceAll(`\"`, ``)
|
||||||
|
: storeSettings.fontsize) || 0
|
||||||
|
)
|
||||||
storage.global.set('app.language', storeSettings.language.replaceAll(`\"`, ``))
|
storage.global.set('app.language', storeSettings.language.replaceAll(`\"`, ``))
|
||||||
storage.global.set('app.theme', storeSettings.theme.replaceAll(`\"`, ``))
|
storage.global.set('app.theme', storeSettings.theme.replaceAll(`\"`, ``))
|
||||||
storage.global.set('app.theme.dark', storeSettings.darkTheme.replaceAll(`\"`, ``))
|
storage.global.set('app.theme.dark', storeSettings.darkTheme.replaceAll(`\"`, ``))
|
||||||
storage.global.set('app.browser', storeSettings.browser.replaceAll(`\"`, ``))
|
storage.global.set('app.browser', storeSettings.browser.replaceAll(`\"`, ``))
|
||||||
storage.global.set('app.auto_play_gifv', storeSettings.autoplayGifv || true)
|
storage.global.set('app.auto_play_gifv', true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +78,8 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
const storeInstances: { instances: string } = JSON.parse(value)
|
const storeInstances: { instances: string } = JSON.parse(value)
|
||||||
const accounts: string[] = []
|
const accounts: string[] = []
|
||||||
|
|
||||||
for (const instance of JSON.parse(storeInstances.instances)) {
|
let instance: LegacyInstance
|
||||||
|
for (instance of JSON.parse(storeInstances.instances)) {
|
||||||
const account = `${instance.url}/${instance.account.id}`
|
const account = `${instance.url}/${instance.account.id}`
|
||||||
|
|
||||||
const temp = new MMKV({ id: account })
|
const temp = new MMKV({ id: account })
|
||||||
@ -83,10 +96,63 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
if (instance.account.preferences) {
|
if (instance.account.preferences) {
|
||||||
temp.set('preferences', JSON.stringify(instance.account.preferences))
|
temp.set('preferences', JSON.stringify(instance.account.preferences))
|
||||||
}
|
}
|
||||||
temp.set('notifications', JSON.stringify(instance.notifications_filter))
|
temp.set(
|
||||||
temp.set('push', JSON.stringify(instance.push))
|
'notifications',
|
||||||
temp.set('page_local', JSON.stringify(instance.followingPage))
|
JSON.stringify({
|
||||||
temp.set('page_me', JSON.stringify(instance.mePage))
|
...instance.notifications_filter,
|
||||||
|
status:
|
||||||
|
typeof instance.notifications_filter.status === 'boolean'
|
||||||
|
? instance.notifications_filter.status
|
||||||
|
: true,
|
||||||
|
update:
|
||||||
|
typeof instance.notifications_filter.update === 'boolean'
|
||||||
|
? instance.notifications_filter.update
|
||||||
|
: true,
|
||||||
|
'admin.sign_up':
|
||||||
|
typeof instance.notifications_filter['admin.sign_up'] === 'boolean'
|
||||||
|
? instance.notifications_filter['admin.sign_up']
|
||||||
|
: true,
|
||||||
|
'admin.report':
|
||||||
|
typeof instance.notifications_filter['admin.report'] === 'boolean'
|
||||||
|
? instance.notifications_filter['admin.report']
|
||||||
|
: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
temp.set(
|
||||||
|
'push',
|
||||||
|
JSON.stringify({
|
||||||
|
global: unwrapPushData(instance.push.global),
|
||||||
|
decode: unwrapPushData(instance.push.decode),
|
||||||
|
alerts: {
|
||||||
|
follow: unwrapPushData(instance.push.alerts.follow),
|
||||||
|
follow_request: unwrapPushData(instance.push.alerts.follow_request),
|
||||||
|
favourite: unwrapPushData(instance.push.alerts.favourite),
|
||||||
|
reblog: unwrapPushData(instance.push.alerts.reblog),
|
||||||
|
mention: unwrapPushData(instance.push.alerts.mention),
|
||||||
|
poll: unwrapPushData(instance.push.alerts.poll),
|
||||||
|
status: unwrapPushData(instance.push.alerts.status),
|
||||||
|
update: unwrapPushData(instance.push.alerts.update),
|
||||||
|
'admin.sign_up': unwrapPushData(instance.push.alerts['admin.sign_up']),
|
||||||
|
'admin.report': unwrapPushData(instance.push.alerts['admin.report'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
temp.set(
|
||||||
|
'page_local',
|
||||||
|
JSON.stringify(
|
||||||
|
instance.followingPage || {
|
||||||
|
showBoosts: true,
|
||||||
|
showReplies: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
temp.set(
|
||||||
|
'page_me',
|
||||||
|
JSON.stringify({
|
||||||
|
...instance.mePage,
|
||||||
|
followedTags: instance.mePage.followedTags || { shown: false }
|
||||||
|
})
|
||||||
|
)
|
||||||
temp.set('drafts', JSON.stringify(instance.drafts))
|
temp.set('drafts', JSON.stringify(instance.drafts))
|
||||||
temp.set('emojis_frequent', JSON.stringify(instance.frequentEmojis))
|
temp.set('emojis_frequent', JSON.stringify(instance.frequentEmojis))
|
||||||
temp.set('version', instance.version)
|
temp.set('version', instance.version)
|
||||||
|
@ -70,7 +70,7 @@ const themeColors: {
|
|||||||
backgroundDefault: {
|
backgroundDefault: {
|
||||||
light: 'rgb(250, 250, 250)',
|
light: 'rgb(250, 250, 250)',
|
||||||
dark_lighter: 'rgb(44, 44, 44)',
|
dark_lighter: 'rgb(44, 44, 44)',
|
||||||
dark_darker: 'rgb(18, 18, 18)'
|
dark_darker: 'rgb(0, 0, 0)'
|
||||||
},
|
},
|
||||||
backgroundDefaultTransparent: {
|
backgroundDefaultTransparent: {
|
||||||
light: 'rgba(250, 250, 250, 0)',
|
light: 'rgba(250, 250, 250, 0)',
|
||||||
@ -89,9 +89,9 @@ const themeColors: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
border: {
|
border: {
|
||||||
light: 'rgba(25, 25, 25, 0.3)',
|
light: 'rgb(180, 180, 180)',
|
||||||
dark_lighter: 'rgba(255, 255, 255, 0.3)',
|
dark_lighter: 'rgb(90, 90, 90)',
|
||||||
dark_darker: 'rgba(255, 255, 255, 0.3)'
|
dark_darker: 'rgb(90, 90, 90)'
|
||||||
},
|
},
|
||||||
|
|
||||||
shimmerDefault: {
|
shimmerDefault: {
|
||||||
|
16
yarn.lock
16
yarn.lock
@ -3408,13 +3408,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-native-base64@npm:^0.2.0":
|
|
||||||
version: 0.2.0
|
|
||||||
resolution: "@types/react-native-base64@npm:0.2.0"
|
|
||||||
checksum: 6a562ae758dfd06f152a559f7380a7a2da783bf14e70eef8bd4aae0df9dc5971a71888b74e8db552546b94ada198bfc3b6d7db51dd537a8f21767d8729bd10e7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react-native-share-menu@npm:5.0.2":
|
"@types/react-native-share-menu@npm:5.0.2":
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
resolution: "@types/react-native-share-menu@npm:5.0.2"
|
resolution: "@types/react-native-share-menu@npm:5.0.2"
|
||||||
@ -9441,13 +9434,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-native-base64@npm:^0.2.1":
|
|
||||||
version: 0.2.1
|
|
||||||
resolution: "react-native-base64@npm:0.2.1"
|
|
||||||
checksum: 5127c857250cabb9c24f494b56185a4339bd0ebb859824a01c2f4eb3264379556e06efc8e59ffb9585cbf1c5e6fee18f50f4a6e8a1c63fe4b5cedf0658f8c25e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-native-blurhash@npm:^1.1.10":
|
"react-native-blurhash@npm:^1.1.10":
|
||||||
version: 1.1.10
|
version: 1.1.10
|
||||||
resolution: "react-native-blurhash@npm:1.1.10"
|
resolution: "react-native-blurhash@npm:1.1.10"
|
||||||
@ -11246,7 +11232,6 @@ __metadata:
|
|||||||
"@types/react": ^18.0.26
|
"@types/react": ^18.0.26
|
||||||
"@types/react-dom": ^18.0.10
|
"@types/react-dom": ^18.0.10
|
||||||
"@types/react-native": ^0.70.8
|
"@types/react-native": ^0.70.8
|
||||||
"@types/react-native-base64": ^0.2.0
|
|
||||||
"@types/react-native-share-menu": ^5.0.2
|
"@types/react-native-share-menu": ^5.0.2
|
||||||
"@types/valid-url": ^1.0.3
|
"@types/valid-url": ^1.0.3
|
||||||
axios: ^1.2.1
|
axios: ^1.2.1
|
||||||
@ -11282,7 +11267,6 @@ __metadata:
|
|||||||
react-intl: ^6.2.5
|
react-intl: ^6.2.5
|
||||||
react-native: ^0.70.6
|
react-native: ^0.70.6
|
||||||
react-native-animated-spinkit: ^1.5.2
|
react-native-animated-spinkit: ^1.5.2
|
||||||
react-native-base64: ^0.2.1
|
|
||||||
react-native-blurhash: ^1.1.10
|
react-native-blurhash: ^1.1.10
|
||||||
react-native-clean-project: ^4.0.1
|
react-native-clean-project: ^4.0.1
|
||||||
react-native-fast-image: ^8.6.3
|
react-native-fast-image: ^8.6.3
|
||||||
|
Reference in New Issue
Block a user