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 navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = () => { 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 const padding = StyleConstants.Spacing.Global.PagePadding

View File

@ -235,12 +235,7 @@ const ComponentInstance: React.FC<Props> = ({
/> />
</View> </View>
) : null} ) : null}
<View <View style={{ marginTop: StyleConstants.Spacing.L }}>
style={{
marginTop: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View <View
style={{ style={{
flexDirection: 'row', 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 [followedTags] = useAccountStorage.object('followed_tags')
const MAX_ALLOWED_LINES = 30 const MAX_ALLOWED_LINES = 35
const [totalLines, setTotalLines] = useState<number>() const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted) const [expanded, setExpanded] = useState(highlighted)
@ -147,7 +147,7 @@ const ParseHTML: React.FC<Props> = ({
tag?.length && tag?.length &&
!disableDetails && !disableDetails &&
!sameHashtag && !sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag }) navigation.push('Tab-Shared-Hashtag', { tag_name: tag })
} }
children={children} children={children}
/> />
@ -203,9 +203,7 @@ const ParseHTML: React.FC<Props> = ({
onPress={async () => { onPress={async () => {
if (!disableDetails) { if (!disableDetails) {
if (shouldBeTag) { if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', { navigation.push('Tab-Shared-Hashtag', { tag_name: content.substring(1) })
hashtag: content.substring(1)
})
} else { } else {
await openLink(href, navigation) await openLink(href, navigation)
} }
@ -309,7 +307,11 @@ const ParseHTML: React.FC<Props> = ({
height: numberOfLines === 1 && !expanded ? 0 : undefined height: numberOfLines === 1 && !expanded ? 0 : undefined
}} }}
numberOfLines={ numberOfLines={
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : MAX_ALLOWED_LINES typeof totalLines === 'number'
? expanded
? 999
: numberOfLines
: Math.max(MAX_ALLOWED_LINES, numberOfLines)
} }
selectable={selectable} selectable={selectable}
/> />

View File

@ -137,8 +137,8 @@ const TimelinePoll: React.FC = () => {
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}} }}
name={ name={
`${poll.own_votes?.includes(index) ? 'Check' : ''}${ `${poll.own_votes?.includes(index) ? 'check-' : ''}${
poll.multiple ? 'Square' : 'Circle' poll.multiple ? 'square' : 'circle'
}` as any }` as any
} }
size={StyleConstants.Font.Size.M} 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() context: (status.pinned || false).toString()
}), }),
icon: status.pinned ? 'pin.slash' : 'pin' 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_false": "Segueix l'usuari",
"action_true": "Deixa de seguir l'usuari" "action_true": "Deixa de seguir l'usuari"
}, },
"inLists": "Llistes que continguin l'usuari", "inLists": "",
"showBoosts": { "showBoosts": {
"action_false": "Mostra els impulsos de l'usuari", "action_false": "Mostra els impulsos de l'usuari",
"action_true": "Oculta els impulsos de l'usuari" "action_true": "Oculta els impulsos de l'usuari"
@ -16,12 +16,12 @@
"action_true": "Deixa de silenciar l'usuari" "action_true": "Deixa de silenciar l'usuari"
}, },
"followAs": { "followAs": {
"trigger": "Segueix com...", "trigger": "",
"succeed_default": "Seguint a @{{target}} com @{{source}}", "succeed_default": "Seguint a @{{target}} com @{{source}}",
"succeed_locked": "Enviada la sol·licitud de seguiment a @{{target}} com {{source}}, pendent d'aprovar-la", "succeed_locked": "Enviada la sol·licitud de seguiment a @{{target}} com {{source}}, pendent d'aprovar-la",
"failed": "Segueix com" "failed": "Segueix com"
}, },
"blockReport": "Bloqueja i denuncia...", "blockReport": "",
"block": { "block": {
"action_false": "Bloqueja l'usuari", "action_false": "Bloqueja l'usuari",
"action_true": "Deixa de bloquejar l'usuari", "action_true": "Deixa de bloquejar l'usuari",
@ -54,6 +54,15 @@
} }
} }
}, },
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
},
"filter": {
"action": ""
}
},
"share": { "share": {
"status": { "status": {
"action": "Comparteix la publicació" "action": "Comparteix la publicació"
@ -88,6 +97,10 @@
"pin": { "pin": {
"action_false": "Fixa la publicació", "action_false": "Fixa la publicació",
"action_true": "Deixa de fixar 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_one": "Filtrat per {{filters}}.",
"match_v2_other": "Filtrat per {{count}} filtres, {{filters}}." "match_v2_other": "Filtrat per {{count}} filtres, {{filters}}."
}, },
"fullConversation": "Llegeix conversacions", "fullConversation": "Llegeix converses",
"translate": { "translate": {
"default": "Tradueix", "default": "Tradueix",
"succeed": "Traduït per {{provider}} des de l'idioma {{source}}", "succeed": "Traduït per {{provider}} des de l'idioma {{source}}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,32 @@
{ {
"buttons": { "buttons": {
"OK": "", "OK": "Ados",
"apply": "", "apply": "Aplikatu",
"cancel": "", "cancel": "Utzi",
"discard": "", "discard": "Baztertu",
"continue": "", "continue": "Jarraitu",
"create": "", "create": "Sortu",
"delete": "", "delete": "Ezabatu",
"done": "", "done": "Eginda",
"confirm": "" "confirm": "Berretsi"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "" "accessibilityLabel": "Emoji pertsonalizatua {{emoji}}"
}, },
"message": { "message": {
"success": { "success": {
"message": "" "message": "{{function}} ongi burutu da"
}, },
"warning": { "warning": {
"message": "" "message": ""
}, },
"error": { "error": {
"message": "" "message": "{{function}}-(e)k huts egin du, mesedez, saia zaitez berriro"
} }
}, },
"separator": "", "separator": ", ",
"discard": { "discard": {
"title": "", "title": "Aldaketa ez da gorde",
"message": "" "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": { "share": {
"status": { "status": {
"action": "" "action": ""
@ -88,6 +97,10 @@
"pin": { "pin": {
"action_false": "", "action_false": "",
"action_true": "" "action_true": ""
},
"filter": {
"action_false": "",
"action_true": ""
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
"action_false": "Theo dõi người này", "action_false": "Theo dõi người này",
"action_true": "Ngưng 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": { "showBoosts": {
"action_false": "Hiển lượt đăng lại", "action_false": "Hiển lượt đăng lại",
"action_true": "Ẩ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": { "share": {
"status": { "status": {
"action": "Đăng lại" "action": "Đăng lại"
@ -88,6 +97,10 @@
"pin": { "pin": {
"action_false": "Tút ghim", "action_false": "Tút ghim",
"action_true": "Bỏ ghim tút" "action_true": "Bỏ ghim tút"
},
"filter": {
"action_false": "",
"action_true": ""
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import ComponentHashtag from '@components/Hashtag'
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { TabMeStackScreenProps } from '@utils/navigation/navigators' 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 { flattenPages } from '@utils/queryHooks/utils'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,7 +13,7 @@ import { FlatList } from 'react-native-gesture-handler'
const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({
navigation navigation
}) => { }) => {
const { t } = useTranslation(['common', 'screenTabs']) const { t } = useTranslation(['common', 'screenTabs', 'componentContextMenu'])
const { data, fetchNextPage, refetch } = useFollowedTagsQuery() const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = flattenPages(data) const flattenData = flattenPages(data)
@ -24,7 +24,7 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
} }
}, [flattenData.length]) }, [flattenData.length])
const mutation = useTagsMutation({ const mutation = useTagMutation({
onSuccess: () => { onSuccess: () => {
haptics('Light') haptics('Light')
refetch() refetch()
@ -33,9 +33,10 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
displayMessage({ displayMessage({
type: 'error', type: 'error',
message: t('common:message.error.message', { message: t('common:message.error.message', {
function: to function: t('componentContextMenu:hashtag.follow.action', {
? t('screenTabs:shared.hashtag.follow') defaultValue: 'false',
: t('screenTabs:shared.hashtag.unfollow') context: to.toString()
})
}), }),
...(err.status && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
@ -58,8 +59,11 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
children={ children={
<Button <Button
type='text' type='text'
content={t('screenTabs:shared.hashtag.unfollow')} content={t('componentContextMenu:hashtag.follow.action', {
onPress={() => mutation.mutate({ tag: item.name, to: !item.following })} 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 Hr from '@components/Hr'
import ComponentInput from '@components/Input' import ComponentInput from '@components/Input'
import { MenuRow } from '@components/Menu' import { MenuRow } from '@components/Menu'
import { ModalScrollView } from '@components/ModalScrollView'
import Selections from '@components/Selections' import Selections from '@components/Selections'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import browserPackage from '@utils/helpers/browserPackage'
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators' import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks' import { queryClient } from '@utils/queryHooks'
import { QueryKeyFilters } from '@utils/queryHooks/filters' import { QueryKeyFilters } from '@utils/queryHooks/filters'
import { getAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import 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 { 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 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< const TabMePreferencesFilter: React.FC<
TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filter'> & { TabMePreferencesStackScreenProps<'Tab-Me-Preferences-Filter'> & {
@ -101,7 +103,11 @@ const TabMePreferencesFilter: React.FC<
]) ])
const [keywords, setKeywords] = useState<string[]>( 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(() => { useEffect(() => {
@ -152,6 +158,48 @@ const TabMePreferencesFilter: React.FC<
}) })
break break
case 'edit': 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 break
} }
}} }}
@ -160,105 +208,132 @@ const TabMePreferencesFilter: React.FC<
}) })
}, [titleState[0], expiration, contexts, actions, keywords]) }, [titleState[0], expiration, contexts, actions, keywords])
const scrollViewRef = useRef<ScrollView>(null)
return ( return (
<KeyboardAvoidingView <ModalScrollView ref={scrollViewRef}>
style={{ flex: 1 }} <ComponentInput title={t('screenTabs:me.preferencesFilter.name')} value={titleState} />
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} <MenuRow
> title={t('screenTabs:me.preferencesFilter.expiration')}
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}> content={t(`screenTabs:me.preferencesFilter.expirationOptions.${expiration}`)}
<ScrollView style={{ padding: StyleConstants.Spacing.Global.PagePadding }}> iconBack='chevron-right'
<ComponentInput title={t('screenTabs:me.preferencesFilter.name')} value={titleState} /> onPress={() =>
showActionSheetWithOptions(
{
title: t('screenTabs:me.preferencesFilter.expiration'),
options: [
...expirations.map(opt =>
t(`screenTabs:me.preferencesFilter.expirationOptions.${opt}`)
),
t('common:buttons.cancel')
],
cancelButtonIndex: expirations.length,
...androidActionSheetStyles(colors)
},
(selectedIndex: number) => {
selectedIndex < expirations.length && setExpiration(expirations[selectedIndex])
}
)
}
/>
<Hr />
<Selections
title={t('screenTabs:me.preferencesFilter.context')}
multiple
invalid={!contexts.filter(context => context.selected).length}
options={contexts}
setOptions={setContexts}
/>
<Selections
title={t('screenTabs:me.preferencesFilter.action')}
options={actions}
setOptions={setActions}
/>
<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,
marginBottom: StyleConstants.Spacing.S
}}
>
{[...Array(keywords.length)].map((_, i) => (
<ComponentInput
key={i}
title={t('screenTabs:me.preferencesFilter.keyword')}
value={[
keywords[i],
k => setKeywords(keywords.map((curr, ii) => (i === ii ? k : curr)))
]}
/>
))}
</View>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
marginRight: StyleConstants.Spacing.M
}}
>
<Button
onPress={() => setKeywords(keywords.slice(0, keywords.length - 1))}
type='icon'
content='minus'
round
disabled={keywords.length < 1}
style={{ marginRight: StyleConstants.Spacing.M }}
/>
<Button
onPress={() => {
setKeywords([...keywords, ''])
setTimeout(() => scrollViewRef.current?.scrollToEnd(), 50)
}}
type='icon'
content='plus'
round
/>
</View>
{params.type === 'edit' && params.filter.statuses?.length ? (
<>
<Hr style={{ marginVertical: StyleConstants.Spacing.M }} />
<MenuRow <MenuRow
title={t('screenTabs:me.preferencesFilter.expiration')} title={t('screenTabs:me.preferencesFilter.statuses')}
content={t(`screenTabs:me.preferencesFilter.expirationOptions.${expiration}`)} content={t('screenTabs:me.preferencesFilters.statuses', {
iconBack='chevron-right' count: params.filter.statuses.length
onPress={() => })}
showActionSheetWithOptions( iconBack='external-link'
onPress={async () =>
WebBrowser.openAuthSessionAsync(
`https://${getAccountStorage.string('auth.domain')}/filters/${
params.filter.id
}/statuses`,
'tooot://tooot',
{ {
title: t('screenTabs:me.preferencesFilter.expiration'), ...(await browserPackage()),
options: [ dismissButtonStyle: 'done',
...expirations.map(opt => readerMode: false
t(`screenTabs:me.preferencesFilter.expirationOptions.${opt}`)
),
t('common:buttons.cancel')
],
cancelButtonIndex: expirations.length,
...androidActionSheetStyles(colors)
},
(selectedIndex: number) => {
selectedIndex < expirations.length && setExpiration(expirations[selectedIndex])
} }
) )
} }
/> />
<Hr /> </>
) : null}
<Selections </ModalScrollView>
title={t('screenTabs:me.preferencesFilter.context')}
multiple
invalid={!contexts.filter(context => context.selected).length}
options={contexts}
setOptions={setContexts}
/>
<Selections
title={t('screenTabs:me.preferencesFilter.action')}
options={actions}
setOptions={setActions}
/>
<Hr style={{ marginVertical: StyleConstants.Spacing.M }} />
<CustomText
fontStyle='M'
children={t('screenTabs:me.preferencesFilter.keywords')}
style={{ color: colors.primaryDefault }}
/>
<View
style={{
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S
}}
>
{[...Array(keywords.length)].map((_, i) => (
<ComponentInput
key={i}
title={t('screenTabs:me.preferencesFilter.keyword')}
value={[
keywords[i],
k => setKeywords(keywords.map((curr, ii) => (i === ii ? k : curr)))
]}
/>
))}
</View>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
marginRight: StyleConstants.Spacing.M
}}
>
<Button
onPress={() => setKeywords(keywords.slice(0, keywords.length - 1))}
type='icon'
content='minus'
round
disabled={keywords.length < 1}
/>
<CustomText
style={{ marginHorizontal: StyleConstants.Spacing.M, color: colors.secondary }}
children={keywords.length}
/>
<Button
onPress={() => setKeywords([...keywords, ''])}
type='icon'
content='plus'
round
/>
</View>
</ScrollView>
</SafeAreaView>
</KeyboardAvoidingView>
) )
} }

View File

@ -1,15 +1,15 @@
import { Filter } from '@components/Filter'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators' import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
import { useFiltersQuery } from '@utils/queryHooks/filters' import { useFiltersQuery } from '@utils/queryHooks/filters'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useEffect } from 'react' import React, { useEffect } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, TouchableNativeFeedback, View } from 'react-native' import { Pressable, View } from 'react-native'
import { SwipeListView } from 'react-native-swipe-list-view' import { SwipeListView } from 'react-native-swipe-list-view'
const TabMePreferencesFilters: React.FC< const TabMePreferencesFilters: React.FC<
@ -39,6 +39,7 @@ const TabMePreferencesFilters: React.FC<
return ( return (
<SwipeListView <SwipeListView
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
renderHiddenItem={({ item }) => ( renderHiddenItem={({ item }) => (
<Pressable <Pressable
style={{ style={{
@ -65,98 +66,10 @@ const TabMePreferencesFilters: React.FC<
filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1 filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1
)} )}
renderItem={({ item: filter }) => ( renderItem={({ item: filter }) => (
<TouchableNativeFeedback <Filter
filter={filter}
onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'edit', 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} ItemSeparatorComponent={ComponentSeparator}
/> />

View File

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

View File

@ -1,12 +1,13 @@
import AccountButton from '@components/AccountButton' import AccountButton from '@components/AccountButton'
import ComponentInstance from '@components/Instance' import ComponentInstance from '@components/Instance'
import { ModalScrollView } from '@components/ModalScrollView'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { getReadableAccounts } from '@utils/storage/actions' import { getReadableAccounts } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform, View } from 'react-native' import { View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
const TabMeSwitch: React.FC = () => { const TabMeSwitch: React.FC = () => {
@ -20,63 +21,53 @@ const TabMeSwitch: React.FC = () => {
}, [scrollViewRef.current]) }, [scrollViewRef.current])
return ( return (
<KeyboardAvoidingView <ModalScrollView>
style={{ flex: 1 }} <View>
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} <CustomText
> fontStyle='M'
<ScrollView
ref={scrollViewRef}
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
keyboardShouldPersistTaps='always'
>
<View>
<CustomText
fontStyle='M'
style={{
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S,
color: colors.primaryDefault
}}
>
{t('me.switch.new')}
</CustomText>
<ComponentInstance scrollViewRef={scrollViewRef} disableHeaderImage goBack />
</View>
<View
style={{ style={{
marginTop: StyleConstants.Spacing.S, textAlign: 'center',
paddingTop: StyleConstants.Spacing.M, paddingVertical: StyleConstants.Spacing.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding, color: colors.primaryDefault
borderTopWidth: 1,
borderTopColor: colors.border
}} }}
> >
<CustomText {t('me.switch.new')}
fontStyle='M' </CustomText>
style={{ <ComponentInstance scrollViewRef={scrollViewRef} disableHeaderImage goBack />
textAlign: 'center', </View>
paddingVertical: StyleConstants.Spacing.S,
color: colors.primaryDefault <View
}} style={{
> marginTop: StyleConstants.Spacing.S,
{t('me.switch.existing')} paddingTop: StyleConstants.Spacing.M,
</CustomText> borderTopWidth: 1,
<View borderTopColor: colors.border
style={{ }}
flex: 1, >
flexDirection: 'row', <CustomText
flexWrap: 'wrap', fontStyle='M'
marginTop: StyleConstants.Spacing.M style={{
}} textAlign: 'center',
> paddingVertical: StyleConstants.Spacing.S,
{accounts.map((account, index) => { color: colors.primaryDefault
return <AccountButton key={index} account={account} /> }}
})} >
</View> {t('me.switch.existing')}
</CustomText>
<View
style={{
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M
}}
>
{accounts.map((account, index) => {
return <AccountButton key={index} account={account} />
})}
</View> </View>
</ScrollView> </View>
</KeyboardAvoidingView> </ModalScrollView>
) )
} }

View File

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

View File

@ -38,7 +38,8 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
]} ]}
/> />
</CustomText> </CustomText>
) ),
headerBackVisible: false
}) })
navigation.setParams({ queryKey }) 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 { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck' import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect } from 'react' import React, { Fragment, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import * as DropdownMenu from 'zeego/dropdown-menu'
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
navigation, navigation,
route: { 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(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />, headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
title: `#${decodeURIComponent(hashtag)}` title: `#${decodeURIComponent(tag_name)}`
}) })
navigation.setParams({ queryKey: queryKey }) 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(() => { useEffect(() => {
if (!canFollowTags) return if (!canFollowTags && !canFilterTag) return
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<HeaderRight <DropdownMenu.Root>
loading={isFetching || mutation.isLoading} <DropdownMenu.Trigger>
type='text' <HeaderRight content='more-horizontal' onPress={() => {}} />
content={ </DropdownMenu.Trigger>
data?.following
? t('screenTabs:shared.hashtag.unfollow') <DropdownMenu.Content>
: t('screenTabs:shared.hashtag.follow') {[mHashtag].map((menu, i) => (
} <Fragment key={i}>
onPress={() => {menu.map((group, index) => (
typeof data?.following === 'boolean' && <DropdownMenu.Group key={index}>
mutation.mutate({ tag: hashtag, to: !data.following }) {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>
)
}
})}
</DropdownMenu.Group>
))}
</Fragment>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
) )
}) })
}, [canFollowTags, data?.following, isFetching]) }, [mHashtag])
return <Timeline queryKey={queryKey} /> return <Timeline queryKey={queryKey} />
} }

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot' import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users' import TabSharedUsers from '@screens/Tabs/Shared/Users'
import React from 'react' import React from 'react'
import TabSharedFilter from './Filter'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => { const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
return ( return (
@ -28,6 +29,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNaviga
name='Tab-Shared-Attachments' name='Tab-Shared-Attachments'
component={TabSharedAttachments} component={TabSharedAttachments}
/> />
<Stack.Screen
key='Tab-Shared-Filter'
name='Tab-Shared-Filter'
component={TabSharedFilter}
options={{ presentation: 'modal' }}
/>
<Stack.Screen <Stack.Screen
key='Tab-Shared-Hashtag' key='Tab-Shared-Hashtag'
name='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-Account-In-Lists': { account: Pick<Mastodon.Account, 'id' | 'username'> }
'Tab-Shared-Attachments': { account: Mastodon.Account; queryKey?: QueryKeyTimeline } '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-History': { status: Mastodon.Status; detectedLanguage: string }
'Tab-Shared-Report': { 'Tab-Shared-Report': {
account: Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'> account: Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'>

View File

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

View File

@ -6,11 +6,12 @@ import { useEffect } from 'react'
import pushUseNavigate from './useNavigate' import pushUseNavigate from './useNavigate'
const pushUseRespond = () => { const pushUseRespond = () => {
const [accountActive] = useGlobalStorage.string('account.active')
const [accounts] = useGlobalStorage.object('accounts') const [accounts] = useGlobalStorage.object('accounts')
useEffect(() => { useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener( const subscription = Notifications.addNotificationResponseReceivedListener(
({ notification }) => { async ({ notification }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey)
const payloadData = notification.request.content.data as { const payloadData = notification.request.content.data as {
@ -19,19 +20,19 @@ const pushUseRespond = () => {
accountId: string accountId: string
} }
const currAccount = accounts?.find( const incomingAccount = generateAccountKey({
account => domain: payloadData.instanceUrl,
account === id: payloadData.accountId
generateAccountKey({ domain: payloadData.instanceUrl, id: payloadData.accountId }) })
) const foundAccount = accounts?.find(account => account === incomingAccount)
if (currAccount) { if (foundAccount && foundAccount !== accountActive) {
setAccount(currAccount) await setAccount(foundAccount)
} }
pushUseNavigate(payloadData.notification_id) pushUseNavigate(payloadData.notification_id)
} }
) )
return () => subscription.remove() return () => subscription.remove()
}, [accounts]) }, [accountActive, accounts])
} }
export default pushUseRespond 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 apiInstance from '@utils/api/instance'
import { AxiosError } from 'axios' 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' }] export type QueryKeyFilters = ['Filters', { version: 'v1' | 'v2' }]
const filtersQueryFunction = async <T extends 'v1' | 'v2' = 'v1'>({ 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 }] export type QueryKeyTag = ['Tag', { tag_name: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => { const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTag>) => {
const { tag } = queryKey[1] 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 return res.body
} }
const useTagsQuery = ({ const useTagQuery = ({
options, tag_name,
...queryKeyParams options
}: QueryKeyTags[1] & { }: {
tag_name: Mastodon.Tag['name']
options?: UseQueryOptions<Mastodon.Tag, AxiosError> options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => { }) => {
const queryKey: QueryKeyTags = ['Tags', { ...queryKeyParams }] const queryKey: QueryKeyTag = ['Tag', { tag_name }]
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
type MutationVarsAnnouncement = { tag: string; to: boolean } type MutationVarsTag = { tag_name: Mastodon.Tag['name']; to: boolean }
const mutationFunction = async ({ tag, to }: MutationVarsAnnouncement) => { const mutationFunction = async ({ tag_name, to }: MutationVarsTag) => {
return apiInstance<{}>({ return apiInstance<{}>({
method: 'post', 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) return useMutation(mutationFunction, options)
} }
export { useFollowedTagsQuery, useTagsQuery, useTagsMutation } export { useFollowedTagsQuery, useTagQuery, useTagMutation }

View File

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