Merge pull request #318 from tooot-app/main

Release v4.0.4
This commit is contained in:
xmflsct 2022-06-03 23:49:59 +02:00 committed by GitHub
commit 84deb2ba58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2987 additions and 995 deletions

View File

@ -14,11 +14,11 @@ name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '35 4 * * 4'
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ main ]
# schedule:
# - cron: '35 4 * * 4'
jobs:
analyze:

View File

@ -6,10 +6,12 @@
## Special thanks
@forenta for German translation
[@forenta](https://github.com/forenta) for German translation
@andrigamerita for Italian translation
[@andrigamerita](https://github.com/andrigamerita) for Italian translation
@hellojaccc for Korean translation
[@hellojaccc](https://github.com/hellojaccc) for Korean translation
@duy@mas.to for Vietnamese translation
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation

View File

@ -29,7 +29,19 @@ module.exports = function (api) {
}
return {
presets: ['babel-preset-expo'],
presets: [
'babel-preset-expo',
[
'@babel/preset-react',
{
importSource: '@welldone-software/why-did-you-render',
runtime: 'automatic',
development:
process.env.NODE_ENV === 'development' ||
process.env.BABEL_ENV === 'development'
}
]
],
plugins
}
}

View File

@ -4,7 +4,7 @@
"native": "220508",
"major": 4,
"minor": 0,
"patch": 3,
"patch": 4,
"expo": "45.0.0"
},
"description": "tooot app for Mastodon",
@ -26,12 +26,12 @@
},
"dependencies": {
"@expo/react-native-action-sheet": "3.13.0",
"@formatjs/intl-datetimeformat": "^5.0.2",
"@formatjs/intl-getcanonicallocales": "^1.9.2",
"@formatjs/intl-locale": "^2.4.47",
"@formatjs/intl-numberformat": "^7.4.3",
"@formatjs/intl-pluralrules": "^4.3.3",
"@formatjs/intl-relativetimeformat": "^10.0.1",
"@formatjs/intl-datetimeformat": "^6.0.1",
"@formatjs/intl-getcanonicallocales": "^2.0.1",
"@formatjs/intl-locale": "^3.0.1",
"@formatjs/intl-numberformat": "^8.0.1",
"@formatjs/intl-pluralrules": "^5.0.1",
"@formatjs/intl-relativetimeformat": "^11.0.1",
"@neverdull-agency/expo-unlimited-secure-store": "1.0.10",
"@react-native-async-storage/async-storage": "1.17.4",
"@react-native-community/blur": "3.6.0",
@ -42,7 +42,7 @@
"@react-navigation/native": "6.0.10",
"@react-navigation/native-stack": "6.6.2",
"@react-navigation/stack": "6.2.1",
"@reduxjs/toolkit": "1.8.1",
"@reduxjs/toolkit": "1.8.2",
"@sentry/react-native": "3.4.2",
"@sharcoux/slider": "6.0.3",
"axios": "0.27.2",
@ -68,13 +68,13 @@
"expo-updates": "0.13.1",
"expo-video-thumbnails": "6.3.0",
"expo-web-browser": "10.2.0",
"i18next": "21.8.2",
"i18next": "21.8.8",
"li": "1.3.0",
"lodash": "4.17.21",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "11.16.9",
"react-intl": "^5.25.1",
"react-i18next": "11.17.0",
"react-intl": "^6.0.3",
"react-native": "0.68.2",
"react-native-animated-spinkit": "1.5.2",
"react-native-base64": "^0.2.1",
@ -93,8 +93,8 @@
"react-native-svg": "12.3.0",
"react-native-swipe-list-view": "3.2.9",
"react-native-tab-view": "3.1.1",
"react-query": "3.39.0",
"react-redux": "8.0.1",
"react-query": "3.39.1",
"react-redux": "8.0.2",
"redux-persist": "6.0.0",
"rn-placeholder": "3.0.3",
"sentry-expo": "4.1.1",
@ -102,14 +102,15 @@
"valid-url": "1.0.9"
},
"devDependencies": {
"@babel/core": "7.17.10",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@babel/core": "7.18.2",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "7.17.12",
"@expo/config": "6.0.24",
"@types/lodash": "4.14.182",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-native": "0.67.7",
"@types/react-native-base64": "^0.2.0",
"@types/react-native-share-menu": "^5.0.2",
"@types/react-timeago": "4.1.3",
@ -122,7 +123,7 @@
"patch-package": "6.4.7",
"postinstall-postinstall": "2.1.0",
"react-native-clean-project": "4.0.1",
"typescript": "4.6.4"
"typescript": "4.7.3"
},
"resolutions": {
"@types/react": "17.0.43",
@ -150,4 +151,4 @@
}
}
}
}
}

View File

