1
0
mirror of https://github.com/tooot-app/app synced 2025-05-28 01:44:24 +02:00

Merge pull request #687 from tooot-app/main

Release v4.8.6
This commit is contained in:
xmflsct 2023-01-30 14:35:31 +01:00 committed by GitHub
commit af5bfecb06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 772 additions and 310 deletions

View File

@ -1,6 +1,3 @@
name({
'default' => "tooot"
})
keywords({
'default' => "Mastodon,tooot,social,decentralized,长毛象,社交,去中心"
})

View File

@ -0,0 +1 @@
../../en-US/name.txt

View File

@ -1 +0,0 @@
tooot

View File

@ -0,0 +1 @@
../../zh-Hans/name.txt

View File

@ -0,0 +1 @@
../en-US/name.txt

View File

@ -0,0 +1 @@
tooot - multilingual Mastodon app

View File

@ -1 +1 @@
Open source Mastodon client
Simple, just works

View File

@ -0,0 +1 @@
tooot - 探索联邦宇宙

View File

@ -1 +1 @@
开源毛象客户端
简约,想你所想

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.8.5",
"version": "4.8.6",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later",

View File

@ -2,6 +2,7 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import * as Sentry from '@sentry/react-native'
import { QueryClientProvider } from '@tanstack/react-query'
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
import { connectVerify } from '@utils/api/helpers/connect'
import getLanguage from '@utils/helpers/getLanguage'
import { queryClient } from '@utils/queryHooks'
import audio from '@utils/startup/audio'
@ -23,6 +24,10 @@ import { enableFreeze } from 'react-native-screens'
import i18n from './i18n'
import Screens from './screens'
export const GLOBAL: { connect?: boolean } = {
connect: undefined
}
Platform.select({
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
})
@ -50,20 +55,29 @@ const App: React.FC = () => {
await migrateFromAsyncStorage()
setHasMigrated(true)
} catch {}
}
const useConnect = getGlobalStorage.boolean('app.connect')
GLOBAL.connect = useConnect
log('log', 'App', `connect: ${useConnect}`)
if (useConnect) {
await connectVerify()
.then(() => log('log', 'App', 'connected'))
.catch(() => log('warn', 'App', 'connect verify failed'))
}
log('log', 'App', 'loading from MMKV')
const account = getGlobalStorage.string('account.active')
if (account) {
await setAccount(account)
} else {
log('log', 'App', 'loading from MMKV')
const account = getGlobalStorage.string('account.active')
if (account) {
await setAccount(account)
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]}`)
await setAccount(accounts[accounts.length - 1])
} 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]}`)
await setAccount(accounts[accounts.length - 1])
} else {
setGlobalStorage('account.active', undefined)
}
setGlobalStorage('account.active', undefined)
}
}

View File

@ -45,6 +45,7 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
dim
/>
<View style={{ flex: 1 }}>
<CustomText numberOfLines={1}>

View File

