Merge branch 'main' into candidate

This commit is contained in:
xmflsct 2022-12-21 15:33:41 +01:00
commit fbf282c78a
40 changed files with 221 additions and 291 deletions

View File

@ -1,4 +1,3 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Align filter experience with v4.0 and above - Fixed wrongly update notification
- Supports enlarging user's avatar and banner - Fix some random crashes
- Fix iPad weird sizing (not optimisation)

View File

@ -1 +1 @@
Client open source per Mastodon App open source per Mastodon

View File

@ -1,4 +1,3 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 改进过滤体验与v4.0以上版本一致 - 修复错误的升级通知
- 支持查看用户的头像和横幅图片 - 修复部分应用崩溃
- 修复iPad部分尺寸问题非优化

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.7.1", "version": "4.7.2",
"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",

View File

@ -44,7 +44,10 @@ const apiGeneral = async <T = unknown>({
...userAgent, ...userAgent,
...headers ...headers
}, },
...(body && { data: body }) ...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
}) })
.then(response => { .then(response => {
let links: { let links: {

View File

@ -64,7 +64,7 @@ const apiInstance = async <T = unknown>({
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}) })
}, },
...(body && { data: body }), ...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
...extras ...extras
}) })
.then(response => { .then(response => {

View File

@ -47,7 +47,10 @@ const apiTooot = async <T = unknown>({
...userAgent, ...userAgent,
...headers ...headers
}, },
...(body && { data: body }) ...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
}) })
.then(response => { .then(response => {
return Promise.resolve({ return Promise.resolve({

View File

@ -53,26 +53,17 @@ const GracefullyImage = ({
}: Props) => { }: Props) => {
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const { colors } = useTheme() const { colors } = useTheme()
const [originalFailed, setOriginalFailed] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const source = originalFailed const source = {
? { uri: uri.remote || undefined } uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
: { }
uri: reduceMotionEnabled && uri.static ? uri.static : uri.original
}
const onLoad = () => { const onLoad = () => {
setImageLoaded(true) setImageLoaded(true)
if (setImageDimensions && source.uri) { if (setImageDimensions && source.uri) {
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height })) Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
} }
} }
const onError = () => {
if (!originalFailed) {
setOriginalFailed(true)
}
}
const blurhashView = useMemo(() => { const blurhashView = useMemo(() => {
if (hidden || !imageLoaded) { if (hidden || !imageLoaded) {
@ -101,10 +92,11 @@ 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={onLoad}
onError={onError}
/> />
{blurhashView} {blurhashView}
</Pressable> </Pressable>

View File

@ -52,22 +52,24 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
> >
#{hashtag.name} #{hashtag.name}
</CustomText> </CustomText>
<View {hashtag.history?.length ? (
style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch' }} <View
onLayout={({ style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch' }}
nativeEvent: { onLayout={({
layout: { height } nativeEvent: {
} layout: { height }
}) => setHeight(height)} }
> }) => setHeight(height)}
<Sparkline >
data={hashtag.history.map(h => parseInt(h.uses)).reverse()} <Sparkline
width={width} data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
height={height} width={width}
margin={children ? StyleConstants.Spacing.S : undefined} height={height}
/> margin={children ? StyleConstants.Spacing.S : undefined}
{children} />
</View> {children}
</View>
) : null}
</Pressable> </Pressable>
) )
} }

View File

@ -102,7 +102,7 @@ const renderNode = ({
) )
} }
} else { } else {
const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/)) const domain = href?.split(new RegExp(/:\/\/(.[^\/]+\/.{3})/))
// Need example here // Need example here
const content = node.children && node.children[0] && node.children[0].data const content = node.children && node.children[0] && node.children[0].data
const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0 const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0
@ -128,17 +128,7 @@ const renderNode = ({
}} }}
> >
{content && content !== href ? content : showFullLink ? href : domain?.[1]} {content && content !== href ? content : showFullLink ? href : domain?.[1]}
{!shouldBeTag ? ( {!shouldBeTag ? '...' : null}
<Icon
color={colors.blue}
name='ExternalLink'
size={adaptedFontsize}
style={{
marginLeft: StyleConstants.Spacing.XS,
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
}}
/>
) : null}
</CustomText> </CustomText>
) )
} }
@ -228,7 +218,7 @@ const ParseHTML = React.memo(
if (children) { if (children) {
return ( return (
<ParseEmojis <ParseEmojis
content={children.toString()} content={children?.toString()}
emojis={emojis} emojis={emojis}
size={size} size={size}
adaptiveSize={adaptiveSize} adaptiveSize={adaptiveSize}

View File

@ -13,6 +13,8 @@ import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { checkInstanceFeature } from '@utils/slices/instancesSlice' import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { View } from 'react-native'
import { useRoute } from '@react-navigation/native'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']
@ -122,9 +124,12 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
} }
} }
const { name } = useRoute()
const isPageNotifications = name === 'Tab-Notifications-Root'
return ( return (
<> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
{canFollowNotify && query.data?.following ? ( {!isPageNotifications && canFollowNotify && query.data?.following ? (
<Button <Button
type='icon' type='icon'
content={query.data.notifying ? 'BellOff' : 'Bell'} content={query.data.notifying ? 'BellOff' : 'Bell'}
@ -151,7 +156,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
loading={query.isLoading || mutation.isLoading} loading={query.isLoading || mutation.isLoading}
disabled={query.isError || query.data?.blocked_by} disabled={query.isError || query.data?.blocked_by}
/> />
</> </View>
) )
} }

View File

@ -54,7 +54,7 @@ const TimelineTranslate = () => {
<CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${ <CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${
detected?.language detected?.language
}; Confidence: ${ }; Confidence: ${
detected?.confidence.toString().slice(0, 5) || 'null' detected?.confidence?.toString().slice(0, 5) || 'null'
}; Target: ${targetLanguage}`}</CustomText> }; Target: ${targetLanguage}`}</CustomText>
) : null ) : null
} }

