mirror of https://github.com/tooot-app/app
commit
84deb2ba58
|
@ -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:
|
||||
|
|
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
39
package.json
39
package.json
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
42
src/App.tsx
42
src/App.tsx
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface Props {
|
|||
switchDisabled?: boolean
|
||||
switchOnValueChange?: () => void
|
||||
|
||||
iconBack?: 'ChevronRight' | 'ExternalLink'
|
||||
iconBack?: 'ChevronRight' | 'ExternalLink' | 'Check'
|
||||
iconBackColor?: ColorDefinitions
|
||||
|
||||
loading?: boolean
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"buttons": {
|
||||
"OK": "",
|
||||
"OK": "OK",
|
||||
"apply": "Übernehmen",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}} hat geboostet",
|
||||
"notification": "{{name}} hat deinen Tröt geboostet"
|
||||
}
|
||||
},
|
||||
"update": ""
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"header": {
|
||||
"title": "Entwurf"
|
||||
},
|
||||
"warning": "",
|
||||
"content": {
|
||||
"accessibilityHint": "Gespeicherter Entwurf, tippe, um diesen zu bearbeiten",
|
||||
"textEmpty": "Kein Inhalt"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"options": {
|
||||
"save": "Bild speichern",
|
||||
"share": "Bild teilen",
|
||||
"cancel": ""
|
||||
"cancel": "$t(common:buttons.cancel)"
|
||||
},
|
||||
"save": {
|
||||
"succeed": "Bild gespeichert",
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}} boosted",
|
||||
"notification": "{{name}} boosted your toot"
|
||||
}
|
||||
},
|
||||
"update": "Reblog has been edited"
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}} ha ricondiviso",
|
||||
"notification": "{{name}} ha ricondiviso il tuo toot"
|
||||
}
|
||||
},
|
||||
"update": ""
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"header": {
|
||||
"title": "Bozza"
|
||||
},
|
||||
"warning": "",
|
||||
"content": {
|
||||
"accessibilityHint": "Bozza salvata, premi per modificarla",
|
||||
"textEmpty": "Testo vuoto"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"buttons": {
|
||||
"OK": "",
|
||||
"OK": "확인",
|
||||
"apply": "적용",
|
||||
"cancel": "취소"
|
||||
},
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}}님이 부스트했어요",
|
||||
"notification": "{{name}}이 내 툿을 부스트했어요"
|
||||
}
|
||||
},
|
||||
"update": ""
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"header": {
|
||||
"title": "초안"
|
||||
},
|
||||
"warning": "",
|
||||
"content": {
|
||||
"accessibilityHint": "저장된 초안, 수정하려면 탭하세요",
|
||||
"textEmpty": "콘텐츠 빔"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const LOCALES = {
|
|||
en: 'English',
|
||||
it: 'Italiano',
|
||||
ko: '한국어',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
vi: 'Tiếng Việt',
|
||||
'zh-Hans': '简体中文'
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
|
@ -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": ", "
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"frequentUsed": "Usados frequentemente"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"HTML": {
|
||||
"expanded": {
|
||||
"true": "Fechar {{hint}}",
|
||||
"false": "Expandir {{hint}}"
|
||||
},
|
||||
"defaultHint": "artigo"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 />"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"heading": "Comunicados",
|
||||
"content": {
|
||||
"published": "Publicado <0 />",
|
||||
"button": {
|
||||
"read": "Ler",
|
||||
"unread": "Marcar como lido"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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..."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}} 转嘟了",
|
||||
"notification": "{{name}} 转嘟了你的嘟文"
|
||||
}
|
||||
},
|
||||
"update": "转嘟已被编辑"
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -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": "转嘟被编辑"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"header": {
|
||||
"title": "草稿"
|
||||
},
|
||||
"warning": "草稿只存储在本地,在部分情况下可能丢失。建议不要长期存储草稿。",
|
||||
"content": {
|
||||
"accessibilityHint": "已保存的草稿,点击编辑此草稿",
|
||||
"textEmpty": "无正文内容"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"reblog": {
|
||||
"default": "{{name}} 轉嘟了",
|
||||
"notification": "{{name}} 轉嘟了您的嘟文"
|
||||
}
|
||||
},
|
||||
"update": ""
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"content": {
|
||||
"button": {
|
||||
"apply": "",
|
||||
"cancel": ""
|
||||
"altText": {
|
||||
"heading": ""
|
||||
},
|
||||
"notificationsFilter": {
|
||||
"heading": "",
|
||||
"content": {
|
||||
"follow": "",
|
||||
"follow_request": "",
|
||||
"favourite": "",
|
||||
"reblog": "",
|
||||
"mention": "",
|
||||
"poll": "",
|
||||
"follow_request": ""
|
||||
"status": "",
|
||||
"update": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"header": {
|
||||
"title": ""
|
||||
},
|
||||
"warning": "",
|
||||
"content": {
|
||||
"accessibilityHint": "",
|
||||
"textEmpty": ""
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/// <reference types="@welldone-software/why-did-you-render" />
|
||||
|
||||
import React from 'react'
|
||||
import log from './log'
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
11
src/store.ts
11
src/store.ts
|
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}[]
|
||||
}
|
|
@ -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> =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue