1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into candidate

This commit is contained in:
xmflsct
2023-01-27 00:44:45 +01:00
78 changed files with 1623 additions and 927 deletions

114
src/components/Filter.tsx Normal file
View File

@ -0,0 +1,114 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { TouchableNativeFeedback } from 'react-native-gesture-handler'
import Icon from './Icon'
import CustomText from './Text'
export type Props = {
onPress: () => void
filter: Mastodon.Filter<'v2'>
button?: React.ReactNode
}
export const Filter: React.FC<Props> = ({ onPress, filter, button }) => {
const { t } = useTranslation(['common', 'screenTabs'])
const { colors } = useTheme()
return (
<TouchableNativeFeedback onPress={onPress}>
<View
style={{
paddingVertical: StyleConstants.Spacing.S,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundDefault
}}
>
<View style={{ flex: 1 }}>
<CustomText
fontStyle='M'
children={filter.title}
style={{ color: colors.primaryDefault }}
numberOfLines={1}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: StyleConstants.Spacing.XS
}}
>
{filter.expires_at && new Date() > new Date(filter.expires_at) ? (
<CustomText
fontStyle='S'
fontWeight='Bold'
children={t('screenTabs:me.preferencesFilters.expired')}
style={{ color: colors.red, marginRight: StyleConstants.Spacing.M }}
/>
) : null}
{filter.keywords?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.keywords', {
count: filter.keywords.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.keywords?.length && filter.statuses?.length ? (
<CustomText
children={t('common:separator')}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.statuses?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.statuses', {
count: filter.statuses.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
</View>
<CustomText
style={{ color: colors.secondary }}
children={
<Trans
ns='screenTabs'
i18nKey='me.preferencesFilters.context'
components={[
<>
{filter.context.map((c, index) => (
<Fragment key={index}>
<CustomText
style={{
color: colors.secondary,
textDecorationColor: colors.disabled,
textDecorationLine: 'underline',
textDecorationStyle: 'solid'
}}
children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)}
/>
<CustomText children={t('common:separator')} />
</Fragment>
))}
</>
]}
/>
}
/>
</View>
{button || (
<Icon
name='chevron-right'
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
style={{ marginLeft: 8 }}
/>
)}
</View>
</TouchableNativeFeedback>
)
}

View File

@ -22,7 +22,7 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = () => {
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
navigation.push('Tab-Shared-Hashtag', { tag_name: hashtag.name })
}
const padding = StyleConstants.Spacing.Global.PagePadding

View File