View File

@ -7,6 +7,7 @@ import { SearchResult } from '@utils/queryHooks/search'
import { getSettingsBrowser } from '@utils/slices/settingsSlice' import { getSettingsBrowser } from '@utils/slices/settingsSlice'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import validUrl from 'valid-url'
export let loadingLink = false export let loadingLink = false
@ -86,18 +87,21 @@ const openLink = async (url: string, navigation?: any) => {
} }
loadingLink = false loadingLink = false
switch (getSettingsBrowser(store.getState())) { const validatedUrl = validUrl.isWebUri(url)
// Some links might end with an empty space at the end that triggers an error if (validatedUrl) {
case 'internal': switch (getSettingsBrowser(store.getState())) {
await WebBrowser.openBrowserAsync(encodeURI(url), { // Some links might end with an empty space at the end that triggers an error
dismissButtonStyle: 'close', case 'internal':
enableBarCollapsing: true, await WebBrowser.openBrowserAsync(validatedUrl, {
...(await browserPackage()) dismissButtonStyle: 'close',
}) enableBarCollapsing: true,
break ...(await browserPackage())
case 'external': })
await Linking.openURL(encodeURI(url)) break
break case 'external':
await Linking.openURL(validatedUrl)
break
}
} }
} }

View File

@ -1,5 +1,21 @@
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } }) const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: (failureCount, error: any) => {
if (error?.status === 404) {
return false
}
if (failureCount <= 3) {
return true
} else {
return false
}
}
}
}
})
export default queryClient export default queryClient

View File

