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
[@janlindblom](https://github.com/janlindblom) for Swedish
[@duy@mas.to](https://mas.to/@duy) for Vietnamese 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>"; };
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>"; };
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>"; };
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>"; };
@ -295,6 +296,7 @@
"zh-Hant",
fr,
es,
sv,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@ -525,6 +527,7 @@
E671BDF8290EAFB800287BD0 /* zh-Hant */,
E66C0842291F095800DFFF60 /* fr */,
E690AF692926B737002C38A8 /* es */,
E63E7FF0292A828100C76FD4 /* sv */,
);
name = InfoPlist.strings;
sourceTree = "<group>";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual, uniqBy } from 'lodash'
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 TimelineContextMenu from './Shared/ContextMenu'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
@ -29,129 +29,146 @@ export interface Props {
highlighted?: boolean
}
const TimelineNotifications = React.memo(
({ notification, queryKey, highlighted = false }: Props) => {
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
const TimelineNotifications: React.FC<Props> = ({
notification,
queryKey,
highlighted = 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 =
notification.status &&
shouldFilter({
copiableContent,
status: notification.status,
queryKey
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
})
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 (
<TimelineContextMenu
copiableContent={copiableContent}
status={notification.status}
queryKey={queryKey}
>
<Pressable
<>
{notification.type !== 'mention' ? (
<TimelineActioned
action={notification.type}
account={notification.account}
notification
/>
) : null}
<View
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
opacity:
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'mention' ||
notification.type === 'status'
? 1
: 0.5
}}
onPress={onPress}
onLongPress={() => {}}
>
{notification.type !== 'mention' ? (
<TimelineActioned
action={notification.type}
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 style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
</View>
{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}
/>
<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}
</Pressable>
</TimelineContextMenu>
</View>
{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

View File

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

View File

@ -64,7 +64,7 @@ const AttachmentVideo: React.FC<Props> = ({
}, [])
const _handleAppStateChange = async (nextAppState: AppStateStatus) => {
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/)) {
await videoPlayer.current?.setIsMutedAsync(true)
await videoPlayer.current?.playAsync()

View File

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

View File

@ -23,5 +23,10 @@
"feature": "notification_types_positive_filter",
"version": 3.5,
"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": {
"name": "<0 /><1>\"s Medien</1>"
},
"hashtag": {
"follow": "Folgen",
"unfollow": "Entfolgen"
},
"history": {
"name": "Bearbeitungsverlauf"
},
"search": {
"header": {
"prefix": "Suche",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} mal geboosted",
"favourited_by": "{{count}} mal geherzt"
}
},
"history": {
"name": "Bearbeitungsverlauf"
}
}
}

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
"name": "Nombre",
"accounts": "Usuarios",
"statuses": "Toots",
"domains": ""
"domains": "Universos"
},
"disclaimer": {
"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",
"options": {
"image": "Subir fotos",

View File

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

View File

@ -1,7 +1,7 @@
{
"screenshot": {
"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"
},
"localCorrupt": {

View File

@ -117,8 +117,8 @@
},
"fields": {
"title": "Metadatos",
"total_one": "Campo {{count}}",
"total_other": "Campos {{count}}"
"total_one": "{{count}} campo",
"total_other": "{{count}} campos"
},
"visibility": {
"title": "Visibilidad de publicación",
@ -134,7 +134,7 @@
},
"lock": {
"title": "Bloquear cuenta",
"description": ""
"description": "Necesitas aprobar manualmente los seguidores nuevos"
},
"bot": {
"title": "Cuenta bot",
@ -156,11 +156,11 @@
},
"global": {
"heading": "Habilitar para {{acct}}",
"description": ""
"description": "Los mensajes se envían a través del servidor de tooot"
},
"decode": {
"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": {
"heading": "Predeterminado"
@ -172,7 +172,7 @@
"heading": "Solicitud de seguimiento"
},
"favourite": {
"heading": ""
"heading": "Favoritos"
},
"reblog": {
"heading": "Impulsado"
@ -186,14 +186,14 @@
"status": {
"heading": "Toot de usuarios suscritos"
},
"howitworks": ""
"howitworks": "Más información sobre las notificaciones push"
},
"root": {
"announcements": {
"content": {
"unread": "{{amount}} no leídos",
"read": "",
"empty": ""
"read": "Todo leído",
"empty": "Nada"
}
},
"push": {
@ -203,7 +203,7 @@
}
},
"update": {
"title": ""
"title": "Actualizar a la última versión"
},
"logout": {
"button": "Cerrar sesión",
@ -310,10 +310,17 @@
"attachments": {
"name": ""
},
"hashtag": {
"follow": "Seguir",
"unfollow": "Dejar de seguir"
},
"history": {
"name": "Historial de ediciones"
},
"search": {
"header": {
"prefix": "Buscando",
"placeholder": ""
"placeholder": "algo..."
},
"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>",
@ -332,23 +339,20 @@
"hashtags": "Hashtag",
"statuses": "Toot"
},
"notFound": ""
"notFound": "No se pudo encontrar <bold>{{searchTerm}}</bold> relacionado con {{type}}"
},
"toot": {
"name": ""
"name": "Discusiones"
},
"users": {
"accounts": {
"following": "Siguiendo {{count}}",
"following": "{{count}} seguidos",
"followers": "{{count}} seguidores"
},
"statuses": {
"reblogged_by": "{{count}} impulsados",
"favourited_by": "{{count}} favoritos"
}
},
"history": {
"name": ""
}
}
}

View File

@ -310,6 +310,13 @@
"attachments": {
"name": "<0 /><1>\"s media</1>"
},
"hashtag": {
"follow": "Suivre",
"unfollow": "Ne plus suivre"
},
"history": {
"name": "Modifier l'historique"
},
"search": {
"header": {
"prefix": "Recherche en cours",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boosté",
"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 ko from '@root/i18n/ko'
import pt_BR from '@root/i18n/pt_BR'
import sv from '@root/i18n/sv'
import vi from '@root/i18n/vi'
import zh_Hans from '@root/i18n/zh-Hans'
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/ko'
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/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/ko'
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/zh-Hans'
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/ko'
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/zh-Hans'
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/ko'
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/zh-Hans'
import '@formatjs/intl-relativetimeformat/locale-data/zh-Hant'
@ -84,6 +89,7 @@ i18n.use(initReactI18next).init({
ja,
ko,
'pt-BR': pt_BR,
sv,
vi,
'zh-Hans': zh_Hans,
'zh-Hant': zh_Hant

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -310,6 +310,13 @@
"attachments": {
"name": "<0 /><1>\"s mídia</1>"
},
"hashtag": {
"follow": "Seguir",
"unfollow": "Deixar de seguir"
},
"history": {
"name": "Histórico de Edição"
},
"search": {
"header": {
"prefix": "Procurando",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} boostou",
"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": {
"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": {
"header": {
"prefix": "Tìm kiếm",
@ -346,9 +353,6 @@
"reblogged_by": "{{count}} đăng lại",
"favourited_by": "{{count}} thích"
}
},
"history": {
"name": "Lịch sử chỉnh sửa"
}
}
}

View File

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

View File

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

View File

@ -310,6 +310,13 @@
"attachments": {
"name": "<0 /><1>的媒體</1>"
},
"hashtag": {
"follow": "跟隨",
"unfollow": "取消跟隨"
},
"history": {
"name": "編輯歷史"
},
"search": {
"header": {
"prefix": "搜尋",
@ -346,9 +353,6 @@
"reblogged_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 { useTranslation } from 'react-i18next'
import { FlatList, Image, ScrollView, View } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux'
const Share = ({
@ -76,9 +75,7 @@ const Share = ({
renderItem={({ item }) => (
<Image source={{ uri: item }} style={{ width: 88, height: 88 }} />
)}
ItemSeparatorComponent={() => (
<View style={{ width: StyleConstants.Spacing.S }} />
)}
ItemSeparatorComponent={() => <View style={{ width: StyleConstants.Spacing.S }} />}
/>
</View>
)
@ -99,64 +96,60 @@ const ScreenAccountSelection = ({
const instances = useSelector(getInstances, () => true)
return (
<SafeAreaProvider>
<ScrollView
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
keyboardShouldPersistTaps='always'
<ScrollView
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
keyboardShouldPersistTaps='always'
>
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View
{share ? <Share {...share} /> : null}
<CustomText
fontStyle='M'
fontWeight='Bold'
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
textAlign: 'center',
marginTop: StyleConstants.Spacing.L,
marginBottom: StyleConstants.Spacing.S,
color: colors.primaryDefault
}}
>
{share ? <Share {...share} /> : null}
<CustomText
fontStyle='M'
fontWeight='Bold'
style={{
textAlign: 'center',
marginTop: StyleConstants.Spacing.L,
marginBottom: StyleConstants.Spacing.S,
color: colors.primaryDefault
}}
>
{t('content.select_account')}
</CustomText>
<View
style={{
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
}}
>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
{t('content.select_account')}
</CustomText>
<View
style={{
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
}}
>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
)
.map((instance, index) => {
return (
<AccountButton
key={index}
instance={instance}
additionalActions={() => {
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}}
/>
)
.map((instance, index) => {
return (
<AccountButton
key={index}
instance={instance}
additionalActions={() => {
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}}
/>
)
})
: null}
</View>
})
: null}
</View>
</ScrollView>
</SafeAreaProvider>
</View>
</ScrollView>
)
}

View File

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

View File

@ -20,7 +20,7 @@ import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gestu
import { LiveTextImageView } from 'react-native-live-text-image-view'
import { runOnJS, useSharedValue } from 'react-native-reanimated'
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'
const ZoomFlatList = createZoomListComponent(FlatList)
@ -153,7 +153,7 @@ const ScreenImagesViewer = ({
)
return (
<SafeAreaProvider style={{ backgroundColor: 'black' }}>
<View style={{ backgroundColor: 'black' }}>
<StatusBar hidden />
<View
style={{
@ -232,7 +232,7 @@ const ScreenImagesViewer = ({
})}
/>
</LongPressGestureHandler>
</SafeAreaProvider>
</View>
)
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions } from 'react-native'
import { TabView } from 'react-native-tab-view'
import TabSharedRoot from './Shared/Root'
import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabPublicStackParamList>()
@ -107,7 +107,7 @@ const TabPublic = React.memo(
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen name='Tab-Public-Root' options={screenOptionsRoot} children={children} />
{TabSharedRoot({ Stack })}
{TabShared({ Stack })}
</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 TimelineDefault from '@components/Timeline/Default'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
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<
TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
> = ({
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
navigation,
route: {
params: { 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 (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => (
<TimelineDefault item={item} queryKey={queryKey} />
)
renderItem: ({ item }) => <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 ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view'
const TabSharedRoot = ({
Stack
}: {
Stack: ReturnType<typeof createNativeStackNavigator>
}) => {
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs')
@ -50,9 +46,7 @@ const TabSharedRoot = ({
backgroundColor: `rgba(255, 255, 255, 0)`
},
title: '',
headerLeft: () => (
<HeaderLeft onPress={() => navigation.goBack()} background />
),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />,
headerRight: () => {
const actions: ContextMenuAction[] = []
@ -77,13 +71,10 @@ const TabSharedRoot = ({
dropdownMenuMode
>
<HeaderRight
accessibilityLabel={t(
'shared.account.actions.accessibilityLabel',
{ user: account.acct }
)}
accessibilityHint={t(
'shared.account.actions.accessibilityHint'
)}
accessibilityLabel={t('shared.account.actions.accessibilityLabel', {
user: account.acct
})}
accessibilityHint={t('shared.account.actions.accessibilityHint')}
content='MoreHorizontal'
onPress={() => {}}
background
@ -132,9 +123,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Hashtag'
name='Tab-Shared-Hashtag'
component={TabSharedHashtag}
options={({
route
}: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
options={({ route }: TabSharedStackScreenProps<'Tab-Shared-Hashtag'>) => ({
title: `#${decodeURIComponent(route.params.hashtag)}`
})}
/>
@ -150,24 +139,16 @@ const TabSharedRoot = ({
key='Tab-Shared-Search'
name='Tab-Shared-Search'
component={TabSharedSearch}
options={({
navigation
}: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
options={({ navigation }: TabSharedStackScreenProps<'Tab-Shared-Search'>) => ({
...(Platform.OS === 'ios'
? {
headerLeft: () => (
<HeaderLeft onPress={() => navigation.goBack()} />
)
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
}
: { headerLeft: () => null }),
headerTitle: () => {
const onChangeText = debounce(
(text: string) => navigation.setParams({ text }),
1000,
{
trailing: true
}
)
const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
trailing: true
})
return (
<View
style={{
@ -199,9 +180,7 @@ const TabSharedRoot = ({
autoCorrect={false}
clearButtonMode='never'
keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) =>
navigation.setParams({ text })
}
onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
placeholder={t('shared.search.header.placeholder')}
placeholderTextColor={colors.secondary}
returnKeyType='go'
@ -216,9 +195,7 @@ const TabSharedRoot = ({
key='Tab-Shared-Toot'
name='Tab-Shared-Toot'
component={TabSharedToot}
options={{
title: t('shared.toot.name')
}}
options={{ title: t('shared.toot.name') }}
/>
<Stack.Screen
@ -233,9 +210,7 @@ const TabSharedRoot = ({
title: t(`shared.users.${reference}.${type}`, { count }),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter
content={t(`shared.users.${reference}.${type}`, { count })}
/>
<HeaderCenter 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 = () => {
log('log', 'audio', 'setting audio playback default options')
Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
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',
initialState: instancesInitialState,
reducers: {
updateInstanceActive: (
{ instances },
action: PayloadAction<InstanceLatest>
) => {
updateInstanceActive: ({ instances }, action: PayloadAction<InstanceLatest>) => {
instances = instances.map(instance => {
instance.active =
instance.url === action.payload.url &&
@ -43,9 +40,7 @@ const instancesSlice = createSlice({
},
updateInstanceAccount: (
{ instances },
action: PayloadAction<
Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>
>
action: PayloadAction<Pick<InstanceLatest['account'], 'acct' & 'avatarStatic'>>
) => {
const activeIndex = findInstanceActive(instances)
instances[activeIndex].account = {
@ -60,10 +55,7 @@ const instancesSlice = createSlice({
const activeIndex = findInstanceActive(instances)
instances[activeIndex].notifications_filter = action.payload
},
updateInstanceDraft: (
{ instances },
action: PayloadAction<ComposeStateDraft>
) => {
updateInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft>) => {
const activeIndex = findInstanceActive(instances)
const draftIndex = instances[activeIndex].drafts.findIndex(
({ timestamp }) => timestamp === action.payload.timestamp
@ -74,10 +66,7 @@ const instancesSlice = createSlice({
instances[activeIndex].drafts[draftIndex] = action.payload
}
},
removeInstanceDraft: (
{ instances },
action: PayloadAction<ComposeStateDraft['timestamp']>
) => {
removeInstanceDraft: ({ instances }, action: PayloadAction<ComposeStateDraft['timestamp']>) => {
const activeIndex = findInstanceActive(instances)
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload
@ -126,9 +115,7 @@ const instancesSlice = createSlice({
action: PayloadAction<InstanceLatest['frequentEmojis'][0]['emoji']>
) => {
const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
const calculateScore = (
emoji: InstanceLatest['frequentEmojis'][0]
): number => {
const calculateScore = (emoji: InstanceLatest['frequentEmojis'][0]): number => {
var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
var score = emoji.count + 1
var order = Math.log(Math.max(score, 1)) / Math.LN10
@ -137,9 +124,7 @@ const instancesSlice = createSlice({
}
const activeIndex = findInstanceActive(instances)
const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex(
e =>
e.emoji.shortcode === action.payload.shortcode &&
e.emoji.url === action.payload.url
e => e.emoji.shortcode === action.payload.shortcode && e.emoji.url === action.payload.url
)
let newEmojisSort: InstanceLatest['frequentEmojis']
if (foundEmojiIndex > -1) {
@ -147,11 +132,11 @@ const instancesSlice = createSlice({
.map((e, i) =>
i === foundEmojiIndex
? {
...e,
score: calculateScore(e),
count: e.count + 1,
lastUsed: new Date().getTime()
}
...e,
score: calculateScore(e),
count: e.count + 1,
lastUsed: new Date().getTime()
}
: e
)
.sort((a, b) => b.score - a.score)
@ -218,8 +203,7 @@ const instancesSlice = createSlice({
return true
}
})
state.instances.length &&
(state.instances[state.instances.length - 1].active = true)
state.instances.length && (state.instances[state.instances.length - 1].active = true)
analytics('logout')
})
@ -250,8 +234,7 @@ const instancesSlice = createSlice({
.addCase(updateConfiguration.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].version = action.payload?.version || '0'
state.instances[activeIndex].configuration =
action.payload.configuration
state.instances[activeIndex].configuration = action.payload.configuration
})
.addCase(updateConfiguration.rejected, (_, action) => {
console.error(action.error)
@ -291,22 +274,16 @@ const instancesSlice = createSlice({
// Update Instance Push Individual Alert
.addCase(updateInstancePushAlert.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[
action.meta.arg.changed
].loading = false
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
state.instances[activeIndex].push.alerts = action.payload
})
.addCase(updateInstancePushAlert.rejected, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[
action.meta.arg.changed
].loading = false
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
})
.addCase(updateInstancePushAlert.pending, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].push.alerts[
action.meta.arg.changed
].loading = true
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = true
})
// Check if frequently used emojis still exist
@ -317,8 +294,7 @@ const instancesSlice = createSlice({
activeIndex
].frequentEmojis?.filter(emoji => {
return action.payload?.find(
e =>
e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
e => e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
)
})
})
@ -331,8 +307,7 @@ const instancesSlice = createSlice({
export const getInstanceActive = ({ instances: { instances } }: RootState) =>
findInstanceActive(instances)
export const getInstances = ({ instances: { instances } }: RootState) =>
instances
export const getInstances = ({ instances: { instances } }: RootState) => instances
export const getInstance = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]
@ -350,42 +325,30 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.version
export const checkInstanceFeature =
(feature: string) =>
({ instances: { instances } }: RootState): Boolean => {
return (
features
.filter(f => f.feature === feature)
.filter(
f =>
parseFloat(instances[findInstanceActive(instances)]?.version) >=
f.version
)?.length > 0
)
}
({ instances: { instances } }: RootState): boolean => {
return (
features
.filter(f => f.feature === feature)
.filter(f => parseFloat(instances[findInstanceActive(instances)]?.version) >= f.version)
?.length > 0
)
}
/* Get Instance Configuration */
export const getInstanceConfigurationStatusMaxChars = ({
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses
.max_characters || 500
export const getInstanceConfigurationStatusMaxChars = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses.max_characters || 500
export const getInstanceConfigurationStatusMaxAttachments = ({
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses
.max_media_attachments || 4
instances[findInstanceActive(instances)]?.configuration?.statuses.max_media_attachments || 4
export const getInstanceConfigurationStatusCharsURL = ({
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses
.characters_reserved_per_url || 23
export const getInstanceConfigurationStatusCharsURL = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.statuses.characters_reserved_per_url ||
23
export const getInstanceConfigurationMediaAttachments = ({
instances: { instances }
}: RootState) =>
instances[findInstanceActive(instances)]?.configuration
?.media_attachments || {
export const getInstanceConfigurationMediaAttachments = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.media_attachments || {
supported_mime_types: [
'image/jpeg',
'image/png',
@ -418,9 +381,7 @@ export const getInstanceConfigurationMediaAttachments = ({
video_matrix_limit: 2304000
}
export const getInstanceConfigurationPoll = ({
instances: { instances }
}: RootState) =>
export const getInstanceConfigurationPoll = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.configuration?.polls || {
max_options: 4,
max_characters_per_option: 50,
@ -432,16 +393,14 @@ export const getInstanceConfigurationPoll = ({
export const getInstanceAccount = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.account
export const getInstanceNotificationsFilter = ({
instances: { instances }
}: RootState) => instances[findInstanceActive(instances)]?.notifications_filter
export const getInstanceNotificationsFilter = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.notifications_filter
export const getInstancePush = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.push
export const getInstanceTimelinesLookback = ({
instances: { instances }
}: RootState) => instances[findInstanceActive(instances)]?.timelinesLookback
export const getInstanceTimelinesLookback = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.timelinesLookback
export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.mePage
@ -449,9 +408,8 @@ export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.drafts
export const getInstanceFrequentEmojis = ({
instances: { instances }
}: RootState) => instances[findInstanceActive(instances)]?.frequentEmojis
export const getInstanceFrequentEmojis = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.frequentEmojis
export const {
updateInstanceActive,