@ -341,6 +341,7 @@ declare namespace Mastodon {
| 'favourite'
| 'poll'
| 'status'
| 'update'
created_at: string
account: Account
@ -375,10 +376,12 @@ declare namespace Mastodon {
endpoint: string
alerts: {
follow: boolean
follow_request: boolean
favourite: boolean
reblog: boolean
mention: boolean
poll: boolean
status: boolean
}
server_key: string
}

View File

@ -1,31 +1,4 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import '@formatjs/intl-getcanonicallocales/polyfill'
import '@formatjs/intl-locale/polyfill'
import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/de'
import '@formatjs/intl-pluralrules/locale-data/en'
import '@formatjs/intl-pluralrules/locale-data/ko'
import '@formatjs/intl-pluralrules/locale-data/vi'
import '@formatjs/intl-pluralrules/locale-data/zh'
import '@formatjs/intl-numberformat/polyfill'
import '@formatjs/intl-numberformat/locale-data/de'
import '@formatjs/intl-numberformat/locale-data/en'
import '@formatjs/intl-numberformat/locale-data/ko'
import '@formatjs/intl-numberformat/locale-data/vi'
import '@formatjs/intl-numberformat/locale-data/zh'
import '@formatjs/intl-datetimeformat/polyfill'
import '@formatjs/intl-datetimeformat/locale-data/de'
import '@formatjs/intl-datetimeformat/locale-data/en'
import '@formatjs/intl-datetimeformat/locale-data/ko'
import '@formatjs/intl-datetimeformat/locale-data/vi'
import '@formatjs/intl-datetimeformat/locale-data/zh'
import '@formatjs/intl-datetimeformat/add-all-tz'
import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/de'
import '@formatjs/intl-relativetimeformat/locale-data/en'
import '@formatjs/intl-relativetimeformat/locale-data/ko'
import '@formatjs/intl-relativetimeformat/locale-data/vi'
import '@formatjs/intl-relativetimeformat/locale-data/zh'
import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens'
@ -44,10 +17,9 @@ import {
} from '@utils/slices/settingsSlice'
import ThemeManager from '@utils/styles/ThemeManager'
import 'expo-asset'
import * as Notifications from 'expo-notifications'
import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
import { AppState, LogBox, Platform } from 'react-native'
import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import 'react-native-image-keyboard'
import { enableFreeze } from 'react-native-screens'
@ -71,18 +43,6 @@ const App: React.FC = () => {
log('log', 'App', 'rendering App')
const [localCorrupt, setLocalCorrupt] = useState<string>()
const appStateEffect = useCallback(() => {
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
}, [])
useEffect(() => {
const appStateListener = AppState.addEventListener('change', appStateEffect)
return () => {
appStateListener.remove()
}
}, [])
useEffect(() => {
const delaySplash = async () => {
log('log', 'App', 'delay splash')

View File

@ -19,7 +19,7 @@ export type Params = {
export const TOOOT_API_DOMAIN = mapEnvironment({
release: 'api.tooot.app',
candidate: 'api.tooot.app',
candidate: 'api-candidate.tooot.app',
development: 'api-development.tooot.app'
})

View File

@ -29,6 +29,7 @@ export interface Props {
strokeWidth?: number
size?: 'S' | 'M' | 'L'
fontBold?: boolean
spacing?: 'XS' | 'S' | 'M' | 'L'
round?: boolean
overlay?: boolean
@ -48,6 +49,7 @@ const Button: React.FC<Props> = ({
disabled = false,
strokeWidth,
size = 'M',
fontBold = false,
spacing = 'S',
round = false,
overlay = false,
@ -122,6 +124,7 @@ const Button: React.FC<Props> = ({
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
opacity: loading ? 0 : 1
}}
fontWeight={fontBold ? 'Bold' : 'Normal'}
children={content}
testID='text'
/>

View File

@ -1,8 +1,9 @@
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
import addInstance from '@utils/slices/instances/add'
import { checkInstanceFeature, Instance } from '@utils/slices/instancesSlice'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import * as AuthSession from 'expo-auth-session'
import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
@ -12,7 +13,7 @@ export interface Props {
instanceDomain: string
// Domain can be different than uri
instance: Mastodon.Instance
appData: Instance['appData']
appData: InstanceLatest['appData']
goBack?: boolean
}

View File

@ -22,7 +22,7 @@ export interface Props {
switchDisabled?: boolean
switchOnValueChange?: () => void
iconBack?: 'ChevronRight' | 'ExternalLink'
iconBack?: 'ChevronRight' | 'ExternalLink' | 'Check'
iconBackColor?: ColorDefinitions
loading?: boolean

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native'
import { Platform, StyleSheet } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
import validUrl from 'valid-url'
@ -51,7 +51,13 @@ const ParseEmojis = React.memo(
image: {
width: adaptedFontsize,
height: adaptedFontsize,
transform: [{ translateY: -2 }]
...(Platform.OS === 'ios'
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
}
})
}, [theme, adaptiveFontsize])

View File

@ -13,7 +13,7 @@ import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { Platform, Pressable, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import { useSelector } from 'react-redux'
@ -139,7 +139,13 @@ const renderNode = ({
name='ExternalLink'
size={adaptedFontsize}
style={{
transform: [{ translateY: -2 }]
...(Platform.OS === 'ios'
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
}}
/>
) : null}

View File

@ -1,10 +1,6 @@
import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native'
import {
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -16,10 +12,20 @@ import {
RefreshControl,
StyleSheet
} from 'react-native'
import { InfiniteData, useQueryClient } from 'react-query'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, {
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props {
flRef?: RefObject<FlatList<any>>
@ -40,15 +46,12 @@ const Timeline: React.FC<Props> = ({
}) => {
const { colors } = useTheme()
const queryClient = useQueryClient()
const {
data,
refetch,
isFetching,
isLoading,
fetchPreviousPage,
fetchNextPage,
isFetchingPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
@ -57,12 +60,6 @@ const Timeline: React.FC<Props> = ({
ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
}),
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239
limit: '10'
},
getNextPageParam: lastPage =>
lastPage?.links?.next && {
max_id: lastPage.links.next
@ -92,6 +89,27 @@ const Timeline: React.FC<Props> = ({
const flRef = useRef<FlatList>(null)
const scrollY = useSharedValue(0)
const fetchingType = useSharedValue<0 | 1 | 2>(0)
const onScroll = useAnimatedScrollHandler(
{
onScroll: ({ contentOffset: { y } }) => {
scrollY.value = y
},
onEndDrag: ({ contentOffset: { y } }) => {
if (!disableRefresh && !isFetching) {
if (y <= SEPARATION_Y_2) {
fetchingType.value = 2
} else if (y <= SEPARATION_Y_1) {
fetchingType.value = 1
}
}
}
},
[isFetching]
)
const androidRefreshControl = Platform.select({
android: {
refreshControl: (
@ -115,46 +133,40 @@ const Timeline: React.FC<Props> = ({
})
return (
<FlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
}
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
{...(isFetchingPreviousPage && {
maintainVisibleContentPosition: { minIndexForVisible: 0 }
})}
refreshing={isFetchingPreviousPage}
onRefresh={() => {
if (!disableRefresh && !isFetchingPreviousPage) {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data?.pages[0] && data.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
}
}
)
fetchPreviousPage()
<>
<TimelineRefresh
flRef={flRef}
queryKey={queryKey}
scrollY={scrollY}
fetchingType={fetchingType}
disableRefresh={disableRefresh}
/>
<AnimatedFlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
onScroll={onScroll}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
}
}}
{...androidRefreshControl}
{...customProps}
/>
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
maintainVisibleContentPosition={{
minIndexForVisible: 0
}}
{...androidRefreshControl}
{...customProps}
/>
</>
)
}

View File

@ -14,7 +14,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual, uniqBy } from 'lodash'
import { uniqBy } from 'lodash'
import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
@ -34,145 +34,136 @@ export interface Props {
}
// When the poll is long
const TimelineDefault = React.memo(
({
item,
queryKey,
rootQueryKey,
origin,
highlighted = false,
disableDetails = false,
disableOnPress = false
}: Props) => {
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const TimelineDefault: React.FC<Props> = ({
item,
queryKey,
rootQueryKey,
origin,
highlighted = false,
disableDetails = false,
disableOnPress = false
}) => {
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualStatus = item.reblog ? item.reblog : item
const actualStatus = item.reblog ? item.reblog : item
const ownAccount = actualStatus.account?.id === instanceAccount?.id
const ownAccount = actualStatus.account?.id === instanceAccount?.id
if (
if (
!highlighted &&
queryKey &&
shouldFilter({ status: actualStatus, queryKey })
) {
return <TimelineFiltered />
}
const onPress = useCallback(() => {
analytics('timeline_default_press', {
page: queryKey ? queryKey[1].page : origin
})
!disableOnPress &&
!highlighted &&
queryKey &&
shouldFilter({ status: actualStatus, queryKey })
) {
return <TimelineFiltered />
}
const onPress = useCallback(() => {
analytics('timeline_default_press', {
page: queryKey ? queryKey[1].page : origin
navigation.push('Tab-Shared-Toot', {
toot: actualStatus,
rootQueryKey: queryKey
})
!disableOnPress &&
!highlighted &&
navigation.push('Tab-Shared-Toot', {
toot: actualStatus,
rootQueryKey: queryKey
})
}, [])
}, [])
return (
<Pressable
accessible={highlighted ? false : true}
return (
<Pressable
accessible={highlighted ? false : true}
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom:
disableDetails && disableOnPress
? StyleConstants.Spacing.Global.PagePadding
: 0
}}
onPress={onPress}
>
{item.reblog ? (
<TimelineActioned action='reblog' account={item.account} />
) : item._pinned ? (
<TimelineActioned action='pinned' account={item.account} />
) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar
queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account}
highlighted={highlighted}
/>
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={disableOnPress ? undefined : rootQueryKey}
status={actualStatus}
highlighted={highlighted}
/>
</View>
<View
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom:
disableDetails && disableOnPress
? StyleConstants.Spacing.Global.PagePadding
: 0
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
onPress={onPress}
>
{item.reblog ? (
<TimelineActioned action='reblog' account={item.account} />
) : item._pinned ? (
<TimelineActioned action='pinned' account={item.account} />
) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar
queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account}
highlighted={highlighted}
/>
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={disableOnPress ? undefined : rootQueryKey}
{typeof actualStatus.content === 'string' &&
actualStatus.content.length > 0 ? (
<TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
</View>
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{typeof actualStatus.content === 'string' &&
actualStatus.content.length > 0 ? (
<TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
) : null}
{queryKey && actualStatus.poll ? (
<TimelinePoll
queryKey={queryKey}
rootQueryKey={rootQueryKey}
statusId={actualStatus.id}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={ownAccount}
/>
) : null}
{!disableDetails &&
Array.isArray(actualStatus.media_attachments) &&
actualStatus.media_attachments.length ? (
<TimelineAttachment status={actualStatus} />
) : null}
{!disableDetails && actualStatus.card ? (
<TimelineCard card={actualStatus.card} />
) : null}
{!disableDetails ? (
<TimelineFullConversation
queryKey={queryKey}
status={actualStatus}
/>
) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineFeedback status={actualStatus} highlighted={highlighted} />
</View>
{queryKey && !disableDetails ? (
<TimelineActions
) : null}
{queryKey && actualStatus.poll ? (
<TimelinePoll
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={highlighted}
status={actualStatus}
ownAccount={ownAccount}
accts={uniqBy(
(
[actualStatus.account] as Mastodon.Account[] &
Mastodon.Mention[]
)
.concat(actualStatus.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
statusId={actualStatus.id}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={ownAccount}
/>
) : null}
</Pressable>
)
},
(prev, next) => isEqual(prev.item, next.item)
)
{!disableDetails &&
Array.isArray(actualStatus.media_attachments) &&
actualStatus.media_attachments.length ? (
<TimelineAttachment status={actualStatus} />
) : null}
{!disableDetails && actualStatus.card ? (
<TimelineCard card={actualStatus.card} />
) : null}
{!disableDetails ? (
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineFeedback status={actualStatus} highlighted={highlighted} />
</View>
{queryKey && !disableDetails ? (
<TimelineActions
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={highlighted}
status={actualStatus}
ownAccount={ownAccount}
accts={uniqBy(
([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(actualStatus.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={item.reblog ? true : false}
/>
) : null}
</Pressable>
)
}
export default TimelineDefault

View File

@ -0,0 +1,323 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import {
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
import { InfiniteData, useQueryClient } from 'react-query'
export interface Props {
flRef: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
scrollY: Animated.SharedValue<number>
fetchingType: Animated.SharedValue<0 | 1 | 2>
disableRefresh?: boolean
}
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
export const SEPARATION_Y_1 = -(
CONTAINER_HEIGHT / 2 +
StyleConstants.Font.Size.S / 2
)
export const SEPARATION_Y_2 = -(
CONTAINER_HEIGHT * 1.5 +
StyleConstants.Font.Size.S / 2
)
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
scrollY,
fetchingType,
disableRefresh = false
}) => {
if (Platform.OS !== 'ios') {
return null
}
if (disableRefresh) {
return null
}
const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false)
const {
refetch,
isFetching,
isLoading,
fetchPreviousPage,
hasPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
limit: '3'
},
select: data => {
if (refetchActive.current) {
data.pageParams = [data.pageParams[0]]
data.pages = [data.pages[0]]
refetchActive.current = false
}
return data
},
onSuccess: () => {
if (fetchingLatestIndex.current > 0) {
if (fetchingLatestIndex.current > 5) {
clearFirstPage()
fetchingLatestIndex.current = 0
} else {
if (hasPreviousPage) {
fetchPreviousPage()
fetchingLatestIndex.current++
} else {
clearFirstPage()
fetchingLatestIndex.current = 0
}
}
}
}
}
})
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const queryClient = useQueryClient()
const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data?.pages[0] && data.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
}
}
)
}
const prepareRefetch = () => {
refetchActive.current = true
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data) {
data.pageParams = [undefined]
const newFirstPage: TimelineData = { body: [] }
for (let page of data.pages) {
// @ts-ignore
newFirstPage.body.push(...page.body)
if (newFirstPage.body.length > 10) break
}
data.pages = [newFirstPage]
}
return data
}
)
}
const callRefetch = async () => {
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 1 }), 50)
}
const [textRight, setTextRight] = useState(0)
const arrowY = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
scrollY.value,
[0, SEPARATION_Y_1],
[
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
],
Extrapolate.CLAMP
)
}
]
}))
const arrowTop = useAnimatedStyle(() => ({
marginTop:
scrollY.value < SEPARATION_Y_2
? withTiming(CONTAINER_HEIGHT)
: withTiming(0)
}))
const arrowStage = useSharedValue(0)
const onLayout = useCallback(
({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
},
[textRight]
)
useAnimatedReaction(
() => {
if (isFetching) {
return false
}
switch (arrowStage.value) {
case 0:
if (scrollY.value < SEPARATION_Y_1) {
arrowStage.value = 1
return true
}
return false
case 1:
if (scrollY.value < SEPARATION_Y_2) {
arrowStage.value = 2
return true
}
if (scrollY.value > SEPARATION_Y_1) {
arrowStage.value = 0
return false
}
return false
case 2:
if (scrollY.value > SEPARATION_Y_2) {
arrowStage.value = 1
return false
}
return false
}
},
data => {
if (data) {
runOnJS(haptics)('Light')
}
},
[isFetching]
)
const wrapperStartLatest = () => {
fetchingLatestIndex.current = 1
}
useAnimatedReaction(
() => {
return fetchingType.value
},
data => {
fetchingType.value = 0
switch (data) {
case 1:
runOnJS(wrapperStartLatest)()
runOnJS(clearFirstPage)()
runOnJS(fetchPreviousPage)()
break
case 2:
runOnJS(prepareRefetch)()
runOnJS(callRefetch)()
break
}
},
[]
)
const headerPadding = useAnimatedStyle(
() => ({
paddingTop:
fetchingLatestIndex.current !== 0 ||
(isFetching && !isLoading && !isFetchingNextPage)
? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0)
}),
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
)
return (
<Animated.View style={headerPadding}>
<View style={styles.base}>
{isFetching ? (
<View style={styles.container2}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View>
) : (
<>
<View style={styles.container1}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
</Animated.View>
)
}
const styles = StyleSheet.create({
base: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
},
container1: {
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
},
container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' },
explanation: {
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT
}
})
export default TimelineRefresh

View File

@ -34,7 +34,7 @@ const TimelineActioned = React.memo(
navigation.push('Tab-Shared-Account', { account })
}, [])
const children = useMemo(() => {
const children = () => {
switch (action) {
case 'pinned':
return (
@ -48,7 +48,6 @@ const TimelineActioned = React.memo(
{content(t('shared.actioned.pinned'))}
</>
)
break
case 'favourite':
return (
<>
@ -63,7 +62,6 @@ const TimelineActioned = React.memo(
</Pressable>
</>
)
break
case 'follow':
return (
<>
@ -78,7 +76,6 @@ const TimelineActioned = React.memo(
</Pressable>
</>
)
break
case 'follow_request':
return (
<>
@ -93,7 +90,6 @@ const TimelineActioned = React.memo(
</Pressable>
</>
)
break
case 'poll':
return (
<>
@ -106,7 +102,6 @@ const TimelineActioned = React.memo(
{content(t('shared.actioned.poll'))}
</>
)
break
case 'reblog':
return (
<>
@ -125,7 +120,6 @@ const TimelineActioned = React.memo(
</Pressable>
</>
)
break
case 'status':
return (
<>
@ -140,9 +134,22 @@ const TimelineActioned = React.memo(
</Pressable>
</>
)
break
case 'update':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.update'))}
</>
)
default:
return <></>
}
}, [])
}
return (
<View
@ -153,8 +160,9 @@ const TimelineActioned = React.memo(
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}}
children={children}
/>
>
{children()}
</View>
)
},
() => true

View File

@ -8,11 +8,13 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
@ -22,7 +24,23 @@ const TimelineAttachment = React.memo(
({ status }: Props) => {
const { t } = useTranslation('componentTimeline')
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] ===
next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
const imageUrls = useRef<
RootStackParamList['Screen-ImagesViewer']['imageUrls']
@ -151,7 +169,7 @@ const TimelineAttachment = React.memo(
})}
</View>
{status.sensitive &&
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{

View File

@ -0,0 +1,38 @@
import Button from '@components/Button'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
sensitiveShown: boolean
text?: string
}
const AttachmentAltText: React.FC<Props> = ({ sensitiveShown, text }) => {
if (!text) {
return null
}
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
return !sensitiveShown ? (
<Button
style={{
position: 'absolute',
right: StyleConstants.Spacing.S,
bottom: StyleConstants.Spacing.S
}}
overlay
size='S'
type='text'
content='ALT'
fontBold
onPress={() => {
navigation.navigate('Screen-Actions', { type: 'alt_text', text })
}}
/>
) : null
}
export default AttachmentAltText

View File

@ -8,6 +8,7 @@ import { Audio } from 'expo-av'
import React, { useCallback, useState } from 'react'
import { StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
@ -127,6 +128,10 @@ const AttachmentAudio: React.FC<Props> = ({
/>
</View>
) : null}
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={audio.description}
/>
</View>
)
}

View File

@ -3,6 +3,7 @@ import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { View } from 'react-native'
import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
@ -34,7 +35,9 @@ const AttachmentImage = ({
uri={{ original: image.preview_url, remote: image.remote_url }}
blurhash={image.blurhash}
onPress={() => {
analytics('timeline_shared_attachment_image_press', { id: image.id })
analytics('timeline_shared_attachment_image_press', {
id: image.id
})
navigateToImagesViewer(image.id)
}}
style={{
@ -48,6 +51,10 @@ const AttachmentImage = ({
: image.meta.original.width / image.meta.original.height
}}
/>
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={image.description}
/>
</View>
)
}

View File

@ -8,6 +8,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio'
export interface Props {
@ -75,6 +76,10 @@ const AttachmentUnsupported: React.FC<Props> = ({
) : null}
</>
) : null}
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={attachment.description}
/>
</View>
)
}

View File

@ -2,16 +2,11 @@ import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants'
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
AppState,
AppStateStatus,
Pressable,
StyleSheet,
View
} from 'react-native'
import { AppState, AppStateStatus, Pressable, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
import analytics from '@components/analytics'
import AttachmentAltText from './AltText'
export interface Props {
total: number
@ -88,10 +83,12 @@ const AttachmentVideo: React.FC<Props> = ({
return (
<View
style={[
styles.base,
{ aspectRatio: attachmentAspectRatio({ total, index }) }
]}
style={{
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
aspectRatio: attachmentAspectRatio({ total, index })
}}
>
<Video
accessibilityLabel={video.description}
@ -127,7 +124,17 @@ const AttachmentVideo: React.FC<Props> = ({
}
}}
/>
<Pressable style={styles.overlay} onPress={gifv ? playOnPress : null}>
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
onPress={gifv ? playOnPress : null}
>
{sensitiveShown ? (
video.blurhash ? (
<Blurhash
@ -149,25 +156,13 @@ const AttachmentVideo: React.FC<Props> = ({
loading={videoLoading}
/>
) : null}
<AttachmentAltText
sensitiveShown={sensitiveShown}
text={video.description}
/>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2
},
overlay: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default AttachmentVideo

View File

@ -55,7 +55,7 @@ export const shouldFilter = ({
) // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp('\\B' + escapedPhrase + '\\B').test(text)) {
if (new RegExp('\\b' + escapedPhrase + '\\b').test(text)) {
shouldFilter = true
}
break
@ -100,6 +100,7 @@ export const shouldFilter = ({
})
}
})
status.spoiler_text && parser.write(status.spoiler_text)
parser.write(status.content)
parser.end()
}

View File

@ -91,7 +91,9 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
}}
>
<HeaderSharedCreated
created_at={notification.created_at}
created_at={
notification.status?.created_at || notification.created_at
}
edited_at={notification.status?.edited_at}
/>
{notification.status?.visibility ? (

View File

@ -145,36 +145,6 @@ const TimelinePoll: React.FC<Props> = ({
mutation.isLoading
])
const pollExpiration = useMemo(() => {
if (poll.expired) {
return (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.expiration.expired')}
</CustomText>
)
} else {
if (poll.expires_at) {
return (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
<Trans
i18nKey='componentTimeline:shared.poll.meta.expiration.until'
components={[
<FormattedRelativeTime
value={
(new Date(poll.expires_at).getTime() -
new Date().getTime()) /
1000
}
updateIntervalInSeconds={1}
/>
]}
/>
</CustomText>
)
}
}
}, [theme, i18n.language, poll.expired, poll.expires_at])
const isSelected = useCallback(
(index: number): string =>
allOptions[index]
@ -302,21 +272,38 @@ const TimelinePoll: React.FC<Props> = ({
const pollVoteCounts = useMemo(() => {
if (poll.voters_count !== null) {
return (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.count.voters', { count: poll.voters_count })}
{' • '}
</CustomText>
t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
)
} else if (poll.votes_count !== null) {
return (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.count.votes', { count: poll.votes_count })}
{' • '}
</CustomText>
t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
)
}
}, [poll.voters_count, poll.votes_count])
const pollExpiration = useMemo(() => {
if (poll.expired) {
return t('shared.poll.meta.expiration.expired')
} else {
if (poll.expires_at) {
return (
<Trans
i18nKey='componentTimeline:shared.poll.meta.expiration.until'
components={[
<FormattedRelativeTime
value={
(new Date(poll.expires_at).getTime() - new Date().getTime()) /
1000
}
updateIntervalInSeconds={1}
/>
]}
/>
)
}
}
}, [theme, i18n.language, poll.expired, poll.expires_at])
return (
<View style={{ marginTop: StyleConstants.Spacing.M }}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
@ -329,8 +316,13 @@ const TimelinePoll: React.FC<Props> = ({
}}
>
{pollButton}
{pollVoteCounts}
{pollExpiration}
<CustomText
fontStyle='S'
style={{ flexShrink: 1, color: colors.secondary }}
>
{pollVoteCounts}
{pollExpiration}
</CustomText>
</View>
</View>
)

View File

@ -8,5 +8,20 @@
"feature": "deprecate_auth_follow",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "notification_type_status",
"version": 3.3,
"reference": "https://docs.joinmastodon.org/entities/notification/#required-attributes"
},
{
"feature": "notification_type_update",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "notification_types_positive_filter",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
}
]

View File

@ -1,6 +1,6 @@
{
"buttons": {
"OK": "",
"OK": "OK",
"apply": "Übernehmen",
"cancel": "Abbrechen"
},

View File

@ -3,7 +3,7 @@
"options": {
"library": "Hochladen",
"photo": "Bild aufnehmen",
"cancel": ""
"cancel": "$t(common:buttons.cancel)"
},
"library": {
"alert": {
@ -11,7 +11,7 @@
"message": "Für den Upload ist eine Zugriffsgenehmigung erforderlich",
"buttons": {
"settings": "Einstellungen bestätigen",
"cancel": ""
"cancel": "$t(common:buttons.cancel)"
}
}
},
@ -21,7 +21,7 @@
"message": "Zugriff auf die Kamera erforderlich",
"buttons": {
"settings": "Einstellungen übernehmen",
"cancel": ""
"cancel": "$t(common:buttons.cancel)"
}
}
}

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} hat geboostet",
"notification": "{{name}} hat deinen Tröt geboostet"
}
},
"update": ""
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "Benachrichtigungsart anzeigen",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Followeranfrage",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "Followeranfrage"
"status": "",
"update": ""
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Entwurf"
},
"warning": "",
"content": {
"accessibilityHint": "Gespeicherter Entwurf, tippe, um diesen zu bearbeiten",
"textEmpty": "Kein Inhalt"

View File

@ -7,7 +7,7 @@
"options": {
"save": "Bild speichern",
"share": "Bild teilen",
"cancel": ""
"cancel": "$t(common:buttons.cancel)"
},
"save": {
"succeed": "Bild gespeichert",

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "Schriftgröße"
},
"language": {
"name": "Sprache"
},
"lists": {
"name": "Listen"
},
@ -141,7 +144,7 @@
}
},
"fields": {
"group": "",
"group": "Gruppe {{index}}",
"label": "Kennzeichnung",
"content": "Inhalt"
}
@ -166,6 +169,9 @@
"follow": {
"heading": "Neue Follower"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Favoriten"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "Umfrageupdate"
},
"status": {
"heading": ""
},
"howitworks": "Erfahre, wie das Routing funktioniert"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "Sprache",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} boosted",
"notification": "{{name}} boosted your toot"
}
},
"update": "Reblog has been edited"
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": "Alternative Text"
},
"notificationsFilter": {
"heading": "Show notification types",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Follow request",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "Follow request"
"status": "Toot from subscribed users",
"update": "Reblog has been edited"
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Draft"
},
"warning": "Drafts are only stored locally, and can be lost in unfortunate events. Advise not using drafts for long term storage.",
"content": {
"accessibilityHint": "Saved draft, tap to edit this draft",
"textEmpty": "Content empty"

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "Toot Font Size"
},
"language": {
"name": "Language"
},
"lists": {
"name": "Lists"
},
@ -166,6 +169,9 @@
"follow": {
"heading": "New follower"
},
"follow_request": {
"heading": "Follow request"
},
"favourite": {
"heading": "Favourited"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "Poll updates"
},
"status": {
"heading": "Toot from subscribed users"
},
"howitworks": "Learn how routing works"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "Language",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -5,9 +5,50 @@ import de from '@root/i18n/de/_all'
import en from '@root/i18n/en/_all'
import it from '@root/i18n/it/_all'
import ko from '@root/i18n/ko/_all'
import pt_BR from '@root/i18n/pt_BR/_all'
import vi from '@root/i18n/vi/_all'
import zh_Hans from '@root/i18n/zh-Hans/_all'
import '@formatjs/intl-getcanonicallocales/polyfill'
import '@formatjs/intl-locale/polyfill'
import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/de'
import '@formatjs/intl-pluralrules/locale-data/en'
import '@formatjs/intl-pluralrules/locale-data/it'
import '@formatjs/intl-pluralrules/locale-data/ko'
import '@formatjs/intl-pluralrules/locale-data/pt'
import '@formatjs/intl-pluralrules/locale-data/vi'
import '@formatjs/intl-pluralrules/locale-data/zh'
import '@formatjs/intl-numberformat/polyfill'
import '@formatjs/intl-numberformat/locale-data/de'
import '@formatjs/intl-numberformat/locale-data/en'
import '@formatjs/intl-numberformat/locale-data/it'
import '@formatjs/intl-numberformat/locale-data/ko'
import '@formatjs/intl-numberformat/locale-data/pt'
import '@formatjs/intl-numberformat/locale-data/vi'
import '@formatjs/intl-numberformat/locale-data/zh-Hans'
import '@formatjs/intl-datetimeformat/polyfill'
import '@formatjs/intl-datetimeformat/locale-data/de'
import '@formatjs/intl-datetimeformat/locale-data/en'
import '@formatjs/intl-datetimeformat/locale-data/it'
import '@formatjs/intl-datetimeformat/locale-data/ko'
import '@formatjs/intl-datetimeformat/locale-data/pt'
import '@formatjs/intl-datetimeformat/locale-data/vi'
import '@formatjs/intl-datetimeformat/locale-data/zh-Hans'
import '@formatjs/intl-datetimeformat/add-all-tz'
import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/de'
import '@formatjs/intl-relativetimeformat/locale-data/en'
import '@formatjs/intl-relativetimeformat/locale-data/it'
import '@formatjs/intl-relativetimeformat/locale-data/ko'
import '@formatjs/intl-relativetimeformat/locale-data/pt'
import '@formatjs/intl-relativetimeformat/locale-data/vi'
import '@formatjs/intl-relativetimeformat/locale-data/zh-Hans'
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
@ -15,7 +56,7 @@ i18n.use(initReactI18next).init({
ns: ['common'],
defaultNS: 'common',
resources: { 'zh-Hans': zh_Hans, vi, ko, it, en, de },
resources: { 'zh-Hans': zh_Hans, vi, 'pt-BR': pt_BR, ko, it, en, de },
returnEmptyString: false,
saveMissing: true,

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} ha ricondiviso",
"notification": "{{name}} ha ricondiviso il tuo toot"
}
},
"update": ""
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "Filtra notifiche per tipo",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Richieste di seguirti",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "Richieste di seguirti"
"status": "",
"update": ""
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Bozza"
},
"warning": "",
"content": {
"accessibilityHint": "Bozza salvata, premi per modificarla",
"textEmpty": "Testo vuoto"

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "Grandezza del testo dei toot"
},
"language": {
"name": "Lingua"
},
"lists": {
"name": "Liste"
},
@ -166,6 +169,9 @@
"follow": {
"heading": "Nuovi seguaci"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Apprezzamenti"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "Novità sui sondaggi"
},
"status": {
"heading": ""
},
"howitworks": "Scopri come funziona il traversamento dei messaggi"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "Lingua",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -1,6 +1,6 @@
{
"buttons": {
"OK": "",
"OK": "확인",
"apply": "적용",
"cancel": "취소"
},

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}}님이 부스트했어요",
"notification": "{{name}}이 내 툿을 부스트했어요"
}
},
"update": ""
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "알림 종류 표시",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "팔로우 요청",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "팔로우 요청"
"status": "",
"update": ""
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "초안"
},
"warning": "",
"content": {
"accessibilityHint": "저장된 초안, 수정하려면 탭하세요",
"textEmpty": "콘텐츠 빔"

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "툿 폰트 크기"
},
"language": {
"name": "언어"
},
"lists": {
"name": "목록"
},
@ -166,6 +169,9 @@
"follow": {
"heading": "새 팔로워"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "즐겨찾기됨"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "투표 업데이트"
},
"status": {
"heading": ""
},
"howitworks": "라우팅 방법 알아보기"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "언어",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -3,6 +3,7 @@ const LOCALES = {
en: 'English',
it: 'Italiano',
ko: '한국어',
'pt-BR': 'Português (Brasil)',
vi: 'Tiếng Việt',
'zh-Hans': '简体中文'
}

