Merge pull request #478 from tooot-app/main

Release v4.6.2
This commit is contained in:
xmflsct 2022-11-20 23:36:35 +01:00 committed by GitHub
commit c89115b58c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1564 additions and 519 deletions

View File

@ -25,6 +25,8 @@ Please **do not** create a pull request to update translation. tooot's translati
[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese
[@janlindblom](https://github.com/janlindblom) for Swedish
[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation
[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation

View File

@ -0,0 +1,2 @@
"NSPhotoLibraryAddUsageDescription" = "Låt toooot spara bilder på din kamerarulle";
"NSPhotoLibraryUsageDescription" = "Låt toooot spara bilder på din kamerarulle";

View File

@ -76,6 +76,7 @@
E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E633A42F281EAF38000E540F /* ShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ShareViewController.swift"; sourceTree = "<group>"; }; E633A42F281EAF38000E540F /* ShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ShareViewController.swift"; sourceTree = "<group>"; };
E633A431281EB55C000E540F /* ShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ShareExtension-Bridging-Header.h"; sourceTree = "<group>"; }; E633A431281EB55C000E540F /* ShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
E63E7FF0292A828100C76FD4 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E66C0842291F095800DFFF60 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; E66C0842291F095800DFFF60 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E671BDF8290EAFB800287BD0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; E671BDF8290EAFB800287BD0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
E690AF692926B737002C38A8 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; E690AF692926B737002C38A8 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -295,6 +296,7 @@
"zh-Hant", "zh-Hant",
fr, fr,
es, es,
sv,
); );
mainGroup = 83CBB9F61A601CBA00E9B192; mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@ -525,6 +527,7 @@
E671BDF8290EAFB800287BD0 /* zh-Hant */, E671BDF8290EAFB800287BD0 /* zh-Hant */,
E66C0842291F095800DFFF60 /* fr */, E66C0842291F095800DFFF60 /* fr */,
E690AF692926B737002C38A8 /* es */, E690AF692926B737002C38A8 /* es */,
E63E7FF0292A828100C76FD4 /* sv */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";

View File

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

View File

@ -475,7 +475,8 @@ declare namespace Mastodon {
// Base // Base
name: string name: string
url: string url: string
// history: types history: { day: string; accounts: string; uses: string }[]
following: boolean // Since v4.0
} }
type WebSocketStream = type WebSocketStream =

View File

@ -20,6 +20,7 @@ import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { LogBox, Platform } from 'react-native' import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { QueryClientProvider } from 'react-query' import { QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
@ -95,13 +96,15 @@ const App: React.FC = () => {
} }
return ( return (
<ActionSheetProvider> <SafeAreaProvider>
<AccessibilityManager> <ActionSheetProvider>
<ThemeManager> <AccessibilityManager>
<Screens localCorrupt={localCorrupt} /> <ThemeManager>
</ThemeManager> <Screens localCorrupt={localCorrupt} />
</AccessibilityManager> </ThemeManager>
</ActionSheetProvider> </AccessibilityManager>
</ActionSheetProvider>
</SafeAreaProvider>
) )
} else { } else {
return null return null

View File

@ -166,9 +166,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
} }
| { data: string | string[]; mimeType: string } | { data: string | string[]; mimeType: string }
) => { ) => {
if (Platform.OS === 'android') {
return
}
if (instanceActive < 0) { if (instanceActive < 0) {
return return
} }
@ -233,14 +230,18 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
} }
} }
break break
// case 'android': case 'android':
// if (!item.mimeType) { if (!item.mimeType) {
// return return
// } }
// for (const d of item.data) { if (Array.isArray(item.data)) {
// filterMedia({ uri: d, mime: item.mimeType }) for (const d of item.data) {
// } filterMedia({ uri: d, mime: item.mimeType })
// break }
} else {
filterMedia({ uri: item.data, mime: item.mimeType })
}
break
} }
if (!text && !media.length) { if (!text && !media.length) {

View File

@ -4,10 +4,8 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { getColors, Theme } from '@utils/styles/themes' import { getColors, Theme } from '@utils/styles/themes'
import React, { RefObject } from 'react' import React, { RefObject } from 'react'
import { AccessibilityInfo } from 'react-native' import { AccessibilityInfo } from 'react-native'
import FlashMessage, { import FlashMessage, { hideMessage, showMessage } from 'react-native-flash-message'
hideMessage, import { useSafeAreaInsets } from 'react-native-safe-area-context'
showMessage
} from 'react-native-flash-message'
import haptics from './haptics' import haptics from './haptics'
const displayMessage = ({ const displayMessage = ({
@ -112,6 +110,7 @@ const removeMessage = () => {
const Message = React.forwardRef<FlashMessage>((_, ref) => { const Message = React.forwardRef<FlashMessage>((_, ref) => {
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const insets = useSafeAreaInsets()
return ( return (
<FlashMessage <FlashMessage
@ -125,7 +124,8 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: theme === 'light' ? 0.16 : 0.24, shadowOpacity: theme === 'light' ? 0.16 : 0.24,
shadowRadius: 4, shadowRadius: 4,
paddingRight: StyleConstants.Spacing.M * 2 paddingRight: StyleConstants.Spacing.M * 2,
marginTop: insets.top
}} }}
titleStyle={{ titleStyle={{
color: colors.primaryDefault, color: colors.primaryDefault,

View File

@ -17,7 +17,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
import React, { useRef } from 'react' import React, { useRef } from 'react'
import { Pressable, StyleProp, View, ViewStyle } from 'react-native' import { Platform, Pressable, StyleProp, View, ViewStyle } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineContextMenu from './Shared/ContextMenu' import TimelineContextMenu from './Shared/ContextMenu'
import TimelineFeedback from './Shared/Feedback' import TimelineFeedback from './Shared/Feedback'
@ -64,6 +64,7 @@ const TimelineDefault: React.FC<Props> = ({
} }
const onPress = () => { const onPress = () => {
if (highlighted) return
analytics('timeline_default_press', { analytics('timeline_default_press', {
page: queryKey ? queryKey[1].page : origin page: queryKey ? queryKey[1].page : origin
}) })
@ -156,6 +157,15 @@ const TimelineDefault: React.FC<Props> = ({
return disableOnPress ? ( return disableOnPress ? (
<View style={mainStyle}>{main()}</View> <View style={mainStyle}>{main()}</View>
) : Platform.OS === 'android' ? (
<Pressable
accessible={highlighted ? false : true}
style={mainStyle}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
) : ( ) : (
<TimelineContextMenu <TimelineContextMenu
copiableContent={copiableContent} copiableContent={copiableContent}

View File

@ -17,7 +17,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual, uniqBy } from 'lodash' import { isEqual, uniqBy } from 'lodash'
import React, { useCallback, useRef } from 'react' import React, { useCallback, useRef } from 'react'
import { Pressable, View } from 'react-native' import { Platform, Pressable, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineContextMenu from './Shared/ContextMenu' import TimelineContextMenu from './Shared/ContextMenu'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
@ -29,129 +29,146 @@ export interface Props {
highlighted?: boolean highlighted?: boolean
} }
const TimelineNotifications = React.memo( const TimelineNotifications: React.FC<Props> = ({
({ notification, queryKey, highlighted = false }: Props) => { notification,
const copiableContent = useRef<{ content: string; complete: boolean }>({ queryKey,
content: '', highlighted = false
complete: false }) => {
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const filtered =
notification.status &&
shouldFilter({
copiableContent,
status: notification.status,
queryKey
}) })
if (notification.status && filtered) {
return <TimelineFiltered phrase={filtered} />
}
const filtered = const { colors } = useTheme()
notification.status && const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
shouldFilter({ const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
copiableContent,
status: notification.status, const actualAccount = notification.status ? notification.status.account : notification.account
queryKey
const onPress = useCallback(() => {
analytics('timeline_notification_press')
notification.status &&
navigation.push('Tab-Shared-Toot', {
toot: notification.status,
rootQueryKey: queryKey
}) })
if (notification.status && filtered) { }, [])
return <TimelineFiltered phrase={filtered} />
}
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualAccount = notification.status ? notification.status.account : notification.account
const onPress = useCallback(() => {
analytics('timeline_notification_press')
notification.status &&
navigation.push('Tab-Shared-Toot', {
toot: notification.status,
rootQueryKey: queryKey
})
}, [])
const main = () => {
return ( return (
<TimelineContextMenu <>
copiableContent={copiableContent} {notification.type !== 'mention' ? (
status={notification.status} <TimelineActioned
queryKey={queryKey} action={notification.type}
> account={notification.account}
<Pressable notification
/>
) : null}
<View
style={{ style={{
padding: StyleConstants.Spacing.Global.PagePadding, opacity:
backgroundColor: colors.backgroundDefault, notification.type === 'follow' ||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding notification.type === 'follow_request' ||
notification.type === 'mention' ||
notification.type === 'status'
? 1
: 0.5
}} }}
onPress={onPress}
onLongPress={() => {}}
> >
{notification.type !== 'mention' ? ( <View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineActioned <TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
action={notification.type} <TimelineHeaderNotification queryKey={queryKey} notification={notification} />
account={notification.account}
notification
/>
) : null}
<View
style={{
opacity:
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'mention' ||
notification.type === 'status'
? 1
: 0.5
}}
>
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar
queryKey={queryKey}
account={actualAccount}
highlighted={highlighted}
/>
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
</View>
{notification.status ? (
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{notification.status.content.length > 0 ? (
<TimelineContent status={notification.status} highlighted={highlighted} />
) : null}
{notification.status.poll ? (
<TimelinePoll
queryKey={queryKey}
statusId={notification.status.id}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === instanceAccount?.id}
/>
) : null}
{notification.status.media_attachments.length > 0 ? (
<TimelineAttachment status={notification.status} />
) : null}
{notification.status.card ? <TimelineCard card={notification.status.card} /> : null}
<TimelineFullConversation queryKey={queryKey} status={notification.status} />
</View>
) : null}
</View> </View>
{notification.status ? ( {notification.status ? (
<TimelineActions <View
queryKey={queryKey} style={{
status={notification.status} paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
highlighted={highlighted} paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
accts={uniqBy( }}
([notification.status.account] as Mastodon.Account[] & Mastodon.Mention[]) >
.concat(notification.status.mentions) {notification.status.content.length > 0 ? (
.filter(d => d?.id !== instanceAccount?.id), <TimelineContent status={notification.status} highlighted={highlighted} />
d => d?.id ) : null}
).map(d => d?.acct)} {notification.status.poll ? (
reblog={false} <TimelinePoll
/> queryKey={queryKey}
statusId={notification.status.id}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === instanceAccount?.id}
/>
) : null}
{notification.status.media_attachments.length > 0 ? (
<TimelineAttachment status={notification.status} />
) : null}
{notification.status.card ? <TimelineCard card={notification.status.card} /> : null}
<TimelineFullConversation queryKey={queryKey} status={notification.status} />
</View>
) : null} ) : null}
</Pressable> </View>
</TimelineContextMenu>
{notification.status ? (
<TimelineActions
queryKey={queryKey}
status={notification.status}
highlighted={highlighted}
accts={uniqBy(
([notification.status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(notification.status.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={false}
/>
) : null}
</>
) )
}, }
(prev, next) => isEqual(prev.notification, next.notification)
) return Platform.OS === 'android' ? (
<Pressable
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
}}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
) : (
<TimelineContextMenu
copiableContent={copiableContent}
status={notification.status}
queryKey={queryKey}
>
<Pressable
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
}}
onPress={onPress}
onLongPress={() => {}}
>
{main()}
</Pressable>
</TimelineContextMenu>
)
}
export default TimelineNotifications export default TimelineNotifications

View File

@ -5,8 +5,8 @@ import { Slider } from '@sharcoux/slider'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { Audio } from 'expo-av' import { Audio } from 'expo-av'
import React, { useCallback, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { AppState, AppStateStatus, StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import attachmentAspectRatio from './aspectRatio'
@ -18,12 +18,7 @@ export interface Props {
audio: Mastodon.AttachmentAudio audio: Mastodon.AttachmentAudio
} }
const AttachmentAudio: React.FC<Props> = ({ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio }) => {
total,
index,
sensitiveShown,
audio
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const [audioPlayer, setAudioPlayer] = useState<Audio.Sound>() const [audioPlayer, setAudioPlayer] = useState<Audio.Sound>()
@ -51,6 +46,20 @@ const AttachmentAudio: React.FC<Props> = ({
setAudioPlaying(false) setAudioPlaying(false)
}, [audioPlayer]) }, [audioPlayer])
const appState = useRef(AppState.currentState)
useEffect(() => {
const appState = AppState.addEventListener('change', _handleAppStateChange)
return () => appState.remove()
}, [])
const _handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (appState.current.match(/active/) && nextAppState.match(/inactive/)) {
await audioPlayer?.stopAsync()
}
appState.current = nextAppState
}
return ( return (
<View <View
accessibilityLabel={audio.description} accessibilityLabel={audio.description}
@ -90,9 +99,7 @@ const AttachmentAudio: React.FC<Props> = ({
size='L' size='L'
round round
overlay overlay
{...(audioPlaying {...(audioPlaying ? { onPress: pauseAudio } : { onPress: playAudio })}
? { onPress: pauseAudio }
: { onPress: playAudio })}
/> />
</> </>
)} )}
@ -128,10 +135,7 @@ const AttachmentAudio: React.FC<Props> = ({
/> />
</View> </View>
) : null} ) : null}
<AttachmentAltText <AttachmentAltText sensitiveShown={sensitiveShown} text={audio.description} />
sensitiveShown={sensitiveShown}
text={audio.description}
/>
</View> </View>
) )
} }

View File

@ -64,7 +64,7 @@ const AttachmentVideo: React.FC<Props> = ({
}, []) }, [])
const _handleAppStateChange = async (nextAppState: AppStateStatus) => { const _handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (appState.current.match(/active/) && nextAppState.match(/inactive/)) { if (appState.current.match(/active/) && nextAppState.match(/inactive/)) {
await videoPlayer.current?.pauseAsync() await videoPlayer.current?.stopAsync()
} else if (gifv && appState.current.match(/background/) && nextAppState.match(/active/)) { } else if (gifv && appState.current.match(/background/) && nextAppState.match(/active/)) {
await videoPlayer.current?.setIsMutedAsync(true) await videoPlayer.current?.setIsMutedAsync(true)
await videoPlayer.current?.playAsync() await videoPlayer.current?.playAsync()

View File

@ -15,24 +15,10 @@ const HeaderSharedVisibility = React.memo(
const { colors } = useTheme() const { colors } = useTheme()
switch (visibility) { switch (visibility) {
case 'public':
return (
<Icon
accessibilityLabel={t(
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Globe'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={styles.visibility}
/>
)
case 'unlisted': case 'unlisted':
return ( return (
<Icon <Icon
accessibilityLabel={t( accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Unlock' name='Unlock'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={colors.secondary} color={colors.secondary}
@ -42,9 +28,7 @@ const HeaderSharedVisibility = React.memo(
case 'private': case 'private':
return ( return (
<Icon <Icon
accessibilityLabel={t( accessibilityLabel={t('shared.header.shared.visibility.private.accessibilityLabel')}
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Lock' name='Lock'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={colors.secondary} color={colors.secondary}
@ -54,9 +38,7 @@ const HeaderSharedVisibility = React.memo(
case 'direct': case 'direct':
return ( return (
<Icon <Icon
accessibilityLabel={t( accessibilityLabel={t('shared.header.shared.visibility.direct.accessibilityLabel')}
'shared.header.shared.visibility.direct.accessibilityLabel'
)}
name='Mail' name='Mail'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={colors.secondary} color={colors.secondary}

View File

@ -23,5 +23,10 @@
"feature": "notification_types_positive_filter", "feature": "notification_types_positive_filter",
"version": 3.5, "version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0" "reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "follow_tags",
"version": 4.0,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v4.0.0"
} }
] ]

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"s Medien</1>" "name": "<0 /><1>\"s Medien</1>"
}, },
"hashtag": {
"follow": "Folgen",
"unfollow": "Entfolgen"
},
"history": {
"name": "Bearbeitungsverlauf"
},
"search": { "search": {
"header": { "header": {
"prefix": "Suche", "prefix": "Suche",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} mal geboosted", "reblogged_by": "{{count}} mal geboosted",
"favourited_by": "{{count}} mal geherzt" "favourited_by": "{{count}} mal geherzt"
} }
},
"history": {
"name": "Bearbeitungsverlauf"
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"s media</1>" "name": "<0 /><1>\"s media</1>"
}, },
"hashtag": {
"follow": "Follow",
"unfollow": "Unfollow"
},
"history": {
"name": "Edit History"
},
"search": { "search": {
"header": { "header": {
"prefix": "Searching", "prefix": "Searching",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boosted", "reblogged_by": "{{count}} boosted",
"favourited_by": "{{count}} favourited" "favourited_by": "{{count}} favourited"
} }
},
"history": {
"name": "Edit History"
} }
} }
} }

View File

@ -9,7 +9,7 @@
}, },
"message": { "message": {
"success": { "success": {
"message": "" "message": "{{function}} con éxito"
}, },
"warning": { "warning": {
"message": "" "message": ""

View File

@ -1,5 +1,5 @@
{ {
"accessibilityHint": "", "accessibilityHint": "Acciones para este toot, como su usuario o el toot en sí mismo",
"account": { "account": {
"title": "Acciones de usuario", "title": "Acciones de usuario",
"mute": { "mute": {

View File

@ -8,7 +8,7 @@
"name": "Nombre", "name": "Nombre",
"accounts": "Usuarios", "accounts": "Usuarios",
"statuses": "Toots", "statuses": "Toots",
"domains": "" "domains": "Universos"
}, },
"disclaimer": { "disclaimer": {
"base": "El inicio de sesión usa el navegador del sistema, y la información de la cuenta no será visible para tooot." "base": "El inicio de sesión usa el navegador del sistema, y la información de la cuenta no será visible para tooot."

View File

@ -1,5 +1,5 @@
{ {
"title": "", "title": "Elegir origen multimedia",
"message": "Los datos multimedia EXIF no se han subido", "message": "Los datos multimedia EXIF no se han subido",
"options": { "options": {
"image": "Subir fotos", "image": "Subir fotos",

View File

@ -27,7 +27,7 @@
"follow_request": "{{name}} ha solicitado seguirte", "follow_request": "{{name}} ha solicitado seguirte",
"poll": "Una encuesta en la que has votado ha terminado", "poll": "Una encuesta en la que has votado ha terminado",
"reblog": { "reblog": {
"default": "{{name}} impulsado", "default": "{{name}} ha impulsado",
"notification": "{{name}} ha impulsado tu toot" "notification": "{{name}} ha impulsado tu toot"
}, },
"update": "El impulso ha sido editado" "update": "El impulso ha sido editado"
@ -47,11 +47,11 @@
}, },
"favourited": { "favourited": {
"accessibilityLabel": "Agregar este toot en favoritos", "accessibilityLabel": "Agregar este toot en favoritos",
"function": "" "function": "Marcar toot como favorito"
}, },
"bookmarked": { "bookmarked": {
"accessibilityLabel": "Añadir este toot en marcadores", "accessibilityLabel": "Añadir este toot en marcadores",
"function": "" "function": "Añadir toot a marcadores"
} }
}, },
"actionsUsers": { "actionsUsers": {
@ -78,7 +78,7 @@
}, },
"unsupported": { "unsupported": {
"text": "Error al cargar", "text": "Error al cargar",
"button": "" "button": "Probar con el enlace remoto"
} }
}, },
"avatar": { "avatar": {
@ -101,7 +101,7 @@
"shared": { "shared": {
"account": { "account": {
"name": { "name": {
"accessibilityHint": "" "accessibilityHint": "Nombre de usuario"
}, },
"account": { "account": {
"accessibilityHint": "Cuenta de usuario" "accessibilityHint": "Cuenta de usuario"

View File

@ -1,7 +1,7 @@
{ {
"screenshot": { "screenshot": {
"title": "Protección de la privacidad", "title": "Protección de la privacidad",
"message": "Por favor, no revele la identidad de otros usuarios, como nombre de usuario, avatar, etc. ¡Gracias!", "message": "Por favor, no revele la identidad de otros usuarios, como el nombre de usuario, avatar, etc. ¡Gracias!",
"button": "Confirmar" "button": "Confirmar"
}, },
"localCorrupt": { "localCorrupt": {

View File

@ -117,8 +117,8 @@
}, },
"fields": { "fields": {
"title": "Metadatos", "title": "Metadatos",
"total_one": "Campo {{count}}", "total_one": "{{count}} campo",
"total_other": "Campos {{count}}" "total_other": "{{count}} campos"
}, },
"visibility": { "visibility": {
"title": "Visibilidad de publicación", "title": "Visibilidad de publicación",
@ -134,7 +134,7 @@
}, },
"lock": { "lock": {
"title": "Bloquear cuenta", "title": "Bloquear cuenta",
"description": "" "description": "Necesitas aprobar manualmente los seguidores nuevos"
}, },
"bot": { "bot": {
"title": "Cuenta bot", "title": "Cuenta bot",
@ -156,11 +156,11 @@
}, },
"global": { "global": {
"heading": "Habilitar para {{acct}}", "heading": "Habilitar para {{acct}}",
"description": "" "description": "Los mensajes se envían a través del servidor de tooot"
}, },
"decode": { "decode": {
"heading": "Mostrar detalles del mensaje", "heading": "Mostrar detalles del mensaje",
"description": "" "description": "Los mensajes que se envían a través del servidor de tooot están encriptados, pero puedes optar por desencriptarlos en el servidor. El servidor es de código abierto y tenemos una política de cero registros."
}, },
"default": { "default": {
"heading": "Predeterminado" "heading": "Predeterminado"
@ -172,7 +172,7 @@
"heading": "Solicitud de seguimiento" "heading": "Solicitud de seguimiento"
}, },
"favourite": { "favourite": {
"heading": "" "heading": "Favoritos"
}, },
"reblog": { "reblog": {
"heading": "Impulsado" "heading": "Impulsado"
@ -186,14 +186,14 @@
"status": { "status": {
"heading": "Toot de usuarios suscritos" "heading": "Toot de usuarios suscritos"
}, },
"howitworks": "" "howitworks": "Más información sobre las notificaciones push"
}, },
"root": { "root": {
"announcements": { "announcements": {
"content": { "content": {
"unread": "{{amount}} no leídos", "unread": "{{amount}} no leídos",
"read": "", "read": "Todo leído",
"empty": "" "empty": "Nada"
} }
}, },
"push": { "push": {
@ -203,7 +203,7 @@
} }
}, },
"update": { "update": {
"title": "" "title": "Actualizar a la última versión"
}, },
"logout": { "logout": {
"button": "Cerrar sesión", "button": "Cerrar sesión",
@ -310,10 +310,17 @@
"attachments": { "attachments": {
"name": "" "name": ""
}, },
"hashtag": {
"follow": "Seguir",
"unfollow": "Dejar de seguir"
},
"history": {
"name": "Historial de ediciones"
},
"search": { "search": {
"header": { "header": {
"prefix": "Buscando", "prefix": "Buscando",
"placeholder": "" "placeholder": "algo..."
}, },
"empty": { "empty": {
"general": "Escribe para buscar <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold> o <bold>$t(screenTabs:shared.search.sections.statuses)</bold>", "general": "Escribe para buscar <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold> o <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
@ -332,23 +339,20 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "" "notFound": "No se pudo encontrar <bold>{{searchTerm}}</bold> relacionado con {{type}}"
}, },
"toot": { "toot": {
"name": "" "name": "Discusiones"
}, },
"users": { "users": {
"accounts": { "accounts": {
"following": "Siguiendo {{count}}", "following": "{{count}} seguidos",
"followers": "{{count}} seguidores" "followers": "{{count}} seguidores"
}, },
"statuses": { "statuses": {
"reblogged_by": "{{count}} impulsados", "reblogged_by": "{{count}} impulsados",
"favourited_by": "{{count}} favoritos" "favourited_by": "{{count}} favoritos"
} }
},
"history": {
"name": ""
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"s media</1>" "name": "<0 /><1>\"s media</1>"
}, },
"hashtag": {
"follow": "Suivre",
"unfollow": "Ne plus suivre"
},
"history": {
"name": "Modifier l'historique"
},
"search": { "search": {
"header": { "header": {
"prefix": "Recherche en cours", "prefix": "Recherche en cours",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boosté", "reblogged_by": "{{count}} boosté",
"favourited_by": "{{count}} mis en favori" "favourited_by": "{{count}} mis en favori"
} }
},
"history": {
"name": "Modifier l'historique"
} }
} }
} }

View File

@ -9,6 +9,7 @@ import it from '@root/i18n/it'
import ja from '@root/i18n/ja' import ja from '@root/i18n/ja'
import ko from '@root/i18n/ko' import ko from '@root/i18n/ko'
import pt_BR from '@root/i18n/pt_BR' import pt_BR from '@root/i18n/pt_BR'
import sv from '@root/i18n/sv'
import vi from '@root/i18n/vi' import vi from '@root/i18n/vi'
import zh_Hans from '@root/i18n/zh-Hans' import zh_Hans from '@root/i18n/zh-Hans'
import zh_Hant from '@root/i18n/zh-Hant' import zh_Hant from '@root/i18n/zh-Hant'
@ -25,6 +26,7 @@ import '@formatjs/intl-pluralrules/locale-data/it'
import '@formatjs/intl-pluralrules/locale-data/ja' import '@formatjs/intl-pluralrules/locale-data/ja'
import '@formatjs/intl-pluralrules/locale-data/ko' import '@formatjs/intl-pluralrules/locale-data/ko'
import '@formatjs/intl-pluralrules/locale-data/pt' import '@formatjs/intl-pluralrules/locale-data/pt'
import '@formatjs/intl-pluralrules/locale-data/sv'
import '@formatjs/intl-pluralrules/locale-data/vi' import '@formatjs/intl-pluralrules/locale-data/vi'
import '@formatjs/intl-pluralrules/locale-data/zh' import '@formatjs/intl-pluralrules/locale-data/zh'
@ -37,6 +39,7 @@ import '@formatjs/intl-numberformat/locale-data/it'
import '@formatjs/intl-numberformat/locale-data/ja' import '@formatjs/intl-numberformat/locale-data/ja'
import '@formatjs/intl-numberformat/locale-data/ko' import '@formatjs/intl-numberformat/locale-data/ko'
import '@formatjs/intl-numberformat/locale-data/pt' import '@formatjs/intl-numberformat/locale-data/pt'
import '@formatjs/intl-numberformat/locale-data/sv'
import '@formatjs/intl-numberformat/locale-data/vi' import '@formatjs/intl-numberformat/locale-data/vi'
import '@formatjs/intl-numberformat/locale-data/zh-Hans' import '@formatjs/intl-numberformat/locale-data/zh-Hans'
import '@formatjs/intl-numberformat/locale-data/zh-Hant' import '@formatjs/intl-numberformat/locale-data/zh-Hant'
@ -50,6 +53,7 @@ import '@formatjs/intl-datetimeformat/locale-data/it'
import '@formatjs/intl-datetimeformat/locale-data/ja' import '@formatjs/intl-datetimeformat/locale-data/ja'
import '@formatjs/intl-datetimeformat/locale-data/ko' import '@formatjs/intl-datetimeformat/locale-data/ko'
import '@formatjs/intl-datetimeformat/locale-data/pt' import '@formatjs/intl-datetimeformat/locale-data/pt'
import '@formatjs/intl-datetimeformat/locale-data/sv'
import '@formatjs/intl-datetimeformat/locale-data/vi' import '@formatjs/intl-datetimeformat/locale-data/vi'
import '@formatjs/intl-datetimeformat/locale-data/zh-Hans' import '@formatjs/intl-datetimeformat/locale-data/zh-Hans'
import '@formatjs/intl-datetimeformat/locale-data/zh-Hant' import '@formatjs/intl-datetimeformat/locale-data/zh-Hant'
@ -64,6 +68,7 @@ import '@formatjs/intl-relativetimeformat/locale-data/it'
import '@formatjs/intl-relativetimeformat/locale-data/ja' import '@formatjs/intl-relativetimeformat/locale-data/ja'
import '@formatjs/intl-relativetimeformat/locale-data/ko' import '@formatjs/intl-relativetimeformat/locale-data/ko'
import '@formatjs/intl-relativetimeformat/locale-data/pt' import '@formatjs/intl-relativetimeformat/locale-data/pt'
import '@formatjs/intl-relativetimeformat/locale-data/sv'
import '@formatjs/intl-relativetimeformat/locale-data/vi' import '@formatjs/intl-relativetimeformat/locale-data/vi'
import '@formatjs/intl-relativetimeformat/locale-data/zh-Hans' import '@formatjs/intl-relativetimeformat/locale-data/zh-Hans'
import '@formatjs/intl-relativetimeformat/locale-data/zh-Hant' import '@formatjs/intl-relativetimeformat/locale-data/zh-Hant'
@ -84,6 +89,7 @@ i18n.use(initReactI18next).init({
ja, ja,
ko, ko,
'pt-BR': pt_BR, 'pt-BR': pt_BR,
sv,
vi, vi,
'zh-Hans': zh_Hans, 'zh-Hans': zh_Hans,
'zh-Hant': zh_Hant 'zh-Hant': zh_Hant

View File

@ -1,6 +1,6 @@
{ {
"title": "Seleziona origine media", "title": "Seleziona origine media",
"message": "", "message": "I dati EXIF multimediali non sono stati caricati",
"options": { "options": {
"image": "Carica foto", "image": "Carica foto",
"image_max": "Carica foto (massimo {{max}})", "image_max": "Carica foto (massimo {{max}})",

View File

@ -1,8 +1,8 @@
{ {
"HTML": { "HTML": {
"accessibilityHint": "", "accessibilityHint": "Tocca per espandere o comprimere il contenuto",
"expanded": "{{hint}}{{moreLines}}", "expanded": "{{hint}}{{moreLines}}",
"moreLines": "", "moreLines": " (altre{{count}} linee)",
"defaultHint": "" "defaultHint": "Toot lungo"
} }
} }

View File

@ -7,10 +7,10 @@
}, },
"button": { "button": {
"error": "Errore di caricamento", "error": "Errore di caricamento",
"blocked_by": "L'utente ti ha bloccato", "blocked_by": "Bloccato dall'utente",
"blocking": "Sblocca", "blocking": "Sblocca",
"following": "Smetti di seguire", "following": "Smetti di seguire",
"requested": "Annulla richiesta di seguire", "requested": "Annulla la richiesta",
"default": "Segui" "default": "Segui"
} }
} }

View File

@ -16,21 +16,21 @@
}, },
"refresh": { "refresh": {
"fetchPreviousPage": "Più recenti da qui", "fetchPreviousPage": "Più recenti da qui",
"refetch": "Vai a recenti" "refetch": "Al più recente"
}, },
"shared": { "shared": {
"actioned": { "actioned": {
"pinned": "Fissato in cima", "pinned": "Fissato in cima",
"favourite": "{{name}} ha apprezzato il tuo toot", "favourite": "{{name}} ha apprezzato il tuo toot",
"status": "{{name}} ha appena pubblicato", "status": "{{name}} ha appena pubblicato",
"follow": "{{name}} ti sta seguendo", "follow": "{{name}} ti segue",
"follow_request": "{{name}} ha chiesto di seguirti", "follow_request": "{{name}} ha chiesto di seguirti",
"poll": "Un sondaggio in cui hai votato è terminato", "poll": "Un sondaggio in cui hai votato è terminato",
"reblog": { "reblog": {
"default": "{{name}} ha ricondiviso", "default": "{{name}} ha ricondiviso",
"notification": "{{name}} ha ricondiviso il tuo toot" "notification": "{{name}} ha ricondiviso il tuo toot"
}, },
"update": "Retoot ha subito una modifica" "update": "Il link è stato modificato"
}, },
"actions": { "actions": {
"reply": { "reply": {
@ -40,9 +40,9 @@
"accessibilityLabel": "Ricondividi questo toot", "accessibilityLabel": "Ricondividi questo toot",
"function": "Ricondividi toot", "function": "Ricondividi toot",
"options": { "options": {
"title": "", "title": "Scegli visibilità boost",
"public": "", "public": "Boost pubblico",
"unlisted": "" "unlisted": "Rimuovi boost dall'elenco"
} }
}, },
"favourited": { "favourited": {
@ -88,7 +88,7 @@
"content": { "content": {
"expandHint": "Contenuto nascosto" "expandHint": "Contenuto nascosto"
}, },
"filtered": "", "filtered": "Filtrato: {{phrase}}.",
"fullConversation": "Leggi la conversazione", "fullConversation": "Leggi la conversazione",
"translate": { "translate": {
"default": "Traduci", "default": "Traduci",

View File

@ -1,6 +1,6 @@
{ {
"heading": "Condividi con...", "heading": "Condividi con...",
"content": { "content": {
"select_account": "Seleziona conto" "select_account": "Seleziona profilo"
} }
} }

View File

@ -7,7 +7,7 @@
"heading": "Filtra notifiche per tipo", "heading": "Filtra notifiche per tipo",
"content": { "content": {
"follow": "$t(screenTabs:me.push.follow.heading)", "follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Richieste di seguirti", "follow_request": "Richiesta di seguirti",
"favourite": "$t(screenTabs:me.push.favourite.heading)", "favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)", "reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)", "mention": "$t(screenTabs:me.push.mention.heading)",

View File

@ -3,7 +3,7 @@
"content": { "content": {
"published": "Pubblicato <0 />", "published": "Pubblicato <0 />",
"button": { "button": {
"read": "Già letto", "read": "Letto",
"unread": "Segna come letto" "unread": "Segna come letto"
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"heading": { "heading": {
"left": { "left": {
"button": "Scarta", "button": "Annulla",
"alert": { "alert": {
"title": "Scartare il toot?", "title": "Scartare il toot?",
"buttons": { "buttons": {
"save": "Salva come bozza", "save": "Salva bozza",
"delete": "Scarta ed esci", "delete": "Elimina bozza",
"cancel": "Annulla" "cancel": "Annulla"
} }
} }
@ -22,14 +22,14 @@
}, },
"alert": { "alert": {
"default": { "default": {
"title": "Invio fallito", "title": "Pubblicazione fallita",
"button": "Riprova" "button": "Riprova"
}, },
"removeReply": { "removeReply": {
"title": "Impossibile trovare il toot a cui si è risposto", "title": "Impossibile trovare il toot a cui si è risposto",
"description": "Il toot a cui si è risposto potrebbe essere stato cancellato. Vuoi rimuoverne il riferimento?", "description": "Il toot a cui si è risposto potrebbe essere stato cancellato. Vuoi rimuoverne il riferimento?",
"cancel": "$t(common:buttons.cancel)", "cancel": "$t(common:buttons.cancel)",
"confirm": "Rimuovi" "confirm": "Rimuovi riferimento"
} }
} }
} }
@ -37,7 +37,7 @@
"content": { "content": {
"root": { "root": {
"header": { "header": {
"postingAs": "Pubblicando come @{{acct}}@{{domain}}", "postingAs": "Pubblica come @{{acct}}@{{domain}}",
"spoilerInput": { "spoilerInput": {
"placeholder": "Avviso di spoiler" "placeholder": "Avviso di spoiler"
}, },
@ -65,24 +65,24 @@
} }
}, },
"emojis": { "emojis": {
"accessibilityHint": "Premi per aggiungere delle emoji a questo toot" "accessibilityHint": "Tocca per aggiungere emoji al toot"
}, },
"poll": { "poll": {
"option": { "option": {
"placeholder": { "placeholder": {
"accessibilityLabel": "Scelta di sondaggio {{index}}", "accessibilityLabel": "Opzione sondaggio {{index}}",
"single": "Risposta singola", "single": "Risposta singola",
"multiple": "Risposta multipla" "multiple": "Risposta multipla"
} }
}, },
"quantity": { "quantity": {
"reduce": { "reduce": {
"accessibilityLabel": "Riduci le scelte del sondaggio a {{amount}}", "accessibilityLabel": "Riduce le opzioni del sondaggio a {{amount}}",
"accessibilityHint": "Hai raggiunto il minimo di scelte possibili per il sondaggio ({{amount}})" "accessibilityHint": "Quantità minima di opzioni del sondaggio raggiunta, attualmente ne ha {{amount}}"
}, },
"increase": { "increase": {
"accessibilityLabel": "Aumenta le scelte del sondaggio a {{amount}}", "accessibilityLabel": "Aumenta le opzioni del sondaggio a {{amount}}",
"accessibilityHint": "Hai raggiunto il massimo di scelte possibili per il sondaggio ({{amount}})" "accessibilityHint": "Massima quantità di opzioni del sondaggio raggiunta, attualmente ne ha {{amount}}"
} }
}, },
"multiple": { "multiple": {

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "Media di <0 /><1>\"</1>" "name": "Media di <0 /><1>\"</1>"
}, },
"hashtag": {
"follow": "Segui",
"unfollow": "Smetti di seguire"
},
"history": {
"name": "Cronologia delle modifiche"
},
"search": { "search": {
"header": { "header": {
"prefix": "Cerca", "prefix": "Cerca",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} ricondivisioni", "reblogged_by": "{{count}} ricondivisioni",
"favourited_by": "{{count}} apprezzamenti" "favourited_by": "{{count}} apprezzamenti"
} }
},
"history": {
"name": "Cronologia delle modifiche"
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\" のメディア</1>" "name": "<0 /><1>\" のメディア</1>"
}, },
"hashtag": {
"follow": "フォロー",
"unfollow": "フォローをやめる"
},
"history": {
"name": "編集履歴"
},
"search": { "search": {
"header": { "header": {
"prefix": "検索", "prefix": "検索",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} ブースト", "reblogged_by": "{{count}} ブースト",
"favourited_by": "{{count}} お気に入り" "favourited_by": "{{count}} お気に入り"
} }
},
"history": {
"name": "編集履歴"
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"의 미디어</1>" "name": "<0 /><1>\"의 미디어</1>"
}, },
"hashtag": {
"follow": "팔로우",
"unfollow": "팔로우 해제"
},
"history": {
"name": "수정 이력"
},
"search": { "search": {
"header": { "header": {
"prefix": "무엇을", "prefix": "무엇을",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} 부스트함", "reblogged_by": "{{count}} 부스트함",
"favourited_by": "{{count}} 즐겨찾기함" "favourited_by": "{{count}} 즐겨찾기함"
} }
},
"history": {
"name": "수정 이력"
} }
} }
} }

View File

@ -7,6 +7,7 @@ const LOCALES = {
ja: '日本語', ja: '日本語',
ko: '한국어', ko: '한국어',
'pt-br': 'Português (Brasil)', 'pt-br': 'Português (Brasil)',
sv: 'Svenska',
vi: 'Tiếng Việt', vi: 'Tiếng Việt',
'zh-hans': '简体中文', 'zh-hans': '简体中文',
'zh-hant': '繁體中文' 'zh-hant': '繁體中文'

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "" "name": ""
}, },
"hashtag": {
"follow": "",
"unfollow": ""
},
"history": {
"name": ""
},
"search": { "search": {
"header": { "header": {
"prefix": "", "prefix": "",
@ -346,9 +353,6 @@
"reblogged_by": "", "reblogged_by": "",
"favourited_by": "" "favourited_by": ""
} }
},
"history": {
"name": ""
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>\"s mídia</1>" "name": "<0 /><1>\"s mídia</1>"
}, },
"hashtag": {
"follow": "Seguir",
"unfollow": "Deixar de seguir"
},
"history": {
"name": "Histórico de Edição"
},
"search": { "search": {
"header": { "header": {
"prefix": "Procurando", "prefix": "Procurando",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boostou", "reblogged_by": "{{count}} boostou",
"favourited_by": "{{count}} favoritados" "favourited_by": "{{count}} favoritados"
} }
},
"history": {
"name": "Histórico de Edição"
} }
} }
} }

22
src/i18n/sv/common.json Normal file
View File

@ -0,0 +1,22 @@
{
"buttons": {
"OK": "OK",
"apply": "Verkställ",
"cancel": "Avbryt"
},
"customEmoji": {
"accessibilityLabel": "Anpassad emoji {{emoji}}"
},
"message": {
"success": {
"message": "{{function}} lyckades"
},
"warning": {
"message": ""
},
"error": {
"message": "{{function}} misslyckades, försök igen"
}
},
"separator": ", "
}

View File

@ -0,0 +1,76 @@
{
"accessibilityHint": "Åtgärder för detta inlägg, som t.ex. dess författare, själva inlägget",
"account": {
"title": "Användaråtgärder",
"mute": {
"action_false": "Tysta användare",
"action_true": "Sluta tysta användare"
},
"block": {
"action_false": "Blockera användare",
"action_true": "Avblockera användare"
},
"reports": {
"action": "Rapportera och blockera"
}
},
"copy": {
"action": "Kopiera inlägg",
"succeed": "Kopierat"
},
"instance": {
"title": "Instansåtgärd",
"block": {
"action": "Blockera instansen {{instance}}",
"alert": {
"title": "Bekräfta blockering av instansen {{instance}}?",
"message": "Oftast kan du tysta eller blockera vissa användare.\n\nEfter blockering av en instans kommer allt innehåll inklusive följare från denna instans att tas bort!",
"buttons": {
"confirm": "Bekräfta"
}
}
}
},
"share": {
"status": {
"action": "Dela inlägg"
},
"account": {
"action": "Dela användare"
}
},
"status": {
"title": "Inläggsåtgärder",
"edit": {
"action": "Redigera inlägg"
},
"delete": {
"action": "Radera inlägg",
"alert": {
"title": "Bekräfta radering?",
"message": "Alla boostar och favoritmarkeringar kommer tas bort, inklusive alla svar.",
"buttons": {
"confirm": "Bekräfta"
}
}
},
"deleteEdit": {
"action": "Radera inlägg och posta på nytt",
"alert": {
"title": "Bekräfta radera inlägg och posta på nytt?",
"message": "Alla boostar och favoritmarkeringar kommer tas bort, inklusive alla svar.",
"buttons": {
"confirm": "Bekräfta"
}
}
},
"mute": {
"action_false": "Tysta inlägg och svar",
"action_true": "Sluta tysta inlägg och svar"
},
"pin": {
"action_false": "Fäst inlägg",
"action_true": "Avfäst inlägg"
}
}
}

View File

@ -0,0 +1,3 @@
{
"frequentUsed": "Ofta använda"
}

View File

@ -0,0 +1,30 @@
{
"server": {
"textInput": {
"placeholder": "Domän för instansen"
},
"button": "Logga in",
"information": {
"name": "Namn",
"accounts": "Användare",
"statuses": "Inlägg",
"domains": "Universum"
},
"disclaimer": {
"base": "Inloggningsprocessen använder systemets webbläsare så ingen av din kontoinformation är synlig för appen."
},
"terms": {
"base": "Genom att logga in godkänner du <0> integritetspolicyn</0> och <1>användarvillkoren</1>."
}
},
"update": {
"alert": {
"title": "Inloggad till denna instans",
"message": "Du kan logga in på ett annat konto och behålla redan inloggade konton",
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"continue": "Fortsätt"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"title": "Välj mediakälla",
"message": "EXIF-data för media laddas inte upp",
"options": {
"image": "Ladda upp bilder",
"image_max": "Ladda upp bilder (max {{max}})",
"video": "Ladda upp video",
"video_max": "Ladda upp video (max {{max}})"
}
}

View File

@ -0,0 +1,8 @@
{
"HTML": {
"accessibilityHint": "Tryck för att utöka eller dölja innehåll",
"expanded": "{{hint}}{{moreLines}}",
"moreLines": " ({{count}} rader till)",
"defaultHint": "Långt inlägg"
}
}

View File

@ -0,0 +1,16 @@
{
"follow": {
"function": "Följ användare"
},
"block": {
"function": "Blockera användare"
},
"button": {
"error": "Laddningsfel",
"blocked_by": "Blockerad av användare",
"blocking": "Avblockera",
"following": "Sluta följ",
"requested": "Återkalla förfrågan",
"default": "Följ"
}
}

View File

@ -0,0 +1,152 @@
{
"empty": {
"error": {
"message": "Laddningsfel",
"button": "Försök igen"
},
"success": {
"message": "Tidslinjen är tom"
}
},
"end": {
"message": "Slut, vad sägs om en kopp <0 />"
},
"lookback": {
"message": "Senast läst"
},
"refresh": {
"fetchPreviousPage": "Nyare härifrån",
"refetch": "Till senaste"
},
"shared": {
"actioned": {
"pinned": "Fäst",
"favourite": "{{name}} favoritmarkerade ditt inlägg",
"status": "{{name}} publicerade just ett inlägg",
"follow": "{{name}} följde dig",
"follow_request": "{{name}} begärde att få följa dig",
"poll": "En omröstning du röstat i har avslutats",
"reblog": {
"default": "{{name}} boostade",
"notification": "{{name}} boostade ditt inlägg"
},
"update": "Boosten har redigerats"
},
"actions": {
"reply": {
"accessibilityLabel": "Svara på det här inlägget"
},
"reblogged": {
"accessibilityLabel": "Boosta det här inlägget",
"function": "Boosta inlägg",
"options": {
"title": "Välj synlighet på boost",
"public": "Offentlig boost",
"unlisted": "Olistad boost"
}
},
"favourited": {
"accessibilityLabel": "Lägg till detta inlägg till favoriter",
"function": "Favoritmarkera inlägg"
},
"bookmarked": {
"accessibilityLabel": "Lägg till detta inlägg till dina bokmärken",
"function": "Bokmärk inlägg"
}
},
"actionsUsers": {
"reblogged_by": {
"accessibilityLabel": "{{count}} användare har boostat det här inlägget",
"accessibilityHint": "Tryck för att lära känna användarna",
"text": "$t(screenTabs:shared.users.statuses.reblogged_by)"
},
"favourited_by": {
"accessibilityLabel": "{{count}} användare har favoritmarkerat det här inlägget",
"accessibilityHint": "Tryck för att lära känna användarna",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
},
"history": {
"accessibilityLabel": "Detta inlägg har redigerats {{count}} gånger",
"accessibilityHint": "Tryck för att visa hela redigeringshistoriken",
"text_one": "{{count}} redigering",
"text_other": "{{count}} redigeringar"
}
},
"attachment": {
"sensitive": {
"button": "Visa känslig media"
},
"unsupported": {
"text": "Laddningsfel",
"button": "Prova fjärrlänk"
}
},
"avatar": {
"accessibilityLabel": "{{name}}s avatar",
"accessibilityHint": "Tryck för att gå till {{name}}s sida"
},
"content": {
"expandHint": "Dolt innehåll"
},
"filtered": "Filtrerat: {{phrase}}.",
"fullConversation": "Läs konversationer",
"translate": {
"default": "Översätt",
"succeed": "Översatt av {{provider}} från {{source}}",
"failed": "Översättning misslyckades",
"source_not_supported": "inläggets språk stöds inte",
"target_not_supported": "Målspråket stöds inte"
},
"header": {
"shared": {
"account": {
"name": {
"accessibilityHint": "Användarens visningsnamn"
},
"account": {
"accessibilityHint": "Användarens konto"
}
},
"application": "Via {{application}}",
"edited": {
"accessibilityLabel": "Inlägg redigerat"
},
"muted": {
"accessibilityLabel": "Inlägg tystat"
},
"visibility": {
"direct": {
"accessibilityLabel": "Inlägget är ett direktmeddelande"
},
"private": {
"accessibilityLabel": "Inlägget syns endast för följare"
}
}
},
"conversation": {
"withAccounts": "Med",
"delete": {
"function": "Ta bort direktmeddelande"
}
}
},
"poll": {
"meta": {
"button": {
"vote": "Rösta",
"refresh": "Uppdatera"
},
"count": {
"voters_one": "{{count}} användare har röstat",
"voters_other": "{{count}} användare har röstat",
"votes_one": "{{count}} röst",
"votes_other": "{{count}} röster"
},
"expiration": {
"expired": "Omröstning har gått ut",
"until": "Förfaller <0 />"
}
}
}
}
}

18
src/i18n/sv/index.ts Normal file
View File

@ -0,0 +1,18 @@
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'),
componentContextMenu: require('./components/contextMenu'),
componentEmojis: require('./components/emojis'),
componentInstance: require('./components/instance'),
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentTimeline: require('./components/timeline')
}

18
src/i18n/sv/screens.json Normal file
View File

@ -0,0 +1,18 @@
{
"screenshot": {
"title": "Integritetsskydd",
"message": "Vänligen avslöja inte någon annan användares identitet, såsom användarnamn, profilbild etc. Tack!",
"button": "Bekräfta"
},
"localCorrupt": {
"message": "Inloggningen har gått ut. Försök att logga in igen"
},
"pushError": {
"message": "Fel i push-tjänster",
"description": "Vänligen återaktivera pushnotiser i inställningar"
},
"shareError": {
"imageNotSupported": "Bildformatet {{type}} stöds inte",
"videoNotSupported": "Videoformatet {{type}} stöds inte"
}
}

View File

@ -0,0 +1,6 @@
{
"heading": "Dela med ...",
"content": {
"select_account": "Välj konto"
}
}

View File

@ -0,0 +1,20 @@
{
"content": {
"altText": {
"heading": "Alternativtext"
},
"notificationsFilter": {
"heading": "Visa notistyper",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Följarförfrågning",
"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": "Inlägg från följda användare",
"update": "Boosten har redigerats"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"heading": "Meddelanden",
"content": {
"published": "Publicerad <0 />",
"button": {
"read": "Läst",
"unread": "Markera som läst"
}
}
}

View File

@ -0,0 +1,179 @@
{
"heading": {
"left": {
"button": "Avbryt",
"alert": {
"title": "Avbryt redigering?",
"buttons": {
"save": "Spara utkast",
"delete": "Ta bort utkast",
"cancel": "Avbryt"
}
}
},
"right": {
"button": {
"default": "Inlägg",
"conversation": "Skicka DM",
"reply": "Skicka svar",
"deleteEdit": "Inlägg",
"edit": "Publicera",
"share": "Inlägg"
},
"alert": {
"default": {
"title": "Kunde inte publicera",
"button": "Försök igen"
},
"removeReply": {
"title": "Inlägget kunde inte hittas",
"description": "Inlägget du svarar på kan ha raderats. Vill du ta bort referensen till det?",
"cancel": "$t(common:buttons.cancel)",
"confirm": "Ta bort referens"
}
}
}
},
"content": {
"root": {
"header": {
"postingAs": "Skriver som @{{acct}}@{{domain}}",
"spoilerInput": {
"placeholder": "Spoilervarningsmeddelande"
},
"textInput": {
"placeholder": "Vad tänker du på",
"keyboardImage": {
"exceedMaximum": {
"title": "Maximalt antal bilagor har uppnåtts",
"OK": "$t(common:buttons.OK)"
}
}
}
},
"footer": {
"attachments": {
"sensitive": "Markera bilagor som känsliga",
"remove": {
"accessibilityLabel": "Ta bort uppladdad bilaga, nummer {{attachment}}"
},
"edit": {
"accessibilityLabel": "Redigera uppladdad bilaga, nummer {{attachment}}"
},
"upload": {
"accessibilityLabel": "Ladda upp fler bilagor"
}
},
"emojis": {
"accessibilityHint": "Tryck för att lägga till emoji till inlägg"
},
"poll": {
"option": {
"placeholder": {
"accessibilityLabel": "Omröstningsalternativ {{index}}",
"single": "Enval",
"multiple": "Flera val"
}
},
"quantity": {
"reduce": {
"accessibilityLabel": "Minska omröstningsalternativ till {{amount}}",
"accessibilityHint": "Minsta antal omröstningsalternativ har uppnåtts, för närvarande {{amount}}"
},
"increase": {
"accessibilityLabel": "Öka omröstningsalternativ till {{amount}}",
"accessibilityHint": "Maximalt antal omröstningsalternativ har uppnåtts, för närvarande {{amount}}"
}
},
"multiple": {
"heading": "Typ av val",
"options": {
"single": "Enval",
"multiple": "Flera val",
"cancel": "$t(common:buttons.cancel)"
}
},
"expiration": {
"heading": "Giltighet",
"options": {
"300": "5 minuter",
"1800": "30 minuter",
"3600": "1 timme",
"21600": "6 timmar",
"86400": "1 dag",
"259200": "3 dagar",
"604800": "7 dagar",
"cancel": "$t(common:buttons.cancel)"
}
}
}
},
"actions": {
"attachment": {
"accessibilityLabel": "Ladda upp bilaga",
"accessibilityHint": "Omröstningsfunktionen kommer att inaktiveras om det finns bilagor",
"failed": {
"alert": {
"title": "Uppladdningen misslyckades",
"button": "Försök igen"
}
}
},
"poll": {
"accessibilityLabel": "Lägg till omröstning",
"accessibilityHint": "Bilagefunktionen inaktiveras när en omröstning är aktiv"
},
"visibility": {
"accessibilityLabel": "Inläggets synlighet är {{visibility}}",
"title": "Inläggssynlighet",
"options": {
"public": "Offentlig",
"unlisted": "Olistad",
"private": "Endast följare",
"direct": "Direktmeddelande",
"cancel": "$t(common:buttons.cancel)"
}
},
"spoiler": {
"accessibilityLabel": "Spoiler"
},
"emoji": {
"accessibilityLabel": "Lägg till emoji",
"accessibilityHint": "Öppna panelen för emoji-val, svep horisontellt för att byta sida"
}
},
"drafts_one": "Utkast ({{count}})",
"drafts_other": "Utkast ({{count}})"
},
"editAttachment": {
"header": {
"title": "Redigera bilaga",
"right": {
"accessibilityLabel": "Spara redigering av bilaga",
"failed": {
"title": "Kunde inte redigera",
"button": "Försök igen"
}
}
},
"content": {
"altText": {
"heading": "Beskriv media för synskadade",
"placeholder": "Du kan lägga till en beskrivning, som ibland kallas alternativtext, till dina medier så att de är tillgängliga för ännu fler, inklusive dem som är blinda eller synskadade.\n\nBra beskrivningar är kortfattade men presenterar vad som finns i dina medier tillräckligt noggrant för att beskriva deras sammanhang."
},
"imageFocus": "Dra i fokuscirkeln för att uppdatera fokuspunkten"
}
},
"draftsList": {
"header": {
"title": "Utkast"
},
"warning": "Utkasten lagras endast lokalt och kan gå förlorade i sällsynta fall. Ett tips är att inte använda utkast för långtidslagring.",
"content": {
"accessibilityHint": "Sparat utkast, tryck för att redigera detta utkast",
"textEmpty": "Innehållet är tomt"
},
"checkAttachment": "Kontrollerar bilagor på servern..."
}
}
}

View File

@ -0,0 +1,17 @@
{
"content": {
"actions": {
"accessibilityLabel": "Fler åtgärder för denna bild",
"accessibilityHint": "Du kan spara eller dela denna bild"
},
"options": {
"save": "Spara bild",
"share": "Dela bild",
"cancel": "$t(common:buttons.cancel)"
},
"save": {
"succeed": "Bild sparad",
"failed": "Kunde inte spara bild"
}
}
}

View File

@ -0,0 +1,358 @@
{
"tabs": {
"local": {
"name": "Följer"
},
"public": {
"name": "",
"segments": {
"left": "Federerat",
"right": "Lokalt"
}
},
"notifications": {
"name": "Notiser"
},
"me": {
"name": "Om mig"
}
},
"common": {
"search": {
"accessibilityLabel": "Sök",
"accessibilityHint": "Sök efter hashtaggar, användare eller inlägg"
}
},
"notifications": {
"filter": {
"accessibilityLabel": "Filter",
"accessibilityHint": "Filtrera visade notistyper"
}
},
"me": {
"stacks": {
"bookmarks": {
"name": "Bokmärken"
},
"conversations": {
"name": "Direktmeddelanden"
},
"favourites": {
"name": "Favoriter"
},
"fontSize": {
"name": "Storlek på teckensnitt i inlägg"
},
"language": {
"name": "Språk"
},
"lists": {
"name": "Listor"
},
"list": {
"name": "Lista: {{list}}"
},
"push": {
"name": "Pushnotiser"
},
"profile": {
"name": "Redigera profil"
},
"profileName": {
"name": "Redigera visningsnamn"
},
"profileNote": {
"name": "Redigera beskrivning"
},
"profileFields": {
"name": "Redigera metadata"
},
"settings": {
"name": "Appinställningar"
},
"webSettings": {
"name": "Fler kontoinställningar"
},
"switch": {
"name": "Byt konto"
}
},
"fontSize": {
"demo": "<p>Det här är ett demoinlägg😊. Du kan välja mellan flera alternativ nedan.<br /><br />Denna inställning påverkar endast huvudinnehållet i inlägg men inte andra teckenstorlekar.</p>",
"sizes": {
"S": "S",
"M": "M - Standard",
"L": "L",
"XL": "XL",
"XXL": "XXL"
}
},
"profile": {
"cancellation": {
"title": "Ändringen har inte sparats",
"message": "Din ändring har inte sparats. Vill du strunta i att spara ändringarna?",
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"discard": "Kasta bort"
}
},
"feedback": {
"succeed": "{{type}} uppdaterades",
"failed": "{{type}}-uppdateringen misslyckades, vänligen försök igen"
},
"root": {
"name": {
"title": "Visningsnamn"
},
"avatar": {
"title": "Profilbild",
"description": "Kommer att skalas ner till 400x400px"
},
"header": {
"title": "Banderoll",
"description": "Kommer att skalas ner till 1500x500px"
},
"note": {
"title": "Beskrivning"
},
"fields": {
"title": "Metadata",
"total_one": "{{count}} fält",
"total_other": "{{count}} fält"
},
"visibility": {
"title": "Inläggssynlighet",
"options": {
"public": "Offentlig",
"unlisted": "Olistad",
"private": "Endast följare",
"cancel": "$t(common:buttons.cancel)"
}
},
"sensitive": {
"title": "Publicerar känslig media"
},
"lock": {
"title": "Lås konto",
"description": "Kräver att du godkänner följare manuellt"
},
"bot": {
"title": "Botkonto",
"description": "Detta konto utför huvudsakligen automatiserade åtgärder och kanske inte övervakas"
}
},
"fields": {
"group": "Grupp {{index}}",
"label": "Etikett",
"content": "Innehåll"
},
"mediaSelectionFailed": "Bildbearbetningen misslyckades. Försök igen."
},
"push": {
"notAvailable": "Din telefon stöder inte pushnotiser från tooot",
"enable": {
"direct": "Aktivera pushnotiser",
"settings": "Aktivera i inställningar"
},
"global": {
"heading": "Aktivera för {{acct}}",
"description": "Meddelanden dirigeras via tooots server"
},
"decode": {
"heading": "Visa meddelandedetaljer",
"description": "Meddelanden som dirigeras via tooots server är krypterade men du kan välja att avkoda meddelandet på servern. Vår serverkällkod är öppen källkod och ingenting loggas på servern."
},
"default": {
"heading": "Standard"
},
"follow": {
"heading": "Ny följare"
},
"follow_request": {
"heading": "Följarförfrågning"
},
"favourite": {
"heading": "Favorit"
},
"reblog": {
"heading": "Boostad"
},
"mention": {
"heading": "Nämnde dig"
},
"poll": {
"heading": "Uppdateringar för omröstning"
},
"status": {
"heading": "Inlägg från följda användare"
},
"howitworks": "Läs om hur routing fungerar"
},
"root": {
"announcements": {
"content": {
"unread": "{{amount}} olästa",
"read": "Alla lästa",
"empty": "Inga"
}
},
"push": {
"content": {
"enabled": "Aktiverad",
"disabled": "Inaktiverad"
}
},
"update": {
"title": "Uppdatera till senaste versionen"
},
"logout": {
"button": "Logga ut",
"alert": {
"title": "Logga ut?",
"message": "Efter att du loggat ut måste du logga in igen",
"buttons": {
"logout": "Logga ut",
"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": "Utseende",
"options": {
"auto": "Som systemet",
"light": "Ljust läge",
"dark": "Mörkt läge",
"cancel": "$t(common:buttons.cancel)"
}
},
"darkTheme": {
"heading": "Mörkt tema",
"options": {
"lighter": "Ljusare",
"darker": "Mörkare",
"cancel": "$t(common:buttons.cancel)"
}
},
"browser": {
"heading": "Öppnar länk",
"options": {
"internal": "Inuti appen",
"external": "Använd systemets webbläsare",
"cancel": "$t(common:buttons.cancel)"
}
},
"staticEmoji": {
"heading": "Använd statisk emoji",
"description": "Om du stöter på frekventa appkrascher när du visar emoji-listan kan du testa att använda statisk emoji istället."
},
"feedback": {
"heading": "Funktionsförslag"
},
"support": {
"heading": "Stöd tooot"
},
"review": {
"heading": "Recensera tooot"
},
"contact": {
"heading": "Kontakta tooot"
},
"analytics": {
"heading": "Hjälp oss att bli bättre",
"description": "Samlar endast in användningsdata som inte är kopplad till användaren"
},
"version": "Version v{{version}}",
"instanceVersion": "Mastodon version v{{version}}"
},
"switch": {
"existing": "Välj bland inloggade",
"new": "Logga in på instans"
}
},
"shared": {
"account": {
"actions": {
"accessibilityLabel": "Åtgärder för användare {{user}}",
"accessibilityHint": "Du kan tysta, blockera, rapportera eller dela denna användare"
},
"followed_by": " följer dig",
"moved": "Användaren har flyttat",
"created_at": "Gick med: {{date}}",
"summary": {
"statuses_count": "{{count}} inlägg",
"following_count": "$t(shared.users.accounts.following)",
"followers_count": "$t(shared.users.accounts.followers)"
},
"toots": {
"default": "Inlägg",
"all": "Inlägg och svar"
},
"suspended": "Kontot är avstängt av moderatorerna på din server"
},
"attachments": {
"name": "<0 /><1>s media</1>"
},
"hashtag": {
"follow": "Följ",
"unfollow": "Sluta följ"
},
"history": {
"name": "Redigeringshistorik"
},
"search": {
"header": {
"prefix": "Söker",
"placeholder": "för..."
},
"empty": {
"general": "Ange sökord för att söka efter <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold> eller <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
"advanced": {
"header": "Avancerad sökning",
"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": "Användare",
"hashtags": "Hashtagg",
"statuses": "Inlägg"
},
"notFound": "Kan inte hitta <bold>{{searchTerm}}</bold>-relaterade {{type}}"
},
"toot": {
"name": "Diskussioner"
},
"users": {
"accounts": {
"following": "Följer {{count}}",
"followers": "{{count}} följare"
},
"statuses": {
"reblogged_by": "{{count}} boostade",
"favourited_by": "{{count}} favoriter"
}
}
}
}

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>'s media</1>" "name": "<0 /><1>'s media</1>"
}, },
"hashtag": {
"follow": "Theo dõi",
"unfollow": "Ngưng theo dõi"
},
"history": {
"name": "Lịch sử chỉnh sửa"
},
"search": { "search": {
"header": { "header": {
"prefix": "Tìm kiếm", "prefix": "Tìm kiếm",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} đăng lại", "reblogged_by": "{{count}} đăng lại",
"favourited_by": "{{count}} thích" "favourited_by": "{{count}} thích"
} }
},
"history": {
"name": "Lịch sử chỉnh sửa"
} }
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>的媒体</1>" "name": "<0 /><1>的媒体</1>"
}, },
"hashtag": {
"follow": "关注",
"unfollow": "取消关注"
},
"history": {
"name": "编辑历史"
},
"search": { "search": {
"header": { "header": {
"prefix": "搜索", "prefix": "搜索",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} 人转嘟", "reblogged_by": "{{count}} 人转嘟",
"favourited_by": "{{count}} 人喜欢" "favourited_by": "{{count}} 人喜欢"
} }
},
"history": {
"name": "编辑历史"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"follow": { "follow": {
"function": "追蹤使用者" "function": "跟隨使用者"
}, },
"block": { "block": {
"function": "封鎖使用者" "function": "封鎖使用者"
@ -9,8 +9,8 @@
"error": "載入錯誤", "error": "載入錯誤",
"blocked_by": "已被使用者封鎖", "blocked_by": "已被使用者封鎖",
"blocking": "解除封鎖", "blocking": "解除封鎖",
"following": "取消追蹤", "following": "取消跟隨",
"requested": "收回要求", "requested": "收回要求",
"default": "追蹤" "default": "跟隨"
} }
} }

View File

@ -310,6 +310,13 @@
"attachments": { "attachments": {
"name": "<0 /><1>的媒體</1>" "name": "<0 /><1>的媒體</1>"
}, },
"hashtag": {
"follow": "跟隨",
"unfollow": "取消跟隨"
},
"history": {
"name": "編輯歷史"
},
"search": { "search": {
"header": { "header": {
"prefix": "搜尋", "prefix": "搜尋",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} 人轉嘟", "reblogged_by": "{{count}} 人轉嘟",
"favourited_by": "{{count}} 人喜歡" "favourited_by": "{{count}} 人喜歡"
} }
},
"history": {
"name": "編輯歷史"
} }
} }
} }

View File

@ -9,7 +9,6 @@ import * as VideoThumbnails from 'expo-video-thumbnails'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, Image, ScrollView, View } from 'react-native' import { FlatList, Image, ScrollView, View } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const Share = ({ const Share = ({
@ -76,9 +75,7 @@ const Share = ({
renderItem={({ item }) => ( renderItem={({ item }) => (
<Image source={{ uri: item }} style={{ width: 88, height: 88 }} /> <Image source={{ uri: item }} style={{ width: 88, height: 88 }} />
)} )}
ItemSeparatorComponent={() => ( ItemSeparatorComponent={() => <View style={{ width: StyleConstants.Spacing.S }} />}
<View style={{ width: StyleConstants.Spacing.S }} />
)}
/> />
</View> </View>
) )
@ -99,64 +96,60 @@ const ScreenAccountSelection = ({
const instances = useSelector(getInstances, () => true) const instances = useSelector(getInstances, () => true)
return ( return (
<SafeAreaProvider> <ScrollView
<ScrollView style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
style={{ marginBottom: StyleConstants.Spacing.L * 2 }} keyboardShouldPersistTaps='always'
keyboardShouldPersistTaps='always' >
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
> >
<View {share ? <Share {...share} /> : null}
<CustomText
fontStyle='M'
fontWeight='Bold'
style={{ style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding textAlign: 'center',
marginTop: StyleConstants.Spacing.L,
marginBottom: StyleConstants.Spacing.S,
color: colors.primaryDefault
}} }}
> >
{share ? <Share {...share} /> : null} {t('content.select_account')}
<CustomText </CustomText>
fontStyle='M' <View
fontWeight='Bold' style={{
style={{ flex: 1,
textAlign: 'center', flexDirection: 'row',
marginTop: StyleConstants.Spacing.L, flexWrap: 'wrap',
marginBottom: StyleConstants.Spacing.S, marginTop: StyleConstants.Spacing.M
color: colors.primaryDefault }}
}} >
> {instances.length
{t('content.select_account')} ? instances
</CustomText> .slice()
<View .sort((a, b) =>
style={{ `${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
flex: 1, )
flexDirection: 'row', .map((instance, index) => {
flexWrap: 'wrap', return (
marginTop: StyleConstants.Spacing.M <AccountButton
}} key={index}
> instance={instance}
{instances.length additionalActions={() => {
? instances navigationRef.navigate('Screen-Compose', {
.slice() type: 'share',
.sort((a, b) => ...share
`${a.uri}${a.account.acct}`.localeCompare( })
`${b.uri}${b.account.acct}` }}
) />
) )
.map((instance, index) => { })
return ( : null}
<AccountButton
key={index}
instance={instance}
additionalActions={() => {
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}}
/>
)
})
: null}
</View>
</View> </View>
</ScrollView> </View>
</SafeAreaProvider> </ScrollView>
) )
} }

View File

@ -3,11 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native' import { Dimensions, StyleSheet, View } from 'react-native'
import { import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
PanGestureHandler,
State,
TapGestureHandler
} from 'react-native-gesture-handler'
import Animated, { import Animated, {
Extrapolate, Extrapolate,
interpolate, interpolate,
@ -17,10 +13,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming withTiming
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { import { useSafeAreaInsets } from 'react-native-safe-area-context'
SafeAreaProvider,
useSafeAreaInsets
} from 'react-native-safe-area-context'
import ActionsAltText from './Actions/AltText' import ActionsAltText from './Actions/AltText'
import ActionsNotificationsFilter from './Actions/NotificationsFilter' import ActionsNotificationsFilter from './Actions/NotificationsFilter'
@ -39,12 +32,7 @@ const ScreenActions = ({
}, []) }, [])
const styleTop = useAnimatedStyle(() => { const styleTop = useAnimatedStyle(() => {
return { return {
bottom: interpolate( bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
panY.value,
[0, screenHeight],
[0, -screenHeight],
Extrapolate.CLAMP
)
} }
}) })
const dismiss = useCallback(() => { const dismiss = useCallback(() => {
@ -73,45 +61,35 @@ const ScreenActions = ({
} }
return ( return (
<SafeAreaProvider> <Animated.View style={{ flex: 1 }}>
<Animated.View style={{ flex: 1 }}> <TapGestureHandler
<TapGestureHandler onHandlerStateChange={({ nativeEvent }) => {
onHandlerStateChange={({ nativeEvent }) => { if (nativeEvent.state === State.ACTIVE) {
if (nativeEvent.state === State.ACTIVE) { dismiss()
dismiss() }
} }}
}} >
<Animated.View
style={[styles.overlay, { backgroundColor: colors.backgroundOverlayInvert }]}
> >
<Animated.View <PanGestureHandler onGestureEvent={onGestureEvent}>
style={[ <Animated.View
styles.overlay, style={[
{ backgroundColor: colors.backgroundOverlayInvert } styles.container,
]} styleTop,
> {
<PanGestureHandler onGestureEvent={onGestureEvent}> backgroundColor: colors.backgroundDefault,
<Animated.View paddingBottom: insets.bottom || StyleConstants.Spacing.L
style={[ }
styles.container, ]}
styleTop, >
{ <View style={[styles.handle, { backgroundColor: colors.primaryOverlay }]} />
backgroundColor: colors.backgroundDefault, {actions()}
paddingBottom: insets.bottom || StyleConstants.Spacing.L </Animated.View>
} </PanGestureHandler>
]} </Animated.View>
> </TapGestureHandler>
<View </Animated.View>
style={[
styles.handle,
{ backgroundColor: colors.primaryOverlay }
]}
/>
{actions()}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</TapGestureHandler>
</Animated.View>
</SafeAreaProvider>
) )
} }

View File

@ -20,7 +20,7 @@ import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gestu
import { LiveTextImageView } from 'react-native-live-text-image-view' import { LiveTextImageView } from 'react-native-live-text-image-view'
import { runOnJS, useSharedValue } from 'react-native-reanimated' import { runOnJS, useSharedValue } from 'react-native-reanimated'
import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom' import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom'
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import saveImage from './ImageViewer/save' import saveImage from './ImageViewer/save'
const ZoomFlatList = createZoomListComponent(FlatList) const ZoomFlatList = createZoomListComponent(FlatList)
@ -153,7 +153,7 @@ const ScreenImagesViewer = ({
) )
return ( return (
<SafeAreaProvider style={{ backgroundColor: 'black' }}> <View style={{ backgroundColor: 'black' }}>
<StatusBar hidden /> <StatusBar hidden />
<View <View
style={{ style={{
@ -232,7 +232,7 @@ const ScreenImagesViewer = ({
})} })}
/> />
</LongPressGestureHandler> </LongPressGestureHandler>
</SafeAreaProvider> </View>
) )
} }

View File

@ -12,7 +12,7 @@ import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabLocalStackParamList>() const Stack = createNativeStackNavigator<TabLocalStackParamList>()
@ -96,7 +96,7 @@ const TabLocal = React.memo(
/> />
)} )}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -16,7 +16,7 @@ import TabMeSettings from './Me/Settings'
import TabMeSettingsFontsize from './Me/SettingsFontsize' import TabMeSettingsFontsize from './Me/SettingsFontsize'
import TabMeSettingsLanguage from './Me/SettingsLanguage' import TabMeSettingsLanguage from './Me/SettingsLanguage'
import TabMeSwitch from './Me/Switch' import TabMeSwitch from './Me/Switch'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabMeStackParamList>() const Stack = createNativeStackNavigator<TabMeStackParamList>()
@ -187,7 +187,7 @@ const TabMe = React.memo(
})} })}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>() const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
@ -65,7 +65,7 @@ const TabNotifications = React.memo(
children={children} children={children}
options={screenOptionsRoot} options={screenOptionsRoot}
/> />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -12,7 +12,7 @@ import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Dimensions } from 'react-native' import { Dimensions } from 'react-native'
import { TabView } from 'react-native-tab-view' import { TabView } from 'react-native-tab-view'
import TabSharedRoot from './Shared/Root' import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabPublicStackParamList>() const Stack = createNativeStackNavigator<TabPublicStackParamList>()
@ -107,7 +107,7 @@ const TabPublic = React.memo(
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} /> <Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} />
{TabSharedRoot({ Stack })} {TabShared({ Stack })}
</Stack.Navigator> </Stack.Navigator>
) )
}, },

View File

@ -1,24 +1,78 @@
import haptics from '@components/haptics'
import { HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default' import TimelineDefault from '@components/Timeline/Default'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react' import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const TabSharedHashtag: React.FC< const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
TabSharedStackScreenProps<'Tab-Shared-Hashtag'> navigation,
> = ({
route: { route: {
params: { hashtag } params: { hashtag }
} }
}) => { }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
const { theme } = useTheme()
const { t } = useTranslation('screenTabs')
const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
const { data, isFetching, refetch } = useTagsQuery({
tag: hashtag,
options: { enabled: canFollowTags }
})
const mutation = useTagsMutation({
onSuccess: () => {
haptics('Success')
refetch()
},
onError: (err: any, { to }) => {
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow')
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
useEffect(() => {
if (!canFollowTags) return
navigation.setOptions({
headerRight: () => (
<HeaderRight
loading={isFetching || mutation.isLoading}
type='text'
content={data?.following ? t('shared.hashtag.unfollow') : t('shared.hashtag.follow')}
onPress={() =>
typeof data?.following === 'boolean' &&
mutation.mutate({ tag: hashtag, type: 'follow', to: !data.following })
}
/>
)
})
}, [canFollowTags, data?.following, isFetching])
return ( return (
<Timeline <Timeline
queryKey={queryKey} queryKey={queryKey}
customProps={{ customProps={{
renderItem: ({ item }) => ( renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
<TimelineDefault item={item} queryKey={queryKey} />
)
}} }}
/> />
) )

View File

@ -20,11 +20,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { Platform, TextInput, View } from 'react-native' import { Platform, TextInput, View } from 'react-native'
import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view' import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view'
const TabSharedRoot = ({ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
Stack
}: {
Stack: ReturnType<typeof createNativeStackNavigator>
}) => {
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
@ -50,9 +46,7 @@ const TabSharedRoot = ({
backgroundColor: `rgba(255, 255, 255, 0)` backgroundColor: `rgba(255, 255, 255, 0)`
}, },
title: '', title: '',
headerLeft: () => ( headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />,
<HeaderLeft onPress={() => navigation.goBack()} background />
),
headerRight: () => { headerRight: () => {
const actions: ContextMenuAction[] = [] const actions: ContextMenuAction[] = []
@ -77,13 +71,10 @@ const TabSharedRoot = ({
dropdownMenuMode dropdownMenuMode
> >
<HeaderRight <HeaderRight
accessibilityLabel={t( accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
'shared.account.actions.accessibilityLabel', user: account.acct
{ user: account.acct } })}
)} accessibilityHint={t('shared.account.actions.accessibilityHint')}
accessibilityHint={t(
'shared.account.actions.accessibilityHint'
)}
content='MoreHorizontal' content='MoreHorizontal'
onPress={() => {}} onPress={() => {}}
background background
@ -132,9 +123,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Hashtag' key='Tab-Shared-Hashtag'
name='Tab-Shared-Hashtag' name='Tab-Shared-Hashtag'
component={TabSharedHashtag} component={TabSharedHashtag}
options={({ options={({ route }: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
route
}: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
title: `#${decodeURIComponent(route.params.hashtag)}` title: `#${decodeURIComponent(route.params.hashtag)}`
})} })}
/> />
@ -150,24 +139,16 @@ const TabSharedRoot = ({
key='Tab-Shared-Search' key='Tab-Shared-Search'
name='Tab-Shared-Search' name='Tab-Shared-Search'
component={TabSharedSearch} component={TabSharedSearch}
options={({ options={({ navigation }: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
navigation
}: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
...(Platform.OS === 'ios' ...(Platform.OS === 'ios'
? { ? {
headerLeft: () => ( headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
<HeaderLeft onPress={() => navigation.goBack()} />
)
} }
: { headerLeft: () => null }), : { headerLeft: () => null }),
headerTitle: () => { headerTitle: () => {
const onChangeText = debounce( const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
(text: string) => navigation.setParams({ text }), trailing: true
1000, })
{
trailing: true
}
)
return ( return (
<View <View
style={{ style={{
@ -199,9 +180,7 @@ const TabSharedRoot = ({
autoCorrect={false} autoCorrect={false}
clearButtonMode='never' clearButtonMode='never'
keyboardType='web-search' keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) => onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
navigation.setParams({ text })
}
placeholder={t('shared.search.header.placeholder')} placeholder={t('shared.search.header.placeholder')}
placeholderTextColor={colors.secondary} placeholderTextColor={colors.secondary}
returnKeyType='go' returnKeyType='go'
@ -216,9 +195,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Toot' key='Tab-Shared-Toot'
name='Tab-Shared-Toot' name='Tab-Shared-Toot'
component={TabSharedToot} component={TabSharedToot}
options={{ options={{ title: t('shared.toot.name') }}
title: t('shared.toot.name')
}}
/> />
<Stack.Screen <Stack.Screen
@ -233,9 +210,7 @@ const TabSharedRoot = ({
title: t(`shared.users.${reference}.${type}`, { count }), title: t(`shared.users.${reference}.${type}`, { count }),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => (
<HeaderCenter <HeaderCenter content={t(`shared.users.${reference}.${type}`, { count })} />
content={t(`shared.users.${reference}.${type}`, { count })}
/>
) )
}) })
})} })}
@ -244,4 +219,4 @@ const TabSharedRoot = ({
) )
} }
export default TabSharedRoot export default TabShared

View File

@ -4,9 +4,9 @@ import log from './log'
const audio = () => { const audio = () => {
log('log', 'audio', 'setting audio playback default options') log('log', 'audio', 'setting audio playback default options')
Audio.setAudioModeAsync({ Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
interruptionModeIOS: InterruptionModeIOS.MixWithOthers, interruptionModeIOS: InterruptionModeIOS.MixWithOthers,
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
staysActiveInBackground: false
}) })
} }

View File

@ -0,0 +1,59 @@
import apiInstance from '@api/instance'
import { AxiosError } from 'axios'
import {
QueryFunctionContext,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from 'react-query'
type QueryKeyFollowedTags = ['FollowedTags']
const useFollowedTagsQuery = ({
options
}: {
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => {
const queryKey: QueryKeyFollowedTags = ['FollowedTags']
return useQuery(
queryKey,
async ({ pageParam }: QueryFunctionContext<QueryKeyFollowedTags>) => {
const params: { [key: string]: string } = { ...pageParam }
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `followed_tags`, params })
return res.body
},
options
)
}
type QueryKeyTags = ['Tags', { tag: string }]
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
const { tag } = queryKey[1]
return apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` }).then(res => res.body)
}
const useTagsQuery = ({
options,
...queryKeyParams
}: QueryKeyTags[1] & {
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => {
const queryKey: QueryKeyTags = ['Tags', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
}
type MutationVarsAnnouncement = { tag: string; type: 'follow'; to: boolean }
const mutationFunction = async ({ tag, type, to }: MutationVarsAnnouncement) => {
switch (type) {
case 'follow':
return apiInstance<{}>({
method: 'post',
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
})
}
}
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
return useMutation(mutationFunction, options)
}
export { useFollowedTagsQuery, useTagsQuery, useTagsMutation }

View File

@ -29,10 +29,7 @@ const instancesSlice = createSlice({
name: 'instances', name: 'instances',
initialState: instancesInitialState, initialState: instancesInitialState,
reducers: { reducers: {
updateInstanceActive: ( updateInstanceActive: ({ instances }, action: PayloadAction<InstanceLatest>) => {
{ instances },
action: PayloadAction<InstanceLatest>
) => {
instances = instances.map(instance => { instances = instances.map(instance => {
instance.active = instance.active =
instance.url === action.payload.url && instance.url === action.payload.url &&
@ -43,9 +40,7 @@ const instancesSlice = createSlice({
}, },
updateInstanceAccount: ( updateInstanceAccount: (
{ instances }, { instances },
action: PayloadAction< action: PayloadAction<Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>>
Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>
>
) => { ) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].account = { instances[activeIndex].account = {
@ -60,10 +55,7 @@ const instancesSlice = createSlice({
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].notifications_filter = action.payload instances[activeIndex].notifications_filter = action.payload
}, },
updateInstanceDraft: ( updateInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft>) => {
{ instances },
action: PayloadAction<ComposeStateDraft>
) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
const draftIndex = instances[activeIndex].drafts.findIndex( const draftIndex = instances[activeIndex].drafts.findIndex(
({ timestamp }) => timestamp === action.payload.timestamp ({ timestamp }) => timestamp === action.payload.timestamp
@ -74,10 +66,7 @@ const instancesSlice = createSlice({
instances[activeIndex].drafts[draftIndex] = action.payload instances[activeIndex].drafts[draftIndex] = action.payload
} }
}, },
removeInstanceDraft: ( removeInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft['timestamp']>) => {
{ instances },
action: PayloadAction<ComposeStateDraft['timestamp']>
) => {
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter( instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload draft => draft.timestamp !== action.payload
@ -126,9 +115,7 @@ const instancesSlice = createSlice({
action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']> action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']>
) => { ) => {
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
const calculateScore = ( const calculateScore = (emoji: InstanceLatest['frequentEmojis'][0]): number => {
emoji: InstanceLatest['frequentEmojis'][0]
): number => {
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000 var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
var score = emoji.count + 1 var score = emoji.count + 1
var order = Math.log(Math.max(score, 1)) / Math.LN10 var order = Math.log(Math.max(score, 1)) / Math.LN10
@ -137,9 +124,7 @@ const instancesSlice = createSlice({
} }
const activeIndex = findInstanceActive(instances) const activeIndex = findInstanceActive(instances)
const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex( const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex(
e => e => e.emoji.shortcode === action.payload.shortcode && e.emoji.url === action.payload.url
e.emoji.shortcode === action.payload.shortcode &&
e.emoji.url === action.payload.url
) )
let newEmojisSort: InstanceLatest['frequentEmojis'] let newEmojisSort: InstanceLatest['frequentEmojis']
if (foundEmojiIndex > -1) { if (foundEmojiIndex > -1) {
@ -147,11 +132,11 @@ const instancesSlice = createSlice({
.map((e, i) => .map((e, i) =>
i === foundEmojiIndex i === foundEmojiIndex
? { ? {
...e, ...e,
score: calculateScore(e), score: calculateScore(e),
count: e.count + 1, count: e.count + 1,
lastUsed: new Date().getTime() lastUsed: new Date().getTime()
} }
: e : e
) )
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
@ -218,8 +203,7 @@ const instancesSlice = createSlice({
return true return true
} }
}) })
state.instances.length && state.instances.length && (state.instances[state.instances.length - 1].active = true)
(state.instances[state.instances.length - 1].active = true)
analytics('logout') analytics('logout')
}) })
@ -250,8 +234,7 @@ const instancesSlice = createSlice({
.addCase(updateConfiguration.fulfilled, (state, action) => { .addCase(updateConfiguration.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].version = action.payload?.version || '0' state.instances[activeIndex].version = action.payload?.version || '0'
state.instances[activeIndex].configuration = state.instances[activeIndex].configuration = action.payload.configuration
action.payload.configuration
}) })
.addCase(updateConfiguration.rejected, (_, action) => { .addCase(updateConfiguration.rejected, (_, action) => {
console.error(action.error) console.error(action.error)
@ -291,22 +274,16 @@ const instancesSlice = createSlice({
// Update Instance Push Individual Alert // Update Instance Push Individual Alert
.addCase(updateInstancePushAlert.fulfilled, (state, action) => { .addCase(updateInstancePushAlert.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
action.meta.arg.changed
].loading = false
state.instances[activeIndex].push.alerts = action.payload state.instances[activeIndex].push.alerts = action.payload
}) })
.addCase(updateInstancePushAlert.rejected, (state, action) => { .addCase(updateInstancePushAlert.rejected, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
action.meta.arg.changed
].loading = false
}) })
.addCase(updateInstancePushAlert.pending, (state, action) => { .addCase(updateInstancePushAlert.pending, (state, action) => {
const activeIndex = findInstanceActive(state.instances) const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[ state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = true
action.meta.arg.changed
].loading = true
}) })
// Check if frequently used emojis still exist // Check if frequently used emojis still exist
@ -317,8 +294,7 @@ const instancesSlice = createSlice({
activeIndex activeIndex
].frequentEmojis?.filter(emoji => { ].frequentEmojis?.filter(emoji => {
return action.payload?.find( return action.payload?.find(
e => e => e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
) )
}) })
}) })
@ -331,8 +307,7 @@ const instancesSlice = createSlice({
export const getInstanceActive = ({ instances: { instances } }: RootState) => export const getInstanceActive = ({ instances: { instances } }: RootState) =>
findInstanceActive(instances) findInstanceActive(instances)
export const getInstances = ({ instances: { instances } }: RootState) => export const getInstances = ({ instances: { instances } }: RootState) => instances
instances
export const getInstance = ({ instances: { instances } }: RootState) => export const getInstance = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)] instances[findInstanceActive(instances)]
@ -350,42 +325,30 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.version instances[findInstanceActive(instances)]?.version
export const checkInstanceFeature = export const checkInstanceFeature =
(feature: string) => (feature: string) =>
({ instances: { instances } }: RootState): Boolean => { ({ instances: { instances } }: RootState): boolean => {
return ( return (
features features
.filter(f => f.feature === feature) .filter(f => f.feature === feature)
.filter( .filter(f => parseFloat(instances[findInstanceActive(instances)]?.version) >= f.version)
f => ?.length > 0
parseFloat(instances[findInstanceActive(instances)]?.version) >= )
f.version }
)?.length > 0
)
}
/* Get Instance Configuration */ /* Get Instance Configuration */
export const getInstanceConfigurationStatusMaxChars = ({ export const getInstanceConfigurationStatusMaxChars = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.statuses.max_characters || 500
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses
.max_characters || 500
export const getInstanceConfigurationStatusMaxAttachments = ({ export const getInstanceConfigurationStatusMaxAttachments = ({
instances: { instances } instances: { instances }
}: RootState) => }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses instances[findInstanceActive(instances)]?.configuration?.statuses.max_media_attachments || 4
.max_media_attachments || 4
export const getInstanceConfigurationStatusCharsURL = ({ export const getInstanceConfigurationStatusCharsURL = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.statuses.characters_reserved_per_url ||
}: RootState) => 23
instances[findInstanceActive(instances)]?.configuration?.statuses
.characters_reserved_per_url || 23
export const getInstanceConfigurationMediaAttachments = ({ export const getInstanceConfigurationMediaAttachments = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.configuration?.media_attachments || {
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration
?.media_attachments || {
supported_mime_types: [ supported_mime_types: [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@ -418,9 +381,7 @@ export const getInstanceConfigurationMediaAttachments = ({
video_matrix_limit: 2304000 video_matrix_limit: 2304000
} }
export const getInstanceConfigurationPoll = ({ export const getInstanceConfigurationPoll = ({ instances: { instances } }: RootState) =>
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.polls || { instances[findInstanceActive(instances)]?.configuration?.polls || {
max_options: 4, max_options: 4,
max_characters_per_option: 50, max_characters_per_option: 50,
@ -432,16 +393,14 @@ export const getInstanceConfigurationPoll = ({
export const getInstanceAccount = ({ instances: { instances } }: RootState) => export const getInstanceAccount = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.account instances[findInstanceActive(instances)]?.account
export const getInstanceNotificationsFilter = ({ export const getInstanceNotificationsFilter = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.notifications_filter
}: RootState) => instances[findInstanceActive(instances)]?.notifications_filter
export const getInstancePush = ({ instances: { instances } }: RootState) => export const getInstancePush = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.push instances[findInstanceActive(instances)]?.push
export const getInstanceTimelinesLookback = ({ export const getInstanceTimelinesLookback = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.timelinesLookback
}: RootState) => instances[findInstanceActive(instances)]?.timelinesLookback
export const getInstanceMePage = ({ instances: { instances } }: RootState) => export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.mePage instances[findInstanceActive(instances)]?.mePage
@ -449,9 +408,8 @@ export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.drafts instances[findInstanceActive(instances)]?.drafts
export const getInstanceFrequentEmojis = ({ export const getInstanceFrequentEmojis = ({ instances: { instances } }: RootState) =>
instances: { instances } instances[findInstanceActive(instances)]?.frequentEmojis
}: RootState) => instances[findInstanceActive(instances)]?.frequentEmojis
export const { export const {
updateInstanceActive, updateInstanceActive,