@ -254,9 +254,6 @@
"content_true": "Enabled", "content_true": "Enabled",
"content_false": "Disabled" "content_false": "Disabled"
}, },
"update": {
"title": "Update to latest version"
},
"logout": { "logout": {
"button": "Log out", "button": "Log out",
"alert": { "alert": {

View File

@ -21,7 +21,7 @@
"reports": { "reports": {
"action": "Rapporteren en blokkeren", "action": "Rapporteren en blokkeren",
"alert": { "alert": {
"title": "" "title": "Bevestig rapporteren en blokkeren van @{{username}} ?"
} }
} }
}, },

View File

@ -3,7 +3,7 @@
"textInput": { "textInput": {
"placeholder": "Domeinnaam van instantie" "placeholder": "Domeinnaam van instantie"
}, },
"whitelisted": "", "whitelisted": "Dit kan een gewhiteliste instantie zijn waarvan tooot geen gegevens kan ophalen voordat er ingelogd is.",
"button": "Inloggen", "button": "Inloggen",
"information": { "information": {
"name": "Naam", "name": "Naam",

View File

@ -1,32 +1,32 @@
{ {
"buttons": { "buttons": {
"OK": "", "OK": "ОК",
"apply": "", "apply": "Применить",
"cancel": "", "cancel": "Отменить",
"discard": "", "discard": "Отклонить",
"continue": "", "continue": "Продолжить",
"create": "", "create": "Создать",
"delete": "", "delete": "Удалить",
"done": "", "done": "Готово",
"confirm": "" "confirm": "Подтвердить"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "" "accessibilityLabel": "Пользовательские эмодзи {{emoji}}"
}, },
"message": { "message": {
"success": { "success": {
"message": "" "message": "Успешно {{function}}"
}, },
"warning": { "warning": {
"message": "" "message": ""
}, },
"error": { "error": {
"message": "" "message": "Ошибка {{function}}, попробуйте ещё раз"
} }
}, },
"separator": "", "separator": ", ",
"discard": { "discard": {
"title": "", "title": "Изменения не сохранены",
"message": "" "message": "Ваши изменения не были сохранены. Вы хотите отменить изменения?"
} }
} }

View File

@ -15,13 +15,13 @@
"action_false": "Blockera användare", "action_false": "Blockera användare",
"action_true": "Avblockera användare", "action_true": "Avblockera användare",
"alert": { "alert": {
"title": "" "title": "Bekräfta blockering av användaren {{username}}?"
} }
}, },
"reports": { "reports": {
"action": "Rapportera och blockera användare", "action": "Rapportera och blockera användare",
"alert": { "alert": {
"title": "" "title": "Bekräfta rapportera och blockera användaren @{{username}}?"
} }
} }
}, },

View File

@ -15,13 +15,13 @@
"action_false": "Chặn người này", "action_false": "Chặn người này",
"action_true": "Bỏ chặn người dùng", "action_true": "Bỏ chặn người dùng",
"alert": { "alert": {
"title": "" "title": "Bạn có chắc muốn chặn {{username}}?"
} }
}, },
"reports": { "reports": {
"action": "Báo cáo và chặn", "action": "Báo cáo và chặn",
"alert": { "alert": {
"title": "" "title": "Bạn có chắc muốn báo cáo {{username}}?"
} }
} }
}, },

View File

@ -15,13 +15,13 @@
"action_false": "封鎖使用者", "action_false": "封鎖使用者",
"action_true": "解除封鎖使用者", "action_true": "解除封鎖使用者",
"alert": { "alert": {
"title": "" "title": "確認封鎖使用者 @{{username}}"
} }
}, },
"reports": { "reports": {
"action": "檢舉並封鎖使用者", "action": "檢舉並封鎖使用者",
"alert": { "alert": {
"title": "" "title": "確認檢舉並封鎖使用者 @{{username}}"
} }
} }
}, },

View File

@ -196,7 +196,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
rightOpenValue={-actionWidth} rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2} previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator} ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()} keyExtractor={item => item.timestamp?.toString()}
/> />
</PanGestureHandler> </PanGestureHandler>
<Modal <Modal

View File

@ -167,7 +167,7 @@ const ComposeActions: React.FC = () => {
}} }}
style={styles.button} style={styles.button}
onPress={attachmentOnPress} onPress={attachmentOnPress}
children={<Icon name='Aperture' size={24} color={attachmentColor} />} children={<Icon name='Camera' size={24} color={attachmentColor} />}
/> />
<Pressable <Pressable
accessibilityRole='button' accessibilityRole='button'

View File

@ -43,11 +43,11 @@ const composePost = async (
e => e && e.length && formData.append('poll[options][]', e) e => e && e.length && formData.append('poll[options][]', e)
) )
formData.append('poll[expires_in]', composeState.poll.expire) formData.append('poll[expires_in]', composeState.poll.expire)
formData.append('poll[multiple]', composeState.poll.multiple.toString()) formData.append('poll[multiple]', composeState.poll.multiple?.toString())
} }
if (composeState.attachments.uploads.filter(upload => upload.remote && upload.remote.id).length) { if (composeState.attachments.uploads.filter(upload => upload.remote && upload.remote.id).length) {
formData.append('sensitive', composeState.attachments.sensitive.toString()) formData.append('sensitive', composeState.attachments.sensitive?.toString())
composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e.remote!.id!)) composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e.remote!.id!))
} }

View File