@ -235,12 +235,7 @@ const ComponentInstance: React.FC<Props> = ({
/>
</View>
) : null}
<View
style={{
marginTop: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View style={{ marginTop: StyleConstants.Spacing.L }}>
<View
style={{
flexDirection: 'row',

View File

@ -0,0 +1,29 @@
import { useHeaderHeight } from '@react-navigation/elements'
import { StyleConstants } from '@utils/styles/constants'
import { forwardRef, PropsWithChildren, RefObject } from 'react'
import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
export const ModalScrollView = forwardRef(
({ children }: PropsWithChildren, ref: RefObject<ScrollView>) => {
const headerHeight = useHeaderHeight()
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={headerHeight}
>
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
<ScrollView
ref={ref}
keyboardShouldPersistTaps='always'
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
>
{children}
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
)
}
)

View File

@ -69,7 +69,7 @@ const ParseHTML: React.FC<Props> = ({
const [followedTags] = useAccountStorage.object('followed_tags')
const MAX_ALLOWED_LINES = 30
const MAX_ALLOWED_LINES = 35
const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted)
@ -147,7 +147,7 @@ const ParseHTML: React.FC<Props> = ({
tag?.length &&
!disableDetails &&
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
navigation.push('Tab-Shared-Hashtag', { tag_name: tag })
}
children={children}
/>
@ -203,9 +203,7 @@ const ParseHTML: React.FC<Props> = ({
onPress={async () => {
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1)
})
navigation.push('Tab-Shared-Hashtag', { tag_name: content.substring(1) })
} else {
await openLink(href, navigation)
}
@ -309,7 +307,11 @@ const ParseHTML: React.FC<Props> = ({
height: numberOfLines === 1 && !expanded ? 0 : undefined
}}
numberOfLines={
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : MAX_ALLOWED_LINES
typeof totalLines === 'number'
? expanded
? 999
: numberOfLines
: Math.max(MAX_ALLOWED_LINES, numberOfLines)
}
selectable={selectable}
/>

View File

@ -137,8 +137,8 @@ const TimelinePoll: React.FC = () => {
marginRight: StyleConstants.Spacing.S
}}
name={
`${poll.own_votes?.includes(index) ? 'Check' : ''}${
poll.multiple ? 'Square' : 'Circle'
`${poll.own_votes?.includes(index) ? 'check-' : ''}${
poll.multiple ? 'square' : 'circle'
}` as any
}
size={StyleConstants.Font.Size.M}

View File

@ -0,0 +1,96 @@
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagMutation, useTagQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTranslation } from 'react-i18next'
const menuHashtag = ({
tag_name,
queryKey
}: {
tag_name: Mastodon.Tag['name']
queryKey?: QueryKeyTimeline
}): ContextMenu => {
const navigation =
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
const { t } = useTranslation(['common', 'componentContextMenu'])
const canFollowTags = featureCheck('follow_tags')
const queryClient = useQueryClient()
const {
data,
isFetching: tagIsFetching,
refetch: tagRefetch
} = useTagQuery({ tag_name, options: { enabled: canFollowTags } })
const tagMutation = useTagMutation({
onSuccess: () => {
haptics('Light')
tagRefetch()
const queryKeyFollowedTags: QueryKeyFollowedTags = ['FollowedTags']
queryClient.invalidateQueries({ queryKey: queryKeyFollowedTags })
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: t('componentContextMenu:hashtag.follow', {
defaultValue: 'false',
context: to.toString()
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
const menus: ContextMenu = []
menus.push([
{
type: 'item',
key: 'hashtag-follow',
props: {
onSelect: () =>
typeof data?.following === 'boolean' &&
tagMutation.mutate({ tag_name, to: !data.following }),
disabled: tagIsFetching,
destructive: false,
hidden: !canFollowTags
},
title: t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'false',
context: (data?.following || false).toString()
}),
icon: data?.following ? 'rectangle.stack.badge.minus' : 'rectangle.stack.badge.plus'
},
{
type: 'item',
key: 'hashtag-filter',
props: {
onSelect: () => navigation.navigate('Tab-Shared-Filter', { source: 'hashtag', tag_name }),
disabled: false,
destructive: false,
hidden: !canFollowTags
},
title: t('componentContextMenu:hashtag.filter.action'),
icon: 'line.3.horizontal.decrease'
}
])
return menus
}
export default menuHashtag

View File

@ -231,6 +231,22 @@ const menuStatus = ({
context: (status.pinned || false).toString()
}),
icon: status.pinned ? 'pin.slash' : 'pin'
},
{
type: 'item',
key: 'status-filter',
props: {
// @ts-ignore
onSelect: () => navigation.navigate('Tab-Shared-Filter', { source: 'status', status, queryKey }),
disabled: false,
destructive: false,
hidden: !('filtered' in status)
},
title: t('componentContextMenu:status.filter.action', {
defaultValue: 'false',
context: (!!status.filtered?.length).toString()
}),
icon: status.pinned ? 'rectangle.badge.checkmark' : 'rectangle.badge.xmark'
}
])

View File

@ -6,7 +6,7 @@
"action_false": "Segueix l'usuari",
"action_true": "Deixa de seguir l'usuari"
},
"inLists": "Llistes que continguin l'usuari",
"inLists": "",
"showBoosts": {
"action_false": "Mostra els impulsos de l'usuari",
"action_true": "Oculta els impulsos de l'usuari"
@ -16,12 +16,12 @@
"action_true": "Deixa de silenciar l'usuari"
},
"followAs": {
"trigger": "Segueix com...",
"trigger": "",
"succeed_default": "Seguint a @{{target}} com @{{source}}",
"succeed_locked": "Enviada la sol·licitud de seguiment a @{{target}} com {{source}}, pendent d'aprovar-la",
"failed": "Segueix com"
},
"blockReport": "Bloqueja i denuncia...",
"blockReport": "",
"block": {
"action_false": "Bloqueja l'usuari",
"action_true": "Deixa de bloquejar l'usuari",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Comparteix la publicació"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Fixa la publicació",
"action_true": "Deixa de fixar la publicació"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -98,7 +98,7 @@
"match_v2_one": "Filtrat per {{filters}}.",
"match_v2_other": "Filtrat per {{count}} filtres, {{filters}}."
},
"fullConversation": "Llegeix conversacions",
"fullConversation": "Llegeix converses",
"translate": {
"default": "Tradueix",
"succeed": "Traduït per {{provider}} des de l'idioma {{source}}",

View File

@ -70,16 +70,16 @@
"name": "Notificacions push"
},
"preferences": {
"name": ""
"name": "Preferències"
},
"preferencesFilters": {
"name": ""
"name": "Tots els filtres de continguts"
},
"preferencesFilterAdd": {
"name": ""
"name": "Crea un filtre"
},
"preferencesFilterEdit": {
"name": ""
"name": "Edita el filtre"
},
"profile": {
"name": "Edita el teu perfil"
@ -136,81 +136,82 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "Visibilitat de la publicació per defecte",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "Públic",
"unlisted": "Sense llistar",
"private": "Només als seguidors"
}
},
"sensitive": {
"title": ""
"title": "Marca el contingut com a sensible"
},
"media": {
"title": "",
"title": "Mostra el contingut multimèdia",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "Amaga el contingut multimèdia com a sensible",
"show_all": "Mostra sempre el contingut multimèdia",
"hide_all": "Amaga sempre el contingut multimèdia"
}
},
"spoilers": {
"title": ""
"title": "Amplia sempre el contingut amb alertes"
},
"autoplay_gifs": {
"title": ""
"title": "Autoreprodueix els GIF"
},
"filters": {
"title": "",
"content": ""
"title": "Filtres de continguts",
"content": "{{count}} actiu/s"
},
"web_only": {
"title": "",
"description": ""
"title": "Actualitza la configuració",
"description": "Les configuracions següents només es poden actualitzant fent servir el web"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"expired": "Caducat",
"keywords_one": "{{count}} paraula clau",
"keywords_other": "{{count}} paraules clau",
"statuses_one": "{{count}} publicació",
"statuses_other": "{{count}} publicacions",
"context": "Aplica a <0 />",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Seguits i llistes",
"notifications": "Notificació",
"public": "federat",
"thread": "Conversa",
"account": "Perfil"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "Nom",
"expiration": "Venciment",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "Mai",
"1800": "Després de 30 minuts",
"3600": "Després d'una hora",
"43200": "Després de 12 hores",
"86400": "Després d'un dia",
"604800": "Després d'una setmana",
"18144000": "Després d'un mes"
},
"context": "",
"context": "Aplica a",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Seguits i llistes",
"notifications": "Notificació",
"public": "Línia de temps federada",
"thread": "Vista de conversa",
"account": "Vista del perfil"
},
"action": "",
"action": "Quan coincideix",
"actions": {
"warn": "",
"hide": ""
"hide": "Amagat completament"
},
"keywords": "",
"keyword": ""
"keywords": "Coincidències per aquestes paraules claus",
"keyword": "Paraula clau",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "Multimèdia de <0 /><1></1>"
},
"hashtag": {
"follow": "Segueix",
"unfollow": "Deixa de seguir"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Edita l'historial"

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": ""
@ -88,6 +97,10 @@
"pin": {
"action_false": "",
"action_true": ""
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": ""
},
"hashtag": {
"follow": "",
"unfollow": ""
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": ""

View File

@ -6,7 +6,7 @@
"action_false": "Folgen",
"action_true": "Nutzer entfolgen"
},
"inLists": "Listen mit Nutzern",
"inLists": "",
"showBoosts": {
"action_false": "",
"action_true": ""
@ -16,12 +16,12 @@
"action_true": "Stummschaltung des Nutzers aufheben"
},
"followAs": {
"trigger": "Folgen als…",
"trigger": "",
"succeed_default": "Folgt nun @{{target}} als @{{source}}",
"succeed_locked": "Follower-Anfrage an @{{target}} mit {{source}} gesendet, Bestätigung ausstehend",
"failed": "Folgen als…"
},
"blockReport": "Blockieren und melden…",
"blockReport": "",
"block": {
"action_false": "Nutzer blockieren",
"action_true": "User entblocken",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Tröt teilen"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Tröt anheften",
"action_true": "Tröt nicht mehr anheften"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -70,16 +70,16 @@
"name": "Push-Benachrichtigung"
},
"preferences": {
"name": ""
"name": "Einstellungen"
},
"preferencesFilters": {
"name": ""
"name": "Inhaltsfilter"
},
"preferencesFilterAdd": {
"name": ""
"name": "Filterregel erstellen"
},
"preferencesFilterEdit": {
"name": ""
"name": "Filter bearbeiten"
},
"profile": {
"name": "Profil bearbeiten"
@ -136,81 +136,82 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "Standard-Einstellung für die Sichtbarkeit des Tröts",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "Öffentlich",
"unlisted": "Ungelistet",
"private": "Nur für Follower"
}
},
"sensitive": {
"title": ""
"title": "Medien immer als sensibel kennzeichnen"
},
"media": {
"title": "",
"title": "Medien-Anzeige",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "Als heikel gekennzeichnete Medien ausblenden",
"show_all": "Medien immer anzeigen",
"hide_all": "Medien immer verstecken"
}
},
"spoilers": {
"title": ""
"title": "Beiträge mit Inhaltswarnungen immer ausklappen"
},
"autoplay_gifs": {
"title": ""
"title": "GIFs automatisch abspielen"
},
"filters": {
"title": "",
"content": ""
"title": "Inhaltsfilter",
"content": "{{count}} aktiv"
},
"web_only": {
"title": "",
"description": ""
"title": "Einstellungen aktualisieren",
"description": "Nachfolgende Einstellungen können nur über das Web-Interface aktualisiert werden"
}
},
"preferencesFilters": {
"expired": "",
"expired": "Abgelaufen",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"statuses_one": "{{count}} Tröt",
"statuses_other": "{{count}} Tröts",
"context": "Läuft in <0 /> ab",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Gefolgte und Listen",
"notifications": "Benachrichtigung",
"public": "Föderiert",
"thread": "Unterhaltung",
"account": "Profil"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "Name",
"expiration": "Ablaufdatum",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "Niemals",
"1800": "Nach 30 Minuten",
"3600": "Nach einer Stunde",
"43200": "Nach 12 Stunden",
"86400": "Nach einem Tag",
"604800": "Nach einer Woche",
"18144000": "Nach einem Monat"
},
"context": "",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Gefolgte und Listen",
"notifications": "Benachrichtigung",
"public": "Föderierte Timeline",
"thread": "Unterhaltungsansicht",
"account": "Profilansicht"
},
"action": "",
"action": "Bei Übereinstimmung",
"actions": {
"warn": "",
"hide": ""
"warn": "Verdeckt, kann aber aufgedeckt werden",
"hide": "Vollständig verdeckt"
},
"keywords": "",
"keyword": ""
"keyword": "Stichwörter",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>'s Medien</1>"
},
"hashtag": {
"follow": "Folgen",
"unfollow": "Entfolgen"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Bearbeitungsverlauf"

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Κοινοποίηση ανάρτησης"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Κρέμασμα ανάρτησης",
"action_true": "Ξεκρέμασμα ανάρτησης"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1> πολυμέσα</1>"
},
"hashtag": {
"follow": "Ακολουθήστε",
"unfollow": "Διακοπή παρακολούθησης"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Ιστορικό επεξεργασίας"

View File

@ -6,7 +6,7 @@
"action_false": "Follow user",
"action_true": "Unfollow user"
},
"inLists": "Lists containing user",
"inLists": "Lists containing user ...",
"showBoosts": {
"action_false": "Show user's boosts",
"action_true": "Hide users's boosts"
@ -16,12 +16,12 @@
"action_true": "Unmute user"
},
"followAs": {
"trigger": "Follow as...",
"trigger": "Follow as ...",
"succeed_default": "Now following @{{target}} with @{{source}}",
"succeed_locked": "Sent follow request to @{{target}} with {{source}}, pending approval",
"failed": "Follow as"
},
"blockReport": "Block and report...",
"blockReport": "Block and report",
"block": {
"action_false": "Block user",
"action_true": "Unblock user",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "Follow",
"action_true": "Unfollow"
},
"filter": {
"action": "Filter hashtag ..."
}
},
"share": {
"status": {
"action": "Share toot"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Pin toot",
"action_true": "Unpin toot"
},
"filter": {
"action_false": "Filter toot ...",
"action_true": "Manage filters ..."
}
}
}

View File

@ -210,7 +210,8 @@
"hide": "Hidden completely"
},
"keywords": "Matches for these keywords",
"keyword": "Keyword"
"keyword": "Keyword",
"statuses": "Matches these toots"
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>'s media</1>"
},
"hashtag": {
"follow": "Follow",
"unfollow": "Unfollow"
"filter": {
"name": "Add to filter",
"existed": "Existed in these filters"
},
"history": {
"name": "Edit History"

View File

@ -6,7 +6,7 @@
"action_false": "Seguir usuario",
"action_true": "Dejar de seguir usuario"
},
"inLists": "Listas que contienen el usuario",
"inLists": "",
"showBoosts": {
"action_false": "Mostrar los impulsos del usuario",
"action_true": "Ocultar los impulsos del usuario"
@ -16,12 +16,12 @@
"action_true": "Dejar de silenciar al usuario"
},
"followAs": {
"trigger": "Seguir como...",
"trigger": "",
"succeed_default": "Siguiendo @{{target}} como @{{source}}",
"succeed_locked": "Enviado la solicitud de seguimiento a @{{target}} como {{source}}, pendiente de aprobación",
"failed": "Seguir como"
},
"blockReport": "Bloquear y denunciar...",
"blockReport": "",
"block": {
"action_false": "Bloquear usuario",
"action_true": "Desbloquear usuario",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Compartir toot"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Fijar toot",
"action_true": "Desfijar toot"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "Multimedia de <0 /><1></1>"
},
"hashtag": {
"follow": "Seguir",
"unfollow": "Dejar de seguir"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Historial de ediciones"

View File

@ -1,32 +1,32 @@
{
"buttons": {
"OK": "",
"apply": "",
"cancel": "",
"discard": "",
"continue": "",
"create": "",
"delete": "",
"done": "",
"confirm": ""
"OK": "Ados",
"apply": "Aplikatu",
"cancel": "Utzi",
"discard": "Baztertu",
"continue": "Jarraitu",
"create": "Sortu",
"delete": "Ezabatu",
"done": "Eginda",
"confirm": "Berretsi"
},
"customEmoji": {
"accessibilityLabel": ""
"accessibilityLabel": "Emoji pertsonalizatua {{emoji}}"
},
"message": {
"success": {
"message": ""
"message": "{{function}} ongi burutu da"
},
"warning": {
"message": ""
},
"error": {
"message": ""
"message": "{{function}}-(e)k huts egin du, mesedez, saia zaitez berriro"
}
},
"separator": "",
"separator": ", ",
"discard": {
"title": "",
"message": ""
"title": "Aldaketa ez da gorde",
"message": "Zuk eginiko aldaketa ez da gorde. Baztertu nahi al duzu?"
}
}

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": ""
@ -88,6 +97,10 @@
"pin": {
"action_false": "",
"action_true": ""
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -1,27 +1,27 @@
{
"server": {
"textInput": {
"placeholder": ""
"placeholder": "Instantziaren domeinua"
},
"whitelisted": "",
"button": "",
"whitelisted": "Baliteke instantzia hau zerrenda zuri batean egotea eta toootek bertako data ezin eskura ahal izatea saioa hasi aurretik.",
"button": "Saioa hasi",
"information": {
"name": "",
"accounts": "",
"statuses": "",
"domains": ""
"name": "Izena",
"accounts": "Erabiltzaileak",
"statuses": "Tutak",
"domains": "Domeinuak"
},
"disclaimer": {
"base": ""
"base": "Saioa hasteko prozesuak sistemako nabigatzailea erabiltzen du, beraz, zure kontuko informazioa ez da tooot aplikazioarentzat ikusgarri izango."
},
"terms": {
"base": ""
"base": "Saio hastean, <0>pribatutasun-politika</0> eta <1>zerbitzuaren terminoak</1> onartzen dituzu."
}
},
"update": {
"alert": {
"title": "",
"message": ""
"title": "Instantzia honetan saioa hasita",
"message": "Beste kontu batekin saioa has dezakezu, saioa hasita duen kontua mantentzen delarik"
}
}
}

View File

@ -1,8 +1,8 @@
{
"HTML": {
"accessibilityHint": "",
"expanded": "",
"moreLines": "",
"defaultHint": ""
"expanded": "{{hint}}{{moreLines}}",
"moreLines": " ({{count}} lerro gehiago)",
"defaultHint": "Tut luzea"
}
}

View File

@ -1,16 +1,16 @@
{
"follow": {
"function": ""
"function": "Erabiltzailea jarraitu"
},
"block": {
"function": ""
"function": "Erabiltzailea blokeatu"
},
"button": {
"error": "",
"blocked_by": "",
"blocking": "",
"following": "",
"requested": "",
"default": ""
"error": "Errorea kargatzean",
"blocked_by": "Erabiltzaileak blokeatua",
"blocking": "Desblokeatu",
"following": "Jarraitzeari utzi",
"requested": "Eskaera baztertu",
"default": "Jarraitu"
}
}

View File

@ -1,46 +1,46 @@
{
"empty": {
"error": {
"message": "",
"button": ""
"message": "Errorea kargatzean",
"button": "Berriro saiatu"
},
"success": {
"message": ""
"message": "Denbora-lerroa hutsik"
}
},
"end": {
"message": ""
"message": "Amaitu dugu, katilu bat <0 /> nahi?"
},
"lookback": {
"message": ""
"message": "Azkenekoz irakurria"
},
"refresh": {
"fetchPreviousPage": "",
"refetch": ""
"fetchPreviousPage": "Berrienak hemendik hasita",
"refetch": "Azkenekora arte"
},
"shared": {
"actioned": {
"pinned": "",
"favourite": "",
"status": "",
"follow": "",
"follow_request": "",
"poll": "",
"pinned": "Finkatua",
"favourite": "{{name}}-(e)k zure tuta gogoko du",
"status": "{{name}}-(e)k tut bat bidali berri du",
"follow": "{{name}}-(e)k jarraitu zaitu",
"follow_request": "{{name}}-(e)k zu jarraitzeko eskaera egin du",
"poll": "Erantzun zenuen inkesta bat amaitu da",
"reblog": {
"default": "",
"notification": ""
"default": "{{name}}-(e)k bultzatu du",
"notification": "{{name}}-(e)k zure tuta bultzatu du"
},
"update": "",
"admin.sign_up": "",
"admin.report": ""
"update": "Bultzada editatua izan da",
"admin.sign_up": "{{name}} instantziara elkartu da",
"admin.report": "{{name}}-(e)k zera salatu du:"
},
"actions": {
"reply": {
"accessibilityLabel": ""
"accessibilityLabel": "Tut honi erantzun"
},
"reblogged": {
"accessibilityLabel": "",
"function": "",
"accessibilityLabel": "Tut hau bultzatu",
"function": "Tuta bultzatu",
"options": {
"title": "",
"public": "",

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": ""
},
"hashtag": {
"follow": "",
"unfollow": ""
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": ""

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Partager le pouet"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Pouet épinglé",
"action_true": "Détacher le pouet"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "Média de <0 /><1></1>"
},
"hashtag": {
"follow": "Suivre",
"unfollow": "Ne plus suivre"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Modifier l'historique"

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Condividi toot"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Fissa toot",
"action_true": "Togli toot all'alto"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "Media di <0 /><1>\"</1>"
},
"hashtag": {
"follow": "Segui",
"unfollow": "Smetti di seguire"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Cronologia delle modifiche"

View File

@ -6,7 +6,7 @@
"action_false": "ユーザーをフォロー",
"action_true": "ユーザーをフォロー解除"
},
"inLists": "ユーザーを含むリスト一覧",
"inLists": "",
"showBoosts": {
"action_false": "ブーストを表示する",
"action_true": "ブーストを非表示にする"
@ -16,12 +16,12 @@
"action_true": "ユーザーのミュートを解除"
},
"followAs": {
"trigger": "別ユーザーとしてフォロー",
"trigger": "",
"succeed_default": "{{source}} として @{{target}} をフォローしました",
"succeed_locked": "{{source}} として @{{target}} へのフォロー申請を送りました。現在承認待ちです",
"failed": "別ユーザーとしてフォロー"
},
"blockReport": "通報・ブロック...",
"blockReport": "",
"block": {
"action_false": "ユーザーをブロック",
"action_true": "ユーザーのブロックを解除",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "トゥートを共有"
@ -88,6 +97,10 @@
"pin": {
"action_false": "トゥートを固定",
"action_true": "トゥートの固定を解除"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>のメディア</1>"
},
"hashtag": {
"follow": "フォロー",
"unfollow": "フォロー解除"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "編集履歴"

View File

@ -6,7 +6,7 @@
"action_false": "사용자 팔로우",
"action_true": "사용자 팔로우 해제"
},
"inLists": "사용자를 포함한 리스트",
"inLists": "",
"showBoosts": {
"action_false": "사용자의 부스트 보이기",
"action_true": "사용자의 부스트 숨기기"
@ -16,12 +16,12 @@
"action_true": "사용자 뮤트 해제"
},
"followAs": {
"trigger": "특정 계정에서 팔로우...",
"trigger": "",
"succeed_default": "@{{source}} 계정에서 @{{target}} 계정을 팔로우 했어요.",
"succeed_locked": "@{{source}} 계정에서 @{{target}} 계정의 팔로우 승인을 요청했어요.",
"failed": "특정 계정에서 팔로우"
},
"blockReport": "차단 및 신고...",
"blockReport": "",
"block": {
"action_false": "사용자 차단",
"action_true": "사용자 차단 해제",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "툿 공유"
@ -88,6 +97,10 @@
"pin": {
"action_false": "툿 고정",
"action_true": "툿 고정 해제"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>의 미디어</1>"
},
"hashtag": {
"follow": "팔로우",
"unfollow": "팔로우 해제"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "수정 이력"

View File

@ -6,7 +6,7 @@
"action_false": "Volg gebruiker",
"action_true": "Ontvolg"
},
"inLists": "Lijsten waarin gebruiker staat",
"inLists": "",
"showBoosts": {
"action_false": "Boosts van gebruiker weergeven",
"action_true": "Boosts van gebruiker verbergen"
@ -16,12 +16,12 @@
"action_true": "Dempen opheffen voor gebruiker"
},
"followAs": {
"trigger": "Volg als...",
"trigger": "",
"succeed_default": "Je volgt nu @{{target}} met @{{source}}",
"succeed_locked": "Verstuurde het volgverzoek naar @{{target}} met {{source}}, in afwachting van goedkeuring",
"failed": "Volg als"
},
"blockReport": "Blokkeren en rapporten...",
"blockReport": "",
"block": {
"action_false": "Gebruiker blokkeren",
"action_true": "Gebruiker deblokkeren",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Toot delen"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Toot vastzetten",
"action_true": "Toot losmaken"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>'s media</1>"
},
"hashtag": {
"follow": "Volg",
"unfollow": "Ontvolg"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Geschiedenis bewerken"

View File

@ -6,7 +6,7 @@
"action_false": "Obserwuj",
"action_true": "Przestań obserwować"
},
"inLists": "Listy zawierające użytkownika",
"inLists": "",
"showBoosts": {
"action_false": "Pokaż podbicia użytkownika",
"action_true": "Ukryj podbicia użytkowników"
@ -16,12 +16,12 @@
"action_true": "Wyłącz wyciszenie"
},
"followAs": {
"trigger": "Obserwuj jako...",
"trigger": "",
"succeed_default": "Teraz obserwujesz @{{target}} z @{{source}}",
"succeed_locked": "Wysłano prośbę o obserwowanie do @{{target}} z {{source}}, oczekiwanie na zatwierdzenie",
"failed": "Obserwuj jako"
},
"blockReport": "Zablokuj i zgłoś...",
"blockReport": "",
"block": {
"action_false": "Zablokuj użytkownika",
"action_true": "Odblokuj użytkownika",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Udostępnij wpis"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Przypnij wpis",
"action_true": "Odepnij wpis"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -70,16 +70,16 @@
"name": "Powiadomienia Push"
},
"preferences": {
"name": ""
"name": "Preferencje"
},
"preferencesFilters": {
"name": ""
"name": "Wszystkie filtry treści"
},
"preferencesFilterAdd": {
"name": ""
"name": "Utwórz filtr"
},
"preferencesFilterEdit": {
"name": ""
"name": "Edytuj filtr"
},
"profile": {
"name": "Edytuj profil"
@ -136,81 +136,82 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "Domyślna widoczność tootów",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "Publiczny",
"unlisted": "Niepubliczny",
"private": "Tylko obserwujący"
}
},
"sensitive": {
"title": ""
"title": "Domyślnie oznacz media jako wrażliwe"
},
"media": {
"title": "",
"title": "Wyświetlanie multimediów",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "Ukryj media oznaczone jako wrażliwe",
"show_all": "Zawsze pokazuj media",
"hide_all": "Zawsze ukrywaj media"
}
},
"spoilers": {
"title": ""
"title": "Automatycznie rozwijaj tooty z ostrzeżeniem o zawartości"
},
"autoplay_gifs": {
"title": ""
"title": "Autoodtwarzanie GIF w tootach"
},
"filters": {
"title": "",
"content": ""
"title": "Filtry treści",
"content": "{{count}} aktywnych"
},
"web_only": {
"title": "",
"description": ""
"title": "Ustawienia aktualizacji",
"description": "Poniższe ustawienia mogą być aktualizowane tylko przy użyciu interfejsu webowego"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"expired": "Wygasło",
"keywords_one": "{{count}} słowo kluczowe",
"keywords_other": "{{count}} słów kluczowych",
"statuses_one": "{{count}} toot",
"statuses_other": "{{count}} tootów",
"context": "Użyto na <0 />",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "obserwujący i listy",
"notifications": "powiadomienie",
"public": "sfederowane",
"thread": "rozmowa",
"account": "profil"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "Nazwa",
"expiration": "Wygaśnięcie",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "Nigdy",
"1800": "Po 30 minutach",
"3600": "Po 1 godzinie",
"43200": "Po 12 godzinach",
"86400": "Po 1 dniu",
"604800": "Po tygodniu",
"18144000": "Po miesiącu"
},
"context": "",
"context": "Zastosuje na",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Obserwujący i listy",
"notifications": "Powiadomienie",
"public": "Sfederowana oś czasu",
"thread": "Widok rozmów",
"account": "Widok profilu"
},
"action": "",
"action": "Gdy pasuje",
"actions": {
"warn": "",
"hide": ""
"warn": "Zwinięty, ale można go pokazać",
"hide": "Całkowicie ukryte"
},
"keywords": "",
"keyword": ""
"keywords": "Pasuje do tych słów kluczowych",
"keyword": "Słowo kluczowe",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1> multimedia </1>"
},
"hashtag": {
"follow": "Obserwuj",
"unfollow": "Przestań obserwować"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Historia edycji"

View File

@ -6,8 +6,8 @@
"discard": "Descartar",
"continue": "Continuar",
"create": "",
"delete": "",
"done": "",
"delete": "Excluir",
"done": "Concluído",
"confirm": "Confirmar"
},
"customEmoji": {

View File

@ -4,7 +4,7 @@
"title": "Ações do Usuário",
"following": {
"action_false": "Seguir usuário",
"action_true": ""
"action_true": "Deixar de seguir usuário"
},
"inLists": "",
"showBoosts": {
@ -17,7 +17,7 @@
},
"followAs": {
"trigger": "",
"succeed_default": "",
"succeed_default": "Agora seguindo @{{target}} com @{{source}}",
"succeed_locked": "",
"failed": ""
},
@ -26,13 +26,13 @@
"action_false": "Bloquear usuário",
"action_true": "Desbloquear usuário",
"alert": {
"title": ""
"title": "Confirmar o bloqueio do usuário @{{username}} ?"
}
},
"reports": {
"action": "",
"action": "Denunciar e bloquear usuário",
"alert": {
"title": ""
"title": "Confirmar denúncia e bloqueio do usuário @{{username}} ?"
}
}
},
@ -41,7 +41,7 @@
"public": ""
},
"copy": {
"action": "",
"action": "Copiar toot",
"succeed": "Copiado"
},
"instance": {
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Compartilhar toot"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Toot fixado",
"action_true": "Desafixar toot"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -1,6 +1,6 @@
{
"title": "Selecionar fonte de mídia",
"message": "",
"message": "Os dados EXIF de mídia não são enviados",
"options": {
"image": "Enviar fotos",
"image_max": "Carregar fotos (máx. {{max}})",

View File

@ -3,6 +3,6 @@
"accessibilityHint": "Toque para expandir ou recolher conteúdo",
"expanded": "{{hint}}{{moreLines}}",
"moreLines": "",
"defaultHint": ""
"defaultHint": "Toot longo"
}
}

View File

@ -42,9 +42,9 @@
"accessibilityLabel": "Boost este toot",
"function": "Boost toot",
"options": {
"title": "",
"public": "",
"unlisted": ""
"title": "Escolha a visibilidade do boost",
"public": "Boost público",
"unlisted": "Boost não-listado"
}
},
"favourited": {
@ -83,7 +83,7 @@
"text": "Erro ao carregar",
"button": "Experimente o link remoto"
},
"altText": ""
"altText": "Texto Alternativo"
},
"avatar": {
"accessibilityLabel": "Avatar de {{name}}",
@ -93,10 +93,10 @@
"expandHint": "Conteúdo oculto"
},
"filtered": {
"reveal": "",
"match_v1": "",
"match_v2_one": "",
"match_v2_other": ""
"reveal": "Exibir mesmo assim",
"match_v1": "Filtrado: {{phrase}}.",
"match_v2_one": "Filtrado por {{filters}}.",
"match_v2_other": "Filtrado por {{count}} filtros, {{filters}}."
},
"fullConversation": "Ler conversas",
"translate": {
@ -123,7 +123,7 @@
"muted": {
"accessibilityLabel": "Toot silenciado"
},
"replies": "",
"replies": "Respostas <0 />",
"visibility": {
"direct": {
"accessibilityLabel": "Enviar uma mensagem direta"

View File

@ -3,15 +3,15 @@
"local": {
"name": "Seguindo",
"options": {
"showBoosts": "",
"showReplies": ""
"showBoosts": "Mostrar boosts",
"showReplies": "Mostrar respostas"
}
},
"public": {
"segments": {
"federated": "Global",
"local": "Local",
"trending": ""
"trending": "Em alta"
}
},
"notifications": {
@ -28,7 +28,7 @@
"filters": {
"accessibilityLabel": "Filtro",
"accessibilityHint": "Filtrar notificações por tipos",
"title": ""
"title": "Mostrar notificações"
}
},
"me": {
@ -43,7 +43,7 @@
"name": "Favoritos"
},
"followedTags": {
"name": ""
"name": "Hashtags seguidas"
},
"fontSize": {
"name": "Tamanho da fonte do Toot"
@ -55,10 +55,10 @@
"name": "Lista: {{list}}"
},
"listAccounts": {
"name": ""
"name": "Usuários na lista: {{list}}"
},
"listAdd": {
"name": ""
"name": "Criar uma lista"
},
"listEdit": {
"name": ""
@ -70,16 +70,16 @@
"name": "Notificação"
},
"preferences": {
"name": ""
"name": "Preferências"
},
"preferencesFilters": {
"name": ""
},
"preferencesFilterAdd": {
"name": ""
"name": "Criar um Filtro"
},
"preferencesFilterEdit": {
"name": ""
"name": "Editar Filtro"
},
"profile": {
"name": "Editar Perfil"
@ -111,36 +111,36 @@
}
},
"listAccounts": {
"heading": "",
"error": "",
"empty": ""
"heading": "Gerenciar usuários",
"error": "Excluir usuário da lista",
"empty": "Nenhum usuário adicionado a esta lista"
},
"listEdit": {
"heading": "",
"title": "",
"title": "Título",
"repliesPolicy": {
"heading": "",
"heading": "Mostrar respostas para:",
"options": {
"none": "",
"list": "",
"followed": ""
"none": "Ninguém",
"list": "Membros da lista",
"followed": "Qualquer usuário seguido"
}
}
},
"listDelete": {
"heading": "",
"heading": "Excluir lista",
"confirm": {
"title": "",
"message": ""
"title": "Excluir lista \"{{list}}\"?",
"message": "Esta ação não é reversível."
}
},
"preferences": {
"visibility": {
"title": "",
"title": "Visibilidade padrão de postagem",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "Público",
"unlisted": "Não-listado",
"private": "Apenas seguidores"
}
},
"sensitive": {
@ -149,19 +149,19 @@
"media": {
"title": "",
"options": {
"default": "",
"default": "Ocultar mídia marcada como sensível",
"show_all": "",
"hide_all": ""
}
},
"spoilers": {
"title": ""
"title": "Expandir automaticamente toots com Aviso de Conteúdo"
},
"autoplay_gifs": {
"title": ""
"title": "Reproduzir GIF automaticamente nos toots"
},
"filters": {
"title": "",
"title": "Filtros de conteúdo",
"content": ""
},
"web_only": {
@ -185,16 +185,16 @@
}
},
"preferencesFilter": {
"name": "",
"name": "Nome",
"expiration": "",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "Nunca",
"1800": "Após 30 minutos",
"3600": "Após 1 hora",
"43200": "Após 12 horas",
"86400": "Após 1 dia",
"604800": "Após 1 semana",
"18144000": "Após 1 mês"
},
"context": "",
"contexts": {
@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>\"s mídia</1>"
},
"hashtag": {
"follow": "Seguir",
"unfollow": "Deixar de seguir"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Histórico de Edição"

View File

@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": ""
@ -88,6 +97,10 @@
"pin": {
"action_false": "",
"action_true": ""
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": ""
},
"hashtag": {
"follow": "",
"unfollow": ""
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": ""

View File

@ -6,7 +6,7 @@
"action_false": "Följ användare",
"action_true": "Avfölj användaren"
},
"inLists": "Listor som innehåller användaren",
"inLists": "",
"showBoosts": {
"action_false": "Visa användarens boostar",
"action_true": "Göm användarens boostar"
@ -16,12 +16,12 @@
"action_true": "Sluta tysta användare"
},
"followAs": {
"trigger": "Följ som...",
"trigger": "",
"succeed_default": "Följer nu @{{target}} med @{{source}}",
"succeed_locked": "Följförfrågan skickades till @{{target}} med {{source}}, väntar på godkännande",
"failed": "Följ som"
},
"blockReport": "Blockera och anmäl...",
"blockReport": "",
"block": {
"action_false": "Blockera användare",
"action_true": "Avblockera användare",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Dela inlägg"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Fäst inlägg",
"action_true": "Avfäst inlägg"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>s media</1>"
},
"hashtag": {
"follow": "Följ",
"unfollow": "Sluta följ"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Redigeringshistorik"

View File

@ -6,7 +6,7 @@
"action_false": "Підписатися на користувача",
"action_true": "Відписатися від користувача"
},
"inLists": "Списки, в яких є користувач",
"inLists": "",
"showBoosts": {
"action_false": "Показати передмухи користувача",
"action_true": "Приховати передмухи користувача"
@ -16,12 +16,12 @@
"action_true": "Зняти заглушення з користувача"
},
"followAs": {
"trigger": "Стежити як...",
"trigger": "",
"succeed_default": "Тепер ви стежите за @{{target}} з @{{source}}",
"succeed_locked": "Запит на стеження за @{{target}} з {{source}} надіслано, очікується затвердження",
"failed": "Стежити як"
},
"blockReport": "Заблокувати й поскаржитися...",
"blockReport": "",
"block": {
"action_false": "Заблокувати користувача",
"action_true": "Розблокувати користувача",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Поділитися дмухом"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Прикріпити дмух",
"action_true": "Відкріпити дмух"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -70,16 +70,16 @@
"name": "Push-сповіщення"
},
"preferences": {
"name": ""
"name": "Параметри"
},
"preferencesFilters": {
"name": ""
"name": "Усі фільтри вмісту"
},
"preferencesFilterAdd": {
"name": ""
"name": "Створити фільтр"
},
"preferencesFilterEdit": {
"name": ""
"name": "Змінити фільтр"
},
"profile": {
"name": "Редагувати профіль"
@ -136,81 +136,82 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "Типова видимість дописів",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "Загальнодоступні",
"unlisted": "Приватні",
"private": "Лише для підписників"
}
},
"sensitive": {
"title": ""
"title": "Типово позначити медіа делікатними"
},
"media": {
"title": "",
"title": "Показ медіа",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "Приховувати медіа, позначені делікатними",
"show_all": "Завжди показувати медіа",
"hide_all": "Завжди приховувати медіа"
}
},
"spoilers": {
"title": ""
"title": "Автоматично розгортати дмухи із попередженням про вміст"
},
"autoplay_gifs": {
"title": ""
"title": "Автовідтворення GIF у дмухах"
},
"filters": {
"title": "",
"content": ""
"title": "Фільтри вмісту",
"content": "{{count}} активний"
},
"web_only": {
"title": "",
"description": ""
"title": "Оновити налаштування",
"description": "Налаштування можна оновити лише за допомогою вебінтерфейсу"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"expired": "Термін дії минув",
"keywords_one": "{{count}} ключове слово",
"keywords_other": "{{count}} ключових слів",
"statuses_one": "{{count}} дмух",
"statuses_other": "{{count}} дмухів",
"context": "Застосовується в <0 />",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "підписки й списки",
"notifications": "сповіщення",
"public": "федеративні",
"thread": "розмова",
"account": "профіль"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "Ім’я",
"expiration": "Закінчується",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "Ніколи",
"1800": "За 30 хвилин",
"3600": "За 1 годину",
"43200": "За 12 годин",
"86400": "За 1 день",
"604800": "За 1 тиждень",
"18144000": "За 1 місяць"
},
"context": "",
"context": "Застосовується в",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "Підписки та списки",
"notifications": "Сповіщення",
"public": "Федеративна стрічка",
"thread": "Перегляд бесід",
"account": "Перегляд профілю"
},
"action": "",
"action": "За збігу",
"actions": {
"warn": "",
"hide": ""
"warn": "Згорнуто, але можна виявити",
"hide": "Приховано повністю"
},
"keywords": "",
"keyword": ""
"keywords": "Збіг із цими ключовими словами",
"keyword": "Ключове слово",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1> медіа</1>"
},
"hashtag": {
"follow": "Підписатися",
"unfollow": "Відписатися"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Редагувати історію"

View File

@ -6,7 +6,7 @@
"action_false": "Theo dõi người này",
"action_true": "Ngưng theo dõi người này"
},
"inLists": "Danh sách người",
"inLists": "",
"showBoosts": {
"action_false": "Hiển lượt đăng lại",
"action_true": "Ẩn lượt đăng lại"
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "Đăng lại"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Tút ghim",
"action_true": "Bỏ ghim tút"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -210,7 +210,8 @@
"hide": ""
},
"keywords": "",
"keyword": ""
"keyword": "",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>'s media</1>"
},
"hashtag": {
"follow": "Theo dõi",
"unfollow": "Ngưng theo dõi"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "Lịch sử chỉnh sửa"

View File

@ -6,7 +6,7 @@
"action_false": "关注用户",
"action_true": "取消关注用户"
},
"inLists": "包含用户的列表",
"inLists": "包含用户的列表",
"showBoosts": {
"action_false": "显示用户的转嘟",
"action_true": "隐藏用户的转嘟"
@ -16,12 +16,12 @@
"action_true": "取消静音用户"
},
"followAs": {
"trigger": "关注…",
"trigger": "用其它账户关注 …",
"succeed_default": "@{{source}} 正在关注 @{{target}}",
"succeed_locked": "已从 @{{source}} 发送关注请求至 @{{target}},等待通过",
"failed": "用其它账户关注"
},
"blockReport": "屏蔽与举报",
"blockReport": "屏蔽与举报",
"block": {
"action_false": "屏蔽用户",
"action_true": "取消屏蔽用户",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "关注",
"action_true": "取消关注"
},
"filter": {
"action": "过滤话题标签 …"
}
},
"share": {
"status": {
"action": "分享嘟文"
@ -88,6 +97,10 @@
"pin": {
"action_false": "置顶嘟文",
"action_true": "取消置顶嘟文"
},
"filter": {
"action_false": "过滤此嘟文 …",
"action_true": "管理过滤器 …"
}
}
}

View File

@ -210,7 +210,8 @@
"hide": "完全隐藏"
},
"keywords": "匹配这些关键字",
"keyword": "关键字"
"keyword": "关键字",
"statuses": "匹配这些嘟文"
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>的媒体</1>"
},
"hashtag": {
"follow": "关注",
"unfollow": "取消关注"
"filter": {
"name": "添加至过滤器",
"existed": "已被过滤"
},
"history": {
"name": "编辑历史"

View File

@ -6,7 +6,7 @@
"action_false": "跟隨使用者",
"action_true": "取消跟隨使用者"
},
"inLists": "包含使用者的列表",
"inLists": "",
"showBoosts": {
"action_false": "顯示使用者的轉嘟",
"action_true": "隱藏使用者的轉嘟"
@ -16,12 +16,12 @@
"action_true": "解除靜音使用者"
},
"followAs": {
"trigger": "跟隨...",
"trigger": "",
"succeed_default": "@{{source}} 正在跟隨 @{{target}}",
"succeed_locked": "已從 {{source}} 發送跟隨請求至 @{{target}},等待同意",
"failed": "用其它帳號跟隨"
},
"blockReport": "封鎖並檢舉…",
"blockReport": "",
"block": {
"action_false": "封鎖使用者",
"action_true": "解除封鎖使用者",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": {
"status": {
"action": "分享嘟文"
@ -88,6 +97,10 @@
"pin": {
"action_false": "釘選嘟文",
"action_true": "取消釘選嘟文"
},
"filter": {
"action_false": "",
"action_true": ""
}
}
}

View File

@ -70,16 +70,16 @@
"name": "推播通知"
},
"preferences": {
"name": ""
"name": "偏好設定"
},
"preferencesFilters": {
"name": ""
"name": "所有內容過濾器"
},
"preferencesFilterAdd": {
"name": ""
"name": "建立過濾器"
},
"preferencesFilterEdit": {
"name": ""
"name": "編輯過濾器"
},
"profile": {
"name": "編輯個人檔案"
@ -136,81 +136,82 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "預設發嘟範圍",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "公開",
"unlisted": "不公開",
"private": "僅限跟隨者"
}
},
"sensitive": {
"title": ""
"title": "預設標記媒體為敏感內容"
},
"media": {
"title": "",
"title": "媒體顯示",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "隱藏被標記為敏感內容的媒體",
"show_all": "總是顯示媒體",
"hide_all": "總是隱藏媒體"
}
},
"spoilers": {
"title": ""
"title": "自動展開含有警告訊息的嘟文"
},
"autoplay_gifs": {
"title": ""
"title": "在嘟文中自動播放 GIF"
},
"filters": {
"title": "",
"content": ""
"title": "內容過濾器",
"content": "{{count}} 生效中"
},
"web_only": {
"title": "",
"description": ""
"title": "更新設定",
"description": "以下的設定只能通過網頁更新"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"expired": "已過期",
"keywords_one": "{{count}} 關鍵字",
"keywords_other": "{{count}} 關鍵字",
"statuses_one": "{{count}} 條嘟文",
"statuses_other": "{{count}} 條嘟文",
"context": "使用於 <0 />",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "跟隨和列表",
"notifications": "通知",
"public": "聯邦",
"thread": "對話",
"account": "個人資料"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "名稱",
"expiration": "截止於",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "從不",
"1800": "30 分鐘後",
"3600": "1 小時後",
"43200": "12 小時後",
"86400": "1 天後",
"604800": "1 週後",
"18144000": "1 個月後"
},
"context": "",
"context": "使用於",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "跟隨和列表",
"notifications": "通知",
"public": "聯邦時間軸",
"thread": "對話頁面",
"account": "個人資料頁面"
},
"action": "",
"action": "當符合時",
"actions": {
"warn": "",
"hide": ""
"warn": "僅折疊,但可點擊顯示",
"hide": "完全隱藏"
},
"keywords": "",
"keyword": ""
"keywords": "符合這些關鍵字",
"keyword": "關鍵字",
"statuses": ""
},
"profile": {
"feedback": {
@ -398,9 +399,9 @@
"attachments": {
"name": "<0 /><1>的媒體</1>"
},
"hashtag": {
"follow": "跟隨",
"unfollow": "取消跟隨"
"filter": {
"name": "",
"existed": ""
},
"history": {
"name": "編輯歷史"

View File

@ -1,5 +1,6 @@
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { ModalScrollView } from '@components/ModalScrollView'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
@ -7,8 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, KeyboardAvoidingView, Platform, ScrollView, TextInput } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Alert, TextInput } from 'react-native'
import ComposeContext from './utils/createContext'
const ComposeEditAttachment: React.FC<
@ -34,9 +34,7 @@ const ComposeEditAttachment: React.FC<
useEffect(() => {
navigation.setOptions({
title: t('content.editAttachment.header.title'),
headerLeft: () => (
<HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />
),
headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />,
headerRight: () => (
<HeaderRight
accessibilityLabel={t('content.editAttachment.header.right.accessibilityLabel')}
@ -88,22 +86,13 @@ const ComposeEditAttachment: React.FC<
}, [theAttachment])
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<SafeAreaView
style={{ flex: 1, padding: StyleConstants.Spacing.Global.PagePadding }}
edges={['left', 'right', 'bottom']}
>
<ScrollView>
<ModalScrollView>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
{t('content.editAttachment.content.altText.heading')}
</CustomText>
<TextInput
style={{
height:
StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
height: StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
@ -138,9 +127,7 @@ const ComposeEditAttachment: React.FC<
>
{theAttachment.description?.length || 0} / 1500
</CustomText>
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
</ModalScrollView>
)
}

View File

@ -4,7 +4,7 @@ import ComponentHashtag from '@components/Hashtag'
import { displayMessage } from '@components/Message'
import ComponentSeparator from '@components/Separator'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
import { useFollowedTagsQuery, useTagMutation } from '@utils/queryHooks/tags'
import { flattenPages } from '@utils/queryHooks/utils'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -13,7 +13,7 @@ import { FlatList } from 'react-native-gesture-handler'
const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({
navigation
}) => {
const { t } = useTranslation(['common', 'screenTabs'])
const { t } = useTranslation(['common', 'screenTabs', 'componentContextMenu'])
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = flattenPages(data)
@ -24,7 +24,7 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
}
}, [flattenData.length])
const mutation = useTagsMutation({
const mutation = useTagMutation({
onSuccess: () => {
haptics('Light')
refetch()
@ -33,9 +33,10 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to
? t('screenTabs:shared.hashtag.follow')
: t('screenTabs:shared.hashtag.unfollow')
function: t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'false',
context: to.toString()
})
}),
...(err.status &&
typeof err.status === 'number' &&
@ -58,8 +59,11 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
children={
<Button
type='text'
content={t('screenTabs:shared.hashtag.unfollow')}
onPress={() => mutation.mutate({ tag: item.name, to: !item.following })}
content={t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'fase',
context: 'false'
})}
onPress={() => mutation.mutate({ tag_name: item.name, to: !item.following })}
/>
}
/>

View File

@ -4,22 +4,24 @@ import { HeaderLeft, HeaderRight } from '@components/Header'
import Hr from '@components/Hr'
import ComponentInput from '@components/Input'
import { MenuRow } from '@components/Menu'
import { ModalScrollView } from '@components/ModalScrollView'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import apiInstance from '@utils/api/instance'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import browserPackage from '@utils/helpers/browserPackage'
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks'
import { QueryKeyFilters } from '@utils/queryHooks/filters'
import { getAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useEffect, useState } from 'react'
import * as WebBrowser from 'expo-web-browser'
import React, { RefObject, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform, View } from 'react-native'
import { ScrollView, View } from 'react-native'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context'
const TabMePreferencesFilter: React.FC<
TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filter'> & {
@ -101,7 +103,11 @@ const TabMePreferencesFilter: React.FC<
])
const [keywords, setKeywords] = useState<string[]>(
params.type === 'edit' ? params.filter.keywords.map(({ keyword }) => keyword) : []
params.type === 'edit'
? params.filter.keywords.length
? params.filter.keywords.map(({ keyword }) => keyword)
: ['']
: ['']
)
useEffect(() => {
@ -152,6 +158,48 @@ const TabMePreferencesFilter: React.FC<
})
break
case 'edit':
isLoading = true
await apiInstance({
method: 'put',
version: 'v2',
url: `filters/${params.filter.id}`,
body: {
title: titleState[0],
context: contexts
.filter(context => context.selected)
.map(context => context.type),
filter_action: actions.filter(
action => action.type === 'hide' && action.selected
).length
? 'hide'
: 'warn',
...(parseInt(expiration) && {
expires_in: parseInt(expiration)
}),
...(keywords.filter(keyword => keyword.length).length
? {
keywords_attributes: keywords
.filter(keyword => keyword.length)
.map(keyword => ({ keyword, whole_word: true }))
}
: params.filter.keywords.length && {
keywords_attributes: params.filter.keywords.map(keyword => ({
...keyword,
_destroy: true
}))
})
}
})
.then(() => {
isLoading = false
const queryKey: QueryKeyFilters = ['Filters', { version: 'v2' }]
queryClient.refetchQueries(queryKey)
navigation.navigate('Tab-Me-Preferences-Filters')
})
.catch(() => {
isLoading = false
haptics('Error')
})
break
}
}}
@ -160,13 +208,10 @@ const TabMePreferencesFilter: React.FC<
})
}, [titleState[0], expiration, contexts, actions, keywords])
const scrollViewRef = useRef<ScrollView>(null)
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
<ScrollView style={{ padding: StyleConstants.Spacing.Global.PagePadding }}>
<ModalScrollView ref={scrollViewRef}>
<ComponentInput title={t('screenTabs:me.preferencesFilter.name')} value={titleState} />
<MenuRow
title={t('screenTabs:me.preferencesFilter.expiration')}
@ -207,11 +252,17 @@ const TabMePreferencesFilter: React.FC<
/>
<Hr style={{ marginVertical: StyleConstants.Spacing.M }} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<CustomText
fontStyle='M'
children={t('screenTabs:me.preferencesFilter.keywords')}
style={{ color: colors.primaryDefault }}
/>
<CustomText
style={{ marginHorizontal: StyleConstants.Spacing.M, color: colors.secondary }}
children={t('screenTabs:me.preferencesFilters.keywords', { count: keywords.length })}
/>
</View>
<View
style={{
marginTop: StyleConstants.Spacing.M,
@ -244,21 +295,45 @@ const TabMePreferencesFilter: React.FC<
content='minus'
round
disabled={keywords.length < 1}
/>
<CustomText
style={{ marginHorizontal: StyleConstants.Spacing.M, color: colors.secondary }}
children={keywords.length}
style={{ marginRight: StyleConstants.Spacing.M }}
/>
<Button
onPress={() => setKeywords([...keywords, ''])}
onPress={() => {
setKeywords([...keywords, ''])
setTimeout(() => scrollViewRef.current?.scrollToEnd(), 50)
}}
type='icon'
content='plus'
round
/>
</View>
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
{params.type === 'edit' && params.filter.statuses?.length ? (
<>
<Hr style={{ marginVertical: StyleConstants.Spacing.M }} />
<MenuRow
title={t('screenTabs:me.preferencesFilter.statuses')}
content={t('screenTabs:me.preferencesFilters.statuses', {
count: params.filter.statuses.length
})}
iconBack='external-link'
onPress={async () =>
WebBrowser.openAuthSessionAsync(
`https://${getAccountStorage.string('auth.domain')}/filters/${
params.filter.id
}/statuses`,
'tooot://tooot',
{
...(await browserPackage()),
dismissButtonStyle: 'done',
readerMode: false
}
)
}
/>
</>
) : null}
</ModalScrollView>
)
}

View File

@ -1,15 +1,15 @@
import { Filter } from '@components/Filter'
import { HeaderLeft, HeaderRight } from '@components/Header'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
import { useFiltersQuery } from '@utils/queryHooks/filters'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useEffect } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Pressable, TouchableNativeFeedback, View } from 'react-native'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { SwipeListView } from 'react-native-swipe-list-view'
const TabMePreferencesFilters: React.FC<
@ -39,6 +39,7 @@ const TabMePreferencesFilters: React.FC<
return (
<SwipeListView
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
renderHiddenItem={({ item }) => (
<Pressable
style={{
@ -65,98 +66,10 @@ const TabMePreferencesFilters: React.FC<
filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1
)}
renderItem={({ item: filter }) => (
<TouchableNativeFeedback
<Filter
filter={filter}
onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'edit', filter })}
>
<View
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundDefault
}}
>
<View style={{ flex: 1 }}>
<CustomText
fontStyle='M'
children={filter.title}
style={{ color: colors.primaryDefault }}
numberOfLines={1}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: StyleConstants.Spacing.XS
}}
>
{filter.expires_at && new Date() > new Date(filter.expires_at) ? (
<CustomText
fontStyle='S'
fontWeight='Bold'
children={t('screenTabs:me.preferencesFilters.expired')}
style={{ color: colors.red, marginRight: StyleConstants.Spacing.M }}
/>
) : null}
{filter.keywords?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.keywords', {
count: filter.keywords.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.keywords?.length && filter.statuses?.length ? (
<CustomText
children={t('common:separator')}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.statuses?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.statuses', {
count: filter.statuses.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
</View>
<CustomText
style={{ color: colors.secondary }}
children={
<Trans
ns='screenTabs'
i18nKey='me.preferencesFilters.context'
components={[
<>
{filter.context.map((c, index) => (
<Fragment key={index}>
<CustomText
style={{
color: colors.secondary,
textDecorationColor: colors.disabled,
textDecorationLine: 'underline',
textDecorationStyle: 'solid'
}}
children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)}
/>
<CustomText children={t('common:separator')} />
</Fragment>
))}
</>
]}
/>
}
/>
</View>
<Icon
name='chevron-right'
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
style={{ marginLeft: 8 }}
/>
</View>
</TouchableNativeFeedback>
)}
ItemSeparatorComponent={ComponentSeparator}
/>

View File

@ -4,7 +4,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabMeProfileStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native'
import FlashMessage from 'react-native-flash-message'
import TabMeProfileFields from './Fields'
import TabMeProfileName from './Name'
@ -18,10 +17,7 @@ const TabMeProfile: React.FC<TabMeStackScreenProps<'Tab-Me-Switch'>> = ({ naviga
const messageRef = useRef<FlashMessage>(null)
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<>
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name='Tab-Me-Profile-Root'
@ -55,7 +51,7 @@ const TabMeProfile: React.FC<TabMeStackScreenProps<'Tab-Me-Switch'>> = ({ naviga
</Stack.Navigator>
<Message ref={messageRef} />
</KeyboardAvoidingView>
</>
)
}

View File

@ -1,12 +1,13 @@
import AccountButton from '@components/AccountButton'
import ComponentInstance from '@components/Instance'
import { ModalScrollView } from '@components/ModalScrollView'
import CustomText from '@components/Text'
import { getReadableAccounts } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform, View } from 'react-native'
import { View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
const TabMeSwitch: React.FC = () => {
@ -20,15 +21,7 @@ const TabMeSwitch: React.FC = () => {
}, [scrollViewRef.current])
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
ref={scrollViewRef}
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
keyboardShouldPersistTaps='always'
>
<ModalScrollView>
<View>
<CustomText
fontStyle='M'
@ -47,7 +40,6 @@ const TabMeSwitch: React.FC = () => {
style={{
marginTop: StyleConstants.Spacing.S,
paddingTop: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
borderTopWidth: 1,
borderTopColor: colors.border
}}
@ -75,8 +67,7 @@ const TabMeSwitch: React.FC = () => {
})}
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</ModalScrollView>
)
}

View File

@ -27,15 +27,13 @@ const TabSharedAccountInLists: React.FC<
navigation.setOptions({
presentation: 'modal',
title: t('screenTabs:shared.accountInLists.name', { username: account.username }),
headerRight: () => {
return (
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => navigation.pop(1)}
/>
)
}
})
}, [])
@ -66,11 +64,11 @@ const TabSharedAccountInLists: React.FC<
<SectionList
style={{ padding: StyleConstants.Spacing.Global.PagePadding }}
sections={sections}
SectionSeparatorComponent={props => {
return props.leadingItem && props.trailingSection ? (
<View style={{ flex: 1, height: StyleConstants.Spacing.Global.PagePadding * 2 }} />
SectionSeparatorComponent={props =>
props.leadingItem && props.trailingSection ? (
<View style={{ height: StyleConstants.Spacing.XL }} />
) : null
}}
}
renderSectionHeader={({ section: { title, data } }) =>
data.length ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }} children={title} />
@ -117,7 +115,7 @@ const TabSharedAccountInLists: React.FC<
title={item.title}
/>
)}
></SectionList>
/>
)
}

View File

@ -38,7 +38,8 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
]}
/>
</CustomText>
)
),
headerBackVisible: false
})
navigation.setParams({ queryKey })
}, [])

View File

@ -0,0 +1,132 @@
import Button from '@components/Button'
import { Filter } from '@components/Filter'
import { HeaderRight } from '@components/Header'
import Hr from '@components/Hr'
import CustomText from '@components/Text'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps, useNavState } from '@utils/navigation/navigators'
import { useFilterMutation, useFiltersQuery } from '@utils/queryHooks/filters'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { SectionList, View } from 'react-native'
const TabSharedFilter: React.FC<TabSharedStackScreenProps<'Tab-Shared-Filter'>> = ({
navigation,
route: { params }
}) => {
if (!featureCheck('filter_server_side')) {
navigation.goBack()
return null
}
const { colors } = useTheme()
const { t } = useTranslation(['common', 'screenTabs'])
useEffect(() => {
navigation.setOptions({
title: t('screenTabs:shared.filter.name'),
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => navigation.goBack()}
/>
)
})
}, [])
const { data, isFetching, refetch } = useFiltersQuery<'v2'>({ version: 'v2' })
const sections = [
{
id: 'add',
data:
data?.filter(filter => {
switch (params.source) {
case 'hashtag':
return !filter.keywords.find(keyword => keyword.keyword === `#${params.tag_name}`)
case 'status':
return !filter.statuses.find(({ status_id }) => status_id === params.status.id)
}
}) || []
},
{
id: 'remove',
title: t('screenTabs:shared.filter.existed'),
data:
data?.filter(filter => {
switch (params.source) {
case 'hashtag':
return !!filter.keywords.find(keyword => keyword.keyword === `#${params.tag_name}`)
case 'status':
return !!filter.statuses.find(({ status_id }) => status_id === params.status.id)
}
}) || []
}
]
const mutation = useFilterMutation()
const navState = useNavState()
return (
<SectionList
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
sections={sections}
SectionSeparatorComponent={props =>
props.leadingItem && props.trailingSection ? (
<View style={{ height: StyleConstants.Spacing.XL }} />
) : null
}
renderSectionHeader={({ section: { title, data } }) =>
title && data.length ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }} children={title} />
) : null
}
ItemSeparatorComponent={Hr}
renderItem={({ item, section }) => (
<Filter
filter={item}
button={
<Button
type='icon'
content={section.id === 'add' ? 'plus' : 'minus'}
round
disabled={isFetching}
onPress={() => {
if (section.id === 'add' || section.id === 'remove') {
switch (params.source) {
case 'status':
mutation
.mutateAsync({
source: 'status',
filter: item,
action: section.id,
status: params.status
})
.then(() => refetch())
break
case 'hashtag':
mutation
.mutateAsync({
source: 'keyword',
filter: item,
action: section.id,
keyword: `#${params.tag_name}`
})
.then(() => refetch())
break
}
}
}}
/>
}
onPress={() => {}}
/>
)}
/>
)
}
export default TabSharedFilter