17
src/i18n/pt_BR/_all.ts Normal file
View File

@ -0,0 +1,17 @@
export default {
common: require('./common'),
screens: require('./screens'),
screenActions: require('./screens/actions'),
screenAnnouncements: require('./screens/announcements'),
screenCompose: require('./screens/compose'),
screenImageViewer: require('./screens/imageViewer'),
screenTabs: require('./screens/tabs'),
componentEmojis: require('./components/emojis'),
componentInstance: require('./components/instance'),
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentTimeline: require('./components/timeline')
}

View File

@ -0,0 +1,22 @@
{
"buttons": {
"OK": "OK",
"apply": "Aplicar",
"cancel": "Cancelar"
},
"customEmoji": {
"accessibilityLabel": "Emoji personalizado {{emoji}}"
},
"message": {
"success": {
"message": "{{function}} com sucesso"
},
"warning": {
"message": ""
},
"error": {
"message": "{{function}} falhou, por favor, tente novamente"
}
},
"separator": ", "
}

View File

@ -0,0 +1,3 @@
{
"frequentUsed": "Usados frequentemente"
}

View File

@ -0,0 +1,28 @@
{
"server": {
"textInput": {
"placeholder": "Domínio da instância"
},
"button": "Entrar",
"information": {
"name": "Nome",
"accounts": "Usuários",
"statuses": "Toots",
"domains": "Universo"
},
"disclaimer": {
"base": "O processo de login usa o navegador do sistema e as informações da sua conta não estarão visíveis para o aplicativo Tooot. Consulte Mais informação ",
"privacy": "política de privacidade"
}
},
"update": {
"alert": {
"title": "Conectado a esta instância",
"message": "Você pode fazer login em outra conta mantendo a conta existente logada",
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"continue": "Continuar"
}
}
}
}