@ -1,9 +1,13 @@
import { useNavigation } from '@react-navigation/native'
import { ReadableAccountType, setAccount } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import Button from './Button'
import { Pressable } from 'react-native'
import GracefullyImage from './GracefullyImage'
import haptics from './haptics'
import Icon from './Icon'
import CustomText from './Text'
interface Props {
account: ReadableAccountType
@ -11,26 +15,56 @@ interface Props {
}
const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
const { colors } = useTheme()
const navigation = useNavigation()
return (
<Button
type='text'
selected={account.active}
<Pressable
style={{
marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingHorizontal: StyleConstants.Spacing.S * 1.5,
borderColor: account.active ? colors.blue : colors.border,
borderWidth: 1,
borderRadius: 99,
marginBottom: StyleConstants.Spacing.S
}}
content={account.acct}
onPress={() => {
onPress={async () => {
await setAccount(account.key)
haptics('Light')
setAccount(account.key)
navigation.goBack()
if (additionalActions) {
additionalActions()
}
}}
/>
>
<GracefullyImage
uri={{ original: account.avatar_static }}
dimension={{
width: StyleConstants.Font.Size.L,
height: StyleConstants.Font.Size.L
}}
style={{ borderRadius: StyleConstants.Font.Size.L / 2, overflow: 'hidden' }}
/>
<CustomText
fontStyle='M'
fontWeight={account.active ? 'Bold' : 'Normal'}
style={{
color: account.active ? colors.blue : colors.primaryDefault,
marginLeft: StyleConstants.Spacing.S
}}
children={account.acct}
/>
{account.active ? (
<Icon
name='check'
size={StyleConstants.Font.Size.L}
color={colors.blue}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
)
}

View File

@ -2,6 +2,7 @@ import { emojis } from '@components/Emojis'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectImage } from '@utils/api/helpers/connect'
import { StorageAccount } from '@utils/storage/account'
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
@ -133,7 +134,7 @@ const EmojisList = () => {
emoji: emoji.shortcode
})}
accessibilityHint={t('screenCompose:content.root.footer.emojis.accessibilityHint')}
source={{ uri }}
source={connectImage({ uri })}
style={{ width: 32, height: 32 }}
/>
</Pressable>

View File

@ -2,8 +2,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { View, ViewStyle } from 'react-native'
import { TouchableNativeFeedback } from 'react-native-gesture-handler'
import { Pressable, View, ViewStyle } from 'react-native'
import Icon from './Icon'
import CustomText from './Text'
@ -19,7 +18,7 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button, style }) => {
const { colors } = useTheme()
return (
<TouchableNativeFeedback onPress={onPress}>
<Pressable onPress={onPress}>
<View
style={{
paddingVertical: StyleConstants.Spacing.S,
@ -106,6 +105,6 @@ export const Filter: React.FC<Props> = ({ onPress, filter, button, style }) => {
/>
)}
</View>
</TouchableNativeFeedback>
</Pressable>
)
}

View File

@ -1,4 +1,5 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectImage } from '@utils/api/helpers/connect'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import {
@ -37,6 +38,7 @@ export interface Props {
height: number
}>
>
dim?: boolean
}
const GracefullyImage = ({
@ -49,14 +51,15 @@ const GracefullyImage = ({
onPress,
style,
imageStyle,
setImageDimensions
setImageDimensions,
dim
}: Props) => {
const { reduceMotionEnabled } = useAccessibility()
const { colors } = useTheme()
const { colors, theme } = useTheme()
const [imageLoaded, setImageLoaded] = useState(false)
const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote)
const source = {
const source: { uri?: string } = {
uri: reduceMotionEnabled && uri.static ? uri.static : currentUri
}
useEffect(() => {
@ -90,12 +93,12 @@ const GracefullyImage = ({
>
{uri.preview && !imageLoaded ? (
<FastImage
source={{ uri: uri.preview }}
source={connectImage({ uri: uri.preview })}
style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]}
/>
) : null}
<FastImage
source={source}
source={connectImage(source)}
style={[{ flex: 1 }, imageStyle]}
onLoad={() => {
setImageLoaded(true)
@ -110,6 +113,14 @@ const GracefullyImage = ({
}}
/>
{blurhashView()}
{dim && theme !== 'light' ? (
<View
style={[
styles.placeholder,
{ backgroundColor: 'black', opacity: theme === 'dark_lighter' ? 0.18 : 0.36 }
]}
/>
) : null}
</Pressable>
)
}

View File

@ -1,11 +1,12 @@
import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectImage } from '@utils/api/helpers/connect'
import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Platform, TextStyle } from 'react-native'
import { ColorValue, Platform, TextStyle } from 'react-native'
import FastImage from 'react-native-fast-image'
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
@ -14,6 +15,7 @@ export interface Props {
content?: string
emojis?: Mastodon.Emoji[]
size?: 'S' | 'M' | 'L'
color?: ColorValue
adaptiveSize?: boolean
fontBold?: boolean
style?: TextStyle
@ -23,6 +25,7 @@ const ParseEmojis: React.FC<Props> = ({
content,
emojis,
size = 'M',
color,
adaptiveSize = false,
fontBold = false,
style
@ -41,13 +44,13 @@ const ParseEmojis: React.FC<Props> = ({
adaptiveSize ? adaptiveFontsize : 0
)
const { colors, theme } = useTheme()
const { colors } = useTheme()
return (
<CustomText
style={[
{
color: colors.primaryDefault,
color: color || colors.primaryDefault,
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
},
@ -75,7 +78,7 @@ const ParseEmojis: React.FC<Props> = ({
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage
source={{ uri: uri.trim() }}
source={connectImage({ uri: uri.trim() })}
style={{
width: adaptedFontsize,
height: adaptedFontsize,

View File

@ -16,11 +16,12 @@ import { ElementType, parseDocument } from 'htmlparser2'
import i18next from 'i18next'
import React, { useContext, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, Pressable, Text, View } from 'react-native'
import { ColorValue, Platform, Pressable, Text, View } from 'react-native'
export interface Props {
content: string
size?: 'S' | 'M' | 'L'
color?: ColorValue
adaptiveSize?: boolean
showFullLink?: boolean
numberOfLines?: number
@ -34,6 +35,7 @@ export interface Props {
const ParseHTML: React.FC<Props> = ({
content,
size = 'M',
color,
adaptiveSize = false,
showFullLink = false,
numberOfLines = 10,
@ -58,6 +60,7 @@ const ParseHTML: React.FC<Props> = ({
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { params } = useRoute()
const { colors } = useTheme()
const colorPrimary = color || colors.primaryDefault
const { t } = useTranslation('componentParse')
if (!expandHint) {
expandHint = t('HTML.defaultHint')
@ -111,6 +114,7 @@ const ParseHTML: React.FC<Props> = ({
content={content}
emojis={status?.emojis || emojis}
size={size}
color={colorPrimary}
adaptiveSize={adaptiveSize}
/>
)
@ -181,7 +185,7 @@ const ParseHTML: React.FC<Props> = ({
return (
<Text
key={index}
style={{ color: matchedMention ? colors.blue : colors.primaryDefault }}
style={{ color: matchedMention ? colors.blue : colorPrimary }}
onPress={() =>
matchedMention &&
!disableDetails &&

View File

@ -1,5 +1,5 @@
import { StyleConstants } from '@utils/styles/constants'
import { ColorValue, TouchableNativeFeedback, View } from 'react-native'
import { ColorValue, Pressable, View } from 'react-native'
import { SwipeListView } from 'react-native-swipe-list-view'
import haptics from './haptics'
import Icon, { IconName } from './Icon'
@ -25,7 +25,7 @@ export const SwipeToActions = <T extends unknown>({
renderHiddenItem={({ item }) => (
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'flex-end' }}>
{actions.map((action, index) => (
<TouchableNativeFeedback
<Pressable
key={index}
onPress={() => {
haptics(action.haptic || 'Light')
@ -43,7 +43,7 @@ export const SwipeToActions = <T extends unknown>({
>
<Icon name={action.icon} color='white' size={StyleConstants.Font.Size.L} />
</View>
</TouchableNativeFeedback>
</Pressable>
))}
</View>
)}

View File

@ -88,6 +88,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
: StyleConstants.Avatar.M
}}
style={{ flex: 1, flexBasis: '50%' }}
dim
/>
))}
</View>

View File

@ -33,6 +33,7 @@ export interface Props {
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
queryKey?: QueryKeyTimeline
highlighted?: boolean
suppressSpoiler?: boolean // Same content as the main thread, can be dimmed
disableDetails?: boolean
disableOnPress?: boolean
isConversation?: boolean
@ -44,6 +45,7 @@ const TimelineDefault: React.FC<Props> = ({
item,
queryKey,
highlighted = false,
suppressSpoiler = false,
disableDetails = false,
disableOnPress = false,
isConversation = false,
@ -170,6 +172,7 @@ const TimelineDefault: React.FC<Props> = ({
detectedLanguage,
excludeMentions,
highlighted,
suppressSpoiler,
inThread: queryKey?.[1].page === 'Toot',
disableDetails,
disableOnPress,

View File

@ -17,9 +17,9 @@ import Animated, {
Extrapolate,
interpolate,
runOnJS,
SharedValue,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming
} from 'react-native-reanimated'
@ -27,9 +27,10 @@ import Animated, {
export interface Props {
flRef: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
fetchingActive: React.MutableRefObject<boolean>
scrollY: Animated.SharedValue<number>
fetchingType: Animated.SharedValue<0 | 1 | 2>
isFetchingPrev: SharedValue<boolean>
setFetchedCount: React.Dispatch<React.SetStateAction<number | null>>
scrollY: SharedValue<number>
fetchingType: SharedValue<0 | 1 | 2>
disableRefresh?: boolean
readMarker?: 'read_marker_following'
}
@ -41,7 +42,8 @@ export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Siz
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
fetchingActive,
isFetchingPrev,
setFetchedCount,
scrollY,
fetchingType,
disableRefresh = false,
@ -55,20 +57,11 @@ const TimelineRefresh: React.FC<Props> = ({
}
const PREV_PER_BATCH = 1
const prevActive = useRef<boolean>(false)
const prevCache = useRef<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>()
const prevStatusId = useRef<Mastodon.Status['id']>()
const queryClient = useQueryClient()
const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] })
useDerivedValue(() => {
if (prevActive.current || isRefetching) {
fetchingActive.current = true
} else {
fetchingActive.current = false
}
}, [prevActive.current, isRefetching])
const { refetch } = useTimelineQuery({ ...queryKey[1] })
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
@ -96,7 +89,7 @@ const TimelineRefresh: React.FC<Props> = ({
const arrowStage = useSharedValue(0)
useAnimatedReaction(
() => {
if (fetchingActive.current) {
if (isFetchingPrev.value) {
return false
}
switch (arrowStage.value) {
@ -128,13 +121,12 @@ const TimelineRefresh: React.FC<Props> = ({
if (data) {
runOnJS(haptics)('Light')
}
},
[fetchingActive.current]
}
)
const fetchAndScrolled = useSharedValue(false)
const runFetchPrevious = async () => {
if (prevActive.current) return
if (isFetchingPrev.value) return
const firstPage =
queryClient.getQueryData<
@ -143,7 +135,7 @@ const TimelineRefresh: React.FC<Props> = ({
>
>(queryKey)?.pages[0]
prevActive.current = true
isFetchingPrev.value = true
prevStatusId.current = firstPage?.body[0]?.id
await queryFunctionTimeline({
@ -151,7 +143,9 @@ const TimelineRefresh: React.FC<Props> = ({
pageParam: firstPage?.links?.prev,
meta: {}
})
.then(res => {
.then(async res => {
setFetchedCount(res.body.length)
if (!res.body.length) return
queryClient.setQueryData<
@ -172,7 +166,7 @@ const TimelineRefresh: React.FC<Props> = ({
})
.then(async nextLength => {
if (!nextLength) {
prevActive.current = false
isFetchingPrev.value = false
return
}
@ -209,7 +203,7 @@ const TimelineRefresh: React.FC<Props> = ({
}
})
}
prevActive.current = false
isFetchingPrev.value = false
})
}
@ -218,6 +212,18 @@ const TimelineRefresh: React.FC<Props> = ({
if (readMarker) {
setAccountStorage([{ key: readMarker, value: undefined }])
}
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
return {
pages: [old.pages[0]],
pageParams: [old.pageParams[0]]
}
})
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
}

View File

@ -83,11 +83,9 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
<>
{audio.preview_url ? (
<GracefullyImage
uri={{
original: audio.preview_url,
remote: audio.preview_remote_url
}}
uri={{ original: audio.preview_url, remote: audio.preview_remote_url }}
style={styles.background}
dim
/>
) : null}
<Button

View File

@ -39,6 +39,7 @@ const AttachmentImage = ({
blurhash={image.blurhash}
onPress={() => navigateToImagesViewer(image.id)}
style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
dim
/>
</View>
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />

View File

@ -31,8 +31,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
})
})}
onPress={() =>
!disableOnPress &&
navigation.push('Tab-Shared-Account', { account: actualAccount })
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
}
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
dimension={
@ -51,6 +50,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
}}
dim
/>
)
}

View File

@ -86,6 +86,7 @@ const TimelineCard: React.FC = () => {
blurhash={status.card.blurhash}
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
dim
/>
) : null}
<View style={{ flex: 1, padding: StyleConstants.Spacing.S }}>

View File

@ -14,7 +14,7 @@ export interface Props {
}
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
const { status, highlighted, inThread } = useContext(StatusContext)
const { status, highlighted, suppressSpoiler, inThread } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
const { colors } = useTheme()
@ -35,6 +35,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
size={highlighted ? 'L' : 'M'}
adaptiveSize
numberOfLines={999}
color={suppressSpoiler ? colors.disabled : undefined}
/>
{inThread ? (
<CustomText

View File

@ -15,6 +15,7 @@ type StatusContextType = {
excludeMentions?: React.MutableRefObject<Mastodon.Mention[]>
highlighted?: boolean
suppressSpoiler?: boolean
inThread?: boolean
disableDetails?: boolean
disableOnPress?: boolean

View File

@ -15,8 +15,8 @@ const TimelineFullConversation = () => {
return queryKey &&
queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id &&
(status.mentions.length === 0 ||
status.mentions.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
(status.mentions?.length === 0 ||
status.mentions?.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
<CustomText
fontStyle='S'
style={{

View File

@ -22,7 +22,7 @@ const HeaderSharedReplies: React.FC = () => {
excludeMentions &&
(excludeMentions.current =
mentionsBeginning?.length && status?.mentions
? status.mentions.filter(mention => mentionsBeginning.includes(`@${mention.username}`))
? status.mentions?.filter(mention => mentionsBeginning.includes(`@${mention.username}`))
: [])
return excludeMentions?.current.length ? (

View File

@ -1,4 +1,5 @@
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
import { useScrollToTop } from '@react-navigation/native'
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
@ -11,9 +12,21 @@ import {
} from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useRef } from 'react'
import React, { RefObject, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import Animated, {
Easing,
runOnJS,
useAnimatedReaction,
useAnimatedScrollHandler,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withDelay,
withSequence,
withTiming
} from 'react-native-reanimated'
import TimelineEmpty from './Empty'
import TimelineFooter from './Footer'
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh'
@ -42,9 +55,10 @@ const Timeline: React.FC<Props> = ({
readMarker = undefined,
customProps
}) => {
const { colors } = useTheme()
const { colors, theme } = useTheme()
const { t } = useTranslation('componentTimeline')
const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } =
const { data, refetch, isFetching, isLoading, isRefetching, fetchNextPage, isFetchingNextPage } =
useTimelineQuery({
...queryKey[1],
options: {
@ -57,7 +71,47 @@ const Timeline: React.FC<Props> = ({
})
const flRef = useRef<FlatList>(null)
const fetchingActive = useRef<boolean>(false)
const isFetchingPrev = useSharedValue<boolean>(false)
const [fetchedCount, setFetchedCount] = useState<number | null>(null)
const fetchedNoticeHeight = useSharedValue<number>(100)
const notifiedFetchedNotice = useSharedValue<boolean>(false)
useAnimatedReaction(
() => isFetchingPrev.value,
(curr, prev) => {
if (curr === true && prev === false) {
notifiedFetchedNotice.value = true
}
}
)
useAnimatedReaction(
() => fetchedCount,
(curr, prev) => {
if (curr !== null && prev === null) {
notifiedFetchedNotice.value = false
}
},
[fetchedCount]
)
const fetchedNoticeTop = useDerivedValue(() => {
if (notifiedFetchedNotice.value || fetchedCount !== null) {
return withSequence(
withTiming(fetchedNoticeHeight.value + 16 + 4),
withDelay(
2000,
withTiming(
0,
{ easing: Easing.out(Easing.ease) },
finished => finished && runOnJS(setFetchedCount)(null)
)
)
)
} else {
return 0
}
}, [fetchedCount])
const fetchedNoticeAnimate = useAnimatedStyle(() => ({
transform: [{ translateY: fetchedNoticeTop.value }]
}))
const scrollY = useSharedValue(0)
const fetchingType = useSharedValue<0 | 1 | 2>(0)
@ -95,7 +149,12 @@ const Timeline: React.FC<Props> = ({
const marker = readMarker ? getAccountStorage.string(readMarker) : undefined
const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id
if (!fetchingActive.current && firstItemId && firstItemId > (marker || '0')) {
if (
!isFetchingPrev.value &&
!isRefetching &&
firstItemId &&
firstItemId > (marker || '0')
) {
setAccountStorage([{ key: readMarker, value: firstItemId }])
} else {
// setAccountStorage([{ key: readMarker, value: '109519141378761752' }])
@ -135,7 +194,8 @@ const Timeline: React.FC<Props> = ({
<TimelineRefresh
flRef={flRef}
queryKey={queryKey}
fetchingActive={fetchingActive}
isFetchingPrev={isFetchingPrev}
setFetchedCount={setFetchedCount}
scrollY={scrollY}
fetchingType={fetchingType}
disableRefresh={disableRefresh}
@ -176,6 +236,44 @@ const Timeline: React.FC<Props> = ({
{...androidRefreshControl}
{...customProps}
/>
{!disableRefresh ? (
<Animated.View
style={[
{
position: 'absolute',
alignSelf: 'center',
top: -fetchedNoticeHeight.value - 16,
paddingVertical: StyleConstants.Spacing.S,
paddingHorizontal: StyleConstants.Spacing.M,
backgroundColor: colors.backgroundDefault,
shadowColor: colors.primaryDefault,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: theme === 'light' ? 0.16 : 0.24,
borderRadius: 99,
justifyContent: 'center',
alignItems: 'center'
},
fetchedNoticeAnimate
]}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => (fetchedNoticeHeight.value = height)}
>
<CustomText
fontStyle='S'
style={{ color: colors.primaryDefault }}
children={
fetchedCount !== null
? fetchedCount > 0
? t('refresh.fetched.found', { count: fetchedCount })
: t('refresh.fetched.none')
: t('refresh.fetching')
}
/>
</Animated.View>
) : null}
</>
)
}

View File

@ -197,7 +197,7 @@ const menuStatus = ({
hidden:
!ownAccount &&
queryKey[1].page !== 'Notifications' &&
!status.mentions.find(
!status.mentions?.find(
mention => mention.acct === accountAcct && mention.username === accountAcct
) &&
!status.muted

View File

@ -16,7 +16,7 @@
"action_true": "Deixa de silenciar l'usuari"
},
"followAs": {
"trigger": "",
"trigger": "Segueix com...",
"succeed_default": "Seguint a @{{target}} com @{{source}}",
"succeed_locked": "Enviada la sol·licitud de seguiment a @{{target}} com {{source}}, pendent d'aprovar-la",
"failed": "Segueix com"

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Més recent des d'aquí",
"refetch": "A l'últim"
"refetch": "A l'últim",
"fetching": "Obtenint publicacions...",
"fetched": {
"none": "No n'hi ha de noves",
"found": "S'ha obtingut {{count}}"
}
},
"shared": {
"actioned": {

View File

@ -46,7 +46,7 @@
"name": "Etiquetes seguides"
},
"fontSize": {
"name": "Mida de la font de la publicació"
"name": "Mida de la font"
},
"language": {
"name": "Idioma"
@ -136,7 +136,7 @@
},
"preferences": {
"visibility": {
"title": "Visibilitat de la publicació per defecte",
"title": "Visibilitat per defecte",
"options": {
"public": "Públic",
"unlisted": "Sense llistar",
@ -147,11 +147,11 @@
"title": "Marca el contingut com a sensible"
},
"media": {
"title": "Mostra el contingut multimèdia",
"title": "Multimèdia",
"options": {
"default": "Amaga el contingut multimèdia com a sensible",
"show_all": "Mostra sempre el contingut multimèdia",
"hide_all": "Amaga sempre el contingut multimèdia"
"default": "Amaga els sensibles",
"show_all": "Mostra'ls sempre",
"hide_all": "Amaga'ls sempre"
}
},
"spoilers": {
@ -178,7 +178,7 @@
"context": "Aplica a <0 />",
"contexts": {
"home": "Seguits i llistes",
"notifications": "Notificació",
"notifications": "Notificacions",
"public": "federat",
"thread": "Conversa",
"account": "Perfil"
@ -199,17 +199,17 @@
"context": "Aplica a",
"contexts": {
"home": "Seguits i llistes",
"notifications": "Notificació",
"notifications": "Notificacions",
"public": "Línia de temps federada",
"thread": "Vista de conversa",
"account": "Vista del perfil"
},
"action": "Quan coincideix",
"actions": {
"warn": "",
"warn": "Contret però pot ser revelat",
"hide": "Amagat completament"
},
"keywords": "Coincidències per aquestes paraules claus",
"keywords": "Coincidències amb",
"keyword": "Paraula clau",
"statuses": "Coincideixen aquestes publicacions"
},

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "",
"refetch": ""
"refetch": "",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Neuere Einträge",
"refetch": "Zum letzten"
"refetch": "Zum letzten",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Νεότερες από εδώ",
"refetch": "Μέχρι την τελευταία"
"refetch": "Μέχρι την τελευταία",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Newer from here",
"refetch": "To latest"
"refetch": "To latest",
"fetching": "Fetching newer toots ...",
"fetched": {
"none": "No newer toot",
"found": "Fetched {{count}} toots"
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,7 @@
"action_true": "Dejar de silenciar al usuario"
},
"followAs": {
"trigger": "",
"trigger": "Seguir como...",
"succeed_default": "Siguiendo @{{target}} como @{{source}}",
"succeed_locked": "Enviado la solicitud de seguimiento a @{{target}} como {{source}}, pendiente de aprobación",
"failed": "Seguir como"

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Más reciente desde aquí",
"refetch": "Al último"
"refetch": "Al último",
"fetching": "Obteniendo publicaciones...",
"fetched": {
"none": "No hay nuevas",
"found": "Se ha obtenido {{count}}"
}
},
"shared": {
"actioned": {

View File

@ -136,7 +136,7 @@
},
"preferences": {
"visibility": {
"title": "Visibilidad de publicación predeterminada",
"title": "Visibilidad por defecto",
"options": {
"public": "Público",
"unlisted": "No listado",
@ -147,11 +147,11 @@
"title": "Marcar el contenido multimedia como sensibles por defecto"
},
"media": {
"title": "Mostrar el contenido multimedia",
"title": "Multimedia",
"options": {
"default": "Ocultar los contenidos multimedia marcados como sensibles",
"show_all": "Mostrar siempre el contenido multimedia",
"hide_all": "Siempre ocultar el contenido multimedia"
"default": "Ocultar los sensibles",
"show_all": "Mostrar siempre",
"hide_all": "Ocultar siempre"
}
},
"spoilers": {
@ -178,7 +178,7 @@
"context": "Se aplica en <0 />",
"contexts": {
"home": "Seguidos y listas",
"notifications": "Notificación",
"notifications": "Notificaciones",
"public": "Federado",
"thread": "Conversación",
"account": "Perfil"
@ -199,17 +199,17 @@
"context": "Se aplica en",
"contexts": {
"home": "Seguidos y listas",
"notifications": "Notificación",
"notifications": "Notificaciones",
"public": "Cronología federada",
"thread": "Vista de conversación",
"account": "Vista de perfil"
},
"action": "Al coincidir",
"actions": {
"warn": "",
"warn": "Contraído pero puede ser revelado",
"hide": "Oculto completamente"
},
"keywords": "Coincide con estas palabras clave",
"keywords": "Coincide con",
"keyword": "Palabra clave",
"statuses": "Coincide con estas publicaciones"
},

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Berrienak hemendik hasita",
"refetch": "Azkenekora arte"
"refetch": "Azkenekora arte",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Plus récent à partir d'ici",
"refetch": "À la dernière"
"refetch": "À la dernière",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Più recenti da qui",
"refetch": "Al più recente"
"refetch": "Al più recente",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "前のページを取得",
"refetch": "更新"
"refetch": "更新",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "이 시점에 이어서 불러오기",
"refetch": "최신 내용 불러오기"
"refetch": "최신 내용 불러오기",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Nieuwere vanaf hier",
"refetch": "Naar nieuwste"
"refetch": "Naar nieuwste",
"fetching": "Nieuwere toots ophalen...",
"fetched": {
"none": "Geen nieuwere toot",
"found": "{{count}} toots opgehaald"
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Nowsze",
"refetch": "Do najnowszych"
"refetch": "Do najnowszych",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -5,7 +5,7 @@
"cancel": "Cancelar",
"discard": "Descartar",
"continue": "Continuar",
"create": "",
"create": "Criar",
"delete": "Excluir",
"done": "Concluído",
"confirm": "Confirmar"

View File

@ -6,9 +6,9 @@
"action_false": "Seguir usuário",
"action_true": "Deixar de seguir usuário"
},
"inLists": "",
"inLists": "Listas contendo usuário ...",
"showBoosts": {
"action_false": "",
"action_false": "Mostrar boosts do usuário",
"action_true": ""
},
"mute": {
@ -21,7 +21,7 @@
"succeed_locked": "",
"failed": ""
},
"blockReport": "",
"blockReport": "Bloquear e denunciar",
"block": {
"action_false": "Bloquear usuário",
"action_true": "Desbloquear usuário",
@ -56,11 +56,11 @@
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
"action_false": "Seguir",
"action_true": "Deixar de seguir"
},
"filter": {
"action": ""
"action": "Filtrar hashtag ..."
}
},
"share": {
@ -99,8 +99,8 @@
"action_true": "Desafixar toot"
},
"filter": {
"action_false": "",
"action_true": ""
"action_false": "Filtrar toot ...",
"action_true": "Gerenciar filtros ..."
}
}
}

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Mais novo aqui",
"refetch": "Mais recente"
"refetch": "Mais recente",
"fetching": "Buscando toots mais recentes...",
"fetched": {
"none": "Nenhum novo toot",
"found": ""
}
},
"shared": {
"actioned": {
@ -116,7 +121,7 @@
"accessibilityHint": "Conta do usuário"
}
},
"application": "",
"application": "via {{application}}",
"edited": {
"accessibilityLabel": "Toot editado"
},

View File

@ -61,7 +61,7 @@
"name": "Criar uma lista"
},
"listEdit": {
"name": ""
"name": "Editar Detalhes da Lista"
},
"lists": {
"name": "Listas"
@ -116,7 +116,7 @@
"empty": "Nenhum usuário adicionado a esta lista"
},
"listEdit": {
"heading": "",
"heading": "Editar detalhes da lista",
"title": "Título",
"repliesPolicy": {
"heading": "Mostrar respostas para:",
@ -144,7 +144,7 @@
}
},
"sensitive": {
"title": ""
"title": "Marcar mídia como sensível por padrão"
},
"media": {
"title": "",
@ -166,22 +166,22 @@
},
"web_only": {
"title": "",
"description": ""
"description": "As configurações abaixo só podem ser atualizadas usando a interface web"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"keywords_one": "{{count}} palavra-chave",
"keywords_other": "{{count}} palavras-chave",
"statuses_one": "{{count}} toot",
"statuses_other": "{{count}} toots",
"context": "Aplica-se em <0 />",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"home": "seguindo e listas",
"notifications": "notificação",
"public": "global",
"thread": "",
"account": ""
"account": "perfil"
}
},
"preferencesFilter": {
@ -196,22 +196,22 @@
"604800": "Após 1 semana",
"18144000": "Após 1 mês"
},
"context": "",
"context": "Aplica-se em",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"home": "Seguindo e listas",
"notifications": "Notificação",
"public": "Linha do tempo global",
"thread": "",
"account": ""
},
"action": "",
"actions": {
"warn": "",
"hide": ""
"warn": "Recolhido, mas pode ser revelado",
"hide": "Esconder completamente"
},
"keywords": "",
"keyword": "",
"statuses": ""
"keyword": "Palavra-chave",
"statuses": "Corresponde a estes toots"
},
"profile": {
"feedback": {
@ -252,7 +252,7 @@
"label": "Rótulo",
"content": "Conteúdo"
},
"mediaSelectionFailed": ""
"mediaSelectionFailed": "Falha no processamento da imagem. Por favor, tente novamente."
},
"push": {
"notAvailable": "Seu telefone não suporta notificação de envio de tooot",
@ -343,7 +343,7 @@
"heading": "Tema escuro",
"options": {
"lighter": "Padrão",
"darker": ""
"darker": "Preto verdadeiro"
}
},
"browser": {
@ -354,7 +354,7 @@
}
},
"autoplayGifv": {
"heading": ""
"heading": "Reproduzir GIFs automaticamente na linha do tempo"
},
"feedback": {
"heading": "Pedidos de Funcionalidades"
@ -392,37 +392,37 @@
"suspended": "Conta suspensa pelos moderadores do seu servidor"
},
"accountInLists": {
"name": "",
"name": "Listas de @{{username}}",
"inLists": "",
"notInLists": ""
"notInLists": "Outras listas"
},
"attachments": {
"name": "<0 /><1>\"s mídia</1>"
},
"filter": {
"name": "",
"existed": ""
"name": "Adicionar ao filtro",
"existed": "Existe nestes filtros"
},
"history": {
"name": "Histórico de Edição"
},
"report": {
"name": "",
"report": "",
"name": "Denuncia {{acct}}",
"report": "Denunciar",
"forward": {
"heading": ""
"heading": "Encaminhar anonimamente para o servidor remoto {{instance}}"
},
"reasons": {
"heading": "",
"spam": "",
"other": "",
"violation": ""
"heading": "O que há de errado com essa conta?",
"spam": "É spam",
"other": "É outra coisa",
"violation": "Viola as regras do servidor"
},
"comment": {
"heading": ""
"heading": "Deseja nos dizer mais alguma coisa?"
},
"violatedRules": {
"heading": ""
"heading": "Regras violadas do servidor"
}
},
"search": {
@ -442,7 +442,7 @@
}
},
"trending": {
"tags": ""
"tags": "Hashtags em alta"
}
},
"sections": {
@ -451,13 +451,13 @@
"statuses": "Toot"
},
"notFound": "Não foi possível encontrar <bold>{{searchTerm}}</bold> {{type}} relacionado",
"noResult": ""
"noResult": "Não foi possível encontrar nada, tente um termo diferente"
},
"toot": {
"name": "Discussões",
"remoteFetch": {
"title": "",
"message": ""
"message": "O conteúdo global nem sempre está disponível na instância local. Estes conteúdos são obtidos de instâncias remotas e marcados. Você pode interagir com esses conteúdos normalmente."
}
},
"users": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "",
"refetch": ""
"refetch": "",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Nyare härifrån",
"refetch": "Till senaste"
"refetch": "Till senaste",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "З цього моменту",
"refetch": "До кінця"
"refetch": "До кінця",
"fetching": "Отримання нових дмухів ...",
"fetched": {
"none": "Немає нових дмухів",
"found": "Отримано {{count}} дмухів"
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "Trước đó",
"refetch": "Trang cuối"
"refetch": "Trang cuối",
"fetching": "",
"fetched": {
"none": "",
"found": ""
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "较新于此的嘟嘟",
"refetch": "最新的嘟嘟"
"refetch": "最新的嘟嘟",
"fetching": "获取较新的嘟文…",
"fetched": {
"none": "没有更新的嘟文",
"found": "已获取 {{count}} 条嘟文"
}
},
"shared": {
"actioned": {

View File

@ -16,7 +16,12 @@
},
"refresh": {
"fetchPreviousPage": "較新的嘟文",
"refetch": "到最新的位置"
"refetch": "到最新的位置",
"fetching": "取得較新的嘟文 …",
"fetched": {
"none": "沒有更新的嘟文",
"found": "已取得 {{count}} 條嘟文"
}
},
"shared": {
"actioned": {

View File

@ -92,7 +92,7 @@ const ScreenAccountSelection = ({
const { colors } = useTheme()
const { t } = useTranslation('screenAccountSelection')
const accounts = getReadableAccounts(true)
const accounts = getReadableAccounts()
return (
<ScrollView
@ -129,7 +129,7 @@ const ScreenAccountSelection = ({
return (
<AccountButton
key={index}
account={account}
account={{ ...account, active: false }}
additionalActions={() =>
navigationRef.navigate('Screen-Compose', {
type: 'share',

View File

@ -6,6 +6,7 @@ import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import { BlurView } from '@react-native-community/blur'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectImage } from '@utils/api/helpers/connect'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { StyleConstants } from '@utils/styles/constants'
@ -139,9 +140,9 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
>
{reaction.url ? (
<FastImage
source={{
source={connectImage({
uri: reduceMotionEnabled ? reaction.static_url : reaction.url
}}
})}
style={{
width: StyleConstants.Font.LineHeight.M + 3,
height: StyleConstants.Font.LineHeight.M

View File

@ -3,6 +3,7 @@ import Icon from '@components/Icon'
import { SwipeToActions } from '@components/SwipeToActions'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { connectImage } from '@utils/api/helpers/connect'
import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import { getAccountStorage, setAccountStorage, useAccountStorage } from '@utils/storage/actions'
@ -154,9 +155,11 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
4,
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
source={
attachment.local?.thumbnail
? { uri: attachment.local?.thumbnail }
: connectImage({ uri: attachment.remote?.preview_url })
}
/>
))}
</View>

View File

@ -6,6 +6,7 @@ import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native'
import { connectImage } from '@utils/api/helpers/connect'
import { featureCheck } from '@utils/helpers/featureCheck'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
@ -105,7 +106,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
>
<FastImage
style={{ width: '100%', height: '100%' }}
source={{ uri: item.local?.thumbnail || item.remote?.preview_url }}
source={
item.local?.thumbnail
? { uri: item.local?.thumbnail }
: connectImage({ uri: item.remote?.preview_url })
}
/>
{item.remote?.meta?.original?.duration ? (
<CustomText
@ -164,7 +169,8 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
haptics('Success')
}}
/>
{composeState.type === 'edit' && featureCheck('edit_media_details') ? (
{composeState.type !== 'edit' ||
(composeState.type === 'edit' && featureCheck('edit_media_details')) ? (
<Button
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1

View File

@ -96,10 +96,10 @@ const Collections: React.FC = () => {
iconBack='chevron-right'
title={t('screenTabs:me.stacks.push.name')}
content={
typeof instancePush.global === 'boolean'
typeof instancePush?.global === 'boolean'
? t('screenTabs:me.root.push.content', {
defaultValue: 'false',
context: instancePush.global.toString()
context: instancePush?.global.toString()
})
: undefined
}

View File

@ -1,16 +1,18 @@
import haptics from '@components/haptics'
import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { LOCALES } from '@i18n/locales'
import { useNavigation } from '@react-navigation/native'
import { connectVerify } from '@utils/api/helpers/connect'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import { GLOBAL } from '../../../../App'
import { mapFontsizeToName } from '../SettingsFontsize'
import { LOCALES } from '@i18n/locales'
const SettingsApp: React.FC = () => {
const navigation = useNavigation<any>()
@ -24,6 +26,23 @@ const SettingsApp: React.FC = () => {
const [browser, setBrowser] = useGlobalStorage.string('app.browser')
const [autoplayGifv, setAutoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv')
const [connect, setConnect] = useGlobalStorage.boolean('app.connect')
const [showConnect, setShowConnect] = useState(connect)
useEffect(() => {
connectVerify()
.then(() => {
setShowConnect(true)
})
.catch(() => {
if (connect) {
GLOBAL.connect = false
setConnect(false)
} else {
setShowConnect(false)
}
})
}, [])
return (
<MenuContainer>
<MenuRow
@ -152,6 +171,16 @@ const SettingsApp: React.FC = () => {
switchValue={autoplayGifv}
switchOnValueChange={() => setAutoplayGifv(!autoplayGifv)}
/>
{showConnect ? (
<MenuRow
title='使用代理'
switchValue={connect || false}
switchOnValueChange={() => {
GLOBAL.connect = !connect
setConnect(!connect)
}}
/>
) : null}
</MenuContainer>
)
}

View File

@ -93,6 +93,7 @@ const AccountAttachments: React.FC = () => {
dimension={{ width: width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() => navigation.push('Tab-Shared-Toot', { toot: item })}
dim
/>
)
}

View File

@ -28,6 +28,7 @@ const AccountHeader: React.FC = () => {
)
}
}}
dim
/>
)
}

View File

@ -42,6 +42,7 @@ const AccountInformationAvatar: React.FC = () => {
}
}
}}
dim
/>
)
}

View File

@ -35,6 +35,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
account,
_local: true,
options: {
placeholderData: (account._remote
? { ...account, id: undefined }
: account) as Mastodon.Account,
onSuccess: a => {
if (account._remote) {
setQueryKey([

View File

@ -7,6 +7,7 @@ import TimelineDefault from '@components/Timeline/Default'
import { useQuery } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { appendRemote } from '@utils/helpers/appendRemote'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks'
@ -206,26 +207,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
if (localMatch) {
return localMatch
} else {
return {
...ancestor,
_remote: true,
account: { ...ancestor.account, _remote: true },
mentions: ancestor.mentions.map(mention => ({
...mention,
_remote: true
})),
...(ancestor.reblog && {
reblog: {
...ancestor.reblog,
_remote: true,
account: { ...ancestor.reblog.account, _remote: true },
mentions: ancestor.reblog.mentions.map(mention => ({
...mention,
_remote: true
}))
}
})
}
return appendRemote.status(ancestor)
}
})
}
@ -268,23 +250,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
if (localMatch) {
return { ...localMatch, _level: remote._level }
} else {
return {
...remote,
_remote: true,
account: { ...remote.account, _remote: true },
mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })),
...(remote.reblog && {
reblog: {
...remote.reblog,
_remote: true,
account: { ...remote.reblog.account, _remote: true },
mentions: remote.reblog.mentions.map(mention => ({
...mention,
_remote: true
}))
}
})
}
return appendRemote.status(remote)
}
})
}
@ -380,8 +346,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
<TimelineDefault
item={item}
queryKey={item._remote ? queryKey.remote : queryKey.local}
highlighted={toot.id === item.id || item.id === 'cached'}
isConversation={toot.id !== item.id && item.id !== 'cached'}
highlighted={toot.id === item.id}
suppressSpoiler={
toot.id !== item.id &&
!!toot.spoiler_text?.length &&
toot.spoiler_text === item.spoiler_text
}
isConversation={toot.id !== item.id}
noBackground
/>
{/* <CustomText

View File

@ -4,14 +4,12 @@ import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { SearchResult } from '@utils/queryHooks/search'
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
import { flattenPages } from '@utils/queryHooks/utils'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
@ -36,8 +34,6 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
...queryKey[1]
})
const [isSearching, setIsSearching] = useState<number | null>(null)
return (
<FlatList
windowSize={7}
@ -46,38 +42,10 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
minHeight: '100%',
paddingVertical: StyleConstants.Spacing.Global.PagePadding
}}
renderItem={({ item, index }) => (
renderItem={({ item }) => (
<ComponentAccount
account={item}
props={{
disabled: isSearching === index,
onPress: () => {
if (data?.pages[0]?.remoteData) {
setIsSearching(index)
apiInstance<SearchResult>({
version: 'v2',
method: 'get',
url: 'search',
params: {
q: `@${item.acct}`,
type: 'accounts',
limit: 1,
resolve: true
}
})
.then(res => {
setIsSearching(null)
if (res.body.accounts[0]) {
navigation.push('Tab-Shared-Account', { account: res.body.accounts[0] })
}
})
.catch(() => setIsSearching(null))
} else {
navigation.push('Tab-Shared-Account', { account: item })
}
}
}}
children={<Loading />}
props={{ onPress: () => navigation.push('Tab-Shared-Account', { account: item }) }}
/>
)}
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}

View File

@ -2,11 +2,11 @@ import GracefullyImage from '@components/GracefullyImage'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
import { ScreenTabsStackParamList } from '@utils/navigation/navigators'
import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Platform } from 'react-native'
import { Platform, View } from 'react-native'
import TabLocal from './Local'
import TabMe from './Me'
import TabNotifications from './Notifications'
@ -14,7 +14,7 @@ import TabPublic from './Public'
const Tab = createBottomTabNavigator<ScreenTabsStackParamList>()
const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
const ScreenTabs = () => {
const { colors } = useTheme()
const [accountActive] = useGlobalStorage.string('account.active')
@ -50,19 +50,19 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
return <Icon name='bell' size={size} color={color} />
case 'Tab-Me':
return (
<GracefullyImage
uri={{ original: avatarStatic }}
dimension={{
width: size,
height: size
}}
style={{
borderRadius: size,
overflow: 'hidden',
borderWidth: focused ? 2 : 0,
borderColor: focused ? colors.secondary : color
}}
/>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: avatarStatic }}
dimension={{ width: size, height: size }}
style={{
borderRadius: size,
overflow: 'hidden',
borderWidth: focused ? 2 : 0,
borderColor: focused ? colors.primaryDefault : color
}}
/>
<Icon name='more-vertical' size={size / 1.5} color={colors.secondary} />
</View>
)
default:
return <Icon name='alert-octagon' size={size} color={color} />
@ -74,13 +74,13 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
<Tab.Screen name='Tab-Public' component={TabPublic} />
<Tab.Screen
name='Tab-Compose'
listeners={{
listeners={({ navigation }) => ({
tabPress: e => {
e.preventDefault()
haptics('Light')
navigation.navigate('Screen-Compose')
}
}}
})}
>
{() => null}
</Tab.Screen>
@ -88,15 +88,13 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
<Tab.Screen
name='Tab-Me'
component={TabMe}
listeners={{
listeners={({ navigation }) => ({
tabLongPress: () => {
haptics('Light')
//@ts-ignore
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' })
//@ts-ignore
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' })
}
}}
})}
/>
</Tab.Navigator>
)

View File

@ -1,5 +1,7 @@
import axios from 'axios'
import { GLOBAL } from '../../App'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import { CONNECT_DOMAIN } from './helpers/connect'
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
@ -35,14 +37,15 @@ const apiGeneral = async <T = unknown>({
return axios({
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
method,
baseURL: `https://${domain}/`,
baseURL: `https://${GLOBAL.connect ? CONNECT_DOMAIN() : domain}`,
url,
params,
headers: {
Accept: 'application/json',
...userAgent,
...headers,
...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' })
...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }),
...(GLOBAL.connect && { 'x-tooot-domain': domain })
},
data: body
})

View File

@ -0,0 +1,145 @@
import { mapEnvironment } from '@utils/helpers/checkEnvironment'
import { setGlobalStorage } from '@utils/storage/actions'
import axios from 'axios'
import parse from 'url-parse'
import { userAgent } from '.'
import { GLOBAL } from '../../../App'
const list = [
'n61owz4leck',
'z9skyp2f0m',
'nc2dqtyxevj',
'tgl97fgudrf',
'eo2sj0ut2s',
'a75auwihvyi',
'vzkpud5y5b',
'3uivf7yyex',
'pxfoa1wbor',
'3cor5jempc',
'9o32znuepr',
'9ayt1l2dzpi',
'60iu4rz8js',
'dzoa1lbxbv',
'82rpiiqw21',
'fblij1c9gyl',
'wk2x048g8gl',
'9x91yrbtmn',
'dgu5p7eif6',
'uftwyhrkgrh',
'vv5hay15vjk',
'ooj9ihtyur',
'o8r7phzd58',
'pujwyg269s',
'l6yq5nr8lv',
'ocyrlfmdnl',
'rdtpeip5e2',
'ykzb5784js',
'm34z7j5us1i',
'tqsfr0orqa',
'8ncrt0mifa',
'ygce2fdmsm',
'22vk7csljz',
'7mmb6hrih1',
'grla5cpgau',
'0vygyvs4k7',
'1texbe32sf',
'ckwvauiiol',
'qkxryrbpxx',
'ptb19c0ks9g',
'3bpe76o6stg',
'd507ejce9g',
'jpul5v2mqej',
'6m5uxemc79',
'wxbtoo9t3p',
'8qco3d0idh',
'u00c2xiabvf',
'hutkqwrcy8',
't6vrkzhpzo',
'wy6e529mnb',
'kzzrlfa59pg',
'mmo4sv4a7s',
'u0dishl20k',
'8qyx25bq3u',
'd3mucdzlu1',
'y123m81vsjl',
'51opvzdo6k',
'r4z333th9u',
'q77hl0ggfr',
'bsk1f2wi52g',
'eubnxpv0pz',
'h11pk7qm8i',
'brhxw45vd5',
'vtnvlsrn1z',
'0q5w0hhzb5',
'vq2rz02ayf',
'hml3igfwkq',
'39qs7vhenl',
'5vcv775rug',
'kjom5gr7i3',
't2kmaoeb5x',
'ni6ow1z11b',
'yvgtoc3d88',
'iax04eatnz',
'esxyu9zujg',
'73xa28n278',
'5x63a8l24k',
'dy1trb0b3sj',
'd4c31j23m8',
'ho76046l0j',
'sw8lj5u2ef',
'z5cn21mew5',
'wxj73nmqwa',
'gdj00dlx98',
'0v76xag64i',
'j35104qduhj',
'l63r7h0ss6',
'e5xdv7t1q0h',
'4icoh8t4c8',
'nbk36jt4sq',
'zi0n0cv4tk',
'o7qkfp3rxu',
'xd2wefzd27',
'rg7e6tsacx',
'9lrq3s4vfm',
'srs9p21lxoh',
'n8xymau42t',
'q5cik283fg',
'68ye9feqs5',
'xjc5anubnv'
]
export const CONNECT_DOMAIN = () =>
mapEnvironment({
release: `${list[Math.floor(Math.random() * (100 - 0) + 0)]}.tooot.app`,
candidate: 'connect-candidate.tooot.app',
development: 'connect-development.tooot.app'
})
export const connectImage = ({
uri
}: {
uri?: string
}): { uri?: string; headers?: { 'x-tooot-domain': string } } => {
if (GLOBAL.connect) {
if (uri) {
const host = parse(uri).host
return { uri: uri.replace(host, CONNECT_DOMAIN()), headers: { 'x-tooot-domain': host } }
} else {
return { uri }
}
} else {
return { uri }
}
}
export const connectVerify = () =>
axios({
method: 'get',
baseURL: `https://${CONNECT_DOMAIN()}`,
url: 'verify',
headers: { ...userAgent }
}).catch(err => {
GLOBAL.connect = false
setGlobalStorage('app.connect', false)
return Promise.reject(err)
})

View File

@ -1,8 +1,10 @@
import * as Sentry from '@sentry/react-native'
import { setGlobalStorage } from '@utils/storage/actions'
import chalk from 'chalk'
import Constants from 'expo-constants'
import { Platform } from 'react-native'
import parse from 'url-parse'
import { GLOBAL } from '../../../App'
const userAgent = {
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
@ -18,6 +20,12 @@ const handleError =
} | void
) =>
(error: any) => {
if (GLOBAL.connect) {
if (error?.response?.status == 403 && error?.response?.data == 'connect_blocked') {
GLOBAL.connect = false
setGlobalStorage('app.connect', false)
}
}
const shouldReportToSentry = config && (config.captureRequest || config.captureResponse)
shouldReportToSentry && Sentry.setContext('Error object', error)

View File

@ -1,7 +1,9 @@
import { getAccountDetails } from '@utils/storage/actions'
import { StorageGlobal } from '@utils/storage/global'
import axios, { AxiosRequestConfig } from 'axios'
import { GLOBAL } from '../../App'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import { CONNECT_DOMAIN } from './helpers/connect'
export type Params = {
account?: StorageGlobal['account.active']
@ -43,11 +45,13 @@ const apiInstance = async <T = unknown>({
method + ctx.blue(' -> ') + `/${url}` + (params ? ctx.blue(' -> ') : ''),
params ? params : ''
)
console.log('body', body)
return axios({
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
method,
baseURL: `https://${accountDetails['auth.domain']}/api/${version}/`,
baseURL: `https://${
GLOBAL.connect ? CONNECT_DOMAIN() : accountDetails['auth.domain']
}/api/${version}`,
url,
params,
headers: {
@ -55,7 +59,8 @@ const apiInstance = async <T = unknown>({
...userAgent,
...headers,
Authorization: `Bearer ${accountDetails['auth.token']}`,
...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' })
...(body && body instanceof FormData && { 'Content-Type': 'multipart/form-data' }),
...(GLOBAL.connect && { 'x-tooot-domain': accountDetails['auth.domain'] })
},
data: body,
...extras

View File

@ -0,0 +1,23 @@
// Central place appending _remote internal prop
export const appendRemote = {
status: (status: Mastodon.Status) => ({
...status,
...(status.reblog && {
reblog: {
...status.reblog,
account: appendRemote.account(status.reblog.account),
mentions: appendRemote.mentions(status.reblog.mentions)
}
}),
account: appendRemote.account(status.account),
mentions: appendRemote.mentions(status.mentions),
_remote: true
}),
account: (account: Mastodon.Account) => ({
...account,
_remote: true
}),
mentions: (mentions: Mastodon.Mention[]) =>
mentions?.map(mention => ({ ...mention, _remote: true }))
}

View File

@ -1,6 +1,9 @@
import { getAccountStorage } from '@utils/storage/actions'
import parse from 'url-parse'
// Would mess with the /@username format
const BLACK_LIST = ['matters.news', 'medium.com']
export const urlMatcher = (
url: string
):
@ -14,6 +17,10 @@ export const urlMatcher = (
if (!parsed.hostname.length || !parsed.pathname.length) return undefined
const domain = parsed.hostname
if (BLACK_LIST.includes(domain)) {
return
}
const _remote = parsed.hostname !== getAccountStorage.string('auth.domain')
let statusId: string | undefined

View File

@ -1,6 +1,7 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { appendRemote } from '@utils/helpers/appendRemote'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { AxiosError } from 'axios'
import { searchLocalAccount } from './search'
@ -34,14 +35,14 @@ const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyA
method: 'get',
domain: domain,
url: `api/v1/accounts/${id}`
}).then(res => ({ ...res.body, _remote: true }))
}).then(res => appendRemote.account(res.body))
} else if (acct) {
matchedAccount = await apiGeneral<Mastodon.Account>({
method: 'get',
domain: domain,
url: 'api/v1/accounts/lookup',
params: { acct }
}).then(res => ({ ...res.body, _remote: true }))
}).then(res => appendRemote.account(res.body))
}
} catch {}
}

View File

@ -1,6 +1,7 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { appendRemote } from '@utils/helpers/appendRemote'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { AxiosError } from 'axios'
import { searchLocalStatus } from './search'
@ -26,7 +27,7 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatus>)
method: 'get',
domain,
url: `api/v1/statuses/${id}`
}).then(res => ({ ...res.body, _remote: true }))
}).then(res => appendRemote.status(res.body))
} catch {}
}

View File

@ -6,6 +6,7 @@ import {
import apiGeneral from '@utils/api/general'
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
import { appendRemote } from '@utils/helpers/appendRemote'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import { AxiosError } from 'axios'
@ -54,7 +55,11 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
url: `api/v1/accounts/${resLookup.body.id}/${page.type}`,
params
})
return { ...res, remoteData: true }
return {
...res,
body: res.body.map(account => appendRemote.account(account)),
remoteData: true
}
} else {
throw new Error()
}

View File

@ -323,27 +323,30 @@ export const removeAccount = async (account: string, warning: boolean = true) =>
}
export type ReadableAccountType = {
avatar_static: string
acct: string
key: string
active: boolean
}
export const getReadableAccounts = (withoutActive: boolean = false): ReadableAccountType[] => {
const accountActive = !withoutActive && getGlobalStorage.string('account.active')
const accounts = getGlobalStorage.object('accounts')?.sort((a, b) => a.localeCompare(b))
!withoutActive &&
accounts?.splice(
accounts.findIndex(a => a === accountActive),
1
)
!withoutActive && accounts?.unshift(accountActive || '')
export const getReadableAccounts = (): ReadableAccountType[] => {
const accountActive = getGlobalStorage.string('account.active')
const accounts = getGlobalStorage.object('accounts')
return (
accounts?.map(account => {
const details = getAccountDetails(
['auth.account.acct', 'auth.account.domain', 'auth.domain', 'auth.account.id'],
[
'auth.account.avatar_static',
'auth.account.acct',
'auth.account.domain',
'auth.domain',
'auth.account.id'
],
account
)
if (details) {
return {
avatar_static: details['auth.account.avatar_static'],
acct: `@${details['auth.account.acct']}@${details['auth.account.domain']}`,
key: generateAccountKey({
domain: details['auth.domain'],
@ -352,7 +355,7 @@ export const getReadableAccounts = (withoutActive: boolean = false): ReadableAcc
active: account === accountActive
}
} else {
return { acct: '', key: '', active: false }
return { avatar_static: '', acct: '', key: '', active: false }
}
}) || []
).filter(a => a.acct.length)

View File

@ -17,6 +17,7 @@ export type GlobalV0 = {
'version.account': number
// boolean
'app.auto_play_gifv'?: boolean
'app.connect'?: boolean
//// account
// string