View File

@ -1,85 +1,69 @@
import haptics from '@components/haptics'
import menuHashtag from '@components/contextMenu/hashtag'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import React, { Fragment, useEffect } from 'react'
import * as DropdownMenu from 'zeego/dropdown-menu'
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
navigation,
route: {
params: { hashtag }
params: { tag_name }
}
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', tag_name }]
const canFollowTags = featureCheck('follow_tags')
const canFilterTag = featureCheck('filter_server_side')
const mHashtag = menuHashtag({ tag_name, queryKey })
useEffect(() => {
navigation.setOptions({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
title: `#${decodeURIComponent(hashtag)}`
title: `#${decodeURIComponent(tag_name)}`
})
navigation.setParams({ queryKey: queryKey })
}, [])
const { t } = useTranslation(['common', 'screenTabs'])
const canFollowTags = featureCheck('follow_tags')
const { data, isFetching, refetch } = useTagsQuery({
tag: hashtag,
options: { enabled: canFollowTags }
})
const queryClient = useQueryClient()
const mutation = useTagsMutation({
onSuccess: () => {
haptics('Success')
refetch()
const queryKeyFollowedTags: QueryKeyFollowedTags = ['FollowedTags']
queryClient.invalidateQueries({ queryKey: queryKeyFollowedTags })
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to
? t('screenTabs:shared.hashtag.follow')
: t('screenTabs: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
if (!canFollowTags && !canFilterTag) return
navigation.setOptions({
headerRight: () => (
<HeaderRight
loading={isFetching || mutation.isLoading}
type='text'
content={
data?.following
? t('screenTabs:shared.hashtag.unfollow')
: t('screenTabs:shared.hashtag.follow')
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<HeaderRight content='more-horizontal' onPress={() => {}} />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{[mHashtag].map((menu, i) => (
<Fragment key={i}>
{menu.map((group, index) => (
<DropdownMenu.Group key={index}>
{group.map(item => {
switch (item.type) {
case 'item':
return (
<DropdownMenu.Item key={item.key} {...item.props}>
<DropdownMenu.ItemTitle children={item.title} />
{item.icon ? (
<DropdownMenu.ItemIcon ios={{ name: item.icon }} />
) : null}
</DropdownMenu.Item>
)
}
onPress={() =>
typeof data?.following === 'boolean' &&
mutation.mutate({ tag: hashtag, to: !data.following })
}
/>
})}
</DropdownMenu.Group>
))}
</Fragment>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)
})
}, [canFollowTags, data?.following, isFetching])
}, [mHashtag])
return <Timeline queryKey={queryKey} />
}

View File

@ -1,5 +1,6 @@
import ComponentAccount from '@components/Account'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { ModalScrollView } from '@components/ModalScrollView'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
@ -11,7 +12,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, ScrollView, TextInput, View } from 'react-native'
import { Platform, TextInput, View } from 'react-native'
import { Switch } from 'react-native-gesture-handler'
const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> = ({
@ -96,7 +97,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
}, [rulesQuery.data])
return (
<ScrollView>
<ModalScrollView>
<View
style={{
margin: StyleConstants.Spacing.Global.PagePadding,
@ -209,7 +210,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
/>
) : null}
</View>
</ScrollView>
</ModalScrollView>
)
}

View File

@ -24,11 +24,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
const [searchTerm, setSearchTerm] = useState<string>('')
useEffect(() => {
navigation.setOptions({
...(Platform.OS === 'ios'
? {
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
}
: { headerLeft: () => null }),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
headerTitle: () => {
return (
<View
@ -77,7 +73,8 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
/>
</View>
)
}
},
headerBackVisible: false
})
}, [mode])
useEffect(() => {

View File

@ -66,7 +66,8 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
/>
</Pressable>
),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
headerBackVisible: false
})
navigation.setParams({ toot, queryKey: queryKey.local })
}, [hasRemoteContent])
@ -340,7 +341,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
StyleConstants.Spacing.M + StyleConstants.Avatar.XS / 2
} ` +
`a ${ARC} ${ARC} 0 0 0 -${ARC} ${ARC} ` +
`v 999`
`v ${heights[index] || 300}`
}
strokeWidth={1}
stroke={colors.border}
@ -356,7 +357,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${
(heights[index] || 999) -
(heights[index] || 300) -
(StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 -
StyleConstants.Avatar.XS / 2
} ` +
@ -377,7 +378,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${
(heights[index] || 999) -
(heights[index] || 300) -
(StyleConstants.Spacing.S * 1.5 +
StyleConstants.Font.Size.L * 1.35) /
2
@ -394,7 +395,10 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
return (
<Svg key={i} style={{ position: 'absolute' }} fill='none'>
<Path
d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 999`}
d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${heights[index] || 300}`
}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}