@ -2,13 +2,11 @@ import GracefullyImage from '@components/GracefullyImage'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { useAppDispatch } from '@root/store'
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
import { getVersionUpdate, retrieveVersionLatest } from '@utils/slices/appSlice'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TabLocal from './Tabs/Local' import TabLocal from './Tabs/Local'
@ -55,17 +53,6 @@ const ScreenTabs = React.memo(
const previousTab = useSelector(getPreviousTab, () => true) const previousTab = useSelector(getPreviousTab, () => true)
const versionUpdate = useSelector(getVersionUpdate)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(retrieveVersionLatest())
}, [])
const tabMeOptions = useMemo(() => {
if (versionUpdate) {
return { tabBarBadge: 1 }
}
}, [versionUpdate])
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'} initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
@ -121,12 +108,7 @@ const ScreenTabs = React.memo(
<Tab.Screen name='Tab-Public' component={TabPublic} /> <Tab.Screen name='Tab-Public' component={TabPublic} />
<Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} /> <Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} />
<Tab.Screen name='Tab-Notifications' component={TabNotifications} /> <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
<Tab.Screen <Tab.Screen name='Tab-Me' component={TabMe} listeners={meListeners} />
name='Tab-Me'
component={TabMe}
options={tabMeOptions}
listeners={meListeners}
/>
</Tab.Navigator> </Tab.Navigator>
) )
}, },

View File