View File

@ -0,0 +1,28 @@
{
"title": "Selecionar fonte de mídia",
"options": {
"library": "Carregar da biblioteca",
"photo": "Tirar foto",
"cancel": "$t(common:buttons.cancel)"
},
"library": {
"alert": {
"title": "Sem permissão",
"message": "Exigir permissão de leitura da biblioteca de fotos para fazer upload",
"buttons": {
"settings": "Atualizar configurações",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"photo": {
"alert": {
"title": "Sem permissão",
"message": "Requer permissão de uso da câmera para fazer upload",
"buttons": {
"settings": "Atualizar configurações",
"cancel": "$t(common:buttons.cancel)"
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"HTML": {
"expanded": {
"true": "Fechar {{hint}}",
"false": "Expandir {{hint}}"
},
"defaultHint": "artigo"
}
}

View File

@ -0,0 +1,16 @@
{
"follow": {
"function": "Seguir usuário"
},
"block": {
"function": "Bloquear usuário"
},
"button": {
"error": "Erro ao carregar",
"blocked_by": "Bloqueado pelo usuário",
"blocking": "Desbloquear",
"following": "Deixar de seguir",
"requested": "Cancelar a solicitação",
"default": "Seguir"
}
}

View File

@ -0,0 +1,235 @@
{
"empty": {
"error": {
"message": "Erro ao carregar",
"button": "Tentar novamente"
},
"success": {
"message": "Linha do tempo vazia"
}
},
"end": {
"message": "Que tal um copo de <0 />"
},
"lookback": {
"message": "Última leitura em"
},
"refresh": {
"fetchPreviousPage": "Mais novo aqui",
"refetch": "Mais recente"
},
"shared": {
"actioned": {
"pinned": "Fixado",
"favourite": "{{name}} favoritou seu toot",
"status": "{{name}} acabou de postar",
"follow": "{{name}} seguiu você",
"follow_request": "{{name}} pediu para te seguir",
"poll": "Uma enquete em que você votou terminou",
"reblog": {
"default": "{{name}} boostou",
"notification": "{{name}} deu boost no teu toot"
},
"update": ""
},
"actions": {
"reply": {
"accessibilityLabel": "Responder a este toot"
},
"reblogged": {
"accessibilityLabel": "Boost este toot",
"function": "Boost toot"
},
"favourited": {
"accessibilityLabel": "Adicionar este toot aos favoritos",
"function": "Toot favorito"
},
"bookmarked": {
"accessibilityLabel": "Adicionar este toot aos favoritos",
"function": "Favoritos"
}
},
"actionsUsers": {
"reblogged_by": {
"accessibilityLabel": "{{count}} usuários compartilharam este toot",
"accessibilityHint": "Toque para conhecer os usuários",
"text": "$t(screenTabs:shared.users.statuses.reblogged_by)"
},
"favourited_by": {
"accessibilityLabel": "{{count}} usuários favoritaram este toot",
"accessibilityHint": "Toque para conhecer os usuários",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
},
"history": {
"accessibilityLabel": "Este toot foi editado {{count}} vezes",
"accessibilityHint": "Toque para ver o histórico completo de edição",
"text_one": "{{count}} edição",
"text_other": "{{count}} edições"
}
},
"attachment": {
"sensitive": {
"button": "Mostrar mídia sensível"
},
"unsupported": {
"text": "Erro ao carregar",
"button": "Experimente o link remoto"
}
},
"avatar": {
"accessibilityLabel": "Avatar de {{name}}",
"accessibilityHint": "Toque para ir à página de {{name}}"
},
"content": {
"expandHint": "conteúdo oculto"
},
"filtered": "Filtrado",
"fullConversation": "Ler conversas",
"translate": {
"default": "Traduzir",
"succeed": "Traduzido por {{provider}} de {{source}}",
"failed": "Falha na tradução",
"source_not_supported": "idioma do toot não é suportado",
"target_not_supported": "O idioma de destino não é suportado"
},
"header": {
"shared": {
"account": {
"name": {
"accessibilityHint": "Nome de exibição"
},
"account": {
"accessibilityHint": "Conta do usuário"
}
},
"application": "Tooted com {{application}}",
"edited": {
"accessibilityLabel": "Toot editado"
},
"muted": {
"accessibilityLabel": "Toot silenciado"
},
"visibility": {
"direct": {
"accessibilityLabel": "Enviar uma mensagem direta"
},
"private": {
"accessibilityLabel": "Toot é visível apenas para seguidores"
}
}
},
"conversation": {
"withAccounts": "Com",
"delete": {
"function": "Excluir mensagem direta"
}
},
"actions": {
"accessibilityHint": "Ações para este toot, como o seu usuário publicado",
"account": {
"heading": "Sobre o usuário",
"mute": {
"function": "Silenciar usuário",
"button": "Silenciar @{{acct}}"
},
"block": {
"function": "Bloquear usuário",
"button": "Bloquear @{{acct}}"
},
"reports": {
"function": "Denunciar usuário",
"button": "Reportar @{{acct}}"
}
},
"domain": {
"heading": "Sobre a instância",
"block": {
"function": "Bloquear instância",
"button": "Bloquear a instância {{domain}}"
},
"alert": {
"title": "Confirma bloquear {{domain}}?",
"message": "Na maioria das vezes, você pode silenciar ou bloquear determinado usuário.\n\nDepois de bloquear a instância, todo seu conteúdo, incluindo seguidores, será removido!",
"buttons": {
"confirm": "Confirmar bloqueio",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"share": {
"status": {
"heading": "Compartilhar toot",
"button": "Compartilhar o link para este mundo"
},
"account": {
"heading": "Compartilhar Usuário",
"button": "Compartilhar link para este usuário"
}
},
"status": {
"heading": "Sobre o toot",
"edit": {
"function": "Editar toot",
"button": "Editar este toot"
},
"delete": {
"function": "Remover toot",
"button": "Deletar este toot",
"alert": {
"title": "Confirmar exclusão?",
"message": "Tem certeza que deseja excluir este toot? Todos os boosts e favoritos serão apagados, incluindo todas as respostas.",
"buttons": {
"confirm": "Confirme a exclusão",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"deleteEdit": {
"function": "Remover toot",
"button": "Excluir e rascunhar",
"alert": {
"title": "Confirmar exclusão?",
"message": "Tem certeza que deseja excluir este toot? Todos os boosts e favoritos serão apagados, incluindo todas as respostas.",
"buttons": {
"confirm": "Confirme a exclusão",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"mute": {
"function": "Silenciar toot",
"button": {
"positive": "Silenciar este toot e respostas",
"negative": "Desbloquear este toot e respostas"
}
},
"pin": {
"function": "Fixar",
"button": {
"positive": "Fixar este toot",
"negative": "Desafixar este toot"
}
}
}
}
},
"poll": {
"meta": {
"button": {
"vote": "Votar",
"refresh": "Atualizar"
},
"count": {
"voters_one": "{{count}} usuário votou",
"voters_other": "{{count}} usuários votaram",
"votes_one": "{{count}} voto",
"votes_other": "{{count}} votos"
},
"expiration": {
"expired": "Voto expirado",
"until": "Expira em <0 />"
}
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"screenshot": {
"title": "Proteção de privacidade",
"message": "Por favor, não divulgue a identidade de outros usuários, como nome de usuário, avatar, etc. Obrigado!",
"button": "Confirmar"
},
"localCorrupt": {
"message": "Sua sessão expirou. Por favor, faça o login novamente"
},
"pushError": {
"message": "Erro no Serviço Push",
"description": "Por favor, re-ative as notificações push nas configurações"
},
"shareError": {
"imageNotSupported": "Tipo de imagem {{type}} não suportado",
"videoNotSupported": "Tipo de vídeo {{type}} não suportado"
}
}

View File

@ -0,0 +1,20 @@
{
"content": {
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "Exibir notificações",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Seguidores pendentes",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "",
"update": ""
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"heading": "Comunicados",
"content": {
"published": "Publicado <0 />",
"button": {
"read": "Ler",
"unread": "Marcar como lido"
}
}
}

View File

@ -0,0 +1,179 @@
{
"heading": {
"left": {
"button": "Cancelar",
"alert": {
"title": "Cancelar edições?",
"buttons": {
"save": "Salvar rascunho",
"delete": "Apagar rascunho",
"cancel": "Cancelar"
}
}
},
"right": {
"button": {
"default": "Toot",
"conversation": "Toot DM",
"reply": "Resposta de toot",
"deleteEdit": "Toot",
"edit": "Toot",
"share": "Toot"
},
"alert": {
"default": {
"title": "Falha no tooting",
"button": "Tente Novamente"
},
"removeReply": {
"title": "Não foi possível encontrar um toot respondido",
"description": "Um toot respondido pode ter sido excluído. Você quer removê-lo da sua referência?",
"cancel": "$t(common:buttons.cancel)",
"confirm": "Remover referência"
}
}
}
},
"content": {
"root": {
"header": {
"postingAs": "Tooting cono @{{acct}}@{{domain}}",
"spoilerInput": {
"placeholder": "Mensagem de aviso para Spoiler"
},
"textInput": {
"placeholder": "No que você está pensando",
"keyboardImage": {
"exceedMaximum": {
"title": "Quantidade máxima de anexos atingida",
"OK": "$t(common:buttons.OK)"
}
}
}
},
"footer": {
"attachments": {
"sensitive": "Marcar anexos como sensível",
"remove": {
"accessibilityLabel": "Remover anexo enviado, número {{attachment}}"
},
"edit": {
"accessibilityLabel": "Editar o anexo enviado, número {{attachment}}"
},
"upload": {
"accessibilityLabel": "Carregar mais anexos"
}
},
"emojis": {
"accessibilityHint": "Toque para adicionar emojis ao toot"
},
"poll": {
"option": {
"placeholder": {
"accessibilityLabel": "Opção de enquete {{index}}",
"single": "Única escolha",
"multiple": "Múltiplas opções"
}
},
"quantity": {
"reduce": {
"accessibilityLabel": "Reduzir opções da enquete para {{amount}}",
"accessibilityHint": "Quantidade mínima de opções da enquete atingida, tem atualmente {{amount}}"
},
"increase": {
"accessibilityLabel": "Aumentar opções de enquete para {{amount}}",
"accessibilityHint": "Quantidade máxima de opções da enquete atingida, tem atualmente {{amount}}"
}
},
"multiple": {
"heading": "Tipo de opção",
"options": {
"single": "Uma opção",
"multiple": "Múltipla escolha",
"cancel": "$t(common:buttons.cancel)"
}
},
"expiration": {
"heading": "Validade",
"options": {
"300": "5 minutos",
"1800": "30 minutos",
"3600": "1 hora",
"21600": "6 horas",
"86400": "1 dia",
"259200": "3 dias",
"604800": "7 dias",
"cancel": "$t(common:buttons.cancel)"
}
}
}
},
"actions": {
"attachment": {
"accessibilityLabel": "Fazer upload de um anexo",
"accessibilityHint": "A função de enquete será desativada quando houver qualquer anexo",
"failed": {
"alert": {
"title": "Falha no envio",
"button": "Tente Novamente"
}
}
},
"poll": {
"accessibilityLabel": "Adicionar enquete",
"accessibilityHint": "A função Anexo será desabilitada quando a enquete estiver ativa"
},
"visibility": {
"accessibilityLabel": "Visibilidade do toot é {{visibility}}",
"title": "Visibilidade do Toot",
"options": {
"public": "Público",
"unlisted": "Não listado",
"private": "Apenas seguidores",
"direct": "Mensagem Direta",
"cancel": "$t(common:buttons.cancel)"
}
},
"spoiler": {
"accessibilityLabel": "Spoiler"
},
"emoji": {
"accessibilityLabel": "Adicionar emoji",
"accessibilityHint": "Abra o painel de seleção de emojis, deslize horizontalmente para alterar a página"
}
},
"drafts_one": "Rascunho ({{count}})",
"drafts_other": "Rascunhos ({{count}})"
},
"editAttachment": {
"header": {
"title": "Editar anexos",
"right": {
"accessibilityLabel": "Salvar anexo editado",
"failed": {
"title": "Falha ao editar",
"button": "Tente Novamente"
}
}
},
"content": {
"altText": {
"heading": "Descrever mídia para deficientes visuais",
"placeholder": "Você pode adicionar uma descrição, às vezes chamada texto alternativo aos seus meios de comunicação social, assim eles estão acessíveis a ainda mais pessoas, incluindo as que são cegos ou deficientes visuais.\n\nAs boas descrições são concisas, mas apresentam o que está em sua mídia com precisão o suficiente para entender seu contexto."
},
"imageFocus": "Arraste o círculo de foco para atualizar o ponto de foco"
}
},
"draftsList": {
"header": {
"title": "Rascunho"
},
"warning": "",
"content": {
"accessibilityHint": "Toque para editar este rascunho",
"textEmpty": "O conteúdo está vazio"
},
"checkAttachment": "Verificando anexos no servidor..."
}
}
}

View File

@ -0,0 +1,17 @@
{
"content": {
"actions": {
"accessibilityLabel": "Mais ações desta imagem",
"accessibilityHint": "Você pode salvar ou compartilhar esta imagem"
},
"options": {
"save": "Salvar imagem",
"share": "Compartilhar Imagem",
"cancel": "$t(common:buttons.cancel)"
},
"save": {
"succeed": "Imagem salva",
"failed": "Falha ao salvar imagem"
}
}
}

View File

@ -0,0 +1,354 @@
{
"tabs": {
"local": {
"name": "Seguindo"
},
"public": {
"name": "",
"segments": {
"left": "Global",
"right": "Local"
}
},
"notifications": {
"name": "Notificações"
},
"me": {
"name": "Sobre mim"
}
},
"common": {
"search": {
"accessibilityLabel": "Pesquisa",
"accessibilityHint": "Pesquisar por hashtags, usuários ou toots"
}
},
"notifications": {
"filter": {
"accessibilityLabel": "Filtro",
"accessibilityHint": "Filtrar notificações por tipos"
}
},
"me": {
"stacks": {
"bookmarks": {
"name": "Favoritos"
},
"conversations": {
"name": "Mensagens diretas"
},
"favourites": {
"name": "Favoritos"
},
"fontSize": {
"name": "Tamanho da fonte do Toot"
},
"language": {
"name": "Idioma"
},
"lists": {
"name": "Listas"
},
"list": {
"name": "Lista: {{list}}"
},
"push": {
"name": "Notificação"
},
"profile": {
"name": "Editar Perfil"
},
"profileName": {
"name": "Editar nome de exibição"
},
"profileNote": {
"name": "Editar descrição"
},
"profileFields": {
"name": "Editar Metadados"
},
"settings": {
"name": "Configurações do Aplicativo"
},
"webSettings": {
"name": "Mais configurações"
},
"switch": {
"name": "Alterar Conta"
}
},
"fontSize": {
"showcase": "Exemplo de toot",
"demo": "<p>Esta é uma demonstração também😊. Você pode escolher entre várias opções abaixo.<br /><br />Esta configuração afeta apenas o conteúdo principal dos toots, mas não os tamanhos de outra fonte.</p>",
"availableSizes": "Tamanhos disponíveis",
"sizes": {
"S": "S",
"M": "M - Padrão",
"L": "Grande",
"XL": "Extra grande",
"XXL": "Extra grande"
}
},
"profile": {
"cancellation": {
"title": "Alterações não salvas",
"message": "Sua alteração não foi salva. Descartar as alterações?",
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"discard": "Descartar"
}
},
"feedback": {
"succeed": "{{type}} atualizado",
"failed": "{{type}} falhou na atualização. Por favor, tente novamente"
},
"root": {
"name": {
"title": "Nome de exibição"
},
"avatar": {
"title": "Imagem de perfil",
"description": "Será reduzido a 400x400px"
},
"header": {
"title": "Banner",
"description": "Será reduzido para 1500x500px"
},
"note": {
"title": "Descrição"
},
"fields": {
"title": "Metadados",
"total_one": "{{count}} campo",
"total_other": "{{count}} campos"
},
"visibility": {
"title": "Visibilidade da Postagem",
"options": {
"public": "Público",
"unlisted": "Não listado",
"private": "Apenas seguidores",
"cancel": "$t(common:buttons.cancel)"
}
},
"sensitive": {
"title": "Publicação de Mídia Sensiva"
},
"lock": {
"title": "Trancar conta",
"description": "Requer que você aprove manualmente seguidores"
},
"bot": {
"title": "Conta bot",
"description": "Essa conta executa principalmente ações automatizadas e pode não ser monitorada"
}
},
"fields": {
"group": "Grupo {{index}}",
"label": "Rótulo",
"content": "Conteúdo"
}
},
"push": {
"notAvailable": "Seu telefone não suporta notificação de envio de tooot",
"enable": {
"direct": "Habilitar notificações via push",
"settings": "Ativar em configurações"
},
"global": {
"heading": "Habilitar para {{acct}}",
"description": "Mensagens são encaminhadas pelo servidor do tooot"
},
"decode": {
"heading": "Ver detalhes da mensagem",
"description": "As mensagens enviadas através do servidor do tooot são criptografadas, mas você pode optar por decodificar a mensagem no servidor. Nosso código fonte de servidor é open source, e nenhuma política de registro."
},
"default": {
"heading": "Padrão"
},
"follow": {
"heading": "Novo seguidor"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Favoritos"
},
"reblog": {
"heading": "Boosted"
},
"mention": {
"heading": "Mencionou você"
},
"poll": {
"heading": "Pesquisa atualizada"
},
"status": {
"heading": ""
},
"howitworks": "Saiba como funciona o roteamento"
},
"root": {
"announcements": {
"content": {
"unread": "{{amount}} não lidas",
"read": "Não há mensagens não lidas",
"empty": "Nenhum"
}
},
"push": {
"content": {
"enabled": "Habilitado",
"disabled": "Desabilitado"
}
},
"update": {
"title": "Atualize para a versão mais recente"
},
"logout": {
"button": "Sair",
"alert": {
"title": "Desconectar?",
"message": "Após sair, você precisa entrar novamente",
"buttons": {
"logout": "Sair",
"cancel": "$t(common:buttons.cancel)"
}
}
}
},
"settings": {
"fontsize": {
"heading": "$t(me.stacks.fontSize.name)",
"content": {
"S": "$t(me.fontSize.sizes.S)",
"M": "$t(me.fontSize.sizes.M)",
"L": "$t(me.fontSize.sizes.L)",
"XL": "$t(me.fontSize.sizes.XL)",
"XXL": "$t(me.fontSize.sizes.XXL)"
}
},
"language": {
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}
},
"theme": {
"heading": "Aparência",
"options": {
"auto": "Como o sistema",
"light": "Modo claro",
"dark": "Modo escuro",
"cancel": "$t(common:buttons.cancel)"
}
},
"darkTheme": {
"heading": "Tema escuro",
"options": {
"lighter": "Claro",
"darker": "Escuro",
"cancel": "$t(common:buttons.cancel)"
}
},
"browser": {
"heading": "Abrir links",
"options": {
"internal": "Dentro do aplicativo",
"external": "Usar navegador do sistema",
"cancel": "$t(common:buttons.cancel)"
}
},
"staticEmoji": {
"heading": "Usar emojis estáticos",
"description": "Se você encontrar falhas frequentes de apps ao visualizar a lista de emojis, você pode tentar usar emojis estáticos."
},
"feedback": {
"heading": "Pedidos de Funcionalidades"
},
"support": {
"heading": "Suporte tooot"
},
"review": {
"heading": "Revisar tooot"
},
"contact": {
"heading": "Contatar tooot"
},
"analytics": {
"heading": "Ajude-nos a melhorar",
"description": "Coletando somente relativo ao uso, não ao usuário"
},
"version": "Versão v{{version}}",
"instanceVersion": "Versão Mastodon v{{version}}"
},
"switch": {
"existing": "Escolha a partir do login",
"new": "Fazer login para instância"
}
},
"shared": {
"account": {
"actions": {
"accessibilityLabel": "Ações para o usuário {{user}}",
"accessibilityHint": "Você pode silenciar, bloquear, relatar ou compartilhar este usuário"
},
"followed_by": " está seguindo você",
"moved": "Usuário movido",
"created_at": "Registrado em: {{date}}",
"summary": {
"statuses_count": "{{count}} toots",
"following_count": "$t(shared.users.accounts.following)",
"followers_count": "$t(shared.users.accounts.followers)"
},
"toots": {
"default": "Toots",
"all": "Toots e respostas"
}
},
"attachments": {
"name": "<0 /><1>\"s mídia</1>"
},
"search": {
"header": {
"prefix": "Procurando",
"placeholder": "para..."
},
"empty": {
"general": "Digite a palavra-chave para pesquisar por <bold>$t(screenTabs:shared.search.sections.accounts)</bold>£<bold>$t(screenTabs:shared.search.sections.hashtags)</bold> ou <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
"advanced": {
"header": "Pesquisa avançada",
"example": {
"account": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)",
"hashtag": "$t(shared.search.header.prefix) $t(shared.search.sections.hashtags)",
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
}
}
},
"sections": {
"accounts": "Usuário",
"hashtags": "Palavras-chave",
"statuses": "Toot"
},
"notFound": "Não foi possível encontrar <bold>{{searchTerm}}</bold> {{type}} relacionado"
},
"toot": {
"name": "Discussões"
},
"users": {
"accounts": {
"following": "Seguindo {{count}}",
"followers": "{{count}} seguidores"
},
"statuses": {
"reblogged_by": "{{count}} boostou",
"favourited_by": "{{count}} favoritados"
}
},
"history": {
"name": "Histórico de Edição"
}
}
}

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} đăng lại",
"notification": "{{name}} đăng lại tút của bạn"
}
},
"update": "Đăng lại đã được sửa"
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "Những kiểu thông báo cho phép",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Yêu cầu theo dõi",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "Yêu cầu theo dõi"
"status": "Tút từ người đã theo dõi",
"update": "Đăng lại đã được sửa"
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Nháp"
},
"warning": "Tút nháp chỉ được lưu trữ trên điện thoại và có thể bị mất nếu có sự cố. Hãy cẩn thận.",
"content": {
"accessibilityHint": "Đã lưu nháp, nhấn để tiếp tục viết",
"textEmpty": "Chưa có nội dung"

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "Cỡ chữ"
},
"language": {
"name": "Ngôn ngữ"
},
"lists": {
"name": "Danh sách"
},
@ -166,6 +169,9 @@
"follow": {
"heading": "Người theo dõi mới"
},
"follow_request": {
"heading": "Yêu cầu theo dõi"
},
"favourite": {
"heading": "Lượt thích"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "Kết quả bình chọn"
},
"status": {
"heading": "Tút từ người đã theo dõi"
},
"howitworks": "Tìm hiểu cách truyền"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "Ngôn ngữ",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} 转嘟了",
"notification": "{{name}} 转嘟了你的嘟文"
}
},
"update": "转嘟已被编辑"
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "$t(common:buttons.apply)",
"cancel": "$t(common:buttons.cancel)"
"altText": {
"heading": "替代文本"
},
"notificationsFilter": {
"heading": "显示通知",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "关注请求",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"follow_request": "关注请求"
"status": "订阅用户的嘟文",
"update": "转嘟被编辑"
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "草稿"
},
"warning": "草稿只存储在本地,在部分情况下可能丢失。建议不要长期存储草稿。",
"content": {
"accessibilityHint": "已保存的草稿,点击编辑此草稿",
"textEmpty": "无正文内容"

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "嘟文字号"
},
"language": {
"name": "应用语言"
},
"lists": {
"name": "列表"
},
@ -166,6 +169,9 @@
"follow": {
"heading": "新关注者"
},
"follow_request": {
"heading": "关注请求"
},
"favourite": {
"heading": "嘟文被喜欢"
},
@ -178,6 +184,9 @@
"poll": {
"heading": "投票更新"
},
"status": {
"heading": "订阅用户的嘟文"
},
"howitworks": "了解通知消息转发如何工作"
},
"root": {
@ -221,7 +230,7 @@
}
},
"language": {
"heading": "切换语言",
"heading": "$t(me.stacks.language.name)",
"options": {
"cancel": "$t(common:buttons.cancel)"
}

View File

@ -29,7 +29,8 @@
"reblog": {
"default": "{{name}} 轉嘟了",
"notification": "{{name}} 轉嘟了您的嘟文"
}
},
"update": ""
},
"actions": {
"reply": {

View File

@ -1,18 +1,19 @@
{
"content": {
"button": {
"apply": "",
"cancel": ""
"altText": {
"heading": ""
},
"notificationsFilter": {
"heading": "",
"content": {
"follow": "",
"follow_request": "",
"favourite": "",
"reblog": "",
"mention": "",
"poll": "",
"follow_request": ""
"status": "",
"update": ""
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": ""
},
"warning": "",
"content": {
"accessibilityHint": "",
"textEmpty": ""

View File

@ -43,6 +43,9 @@
"fontSize": {
"name": "嘟文字體大小"
},
"language": {
"name": ""
},
"lists": {
"name": "清單"
},
@ -166,6 +169,9 @@
"follow": {
"heading": ""
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": ""
},
@ -178,6 +184,9 @@
"poll": {
"heading": ""
},
"status": {
"heading": ""
},
"howitworks": ""
},
"root": {

View File

@ -7,7 +7,7 @@ import {
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions, StyleSheet, View } from 'react-native'
import {
@ -30,6 +30,7 @@ import {
} from 'react-native-safe-area-context'
import { useSelector } from 'react-redux'
import ActionsAccount from './Actions/Account'
import ActionsAltText from './Actions/AltText'
import ActionsDomain from './Actions/Domain'
import ActionsNotificationsFilter from './Actions/NotificationsFilter'
import ActionsShare from './Actions/Share'
@ -173,6 +174,8 @@ const ScreenActions = ({
)
case 'notifications_filter':
return <ActionsNotificationsFilter />
case 'alt_text':
return <ActionsAltText text={params.text} />
}
}

View File

@ -0,0 +1,44 @@
import Button from '@components/Button'
import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
export interface Props {
text: string
}
const ActionsAltText: React.FC<Props> = ({ text }) => {
const navigation = useNavigation()
const { t } = useTranslation('screenActions')
const { colors } = useTheme()
return (
<>
<MenuContainer>
<MenuHeader heading={t(`content.altText.heading`)} />
<ScrollView style={{ maxHeight: Dimensions.get('window').height / 2 }}>
<CustomText style={{ color: colors.primaryDefault }}>
{text}
</CustomText>
</ScrollView>
</MenuContainer>
<Button
type='text'
content={t('common:buttons.OK')}
onPress={() => navigation.goBack()}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
/>
</>
)
}
export default ActionsAltText

View File

@ -4,12 +4,12 @@ import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import { useNavigation } from '@react-navigation/native'
import {
checkInstanceFeature,
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useQueryClient } from 'react-query'
@ -33,42 +33,63 @@ const ActionsNotificationsFilter: React.FC = () => {
return null
}
const hasTypeStatus = useSelector(
checkInstanceFeature('notification_type_status')
)
const hasTypeUpdate = useSelector(
checkInstanceFeature('notification_type_update')
)
const options = useMemo(() => {
return (
instanceNotificationsFilter &&
(
[
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'follow_request'
'status',
'update'
] as [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'follow_request'
'status',
'update'
]
).map(type => (
<MenuRow
key={type}
title={t(`content.notificationsFilter.content.${type}`)}
switchValue={instanceNotificationsFilter[type]}
switchOnValueChange={() =>
dispatch(
updateInstanceNotificationsFilter({
...instanceNotificationsFilter,
[type]: !instanceNotificationsFilter[type]
})
)
)
.filter(type => {
switch (type) {
case 'status':
return hasTypeStatus
case 'update':
return hasTypeUpdate
default:
return true
}
/>
))
})
.map(type => (
<MenuRow
key={type}
title={t(`content.notificationsFilter.content.${type}`)}
switchValue={instanceNotificationsFilter[type]}
switchOnValueChange={() =>
dispatch(
updateInstanceNotificationsFilter({
...instanceNotificationsFilter,
[type]: !instanceNotificationsFilter[type]
})
)
}
/>
))
)
}, [instanceNotificationsFilter])
}, [instanceNotificationsFilter, hasTypeStatus, hasTypeUpdate])
return (
<>
@ -78,20 +99,16 @@ const ActionsNotificationsFilter: React.FC = () => {
</MenuContainer>
<Button
type='text'
content={t('content.button.apply')}
content={t('common:buttons.apply')}
onPress={() => {
queryClient.resetQueries(queryKey)
}}
style={styles.button}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
/>
</>
)
}
const styles = StyleSheet.create({
button: {
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}
})
export default ActionsNotificationsFilter

View File

@ -53,7 +53,6 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const renderItem = useCallback(
({ item }: { item: ComposeStateDraft }) => {
console.log('timestamp', item.timestamp)
return (
<Pressable
accessibilityHint={t('content.draftsList.content.accessibilityHint')}
@ -193,6 +192,30 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
return (
<>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S
}}
>
<Icon
name='AlertTriangle'
color={colors.secondary}
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText
fontStyle='S'
style={{ flexShrink: 1, color: colors.secondary }}
>
{t('content.draftsList.warning')}
</CustomText>
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}

View File

@ -100,8 +100,8 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
)}
source={{ uri }}
style={{
width: 32,
height: 32,
width: 36,
height: 36,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}}
@ -119,8 +119,8 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
)}
source={{ uri }}
style={{
width: 32,
height: 32,
width: 36,
height: 36,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}}
@ -145,7 +145,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
height: 260
height: 280
}}
>
<SectionList

View File

@ -19,94 +19,6 @@ import {
import ImageViewer from './ImageViewer/Root'
import saveImage from './ImageViewer/save'
const HeaderComponent = React.memo(
({
messageRef,
navigation,
currentIndex,
imageUrls
}: {
messageRef: RefObject<FlashMessage>
navigation: NativeStackNavigationProp<
RootStackParamList,
'Screen-ImagesViewer'
>
currentIndex: number
imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls']
}) => {
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
const { t } = useTranslation('screenImageViewer')
const { showActionSheetWithOptions } = useActionSheet()
const onPress = useCallback(() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({ messageRef, theme, image: imageUrls[currentIndex] })
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({ message: imageUrls[currentIndex].url })
break
}
break
}
}
)
}, [currentIndex])
return (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: insets.top
}}
>
<HeaderLeft
content='X'
native={false}
background
onPress={() => navigation.goBack()}
/>
<HeaderCenter
inverted
content={`${currentIndex + 1} / ${imageUrls.length}`}
/>
<HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
content='MoreHorizontal'
native={false}
background
onPress={onPress}
/>
</View>
)
},
(prev, next) => prev.currentIndex === next.currentIndex
)
const ScreenImagesViewer = ({
route: {
params: { imageUrls, id }
@ -118,13 +30,51 @@ const ScreenImagesViewer = ({
return null
}
const { theme } = useTheme()
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
const { t } = useTranslation('screenImageViewer')
const initialIndex = imageUrls.findIndex(image => image.id === id)
const [currentIndex, setCurrentIndex] = useState(initialIndex)
const messageRef = useRef<FlashMessage>(null)
const { showActionSheetWithOptions } = useActionSheet()
const onPress = useCallback(() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({ messageRef, theme, image: imageUrls[currentIndex] })
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({ message: imageUrls[currentIndex].url })
break
}
break
}
}
)
}, [currentIndex])
return (
<SafeAreaProvider>
<StatusBar hidden />
@ -133,14 +83,74 @@ const ScreenImagesViewer = ({
imageIndex={initialIndex}
onImageIndexChange={index => setCurrentIndex(index)}
onRequestClose={() => navigation.goBack()}
onLongPress={image => saveImage({ messageRef, theme, image })}
onLongPress={() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({
messageRef,
theme,
image: imageUrls[currentIndex]
})
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({
message: imageUrls[currentIndex].url
})
break
}
break
}
}
)
}}
HeaderComponent={() => (
<HeaderComponent
messageRef={messageRef}
navigation={navigation}
currentIndex={currentIndex}
imageUrls={imageUrls}
/>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: insets.top
}}
>
<HeaderLeft
content='X'
native={false}
background
onPress={() => navigation.goBack()}
/>
<HeaderCenter
inverted
content={`${currentIndex + 1} / ${imageUrls.length}`}
/>
<HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
content='MoreHorizontal'
native={false}
background
onPress={onPress}
/>
</View>
)}
/>
<Message ref={messageRef} />