View File

@ -9,6 +9,7 @@ import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users'
import React from 'react'
import TabSharedFilter from './Filter'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
return (
@ -28,6 +29,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNaviga
name='Tab-Shared-Attachments'
component={TabSharedAttachments}
/>
<Stack.Screen
key='Tab-Shared-Filter'
name='Tab-Shared-Filter'
component={TabSharedFilter}
options={{ presentation: 'modal' }}
/>
<Stack.Screen
key='Tab-Shared-Hashtag'
name='Tab-Shared-Hashtag'

View File

@ -99,7 +99,10 @@ export type TabSharedStackParamList = {
}
'Tab-Shared-Account-In-Lists': { account: Pick<Mastodon.Account, 'id' | 'username'> }
'Tab-Shared-Attachments': { account: Mastodon.Account; queryKey?: QueryKeyTimeline }
'Tab-Shared-Hashtag': { hashtag: Mastodon.Tag['name']; queryKey?: QueryKeyTimeline }
'Tab-Shared-Filter':
| { source: 'status'; status: Mastodon.Status }
| { source: 'hashtag'; tag_name: Mastodon.Tag['name'] }
'Tab-Shared-Hashtag': { tag_name: Mastodon.Tag['name']; queryKey?: QueryKeyTimeline }
'Tab-Shared-History': { status: Mastodon.Status; detectedLanguage: string }
'Tab-Shared-Report': {
account: Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'>

View File

@ -7,6 +7,7 @@ import { useEffect } from 'react'
import pushUseNavigate from './useNavigate'
const pushUseReceive = () => {
const [accountActive] = useGlobalStorage.string('account.active')
const [accounts] = useGlobalStorage.object('accounts')
useEffect(() => {
@ -19,25 +20,25 @@ const pushUseReceive = () => {
accountId: string
}
const currAccount = accounts?.find(
account =>
account ===
generateAccountKey({ domain: payloadData.instanceUrl, id: payloadData.accountId })
)
const incomingAccount = generateAccountKey({
domain: payloadData.instanceUrl,
id: payloadData.accountId
})
const foundAccount = accounts?.find(account => account === incomingAccount)
displayMessage({
duration: 'long',
message: notification.request.content.title!,
description: notification.request.content.body!,
onPress: () => {
if (currAccount) {
setAccount(currAccount)
onPress: async () => {
if (foundAccount && foundAccount !== accountActive) {
await setAccount(foundAccount)
}
pushUseNavigate(payloadData.notification_id)
}
})
})
return () => subscription.remove()
}, [accounts])
}, [accountActive, accounts])
}
export default pushUseReceive