@ -8,12 +8,7 @@ import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice' import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { import { PUSH_ADMIN, PUSH_DEFAULT, usePushFeatures } from '@utils/slices/instances/push/utils'
PUSH_ADMIN,
PUSH_DEFAULT,
setChannels,
usePushFeatures
} from '@utils/slices/instances/push/utils'
import { updateInstancePush } from '@utils/slices/instances/updatePush' import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert' import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode' import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
@ -25,7 +20,7 @@ import * as Notifications from 'expo-notifications'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AppState, Linking, Platform, ScrollView, View } from 'react-native' import { AppState, Linking, ScrollView, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const TabMePush: React.FC = () => { const TabMePush: React.FC = () => {
@ -50,11 +45,7 @@ const TabMePush: React.FC = () => {
setPushEnabled(permissions.granted) setPushEnabled(permissions.granted)
setPushCanAskAgain(permissions.canAskAgain) setPushCanAskAgain(permissions.canAskAgain)
layoutAnimation() layoutAnimation()
dispatch(retrieveExpoToken())
if (Platform.OS === 'android') {
await setChannels(instance)
dispatch(retrieveExpoToken())
}
} }
if (appsQuery.data?.vapid_key) { if (appsQuery.data?.vapid_key) {

View File

@ -12,12 +12,8 @@ import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import React, { useReducer, useRef } from 'react' import React, { useReducer, useRef } from 'react'
import Animated, { import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import Update from './Root/Update'
const TabMeRoot: React.FC = () => { const TabMeRoot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
@ -29,10 +25,7 @@ const TabMeRoot: React.FC = () => {
const scrollRef = useRef<Animated.ScrollView>(null) const scrollRef = useRef<Animated.ScrollView>(null)
useScrollToTop(scrollRef) useScrollToTop(scrollRef)
const [accountState, accountDispatch] = useReducer( const [accountState, accountDispatch] = useReducer(accountReducer, accountInitialState)
accountReducer,
accountInitialState
)
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const onScroll = useAnimatedScrollHandler(event => { const onScroll = useAnimatedScrollHandler(event => {
@ -41,22 +34,15 @@ const TabMeRoot: React.FC = () => {
return ( return (
<AccountContext.Provider value={{ accountState, accountDispatch }}> <AccountContext.Provider value={{ accountState, accountDispatch }}>
{instanceActive !== -1 && data ? ( {instanceActive !== -1 && data ? <AccountNav scrollY={scrollY} account={data} /> : null}
<AccountNav scrollY={scrollY} account={data} />
) : null}
<Animated.ScrollView <Animated.ScrollView
ref={scrollRef} ref={scrollRef}
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
onScroll={onScroll} onScroll={onScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
> >
{instanceActive !== -1 ? ( {instanceActive !== -1 ? <MyInfo account={data} /> : <ComponentInstance />}
<MyInfo account={data} />
) : (
<ComponentInstance />
)}
{instanceActive !== -1 ? <Collections /> : null} {instanceActive !== -1 ? <Collections /> : null}
<Update />
<Settings /> <Settings />
{instanceActive !== -1 ? <AccountInformationSwitch /> : null} {instanceActive !== -1 ? <AccountInformationSwitch /> : null}
{instanceActive !== -1 ? <Logout /> : null} {instanceActive !== -1 ? <Logout /> : null}

View File

@ -115,7 +115,11 @@ const Collections: React.FC = () => {
iconFront={instancePush ? 'Bell' : 'BellOff'} iconFront={instancePush ? 'Bell' : 'BellOff'}
iconBack='ChevronRight' iconBack='ChevronRight'
title={t('me.stacks.push.name')} title={t('me.stacks.push.name')}
content={t('me.root.push.content', { context: instancePush.global.toString() })} content={
typeof instancePush.global === 'boolean'
? t('me.root.push.content', { context: instancePush.global.toString() })
: undefined
}
onPress={() => navigation.navigate('Tab-Me-Push')} onPress={() => navigation.navigate('Tab-Me-Push')}
/> />
</MenuContainer> </MenuContainer>

View File

@ -1,32 +0,0 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { getVersionUpdate } from '@utils/slices/appSlice'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import { useSelector } from 'react-redux'
const Update: React.FC = () => {
const { t } = useTranslation('screenTabs')
const versionUpdate = useSelector(getVersionUpdate)
return versionUpdate ? (
<MenuContainer>
<MenuRow
iconFront='ChevronsUp'
iconBack='ExternalLink'
title={t('me.root.update.title')}
badge
onPress={() => {
if (Platform.OS === 'ios') {
Linking.openURL('itms-appss://itunes.apple.com/app/id1549772269')
} else {
Linking.openURL('https://tooot.app')
}
}}
/>
</MenuContainer>
) : null
}
export default Update

View File

@ -27,7 +27,7 @@ const TabMeSettingsLanguage: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Lan
// Update Android notification channel language // Update Android notification channel language
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
instances.forEach(setChannels) instances.forEach(instance => setChannels(instance, true))
} }
navigation.pop(1) navigation.pop(1)

View File

@ -1,9 +1,8 @@
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@helpers/navigationRef' import navigationRef from '@helpers/navigationRef'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { Dimensions, Image, Pressable } from 'react-native' import { Dimensions, Image } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -12,13 +11,14 @@ export interface Props {
} }
const AccountHeader: React.FC<Props> = ({ account }) => { const AccountHeader: React.FC<Props> = ({ account }) => {
const { colors } = useTheme()
const topInset = useSafeAreaInsets().top const topInset = useSafeAreaInsets().top
useSelector(getInstanceActive) useSelector(getInstanceActive)
return ( return (
<Pressable <GracefullyImage
uri={{ original: account?.header, static: account?.header_static }}
style={{ height: Dimensions.get('window').width / 3 + topInset }}
onPress={() => { onPress={() => {
if (account) { if (account) {
Image.getSize(account.header, (width, height) => Image.getSize(account.header, (width, height) =>
@ -30,15 +30,7 @@ const AccountHeader: React.FC<Props> = ({ account }) => {
) )
} }
}} }}
> />
<GracefullyImage
uri={{ original: account?.header, static: account?.header_static }}
style={{
height: Dimensions.get('window').width / 3 + topInset,
backgroundColor: colors.disabled
}}
/>
</Pressable>
) )
} }

View File

@ -17,51 +17,39 @@ export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
} }
const AccountInformation = React.memo( const AccountInformation: React.FC<Props> = ({ account }) => {
({ account }: Props) => { const { colors } = useTheme()
const { colors } = useTheme()
const { name } = useRoute() const { name } = useRoute()
const myInfo = name !== 'Tab-Shared-Account' const myInfo = name !== 'Tab-Shared-Account'
return ( return (
<View style={styles.base}> <View style={styles.base}>
<Placeholder <Placeholder
Animation={props => ( Animation={props => (
<Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} /> <Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} />
)} )}
> >
<View style={styles.avatarAndActions}> <View style={styles.avatarAndActions}>
<AccountInformationAvatar account={account} myInfo={myInfo} /> <AccountInformationAvatar account={account} myInfo={myInfo} />
<AccountInformationActions account={account} myInfo={myInfo} /> <AccountInformationActions account={account} myInfo={myInfo} />
</View> </View>
<AccountInformationName account={account} /> <AccountInformationName account={account} />
<AccountInformationAccount account={account} /> <AccountInformationAccount account={account} />
<AccountInformationFields account={account} myInfo={myInfo} /> <AccountInformationFields account={account} myInfo={myInfo} />
<AccountInformationNote account={account} myInfo={myInfo} /> <AccountInformationNote account={account} myInfo={myInfo} />
<AccountInformationCreated account={account} hidden={myInfo} /> <AccountInformationCreated account={account} hidden={myInfo} />
<AccountInformationStats account={account} myInfo={myInfo} /> <AccountInformationStats account={account} myInfo={myInfo} />
</Placeholder> </Placeholder>
</View> </View>
) )
}, }
(prev, next) => {
let skipUpdate = true
if (prev.account?.id !== next.account?.id) {
skipUpdate = false
}
if (prev.account?.acct === next.account?.acct) {
skipUpdate = false
}
return skipUpdate
}
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {

View File

@ -6,7 +6,6 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { Pressable } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
export interface Props { export interface Props {
@ -18,7 +17,15 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
useSelector(getInstanceActive) useSelector(getInstanceActive)
return ( return (
<Pressable <GracefullyImage
key={account?.avatar}
style={{
borderRadius: 8,
overflow: 'hidden',
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L
}}
uri={{ original: account?.avatar, static: account?.avatar_static }}
onPress={() => { onPress={() => {
if (account) { if (account) {
if (myInfo) { if (myInfo) {
@ -33,19 +40,7 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
} }
} }
}} }}
style={{ />
borderRadius: 8,
overflow: 'hidden',
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L
}}
>
<GracefullyImage
key={account?.avatar}
style={{ flex: 1 }}
uri={{ original: account?.avatar, static: account?.avatar_static }}
/>
</Pressable>
) )
} }

View File

@ -1,38 +1,43 @@
import apiTooot from '@api/tooot'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment' import { isDevelopment } from '@utils/checkEnvironment'
import Constants from 'expo-constants'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import { setChannels } from './instances/push/utils'
import { getInstance } from './instancesSlice'
export const retrieveExpoToken = createAsyncThunk('app/expoToken', async (): Promise<string> => { export const retrieveExpoToken = createAsyncThunk(
if (isDevelopment) { 'app/expoToken',
return 'ExponentPushToken[DEVELOPMENT_1]' async (_, { getState }): Promise<string> => {
} const instance = getInstance(getState() as RootState)
const expoToken = getExpoToken(getState() as RootState)
const res = await Notifications.getExpoPushTokenAsync({ if (Platform.OS === 'android') {
experienceId: '@xmflsct/tooot', await setChannels(instance)
applicationId: 'com.xmflsct.app.tooot' }
})
return res.data
})
export const retrieveVersionLatest = createAsyncThunk( if (expoToken?.length) {
'app/versionUpdate', return expoToken
async (): Promise<string> => { } else {
const res = await apiTooot<{ latest: string }>({ method: 'get', url: 'version.json' }) if (isDevelopment) {
return res.body.latest return 'ExponentPushToken[DEVELOPMENT_1]'
}
const res = await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot',
applicationId: 'com.xmflsct.app.tooot'
})
return res.data
}
} }
) )
export type AppState = { export type AppState = {
expoToken?: string expoToken?: string
versionUpdate: boolean
} }
export const appInitialState: AppState = { export const appInitialState: AppState = {
expoToken: undefined, expoToken: undefined
versionUpdate: false
} }
const appSlice = createSlice({ const appSlice = createSlice({
@ -40,22 +45,14 @@ const appSlice = createSlice({
initialState: appInitialState, initialState: appInitialState,
reducers: {}, reducers: {},
extraReducers: builder => { extraReducers: builder => {
builder builder.addCase(retrieveExpoToken.fulfilled, (state, action) => {
.addCase(retrieveExpoToken.fulfilled, (state, action) => { if (action.payload) {
if (action.payload) { state.expoToken = action.payload
state.expoToken = action.payload }
} })
})
.addCase(retrieveVersionLatest.fulfilled, (state, action) => {
if (action.payload && Constants.expoConfig?.version) {
state.versionUpdate =
parseFloat(action.payload) > parseFloat(Constants.expoConfig.version)
}
})
} }
}) })
export const getExpoToken = (state: RootState) => state.app.expoToken export const getExpoToken = (state: RootState) => state.app.expoToken
export const getVersionUpdate = (state: RootState) => state.app.versionUpdate
export default appSlice.reducer export default appSlice.reducer

View File

@ -50,6 +50,12 @@ const addInstance = createAsyncThunk(
domain, domain,
url: `api/v1/preferences`, url: `api/v1/preferences`,
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}).catch(error => {
if (error?.status === 404) {
return Promise.resolve({ body: {} })
} else {
return Promise.reject()
}
}) })
const { body: filters } = await apiGeneral<Mastodon.Filter<any>[]>({ const { body: filters } = await apiGeneral<Mastodon.Filter<any>[]>({
@ -66,7 +72,7 @@ const addInstance = createAsyncThunk(
appData, appData,
url: domain, url: domain,
token, token,
uri: instance.uri, uri: instance.uri.replace(/^https?:\/\//, ''), // Pleroma includes schema
urls: instance.urls, urls: instance.urls,
account: { account: {
id, id,

View File

@ -28,7 +28,7 @@ const subscribe = async ({
}) => { }) => {
return apiTooot({ return apiTooot({
method: 'post', method: 'post',
url: `/push/subscribe/${expoToken}/${instanceUrl}/${accountId}`, url: `push/subscribe/${expoToken}/${instanceUrl}/${accountId}`,
body: { accountFull, serverKey, auth } body: { accountFull, serverKey, auth }
}) })
} }
@ -97,7 +97,7 @@ const pushRegister = async (
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
setChannels(instance) setChannels(instance, true)
} }
return Promise.resolve(auth) return Promise.resolve(auth)

View File

@ -21,7 +21,7 @@ const pushUnregister = async (state: RootState, expoToken: string) => {
await apiTooot<{ endpoint: string; publicKey: string; auth: string }>({ await apiTooot<{ endpoint: string; publicKey: string; auth: string }>({
method: 'delete', method: 'delete',
url: `/push/unsubscribe/${expoToken}/${instance.url}/${instance.account.id}` url: `push/unsubscribe/${expoToken}/${instance.url}/${instance.account.id}`
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {

View File

@ -62,7 +62,7 @@ export const PUSH_ADMIN = (
} }
}) as { type: 'admin.sign_up' | 'admin.report'; permission: number }[] }) as { type: 'admin.sign_up' | 'admin.report'; permission: number }[]
export const setChannels = async (instance: InstanceLatest) => { export const setChannels = async (instance: InstanceLatest, reset: boolean | undefined = false) => {
const account = `@${instance.account.acct}@${instance.uri}` const account = `@${instance.account.acct}@${instance.uri}`
const deleteChannel = async (type: string) => const deleteChannel = async (type: string) =>
@ -82,6 +82,9 @@ export const setChannels = async (instance: InstanceLatest) => {
const profileQuery = await queryClient.fetchQuery(queryKey, queryFunctionProfile) const profileQuery = await queryClient.fetchQuery(queryKey, queryFunctionProfile)
const channelGroup = await Notifications.getNotificationChannelGroupAsync(account) const channelGroup = await Notifications.getNotificationChannelGroupAsync(account)
if (channelGroup && !reset) {
return
}
if (!channelGroup) { if (!channelGroup) {
await Notifications.setNotificationChannelGroupAsync(account, { name: account }) await Notifications.setNotificationChannelGroupAsync(account, { name: account })
} }

View File

@ -7,6 +7,14 @@ export const updateAccountPreferences = createAsyncThunk(
return apiInstance<Mastodon.Preferences>({ return apiInstance<Mastodon.Preferences>({
method: 'get', method: 'get',
url: `preferences` url: `preferences`
}).then(res => res.body) })
.then(res => res.body)
.catch(error => {
if (error?.status === 404) {
return Promise.resolve({})
} else {
return Promise.reject()
}
})
} }
) )

View File

@ -25,14 +25,14 @@ export const updateInstancePushDecode = createAsyncThunk(
await apiTooot({ await apiTooot({
method: 'put', method: 'put',
url: `/push/update-decode/${expoToken}/${instance.url}/${instance.account.id}`, url: `push/update-decode/${expoToken}/${instance.url}/${instance.account.id}`,
body: { body: {
auth: !disable ? null : instance.push.keys.auth auth: !disable ? null : instance.push.keys.auth
} }
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
setChannels(instance) setChannels(instance, true)
} }
return Promise.resolve({ disable }) return Promise.resolve({ disable })