View File

@ -18,7 +18,7 @@ import {
import {
getVersionUpdate,
retriveVersionLatest
} from '@utils/slices/versionSlice'
} from '@utils/slices/appSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo } from 'react'
import { Platform } from 'react-native'

View File

@ -14,6 +14,7 @@ import TabMePush from './Me/Push'
import TabMeRoot from './Me/Root'
import TabMeSettings from './Me/Settings'
import TabMeSettingsFontsize from './Me/SettingsFontsize'
import TabMeSettingsLanguage from './Me/SettingsLanguage'
import TabMeSwitch from './Me/Switch'
import TabSharedRoot from './Shared/Root'
@ -152,6 +153,19 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen
name='Tab-Me-Settings-Language'
component={TabMeSettingsLanguage}
options={({ navigation }: any) => ({
title: t('me.stacks.language.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.language.name')} />
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen
name='Tab-Me-Switch'
component={TabMeSwitch}

View File

@ -5,6 +5,7 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import CustomText from '@components/Text'
import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment'
import { getExpoToken } from '@utils/slices/appSlice'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
@ -45,16 +46,12 @@ const TabMePush: React.FC = () => {
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
const expoToken = useSelector(getExpoToken)
useEffect(() => {
if (isDevelopment) {
setPushAvailable(true)
} else {
Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot',
applicationId: 'com.xmflsct.app.tooot'
})
.then(data => setPushAvailable(!!data))
.catch(() => setPushAvailable(false))
setPushAvailable(!!expoToken)
}
checkPush()
@ -73,12 +70,22 @@ const TabMePush: React.FC = () => {
const alerts = useMemo(() => {
return instancePush?.alerts
? (
['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
[
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll'
'poll',
'status'
] as [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status'
]
).map(alert => (
<MenuRow

View File

@ -1,5 +1,5 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { getVersionUpdate } from '@utils/slices/versionSlice'
import { getVersionUpdate } from '@utils/slices/appSlice'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'

View File

@ -5,11 +5,8 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native'
import { LOCALES } from '@root/i18n/locales'
import { useAppDispatch } from '@root/store'
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
import { getInstances } from '@utils/slices/instancesSlice'
import {
changeBrowser,
changeLanguage,
changeTheme,
getSettingsTheme,
getSettingsBrowser,
@ -19,11 +16,8 @@ import {
getSettingsStaticEmoji,
changeStaticEmoji
} from '@utils/slices/settingsSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useSelector } from 'react-redux'
import { mapFontsizeToName } from '../SettingsFontsize'
@ -31,10 +25,8 @@ const SettingsApp: React.FC = () => {
const navigation = useNavigation<any>()
const dispatch = useAppDispatch()
const { showActionSheetWithOptions } = useActionSheet()
const { mode } = useTheme()
const { t, i18n } = useTranslation('screenTabs')
const instances = useSelector(getInstances, () => true)
const settingsFontsize = useSelector(getSettingsFontsize)
const settingsTheme = useSelector(getSettingsTheme)
const settingsDarkTheme = useSelector(getSettingsDarkTheme)
@ -49,102 +41,14 @@ const SettingsApp: React.FC = () => {
`me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
)}
iconBack='ChevronRight'
onPress={() => {
navigation.navigate('Tab-Me-Settings-Fontsize')
}}
onPress={() => navigation.navigate('Tab-Me-Settings-Fontsize')}
/>
<MenuRow
title={t('me.settings.language.heading')}
// @ts-ignore
content={LOCALES[i18n.language]}
iconBack='ChevronRight'
onPress={() => {
const options = Object.keys(LOCALES)
// @ts-ignore
.map(locale => LOCALES[locale])
.concat(t('me.settings.language.options.cancel'))
showActionSheetWithOptions(
{
title: t('me.settings.language.heading'),
options,
cancelButtonIndex: options.length - 1,
userInterfaceStyle: mode
},
buttonIndex => {
if (buttonIndex === undefined) return
if (buttonIndex < options.length - 1) {
analytics('settings_language_press', {
current: i18n.language,
new: options[buttonIndex]
})
haptics('Success')
// @ts-ignore
dispatch(changeLanguage(Object.keys(LOCALES)[buttonIndex]))
i18n.changeLanguage(Object.keys(LOCALES)[buttonIndex])
// Update Android notification channel language
if (Platform.OS === 'android') {
instances.forEach(instance => {
const accountFull = `@${instance.account.acct}@${instance.uri}`
if (instance.push.decode.value === false) {
Notifications.setNotificationChannelAsync(
`${accountFull}_default`,
{
groupId: accountFull,
name: t('me.push.default.heading'),
...androidDefaults
}
)
} else {
Notifications.setNotificationChannelAsync(
`${accountFull}_follow`,
{
groupId: accountFull,
name: t('me.push.follow.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_favourite`,
{
groupId: accountFull,
name: t('me.push.favourite.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_reblog`,
{
groupId: accountFull,
name: t('me.push.reblog.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_mention`,
{
groupId: accountFull,
name: t('me.push.mention.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_poll`,
{
groupId: accountFull,
name: t('me.push.poll.heading'),
...androidDefaults
}
)
}
})
}
}
}
)
}}
onPress={() => navigation.navigate('Tab-Me-Settings-Language')}
/>
<MenuRow
title={t('me.settings.theme.heading')}

View File

@ -45,7 +45,7 @@ const TabMeSettingsFontsize: React.FC<
const item = {
id: 'demo',
uri: 'https://tooot.app',
created_at: new Date(),
created_at: new Date(2021, 4, 16),
sensitive: false,
visibility: 'public',
replies_count: 0,
@ -67,6 +67,7 @@ const TabMeSettingsFontsize: React.FC<
username: 'tooot📱',
acct: 'tooot@xmflsct.com',
display_name: 'tooot📱',
avatar: 'https://avatars.githubusercontent.com/u/77554750?s=100',
avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=100'
},
media_attachments: [],
@ -100,7 +101,7 @@ const TabMeSettingsFontsize: React.FC<
}, [theme, initialSize])
return (
<ScrollView scrollEnabled={false}>
<ScrollView>
<CustomText
fontStyle='M'
style={{

View File

@ -0,0 +1,99 @@
import analytics from '@components/analytics'
import haptics from '@components/haptics'
import { MenuContainer, MenuRow } from '@components/Menu'
import { LOCALES } from '@root/i18n/locales'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
import { getInstances } from '@utils/slices/instancesSlice'
import { changeLanguage } from '@utils/slices/settingsSlice'
import * as Notifications from 'expo-notifications'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
const TabMeSettingsLanguage: React.FC<
TabMeStackScreenProps<'Tab-Me-Settings-Language'>
> = ({ navigation }) => {
const { i18n, t } = useTranslation('screenTabs')
const languages = Object.entries(LOCALES)
const instances = useSelector(getInstances)
const dispatch = useDispatch()
const change = (lang: string) => {
analytics('settings_language_press', {
current: i18n.language,
new: lang
})
haptics('Success')
dispatch(changeLanguage(lang))
i18n.changeLanguage(lang)
// Update Android notification channel language
if (Platform.OS === 'android') {
instances.forEach(instance => {
const accountFull = `@${instance.account.acct}@${instance.uri}`
if (instance.push.decode.value === false) {
Notifications.setNotificationChannelAsync(`${accountFull}_default`, {
groupId: accountFull,
name: t('me.push.default.heading'),
...androidDefaults
})
} else {
Notifications.setNotificationChannelAsync(`${accountFull}_follow`, {
groupId: accountFull,
name: t('me.push.follow.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(
`${accountFull}_favourite`,
{
groupId: accountFull,
name: t('me.push.favourite.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(`${accountFull}_reblog`, {
groupId: accountFull,
name: t('me.push.reblog.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${accountFull}_mention`, {
groupId: accountFull,
name: t('me.push.mention.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${accountFull}_poll`, {
groupId: accountFull,
name: t('me.push.poll.heading'),
...androidDefaults
})
}
})
}
navigation.pop(1)
}
return (
<MenuContainer>
<FlatList
data={languages}
renderItem={({ item }) => {
return (
<MenuRow
key={item[0]}
title={item[1]}
iconBack={item[0] === i18n.language ? 'Check' : undefined}
iconBackColor={'blue'}
onPress={() => item[0] !== i18n.language && change(item[0])}
/>
)
}}
/>
</MenuContainer>
)
}
export default TabMeSettingsLanguage

View File

@ -5,11 +5,8 @@ import ComponentInstance from '@components/Instance'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import initQuery from '@utils/initQuery'
import {
getInstanceActive,
getInstances,
Instance
} from '@utils/slices/instancesSlice'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react'
@ -19,7 +16,7 @@ import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
interface Props {
instance: Instance
instance: InstanceLatest
selected?: boolean
}

View File

@ -7,7 +7,7 @@ import {
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { useSelector } from 'react-redux'
@ -35,23 +35,6 @@ const AccountInformationAccount: React.FC<Props> = ({
options: { enabled: account !== undefined }
})
const movedContent = useMemo(() => {
if (account?.moved) {
return (
<CustomText
fontStyle='M'
style={{
marginLeft: StyleConstants.Spacing.S,
color: colors.secondary
}}
selectable
>
@{account.moved.acct}
</CustomText>
)
}
}, [account?.moved])
if (account || (localInstance && instanceAccount)) {
return (
<View
@ -62,23 +45,24 @@ const AccountInformationAccount: React.FC<Props> = ({
marginBottom: StyleConstants.Spacing.L
}}
>
<CustomText
fontStyle='M'
style={{
textDecorationLine: account?.moved ? 'line-through' : undefined,
color: colors.secondary
}}
selectable
>
@{localInstance ? instanceAccount?.acct : account?.acct}
{localInstance ? `@${instanceUri}` : null}
</CustomText>
{relationship?.followed_by ? (
<CustomText fontStyle='M' style={{ color: colors.secondary }}>
{t('shared.account.followed_by')}
<CustomText fontStyle='M' style={{ color: colors.secondary }}>
{account?.moved ? (
<>
{' '}
<CustomText selectable>@{account.moved.acct}</CustomText>
</>
) : null}
<CustomText
style={{
textDecorationLine: account?.moved ? 'line-through' : undefined
}}
selectable
>
@{localInstance ? instanceAccount?.acct : account?.acct}
{localInstance ? `@${instanceUri}` : null}
</CustomText>
) : null}
{movedContent}
{relationship?.followed_by ? t('shared.account.followed_by') : null}
</CustomText>
{account?.locked ? (
<Icon
name='Lock'

View File

@ -1,3 +1,5 @@
/// <reference types="@welldone-software/why-did-you-render" />
import React from 'react'
import log from './log'

View File

@ -1,4 +1,3 @@
import { isRelease } from '@utils/checkEnvironment'
import * as Sentry from 'sentry-expo'
import log from './log'
@ -7,7 +6,7 @@ const sentry = () => {
Sentry.init({
dsn: 'https://53348b60ff844d52886e90251b3a5f41@o917354.ingest.sentry.io/6410576',
enableInExpoDevelopment: false,
debug: !isRelease,
// debug: !isRelease,
autoSessionTracking: true
})
}

View File

@ -4,10 +4,10 @@ import { AnyAction, configureStore, Reducer } from '@reduxjs/toolkit'
import contextsMigration from '@utils/migrations/contexts/migration'
import instancesMigration from '@utils/migrations/instances/migration'
import settingsMigration from '@utils/migrations/settings/migration'
import appSlice from '@utils/slices/appSlice'
import contextsSlice, { ContextsState } from '@utils/slices/contextsSlice'
import instancesSlice, { InstancesState } from '@utils/slices/instancesSlice'
import settingsSlice, { SettingsState } from '@utils/slices/settingsSlice'
import versionSlice from '@utils/slices/versionSlice'
import { Platform } from 'react-native'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import {
@ -39,7 +39,7 @@ const instancesPersistConfig = {
key: 'instances',
prefix,
storage: Platform.OS === 'ios' ? secureStorage : AsyncStorage,
version: 9,
version: 10,
// @ts-ignore
migrate: createMigrate(instancesMigration)
}
@ -67,18 +67,13 @@ const store = configureStore({
SettingsState,
AnyAction
>,
version: versionSlice
app: appSlice
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
}).concat(store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
})
})

View File

@ -1,13 +1,14 @@
import queryClient from '@helpers/queryClient'
import { store } from '@root/store'
import { InstanceLatest } from './migrations/instances/migration'
// import { prefetchTimelineQuery } from './queryHooks/timeline'
import { Instance, updateInstanceActive } from './slices/instancesSlice'
import { updateInstanceActive } from './slices/instancesSlice'
const initQuery = async ({
instance,
prefetch
}: {
instance: Instance
instance: InstanceLatest
prefetch?: { enabled: boolean; page?: 'Following' | 'LocalPublic' }
}) => {
store.dispatch(updateInstanceActive(instance))

View File

@ -5,6 +5,7 @@ import { InstanceV6 } from './v6'
import { InstanceV7 } from './v7'
import { InstanceV8 } from './v8'
import { InstanceV9 } from './v9'
import { InstanceV10 } from './v10'
const instancesMigration = {
4: (state: InstanceV3): InstanceV4 => {
@ -99,7 +100,37 @@ const instancesMigration = {
}
})
}
},
10: (state: { instances: InstanceV9[] }): { instances: InstanceV10[] } => {
return {
instances: state.instances.map(instance => {
return {
...instance,
notifications_filter: {
...instance.notifications_filter,
status: true,
update: true
},
push: {
...instance.push,
alerts: {
...instance.push.alerts,
follow_request: {
loading: false,
value: true
},
status: {
loading: false,
value: true
}
}
}
}
})
}
}
}
export { InstanceV10 as InstanceLatest }
export default instancesMigration

View File

@ -0,0 +1,89 @@
import { ComposeStateDraft } from '@screens/Compose/utils/types'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export type InstanceV10 = {
active: boolean
appData: {
clientId: string
clientSecret: string
}
url: string
token: string
uri: Mastodon.Instance['uri']
urls: Mastodon.Instance['urls']
account: {
id: Mastodon.Account['id']
acct: Mastodon.Account['acct']
avatarStatic: Mastodon.Account['avatar_static']
preferences: Mastodon.Preferences
}
version: string
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
notifications_filter: {
follow: boolean
follow_request: boolean
favourite: boolean
reblog: boolean
mention: boolean
poll: boolean
status: boolean
update: boolean
}
push: {
global: { loading: boolean; value: boolean }
decode: { loading: boolean; value: boolean }
alerts: {
follow: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['follow']
}
follow_request: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['follow_request']
}
favourite: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['favourite']
}
reblog: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['reblog']
}
mention: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['mention']
}
poll: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['poll']
}
status: {
loading: boolean
value: Mastodon.PushSubscription['alerts']['status']
}
}
keys: {
auth?: string
public?: string // legacy
private?: string // legacy
}
}
timelinesLookback?: {
[key: string]: {
queryKey: QueryKeyTimeline
ids: Mastodon.Status['id'][]
}
}
mePage: {
lists: { shown: boolean }
announcements: { shown: boolean; unread: number }
}
drafts: ComposeStateDraft[]
frequentEmojis: {
emoji: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>
score: number
count: number
lastUsed: number
}[]
}

View File

@ -20,6 +20,10 @@ export type RootStackParamList = {
| {
type: 'notifications_filter'
}
| {
type: 'alt_text'
text: string
}
'Screen-Announcements': { showAll: boolean }
'Screen-Compose':
| {
@ -146,6 +150,7 @@ export type TabMeStackParamList = {
'Tab-Me-Push': undefined
'Tab-Me-Settings': undefined
'Tab-Me-Settings-Fontsize': undefined
'Tab-Me-Settings-Language': undefined
'Tab-Me-Switch': undefined
} & TabSharedStackParamList
export type TabMeStackScreenProps<T extends keyof TabMeStackParamList> =

View File

@ -3,84 +3,102 @@ import apiTooot from '@api/tooot'
import { displayMessage } from '@components/Message'
import navigationRef from '@helpers/navigationRef'
import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment'
import { disableAllPushes, Instance } from '@utils/slices/instancesSlice'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { getExpoToken, retriveExpoToken } from '@utils/slices/appSlice'
import { disableAllPushes } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import { TFunction } from 'react-i18next'
import { AppState } from 'react-native'
import { useSelector } from 'react-redux'
export interface Params {
t: TFunction<'screens'>
instances: Instance[]
instances: InstanceLatest[]
}
const pushUseConnect = ({ t, instances }: Params) => {
const dispatch = useAppDispatch()
const { theme } = useTheme()
useEffect(() => {
dispatch(retriveExpoToken())
}, [])
const expoToken = useSelector(getExpoToken)
const connect = () => {
apiTooot({
method: 'get',
url: `push/connect/${expoToken}`,
sentry: true
}).catch(error => {
if (error?.status == 404) {
displayMessage({
theme,
type: 'error',
duration: 'long',
message: t('pushError.message'),
description: t('pushError.description'),
onPress: () => {
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Root'
}
})
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Settings'
}
})
}
})
dispatch(disableAllPushes())
instances.forEach(instance => {
if (instance.push.global.value) {
apiGeneral<{}>({
method: 'delete',
domain: instance.url,
url: 'api/v1/push/subscription',
headers: {
Authorization: `Bearer ${instance.token}`
}
}).catch(() => console.log('error!!!'))
}
})
}
})
}
const pushEnabled = instances.filter(instance => instance.push.global.value)
useEffect(() => {
const appStateListener = AppState.addEventListener('change', state => {
console.log('changing state to', state)
if (expoToken && pushEnabled.length && state === 'active') {
Notifications.getBadgeCountAsync().then(count => {
if (count > 0) {
Notifications.setBadgeCountAsync(0)
connect()
}
})
}
})
return () => {
appStateListener.remove()
}
}, [expoToken, pushEnabled.length])
return useEffect(() => {
const connect = async () => {
const expoToken = isDevelopment
? 'DEVELOPMENT_TOKEN_1'
: (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot',
applicationId: 'com.xmflsct.app.tooot'
})
).data
apiTooot({
method: 'get',
url: `push/connect/${expoToken}`,
sentry: true
}).catch(error => {
if (error?.status == 404) {
displayMessage({
theme,
type: 'error',
duration: 'long',
message: t('pushError.message'),
description: t('pushError.description'),
onPress: () => {
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Root'
}
})
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Settings'
}
})
}
})
dispatch(disableAllPushes())
instances.forEach(instance => {
if (instance.push.global.value) {
apiGeneral<{}>({
method: 'delete',
domain: instance.url,
url: 'api/v1/push/subscription',
headers: {
Authorization: `Bearer ${instance.token}`
}
}).catch(() => console.log('error!!!'))
}
})
}
})
}
const pushEnabled = instances.filter(instance => instance.push.global.value)
if (pushEnabled.length) {
if (expoToken && pushEnabled.length) {
connect()
}
}, [instances])
}, [expoToken, pushEnabled.length])
}
export default pushUseConnect

