Merge branch 'main' into candidate

This commit is contained in:
xmflsct 2023-01-11 22:54:37 +01:00
commit d0db2385ca
33 changed files with 190 additions and 150 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.8.0", "version": "4.8.1",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
@ -81,6 +81,7 @@
"react-native-language-detection": "^0.2.2", "react-native-language-detection": "^0.2.2",
"react-native-mmkv": "^2.5.1", "react-native-mmkv": "^2.5.1",
"react-native-pager-view": "^6.1.2", "react-native-pager-view": "^6.1.2",
"react-native-quick-base64": "^2.0.5",
"react-native-reanimated": "^2.13.0", "react-native-reanimated": "^2.13.0",
"react-native-reanimated-zoom": "^0.3.3", "react-native-reanimated-zoom": "^0.3.3",
"react-native-safe-area-context": "^4.4.1", "react-native-safe-area-context": "^4.4.1",

View File

@ -1,6 +1,6 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { import {
AccessibilityProps, AccessibilityProps,
Image, Image,
@ -55,15 +55,18 @@ const GracefullyImage = ({
const { colors } = useTheme() const { colors } = useTheme()
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote)
const source = { const source = {
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original uri: reduceMotionEnabled && uri.static ? uri.static : currentUri
} }
const onLoad = () => { useEffect(() => {
setImageLoaded(true) if (
if (setImageDimensions && source.uri) { (uri.original ? currentUri !== uri.original : true) &&
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height })) (uri.remote ? currentUri !== uri.remote : true)
) {
setCurrentUri(uri.original || uri.remote)
} }
} }, [currentUri, uri.original, uri.remote])
const blurhashView = () => { const blurhashView = () => {
if (hidden || !imageLoaded) { if (hidden || !imageLoaded) {
@ -92,11 +95,19 @@ const GracefullyImage = ({
/> />
) : null} ) : null}
<FastImage <FastImage
source={{ source={source}
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
}}
style={[{ flex: 1 }, imageStyle]} style={[{ flex: 1 }, imageStyle]}
onLoad={onLoad} onLoad={() => {
setImageLoaded(true)
if (setImageDimensions && source.uri) {
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
}
}}
onError={() => {
if (uri.original && uri.original === currentUri && uri.remote) {
setCurrentUri(uri.remote)
}
}}
/> />
{blurhashView()} {blurhashView()}
</Pressable> </Pressable>

View File

@ -19,12 +19,14 @@ import {
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as AuthSession from 'expo-auth-session' import * as AuthSession from 'expo-auth-session'
import * as Random from 'expo-random'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { RefObject, useCallback, useState } from 'react' import React, { RefObject, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { fromByteArray } from 'react-native-quick-base64'
import parse from 'url-parse' import parse from 'url-parse'
import CustomText from '../Text' import CustomText from '../Text'
@ -67,10 +69,6 @@ const ComponentInstance: React.FC<Props> = ({
const appsMutation = useAppsMutation({ const appsMutation = useAppsMutation({
retry: false, retry: false,
onSuccess: async (data, variables) => { onSuccess: async (data, variables) => {
const scopes = featureCheck('deprecate_auth_follow')
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push']
const clientId = data.client_id const clientId = data.client_id
const clientSecret = data.client_secret const clientSecret = data.client_secret
@ -79,19 +77,19 @@ const ComponentInstance: React.FC<Props> = ({
const request = new AuthSession.AuthRequest({ const request = new AuthSession.AuthRequest({
clientId, clientId,
clientSecret, clientSecret,
scopes, scopes: variables.scopes,
redirectUri redirectUri
}) })
await request.makeAuthUrlAsync(discovery) await request.makeAuthUrlAsync(discovery)
const promptResult = await request.promptAsync(discovery, await browserPackage()) 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(
{ {
clientId, clientId,
clientSecret, clientSecret,
scopes, scopes: variables.scopes,
redirectUri, redirectUri,
code: promptResult.params.code, code: promptResult.params.code,
extraParams: { extraParams: {
@ -162,7 +160,7 @@ const ComponentInstance: React.FC<Props> = ({
'admin.sign_up': false, 'admin.sign_up': false,
'admin.report': false 'admin.report': false
}, },
key: Math.random().toString(36).slice(2, 12) key: fromByteArray(Random.getRandomBytes(16))
}, },
page_local: { page_local: {
showBoosts: true, showBoosts: true,
@ -186,7 +184,7 @@ const ComponentInstance: React.FC<Props> = ({
) )
if (!account) { if (!account) {
setGlobalStorage('accounts', accounts?.concat([accountKey])) setGlobalStorage('accounts', (accounts || []).concat([accountKey]))
} }
setAccount(accountKey) setAccount(accountKey)
@ -195,6 +193,9 @@ const ComponentInstance: React.FC<Props> = ({
} }
}) })
const scopes = featureCheck('deprecate_auth_follow')
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push']
const processUpdate = useCallback(() => { const processUpdate = useCallback(() => {
if (domain) { if (domain) {
const accounts = getGlobalStorage.object('accounts') const accounts = getGlobalStorage.object('accounts')
@ -209,12 +210,12 @@ const ComponentInstance: React.FC<Props> = ({
}, },
{ {
text: t('common:buttons.continue'), text: t('common:buttons.continue'),
onPress: () => appsMutation.mutate({ domain }) onPress: () => appsMutation.mutate({ domain, scopes })
} }
] ]
) )
} else { } else {
appsMutation.mutate({ domain }) appsMutation.mutate({ domain, scopes })
} }
} }
}, [domain]) }, [domain])

View File

@ -126,7 +126,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
} }
if (filterResults?.length && !filterRevealed) { if (filterResults?.length && !filterRevealed) {
return !filterResults.filter(result => result.filter_action === 'hide').length ? ( return !filterResults.filter(result => result.filter_action === 'hide')?.length ? (
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}> <Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
<TimelineFiltered filterResults={filterResults} /> <TimelineFiltered filterResults={filterResults} />
</Pressable> </Pressable>

View File

@ -144,7 +144,7 @@ const TimelineRefresh: React.FC<Props> = ({
>(queryKey)?.pages[0] >(queryKey)?.pages[0]
prevActive.current = true prevActive.current = true
prevStatusId.current = firstPage?.body[0].id prevStatusId.current = firstPage?.body[0]?.id
await queryFunctionTimeline({ await queryFunctionTimeline({
queryKey, queryKey,
@ -182,7 +182,7 @@ const TimelineRefresh: React.FC<Props> = ({
flRef.current?.scrollToOffset({ offset: scrollY.value - 15, animated: true }) flRef.current?.scrollToOffset({ offset: scrollY.value - 15, animated: true })
} }
await new Promise(promise => setTimeout(promise, 32)) await new Promise(promise => setTimeout(promise, 64))
queryClient.setQueryData< queryClient.setQueryData<
InfiniteData< InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]> PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
@ -197,7 +197,8 @@ const TimelineRefresh: React.FC<Props> = ({
const insert = prevCache.current?.slice(-PREV_PER_BATCH) const insert = prevCache.current?.slice(-PREV_PER_BATCH)
prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH) prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH)
if (insert) { if (insert) {
return { ...page, body: [...insert, ...page.body] } page.body.unshift(...insert)
return page
} else { } else {
return page return page
} }

View File

@ -30,7 +30,7 @@ const TimelineCard: React.FC = () => {
const statusQuery = useStatusQuery({ const statusQuery = useStatusQuery({
status: match?.status ? { ...match.status, uri: status.card.url } : undefined, status: match?.status ? { ...match.status, uri: status.card.url } : undefined,
options: { enabled: false, retry: 1 } options: { enabled: false, retry: false }
}) })
useEffect(() => { useEffect(() => {
if (match?.status) { if (match?.status) {
@ -47,7 +47,7 @@ const TimelineCard: React.FC = () => {
const accountQuery = useAccountQuery({ const accountQuery = useAccountQuery({
account: match?.account ? { ...match?.account, url: status.card.url } : undefined, account: match?.account ? { ...match?.account, url: status.card.url } : undefined,
options: { enabled: false, retry: 1 } options: { enabled: false, retry: false }
}) })
useEffect(() => { useEffect(() => {
if (match?.account) { if (match?.account) {

View File

@ -14,9 +14,11 @@ const HeaderSharedReplies: React.FC = () => {
const { t } = useTranslation(['common', 'componentTimeline']) const { t } = useTranslation(['common', 'componentTimeline'])
const { colors } = useTheme() const { colors } = useTheme()
const mentionsBeginning = rawContent?.current?.[0] const mentionsBeginning = rawContent?.current?.[0]?.length
.match(new RegExp(/^(?:@\S+\s+)+/))?.[0] ? rawContent?.current?.[0]
?.match(new RegExp(/@\S+/, 'g')) .match(new RegExp(/^(?:@\S+\s+)+/))?.[0]
?.match(new RegExp(/@\S+/, 'g'))
: undefined
excludeMentions && excludeMentions &&
(excludeMentions.current = (excludeMentions.current =
mentionsBeginning?.length && status?.mentions mentionsBeginning?.length && status?.mentions

View File

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

View File

@ -16,12 +16,12 @@
"action_true": "Stummschaltung des Nutzers aufheben" "action_true": "Stummschaltung des Nutzers aufheben"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "Folgen als…",
"succeed_default": "", "succeed_default": "Folgt nun @{{target}} als @{{source}}",
"succeed_locked": "", "succeed_locked": "Follower-Anfrage an @{{target}} mit {{source}} gesendet, Bestätigung ausstehend",
"failed": "" "failed": "Folgen als…"
}, },
"blockReport": "", "blockReport": "Blockieren und melden…",
"block": { "block": {
"action_false": "Nutzer blockieren", "action_false": "Nutzer blockieren",
"action_true": "User entblocken", "action_true": "User entblocken",

View File

@ -16,12 +16,12 @@
"action_true": "Dejar de silenciar al usuario" "action_true": "Dejar de silenciar al usuario"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "Seguir como...",
"succeed_default": "", "succeed_default": "Siguiendo @{{target}} como @{{source}}",
"succeed_locked": "", "succeed_locked": "Enviado la solicitud de seguimiento a @{{target}} como {{source}}, pendiente de aprobación",
"failed": "" "failed": "Seguir como"
}, },
"blockReport": "", "blockReport": "Bloquear y denunciar...",
"block": { "block": {
"action_false": "Bloquear usuario", "action_false": "Bloquear usuario",
"action_true": "Desbloquear usuario", "action_true": "Desbloquear usuario",

View File

@ -16,12 +16,12 @@
"action_true": "Dempen opheffen voor gebruiker" "action_true": "Dempen opheffen voor gebruiker"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "Volg als...",
"succeed_default": "", "succeed_default": "Je volgt nu @{{target}} met @{{source}}",
"succeed_locked": "", "succeed_locked": "Verstuurde het volgverzoek naar @{{target}} met {{source}}, in afwachting van goedkeuring",
"failed": "" "failed": "Volg als"
}, },
"blockReport": "", "blockReport": "Blokkeren en rapporten...",
"block": { "block": {
"action_false": "Gebruiker blokkeren", "action_false": "Gebruiker blokkeren",
"action_true": "Gebruiker deblokkeren", "action_true": "Gebruiker deblokkeren",

View File

@ -16,12 +16,12 @@
"action_true": "Wyłącz wyciszenie" "action_true": "Wyłącz wyciszenie"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "Obserwuj jako...",
"succeed_default": "", "succeed_default": "Teraz obserwujesz @{{target}} z @{{source}}",
"succeed_locked": "", "succeed_locked": "Wysłano prośbę o obserwowanie do @{{target}} z {{source}}, oczekiwanie na zatwierdzenie",
"failed": "" "failed": "Obserwuj jako"
}, },
"blockReport": "", "blockReport": "Zablokuj i zgłoś...",
"block": { "block": {
"action_false": "Zablokuj użytkownika", "action_false": "Zablokuj użytkownika",
"action_true": "Odblokuj użytkownika", "action_true": "Odblokuj użytkownika",

View File

@ -16,12 +16,12 @@
"action_true": "Зняти заглушення з користувача" "action_true": "Зняти заглушення з користувача"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "Стежити як...",
"succeed_default": "", "succeed_default": "Тепер ви стежите за @{{target}} з @{{source}}",
"succeed_locked": "", "succeed_locked": "Запит на стеження за @{{target}} з {{source}} надіслано, очікується затвердження",
"failed": "" "failed": "Стежити як"
}, },
"blockReport": "", "blockReport": "Заблокувати й поскаржитися...",
"block": { "block": {
"action_false": "Заблокувати користувача", "action_false": "Заблокувати користувача",
"action_true": "Розблокувати користувача", "action_true": "Розблокувати користувача",

View File

@ -17,8 +17,8 @@
}, },
"followAs": { "followAs": {
"trigger": "关注…", "trigger": "关注…",
"succeed_default": "{{source}} 正在关注 {{target}}", "succeed_default": "@{{source}} 正在关注 @{{target}}",
"succeed_locked": "已从 {{source}} 发送关注请求至 {{target}},等待通过", "succeed_locked": "已从 @{{source}} 发送关注请求至 @{{target}},等待通过",
"failed": "用其它账户关注" "failed": "用其它账户关注"
}, },
"blockReport": "屏蔽与举报…", "blockReport": "屏蔽与举报…",

View File

@ -16,12 +16,12 @@
"action_true": "解除靜音使用者" "action_true": "解除靜音使用者"
}, },
"followAs": { "followAs": {
"trigger": "", "trigger": "跟隨...",
"succeed_default": "", "succeed_default": "@{{source}} 正在跟隨 @{{target}}",
"succeed_locked": "", "succeed_locked": "已從 {{source}} 發送跟隨請求至 @{{target}},等待同意",
"failed": "" "failed": "用其它帳號跟隨"
}, },
"blockReport": "", "blockReport": "封鎖並檢舉…",
"block": { "block": {
"action_false": "封鎖使用者", "action_false": "封鎖使用者",
"action_true": "解除封鎖使用者", "action_true": "解除封鎖使用者",

View File

@ -92,7 +92,7 @@ const ScreenAccountSelection = ({
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenAccountSelection') const { t } = useTranslation('screenAccountSelection')
const accounts = getReadableAccounts() const accounts = getReadableAccounts(true)
return ( return (
<ScrollView <ScrollView

View File

@ -17,7 +17,7 @@ const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
const navigation = useNavigation<any>() const navigation = useNavigation<any>()
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
const [drafts] = useAccountStorage.object('drafts') const [drafts] = useAccountStorage.object('drafts')
const draftsCount = drafts.filter(draft => draft.timestamp !== composeState.timestamp).length const draftsCount = drafts?.filter(draft => draft.timestamp !== composeState.timestamp).length
useEffect(() => { useEffect(() => {
layoutAnimation() layoutAnimation()

View File

@ -7,8 +7,8 @@ import { useTranslation } from 'react-i18next'
import { View } from 'react-native' import { View } from 'react-native'
const ComposePostingAs = () => { const ComposePostingAs = () => {
const accounts = useGlobalStorage.object('accounts') const [accounts] = useGlobalStorage.object('accounts')
if (!accounts.length) return null if (!accounts?.length) return null
const { t } = useTranslation('screenCompose') const { t } = useTranslation('screenCompose')
const { colors } = useTheme() const { colors } = useTheme()

View File

@ -7,7 +7,7 @@ import ComposeRoot from '@screens/Compose/Root'
import { formatText } from '@screens/Compose/utils/processText' import { formatText } from '@screens/Compose/utils/processText'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { handleError } from '@utils/api/helpers' import { handleError } from '@utils/api/helpers'
import { RootStackScreenProps, useNavState } from '@utils/navigation/navigators' import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useInstanceQuery } from '@utils/queryHooks/instance' import { useInstanceQuery } from '@utils/queryHooks/instance'
import { usePreferencesQuery } from '@utils/queryHooks/preferences' import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { searchLocalStatus } from '@utils/queryHooks/search' import { searchLocalStatus } from '@utils/queryHooks/search'
@ -346,13 +346,6 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
} }
switch (params?.type) { switch (params?.type) {
case undefined:
queryClient.invalidateQueries({
queryKey: ['Timeline', { page: 'Following' }],
exact: false
})
break
case 'conversation':
case 'edit': // doesn't work case 'edit': // doesn't work
// mutateTimeline.mutate({ // mutateTimeline.mutate({
// type: 'editItem', // type: 'editItem',
@ -361,11 +354,20 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
// }) // })
// break // break
case 'deleteEdit': case 'deleteEdit':
case 'reply':
for (const navState of params.navigationState) { for (const navState of params.navigationState) {
navState && queryClient.invalidateQueries(navState) navState && queryClient.invalidateQueries(navState)
} }
break break
case 'conversation':
case 'reply':
if (params.navigationState) {
for (const navState of params.navigationState) {
navState &&
navState[1].page !== 'Following' &&
queryClient.invalidateQueries(navState)
}
}
break
} }
removeDraft(composeState.timestamp) removeDraft(composeState.timestamp)
navigation.goBack() navigation.goBack()

View File

@ -55,7 +55,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
: t('tabs.local.name') : t('tabs.local.name')
} }
/> />
{page.page === 'Following' && !pageLocal.showBoosts ? ( {page.page === 'Following' && !pageLocal?.showBoosts ? (
<Icon <Icon
name='Repeat' name='Repeat'
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
@ -64,7 +64,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
crossOut crossOut
/> />
) : null} ) : null}
{page.page === 'Following' && !pageLocal.showReplies ? ( {page.page === 'Following' && !pageLocal?.showReplies ? (
<Icon <Icon
name='MessageCircle' name='MessageCircle'
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
@ -94,20 +94,20 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
key='showBoosts' key='showBoosts'
value={pageLocal.showBoosts ? 'on' : 'off'} value={pageLocal?.showBoosts ? 'on' : 'off'}
onValueChange={() => { onValueChange={() => {
setQueryKey([ setQueryKey([
'Timeline', 'Timeline',
{ {
page: 'Following', page: 'Following',
showBoosts: !pageLocal.showBoosts, showBoosts: !pageLocal?.showBoosts,
showReplies: pageLocal.showReplies showReplies: pageLocal?.showReplies
} }
]) ])
setAccountStorage([ setAccountStorage([
{ {
key: 'page_local', key: 'page_local',
value: { ...pageLocal, showBoosts: !pageLocal.showBoosts } value: { ...pageLocal, showBoosts: !pageLocal?.showBoosts }
} }
]) ])
}} }}
@ -117,20 +117,20 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
key='showReplies' key='showReplies'
value={pageLocal.showReplies ? 'on' : 'off'} value={pageLocal?.showReplies ? 'on' : 'off'}
onValueChange={() => { onValueChange={() => {
setQueryKey([ setQueryKey([
'Timeline', 'Timeline',
{ {
page: 'Following', page: 'Following',
showBoosts: pageLocal.showBoosts, showBoosts: pageLocal?.showBoosts,
showReplies: !pageLocal.showReplies showReplies: !pageLocal?.showReplies
} }
]) ])
setAccountStorage([ setAccountStorage([
{ {
key: 'page_local', key: 'page_local',
value: { ...pageLocal, showReplies: !pageLocal.showReplies } value: { ...pageLocal, showReplies: !pageLocal?.showReplies }
} }
]) ])
}} }}

View File

@ -49,7 +49,7 @@ const TabMeProfileRoot: React.FC<
<ProfileAvatarHeader type='header' messageRef={messageRef} /> <ProfileAvatarHeader type='header' messageRef={messageRef} />
<MenuRow <MenuRow
title={t('screenTabs:me.profile.root.note.title')} title={t('screenTabs:me.profile.root.note.title')}
content={data?.source.note} content={data?.source?.note}
loading={isFetching} loading={isFetching}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => { onPress={() => {

View File

@ -17,10 +17,12 @@ import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import * as Random from 'expo-random'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AppState, Linking, Platform, ScrollView, View } from 'react-native' import { AppState, Linking, Platform, ScrollView, View } from 'react-native'
import { fromByteArray } from 'react-native-quick-base64'
const TabMePush: React.FC = () => { const TabMePush: React.FC = () => {
const { colors } = useTheme() const { colors } = useTheme()
@ -178,6 +180,11 @@ const TabMePush: React.FC = () => {
setAccountStorage([{ key: 'push', value: { ...push, global: false } }]) setAccountStorage([{ key: 'push', value: { ...push, global: false } }])
} else { } else {
// Fix a bug for some users of v4.8.0
let authKey = push.key
if (push.key.length <= 10) {
authKey = fromByteArray(Random.getRandomBytes(16))
}
// Turning on // Turning on
const randomPath = (Math.random() + 1).toString(36).substring(2) const randomPath = (Math.random() + 1).toString(36).substring(2)
@ -189,7 +196,7 @@ const TabMePush: React.FC = () => {
'subscription[keys][p256dh]', 'subscription[keys][p256dh]',
'BMn2PLpZrMefG981elzG6SB1EY9gU7QZwmtZ/a/J2vUeWG+zXgeskMPwHh4T/bxsD4l7/8QT94F57CbZqYRRfJo=' 'BMn2PLpZrMefG981elzG6SB1EY9gU7QZwmtZ/a/J2vUeWG+zXgeskMPwHh4T/bxsD4l7/8QT94F57CbZqYRRfJo='
) )
formData.append('subscription[keys][auth]', push.key) formData.append('subscription[keys][auth]', authKey)
for (const [key, value] of Object.entries(push.alerts)) { for (const [key, value] of Object.entries(push.alerts)) {
formData.append(`data[alerts][${key}]`, value.toString()) formData.append(`data[alerts][${key}]`, value.toString())
} }
@ -225,7 +232,9 @@ const TabMePush: React.FC = () => {
} }
}) })
setAccountStorage([{ key: 'push', value: { ...push, global: true } }]) setAccountStorage([
{ key: 'push', value: { ...push, global: true, key: authKey } }
])
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
setChannels(true) setChannels(true)

View File

@ -2,6 +2,7 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement' import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
import { useAccountStorage } from '@utils/storage/actions' import { useAccountStorage } from '@utils/storage/actions'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -17,6 +18,12 @@ const Collections: React.FC = () => {
onSuccess: data => setPageMe({ ...pageMe, lists: { shown: !!data?.length } }) onSuccess: data => setPageMe({ ...pageMe, lists: { shown: !!data?.length } })
} }
}) })
useFollowedTagsQuery({
options: {
onSuccess: data =>
setPageMe({ ...pageMe, followedTags: { shown: !!data.pages[0].body.length } })
}
})
useAnnouncementQuery({ useAnnouncementQuery({
showAll: true, showAll: true,
options: { options: {
@ -53,7 +60,7 @@ const Collections: React.FC = () => {
title={t('screenTabs:me.stacks.favourites.name')} title={t('screenTabs:me.stacks.favourites.name')}
onPress={() => navigation.navigate('Tab-Me-Favourites')} onPress={() => navigation.navigate('Tab-Me-Favourites')}
/> />
{pageMe.lists.shown ? ( {pageMe.lists?.shown ? (
<MenuRow <MenuRow
iconFront='List' iconFront='List'
iconBack='ChevronRight' iconBack='ChevronRight'
@ -61,7 +68,7 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-List-List')} onPress={() => navigation.navigate('Tab-Me-List-List')}
/> />
) : null} ) : null}
{pageMe.followedTags.shown ? ( {pageMe.followedTags?.shown ? (
<MenuRow <MenuRow
iconFront='Hash' iconFront='Hash'
iconBack='ChevronRight' iconBack='ChevronRight'
@ -69,7 +76,7 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-FollowedTags')} onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
/> />
) : null} ) : null}
{pageMe.announcements.shown ? ( {pageMe.announcements?.shown ? (
<MenuRow <MenuRow
iconFront='Clipboard' iconFront='Clipboard'
iconBack='ChevronRight' iconBack='ChevronRight'

View File

@ -11,7 +11,7 @@ import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
export const mapFontsizeToName = (size: StorageGlobal['app.font_size']) => { export const mapFontsizeToName = (size: StorageGlobal['app.font_size']) => {

View File

@ -70,8 +70,8 @@ const TabNotificationsFilters: React.FC<
<MenuRow <MenuRow
key={index} key={index}
title={t(`screenTabs:me.push.${type}.heading`)} title={t(`screenTabs:me.push.${type}.heading`)}
switchValue={filters[type]} switchValue={filters?.[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })} switchOnValueChange={() => setFilters({ ...filters, [type]: !filters?.[type] })}
/> />
))} ))}
</MenuContainer> </MenuContainer>
@ -80,8 +80,8 @@ const TabNotificationsFilters: React.FC<
<MenuRow <MenuRow
key={type} key={type}
title={t(`screenTabs:me.push.${type}.heading`)} title={t(`screenTabs:me.push.${type}.heading`)}
switchValue={filters[type]} switchValue={filters?.[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })} switchOnValueChange={() => setFilters({ ...filters, [type]: !filters?.[type] })}
/> />
))} ))}
</MenuContainer> </MenuContainer>

View File

@ -38,7 +38,10 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
const previousSegment = getGlobalStorage.string('app.prev_public_segment') const previousSegment = getGlobalStorage.string('app.prev_public_segment')
const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending'] const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending']
const [segment, setSegment] = useState<number>( const [segment, setSegment] = useState<number>(
segments.findIndex(segment => segment === previousSegment) Math.min(
0,
segments.findIndex(segment => segment === previousSegment)
)
) )
const [routes] = useState([ const [routes] = useState([
{ key: 'Local', title: t('tabs.public.segments.local') }, { key: 'Local', title: t('tabs.public.segments.local') },

View File

@ -18,7 +18,7 @@ const AccountInformationAccount: React.FC = () => {
const [acct] = useAccountStorage.string('auth.account.acct') const [acct] = useAccountStorage.string('auth.account.acct')
const domain = getAccountStorage.string('auth.account.domain') const domain = getAccountStorage.string('auth.account.domain')
const localInstance = account?.acct.includes('@') ? account?.acct.includes(`@${domain}`) : true const localInstance = account?.acct?.includes('@') ? account?.acct?.includes(`@${domain}`) : true
if (account || pageMe) { if (account || pageMe) {
return ( return (

View File

@ -162,7 +162,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}).then(res => res.body) }).then(res => res.body)
if (!context?.ancestors.length && !context?.descendants.length) { if (!context?.ancestors.length && !context?.descendants.length) {
return Promise.resolve([]) return Promise.resolve([{ ...toot }])
} }
highlightIndex.current = context.ancestors.length highlightIndex.current = context.ancestors.length
@ -182,7 +182,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}, },
{ {
enabled: enabled:
query.isFetched && (toot._remote ? true : query.isFetched) &&
['public', 'unlisted'].includes(toot.visibility) && ['public', 'unlisted'].includes(toot.visibility) &&
match?.domain !== getAccountStorage.string('auth.domain'), match?.domain !== getAccountStorage.string('auth.domain'),
staleTime: 0, staleTime: 0,
@ -198,16 +198,12 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
queryClient.setQueryData<{ queryClient.setQueryData<{
pages: { body: Mastodon.Status[] }[] pages: { body: Mastodon.Status[] }[]
}>(queryKey.local, old => { }>(queryKey.local, old => {
if (!old) return old
setHasRemoteContent(true) setHasRemoteContent(true)
return { return {
pages: [ pages: [
{ {
body: data.map(remote => { body: data.map(remote => {
const localMatch = query.data?.pages[0].body.find( const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri)
local => local.uri === remote.uri
)
if (localMatch) { if (localMatch) {
return { ...localMatch, _level: remote._level } return { ...localMatch, _level: remote._level }
} else { } else {
@ -257,7 +253,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
} }
) )
const heights = useRef<(number | undefined)[]>([]) const [heights, setHeights] = useState<{ [key: number]: number }>({})
const MAX_LEVEL = 10 const MAX_LEVEL = 10
const ARC = StyleConstants.Avatar.XS / 4 const ARC = StyleConstants.Avatar.XS / 4
@ -284,7 +280,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
nativeEvent: { nativeEvent: {
layout: { height } layout: { height }
} }
}) => (heights.current[index] = height)} }) => setHeights({ ...heights, [index]: height })}
> >
<TimelineDefault <TimelineDefault
item={item} item={item}
@ -326,7 +322,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
d={ d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${ `v ${
(heights.current[index] || 999) - (heights[index] || 999) -
(StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 - (StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 -
StyleConstants.Avatar.XS / 2 StyleConstants.Avatar.XS / 2
} ` + } ` +
@ -347,7 +343,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
d={ d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${ `v ${
(heights.current[index] || 999) - (heights[index] || 999) -
(StyleConstants.Spacing.S * 1.5 + (StyleConstants.Spacing.S * 1.5 +
StyleConstants.Font.Size.L * 1.35) / StyleConstants.Font.Size.L * 1.35) /
2 2

View File

@ -1,9 +1,9 @@
import { import {
QueryFunctionContext, QueryFunctionContext,
useMutation, useMutation,
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
UseQueryOptions UseQueryOptions
} from '@tanstack/react-query' } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general' import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
@ -29,24 +29,23 @@ const useAppsQuery = (
return useQuery(queryKey, queryFunctionApps, params?.options) return useQuery(queryKey, queryFunctionApps, params?.options)
} }
type MutationVarsApps = { domain: string } type MutationVarsApps = { domain: string; scopes: string[] }
export const redirectUri = AuthSession.makeRedirectUri({ export const redirectUri = AuthSession.makeRedirectUri({
native: 'tooot://instance-auth', native: 'tooot://instance-auth',
useProxy: false useProxy: false
}) })
const mutationFunctionApps = async ({ domain }: MutationVarsApps) => { const mutationFunctionApps = async ({ domain, scopes }: MutationVarsApps) => {
const formData = new FormData()
formData.append('client_name', 'tooot')
formData.append('website', 'https://tooot.app')
formData.append('redirect_uris', redirectUri)
formData.append('scopes', 'read write follow push')
return apiGeneral<Mastodon.Apps>({ return apiGeneral<Mastodon.Apps>({
method: 'post', method: 'post',
domain: domain, domain: domain,
url: `api/v1/apps`, url: 'api/v1/apps',
body: formData body: {
client_name: 'tooot',
website: 'https://tooot.app',
redirect_uris: redirectUri,
scopes: scopes.join(' ')
}
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -1,5 +1,7 @@
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
export const globalRetry = (failureCount: number) => failureCount <= 2
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
@ -8,17 +10,9 @@ export const queryClient = new QueryClient({
if ([401, 404].includes(error?.status)) { if ([401, 404].includes(error?.status)) {
return false return false
} }
if (failureCount <= 2) {
return true return globalRetry(failureCount)
} else {
return false
}
} }
} }
},
logger: {
log: log => console.log(log),
warn: () => {},
error: () => {}
} }
}) })

View File

@ -52,7 +52,7 @@ export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise<Ma
return await queryClient return await queryClient
.fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 }) .fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 })
.then(res => .then(res =>
res.statuses[0].uri === uri || res.statuses[0].url === uri res.statuses[0]?.uri === uri || res.statuses[0]?.url === uri
? res.statuses[0] ? res.statuses[0]
: Promise.reject() : Promise.reject()
) )

View File

@ -43,7 +43,7 @@ export const useGlobalStorage = {
: never, : never,
number: <T extends keyof StorageGlobal>(key: T) => { number: <T extends keyof StorageGlobal>(key: T) => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
return useMMKVString(key, storage.global) as NonNullable<StorageGlobal[T]> extends number return useMMKVNumber(key, storage.global) as NonNullable<StorageGlobal[T]> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void] ? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never : never
} else { } else {
@ -233,14 +233,15 @@ export type ReadableAccountType = {
key: string key: string
active: boolean active: boolean
} }
export const getReadableAccounts = (): ReadableAccountType[] => { export const getReadableAccounts = (withoutActive: boolean = false): ReadableAccountType[] => {
const accountActive = getGlobalStorage.string('account.active') const accountActive = !withoutActive && getGlobalStorage.string('account.active')
const accounts = getGlobalStorage.object('accounts')?.sort((a, b) => a.localeCompare(b)) const accounts = getGlobalStorage.object('accounts')?.sort((a, b) => a.localeCompare(b))
accounts?.splice( !withoutActive &&
accounts.findIndex(a => a === accountActive), accounts?.splice(
1 accounts.findIndex(a => a === accountActive),
) 1
accounts?.unshift(accountActive || '') )
!withoutActive && accounts?.unshift(accountActive || '')
return ( return (
accounts?.map(account => { accounts?.map(account => {
const details = getAccountDetails( const details = getAccountDetails(

View File

@ -9718,6 +9718,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-quick-base64@npm:^2.0.5":
version: 2.0.5
resolution: "react-native-quick-base64@npm:2.0.5"
dependencies:
base64-js: ^1.5.1
peerDependencies:
react: "*"
react-native: "*"
checksum: 964599ad68d54dd741357850c72b4a5e08f247fa294590f18866896662107b734b6810703fdd972560cee8087095f14f06fc6ef69944c59c41dbbd885a7832bb
languageName: node
linkType: hard
"react-native-reanimated-zoom@npm:^0.3.3": "react-native-reanimated-zoom@npm:^0.3.3":
version: 0.3.3 version: 0.3.3
resolution: "react-native-reanimated-zoom@npm:0.3.3" resolution: "react-native-reanimated-zoom@npm:0.3.3"
@ -11412,6 +11424,7 @@ __metadata:
react-native-language-detection: ^0.2.2 react-native-language-detection: ^0.2.2
react-native-mmkv: ^2.5.1 react-native-mmkv: ^2.5.1
react-native-pager-view: ^6.1.2 react-native-pager-view: ^6.1.2
react-native-quick-base64: ^2.0.5
react-native-reanimated: ^2.13.0 react-native-reanimated: ^2.13.0
react-native-reanimated-zoom: ^0.3.3 react-native-reanimated-zoom: ^0.3.3
react-native-safe-area-context: ^4.4.1 react-native-safe-area-context: ^4.4.1