View File

@ -6,11 +6,12 @@ import { useEffect } from 'react'
import pushUseNavigate from './useNavigate'
const pushUseRespond = () => {
const [accountActive] = useGlobalStorage.string('account.active')
const [accounts] = useGlobalStorage.object('accounts')
useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
({ notification }) => {
async ({ notification }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
queryClient.invalidateQueries(queryKey)
const payloadData = notification.request.content.data as {
@ -19,19 +20,19 @@ const pushUseRespond = () => {
accountId: string
}
const currAccount = accounts?.find(
account =>
account ===
generateAccountKey({ domain: payloadData.instanceUrl, id: payloadData.accountId })
)
if (currAccount) {
setAccount(currAccount)
const incomingAccount = generateAccountKey({
domain: payloadData.instanceUrl,
id: payloadData.accountId
})
const foundAccount = accounts?.find(account => account === incomingAccount)
if (foundAccount && foundAccount !== accountActive) {
await setAccount(foundAccount)
}
pushUseNavigate(payloadData.notification_id)
}
)
return () => subscription.remove()
}, [accounts])
}, [accountActive, accounts])
}
export default pushUseRespond

View File

@ -1,4 +1,5 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import haptics from '@components/haptics'
import { QueryFunctionContext, useMutation, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { AxiosError } from 'axios'
@ -28,6 +29,73 @@ const useFilterQuery = ({
})
}
/* ----- */
type MutationVarsFilter = { filter: Mastodon.Filter<'v2'> } & (
| { source: 'status'; action: 'add'; status: Mastodon.Status }
| { source: 'status'; action: 'remove'; status: Mastodon.Status }
| { source: 'keyword'; action: 'add'; keyword: string }
| { source: 'keyword'; action: 'remove'; keyword: string }
)
const mutationFunction = async (params: MutationVarsFilter) => {
switch (params.source) {
case 'status':
switch (params.action) {
case 'add':
return apiInstance({
method: 'post',
version: 'v2',
url: `filters/${params.filter.id}/statuses`,
body: { status_id: params.status.id }
})
case 'remove':
for (const status of params.filter.statuses) {
if (status.status_id === params.status.id) {
await apiInstance({
method: 'delete',
version: 'v2',
url: `filters/statuses/${status.id}`
})
}
}
return Promise.resolve()
}
break
case 'keyword':
switch (params.action) {
case 'add':
return apiInstance({
method: 'post',
version: 'v2',
url: `filters/${params.filter.id}/keywords`,
body: { keyword: params.keyword, whole_word: true }
})
case 'remove':
for (const keyword of params.filter.keywords) {
if (keyword.keyword === params.keyword) {
await apiInstance({
method: 'delete',
version: 'v2',
url: `filters/keywords/${keyword.id}`
})
}
}
return Promise.resolve()
}
break
}
}
const useFilterMutation = () => {
return useMutation<any, AxiosError, MutationVarsFilter>(mutationFunction, {
onSuccess: () => haptics('Light'),
onError: () => haptics('Error')
})
}
/* ----- */
export type QueryKeyFilters = ['Filters', { version: 'v1' | 'v2' }]
const filtersQueryFunction = async <T extends 'v1' | 'v2' = 'v1'>({
@ -54,4 +122,4 @@ const useFiltersQuery = <T extends 'v1' | 'v2' = 'v1'>(params?: {
})
}
export { useFilterQuery, useFiltersQuery }
export { useFilterQuery, useFilterMutation, useFiltersQuery }

View File

@ -60,32 +60,33 @@ const useFollowedTagsQuery = (
)
}
export type QueryKeyTags = ['Tags', { tag: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
const { tag } = queryKey[1]
export type QueryKeyTag = ['Tag', { tag_name: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTag>) => {
const { tag_name } = queryKey[1]
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` })
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag_name}` })
return res.body
}
const useTagsQuery = ({
options,
...queryKeyParams
}: QueryKeyTags[1] & {
const useTagQuery = ({
tag_name,
options
}: {
tag_name: Mastodon.Tag['name']
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => {
const queryKey: QueryKeyTags = ['Tags', { ...queryKeyParams }]
const queryKey: QueryKeyTag = ['Tag', { tag_name }]
return useQuery(queryKey, queryFunction, options)
}
type MutationVarsAnnouncement = { tag: string; to: boolean }
const mutationFunction = async ({ tag, to }: MutationVarsAnnouncement) => {
type MutationVarsTag = { tag_name: Mastodon.Tag['name']; to: boolean }
const mutationFunction = async ({ tag_name, to }: MutationVarsTag) => {
return apiInstance<{}>({
method: 'post',
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
url: `tags/${tag_name}/${to ? 'follow' : 'unfollow'}`
})
}
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
const useTagMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsTag>) => {
return useMutation(mutationFunction, options)
}
export { useFollowedTagsQuery, useTagsQuery, useTagsMutation }
export { useFollowedTagsQuery, useTagQuery, useTagMutation }

View File

@ -33,7 +33,7 @@ export type QueryKeyTimeline = [
}
| {
page: 'Hashtag'
hashtag: Mastodon.Tag['name']
tag_name: Mastodon.Tag['name']
}
| {
page: 'List'
@ -219,7 +219,7 @@ export const queryFunctionTimeline = async ({
case 'Hashtag':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `timelines/tag/${page.hashtag}`,
url: `timelines/tag/${page.tag_name}`,
params
})