View File

@ -1,14 +1,14 @@
import { displayMessage } from '@components/Message'
import queryClient from '@helpers/queryClient'
import initQuery from '@utils/initQuery'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import pushUseNavigate from './useNavigate'
export interface Params {
instances: Instance[]
instances: InstanceLatest[]
}
const pushUseReceive = ({ instances }: Params) => {

View File

@ -1,13 +1,13 @@
import queryClient from '@helpers/queryClient'
import initQuery from '@utils/initQuery'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import pushUseNavigate from './useNavigate'
export interface Params {
instances: Instance[]
instances: InstanceLatest[]
}
const pushUseRespond = ({ instances }: Params) => {

View File

@ -2,7 +2,10 @@ import apiInstance, { InstanceResponse } from '@api/instance'
import haptics from '@components/haptics'
import queryClient from '@helpers/queryClient'
import { store } from '@root/store'
import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
import {
checkInstanceFeature,
getInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios'
import { uniqBy } from 'lodash'
import {
@ -62,16 +65,26 @@ const queryFunction = async ({
case 'Notifications':
const rootStore = store.getState()
const notificationsFilter = getInstanceNotificationsFilter(rootStore)
const usePositiveFilter = checkInstanceFeature(
'notification_types_positive_filter'
)(rootStore)
return apiInstance<Mastodon.Notification[]>({
method: 'get',
url: 'notifications',
params: {
...params,
...(notificationsFilter && {
exclude_types: Object.keys(notificationsFilter)
// @ts-ignore
.filter(filter => notificationsFilter[filter] === false)
})
...(notificationsFilter &&
(usePositiveFilter
? {
types: Object.keys(notificationsFilter)
// @ts-ignore
.filter(filter => notificationsFilter[filter] === true)
}
: {
exclude_types: Object.keys(notificationsFilter)
// @ts-ignore
.filter(filter => notificationsFilter[filter] === false)
}))
}
})
@ -427,8 +440,8 @@ const useTimelineMutation = ({
...(onMutate && {
onMutate: params => {
queryClient.cancelQueries(params.queryKey)
let oldData
params.queryKey && (oldData = queryClient.getQueryData(params.queryKey))
const oldData =
params.queryKey && queryClient.getQueryData(params.queryKey)
haptics('Light')
switch (params.type) {

Some files were not shown because too many files have changed in this diff Show More