From 2c7772d4c268dc6e4d93ae92faf57d8149cb85dd Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 18 Dec 2022 17:25:18 +0100 Subject: [PATCH] Fixed #572 --- fastlane/metadata/en-US/release_notes.txt | 1 + fastlane/metadata/zh-Hans/release_notes.txt | 1 + src/@types/mastodon.d.ts | 27 ++- src/components/Timeline/Default.tsx | 53 ++-- src/components/Timeline/Notifications.tsx | 47 ++-- src/components/Timeline/Shared/Context.tsx | 11 +- src/components/Timeline/Shared/Filtered.tsx | 226 +++++++++--------- .../Timeline/Shared/HeaderAndroid.tsx | 5 +- .../Timeline/Shared/HeaderDefault.tsx | 4 +- src/components/Timeline/Shared/Translate.tsx | 27 +-- src/components/contextMenu/share.ts | 11 +- src/helpers/features.json | 4 + src/helpers/removeHTML.ts | 3 + src/i18n/en/components/timeline.json | 7 +- src/utils/migrations/instances/v10.ts | 2 +- src/utils/migrations/instances/v11.ts | 2 +- src/utils/migrations/instances/v5.ts | 2 +- src/utils/migrations/instances/v6.ts | 2 +- src/utils/migrations/instances/v7.ts | 2 +- src/utils/migrations/instances/v8.ts | 2 +- src/utils/migrations/instances/v9.ts | 2 +- src/utils/slices/instances/add.ts | 2 +- src/utils/slices/instances/updateFilters.ts | 4 +- 23 files changed, 251 insertions(+), 196 deletions(-) diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 862623d2..44b82a21 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1 +1,2 @@ Enjoy toooting! This version includes following improvements and fixes: +- Align filter experience with v4.0 and above diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index d9488dcd..352b41ba 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1 +1,2 @@ toooting愉快!此版本包括以下改进和修复: +- 改进过滤体验,与v4.0以上版本一致 diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index fc61ed10..102739ca 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -263,7 +263,8 @@ declare namespace Mastodon { verified_at: string | null } - type Filter = { + type Filter = T extends 'v2' ? Filter_V2 : Filter_V1 + type Filter_V1 = { id: string phrase: string context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] @@ -271,6 +272,25 @@ declare namespace Mastodon { irreversible: boolean whole_word: boolean } + type Filter_V2 = { + id: string + title: string + context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] + expires_at?: string + filter_action: 'warn' | 'hide' + keywords: FilterKeyword[] + statuses: FilterStatus[] + } + + type FilterKeyword = { id: string; keyword: string; whole_word: boolean } + + type FilterStatus = { id: string; status_id: string } + + type FilterResult = { + filter: Filter<'v2'> + keyword_matches?: FilterKeyword['keyword'][] + status_matches?: FilterStatus['id'][] + } type List = { id: string @@ -461,7 +481,7 @@ declare namespace Mastodon { sensitive: boolean spoiler_text?: string media_attachments: Attachment[] - application: Application + application?: Application // Attributes mentions: Mention[] @@ -472,7 +492,7 @@ declare namespace Mastodon { reblogs_count: number favourites_count: number replies_count: number - edited_at?: string // FEATURE edit_post + edited_at?: string favourited: boolean reblogged: boolean muted: boolean @@ -488,6 +508,7 @@ declare namespace Mastodon { card?: Card language?: string text?: string + filtered?: FilterResult[] } type StatusHistory = { diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 8d209d14..5d2ebdf7 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -9,11 +9,12 @@ import TimelineCard from '@components/Timeline/Shared/Card' import TimelineContent from '@components/Timeline/Shared/Content' import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault' import TimelinePoll from '@components/Timeline/Shared/Poll' +import removeHTML from '@helpers/removeHTML' import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { getInstanceAccount } from '@utils/slices/instancesSlice' +import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useRef, useState } from 'react' @@ -22,7 +23,7 @@ import { useSelector } from 'react-redux' import * as ContextMenu from 'zeego/context-menu' import StatusContext from './Shared/Context' import TimelineFeedback from './Shared/Feedback' -import TimelineFiltered, { shouldFilter } from './Shared/Filtered' +import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' import TimelineHeaderAndroid from './Shared/HeaderAndroid' import TimelineTranslate from './Shared/Translate' @@ -47,12 +48,20 @@ const TimelineDefault: React.FC = ({ disableOnPress = false, isConversation = false }) => { + const status = item.reblog ? item.reblog : item + const rawContent = useRef([]) + if (highlighted) { + rawContent.current = [ + removeHTML(status.content), + status.spoiler_text ? removeHTML(status.spoiler_text) : '' + ].filter(c => c.length) + } + const { colors } = useTheme() const navigation = useNavigation>() const instanceAccount = useSelector(getInstanceAccount, () => true) - const status = item.reblog ? item.reblog : item const ownAccount = status.account?.id === instanceAccount?.id const [spoilerExpanded, setSpoilerExpanded] = useState( instanceAccount?.preferences?.['reading:expand:spoilers'] || false @@ -60,17 +69,8 @@ const TimelineDefault: React.FC = ({ const spoilerHidden = status.spoiler_text?.length ? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded : false - const copiableContent = useRef<{ content: string; complete: boolean }>({ - content: '', - complete: false - }) const detectedLanguage = useRef(status.language || '') - const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey }) - if (queryKey && filtered && !highlighted) { - return - } - const mainStyle: StyleProp = { flex: 1, padding: disableDetails @@ -125,11 +125,36 @@ const TimelineDefault: React.FC = ({ visibility: status.visibility, type: 'status', url: status.url || status.uri, - copiableContent + rawContent }) const mStatus = menuStatus({ status, queryKey, rootQueryKey }) const mInstance = menuInstance({ status, queryKey, rootQueryKey }) + if (!ownAccount) { + let filterResults: FilteredProps['filterResults'] = [] + const [filterRevealed, setFilterRevealed] = useState(false) + const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side')) + if (hasFilterServerSide) { + if (status.filtered?.length) { + filterResults = status.filtered?.map(filter => filter.filter) + } + } else { + if (queryKey) { + const checkFilter = shouldFilter({ queryKey, status }) + if (checkFilter?.length) { + filterResults = checkFilter + } + } + } + if (queryKey && !highlighted && filterResults?.length && !filterRevealed) { + return !filterResults.filter(result => result.filter_action === 'hide').length ? ( + setFilterRevealed(!filterRevealed)}> + + + ) : null + } + } + return ( = ({ reblogStatus: item.reblog ? item : undefined, ownAccount, spoilerHidden, - copiableContent, + rawContent, detectedLanguage, highlighted, inThread: queryKey?.[1].page === 'Toot', diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index 81ee0d57..68cead79 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -13,7 +13,7 @@ import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { getInstanceAccount } from '@utils/slices/instancesSlice' +import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useRef, useState } from 'react' @@ -21,7 +21,7 @@ import { Pressable, View } from 'react-native' import { useSelector } from 'react-redux' import * as ContextMenu from 'zeego/context-menu' import StatusContext from './Shared/Context' -import TimelineFiltered, { shouldFilter } from './Shared/Filtered' +import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' import TimelineHeaderAndroid from './Shared/HeaderAndroid' @@ -52,17 +52,6 @@ const TimelineNotifications: React.FC = ({ notification, queryKey }) => { complete: false }) - const filtered = - notification.status && - shouldFilter({ - copiableContent, - status: notification.status, - queryKey - }) - if (notification.status && filtered) { - return - } - const { colors } = useTheme() const navigation = useNavigation>() @@ -124,20 +113,44 @@ const TimelineNotifications: React.FC = ({ notification, queryKey }) => { const mShare = menuShare({ visibility: notification.status?.visibility, type: 'status', - url: notification.status?.url || notification.status?.uri, - copiableContent + url: notification.status?.url || notification.status?.uri }) const mStatus = menuStatus({ status: notification.status, queryKey }) const mInstance = menuInstance({ status: notification.status, queryKey }) + if (!ownAccount) { + let filterResults: FilteredProps['filterResults'] = [] + const [filterRevealed, setFilterRevealed] = useState(false) + const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side')) + if (notification.status) { + if (hasFilterServerSide) { + if (notification.status.filtered?.length) { + filterResults = notification.status.filtered.map(filter => filter.filter) + } + } else { + const checkFilter = shouldFilter({ queryKey, status: notification.status }) + if (checkFilter?.length) { + filterResults = checkFilter + } + } + + if (filterResults?.length && !filterRevealed) { + return !filterResults.filter(result => result.filter_action === 'hide').length ? ( + setFilterRevealed(!filterRevealed)}> + + + ) : null + } + } + } + return ( diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index 218379d1..8917c2fb 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -1,7 +1,9 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { createContext } from 'react' -type ContextType = { +export type HighlightedStatusContextType = {} + +type StatusContextType = { queryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline @@ -10,10 +12,7 @@ type ContextType = { reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status ownAccount?: boolean spoilerHidden?: boolean - copiableContent?: React.MutableRefObject<{ - content: string - complete: boolean - }> + rawContent?: React.MutableRefObject // When highlighted, for translate, edit history detectedLanguage?: React.MutableRefObject highlighted?: boolean @@ -22,6 +21,6 @@ type ContextType = { disableOnPress?: boolean isConversation?: boolean } -const StatusContext = createContext({} as ContextType) +const StatusContext = createContext({} as StatusContextType) export default StatusContext diff --git a/src/components/Timeline/Shared/Filtered.tsx b/src/components/Timeline/Shared/Filtered.tsx index 28c582e5..0f72681d 100644 --- a/src/components/Timeline/Shared/Filtered.tsx +++ b/src/components/Timeline/Shared/Filtered.tsx @@ -1,129 +1,133 @@ import CustomText from '@components/Text' +import removeHTML from '@helpers/removeHTML' import { store } from '@root/store' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice' +import { getInstance } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import htmlparser2 from 'htmlparser2-without-node-native' import React from 'react' import { useTranslation } from 'react-i18next' import { View } from 'react-native' -const TimelineFiltered = React.memo( - ({ phrase }: { phrase: string }) => { - const { colors } = useTheme() - const { t } = useTranslation('componentTimeline') +export interface FilteredProps { + filterResults: { title: string; filter_action: Mastodon.Filter<'v2'>['filter_action'] }[] +} - return ( - - - {t('shared.filtered', { phrase })} - - - ) - }, - () => true -) +const TimelineFiltered: React.FC = ({ filterResults }) => { + const { colors } = useTheme() + const { t } = useTranslation('componentTimeline') -export const shouldFilter = ({ - copiableContent, - status, - queryKey -}: { - copiableContent: React.MutableRefObject<{ - content: string - complete: boolean - }> - status: Mastodon.Status - queryKey: QueryKeyTimeline -}): string | null => { - const page = queryKey[1] - const instance = getInstance(store.getState()) - const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id - - let shouldFilter: string | null = null - - if (!ownAccount) { - let rawContent = '' - const parser = new htmlparser2.Parser({ - ontext: (text: string) => { - if (!copiableContent.current.complete) { - copiableContent.current.content = copiableContent.current.content + text - } - - rawContent = rawContent + text - } - }) - if (status.spoiler_text) { - parser.write(status.spoiler_text) - rawContent = rawContent + `\n\n` + const main = () => { + if (!filterResults?.length) { + return <> } - parser.write(status.content) - parser.end() - - const checkFilter = (filter: Mastodon.Filter) => { - const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string - switch (filter.whole_word) { - case true: - if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContent)) { - shouldFilter = filter.phrase - } - break - case false: - if (new RegExp(escapedPhrase, 'i').test(rawContent)) { - shouldFilter = filter.phrase - } - break - } + switch (typeof filterResults[0]) { + case 'string': // v1 filter + return <>{t('shared.filtered.match', { context: 'v1', phrase: filterResults[0] })} + default: + return ( + <> + {t('shared.filtered.match', { + context: 'v2', + count: filterResults.length, + filters: filterResults.map(result => result.title).join(t('common:separator')) + })} + + + ) } - instance?.filters?.forEach(filter => { - if (shouldFilter) { - return - } - if (filter.expires_at) { - if (new Date().getTime() > new Date(filter.expires_at).getTime()) { - return - } - } - - switch (page.page) { - case 'Following': - case 'Local': - case 'List': - case 'Account': - if (filter.context.includes('home')) { - checkFilter(filter) - } - break - case 'Notifications': - if (filter.context.includes('notifications')) { - checkFilter(filter) - } - break - case 'LocalPublic': - if (filter.context.includes('public')) { - checkFilter(filter) - } - break - case 'Toot': - if (filter.context.includes('thread')) { - checkFilter(filter) - } - } - }) - - copiableContent.current.complete = true } - return shouldFilter + return ( + + + {main()} + + + ) +} + +export const shouldFilter = ({ + queryKey, + status +}: { + queryKey: QueryKeyTimeline + status: Pick +}): FilteredProps['filterResults'] | undefined => { + const page = queryKey[1] + const instance = getInstance(store.getState()) + + let returnFilter: FilteredProps['filterResults'] | undefined + + const rawContentCombined = [ + removeHTML(status.content), + status.spoiler_text ? removeHTML(status.spoiler_text) : '' + ] + .filter(c => c.length) + .join(`\n`) + const checkFilter = (filter: Mastodon.Filter<'v1'>) => { + const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string + switch (filter.whole_word) { + case true: + if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContentCombined)) { + returnFilter = [{ title: filter.phrase, filter_action: 'warn' }] + } + break + case false: + if (new RegExp(escapedPhrase, 'i').test(rawContentCombined)) { + returnFilter = [{ title: filter.phrase, filter_action: 'warn' }] + } + break + } + } + instance?.filters?.forEach(filter => { + if (returnFilter) { + return + } + if (filter.expires_at) { + if (new Date().getTime() > new Date(filter.expires_at).getTime()) { + return + } + } + + switch (page.page) { + case 'Following': + case 'Local': + case 'List': + case 'Account': + if (filter.context.includes('home')) { + checkFilter(filter as Mastodon.Filter<'v1'>) + } + break + case 'Notifications': + if (filter.context.includes('notifications')) { + checkFilter(filter as Mastodon.Filter<'v1'>) + } + break + case 'LocalPublic': + if (filter.context.includes('public')) { + checkFilter(filter as Mastodon.Filter<'v1'>) + } + break + case 'Toot': + if (filter.context.includes('thread')) { + checkFilter(filter as Mastodon.Filter<'v1'>) + } + } + }) + + return returnFilter } export default TimelineFiltered diff --git a/src/components/Timeline/Shared/HeaderAndroid.tsx b/src/components/Timeline/Shared/HeaderAndroid.tsx index 7d67db1d..ece4ef79 100644 --- a/src/components/Timeline/Shared/HeaderAndroid.tsx +++ b/src/components/Timeline/Shared/HeaderAndroid.tsx @@ -10,7 +10,7 @@ import * as DropdownMenu from 'zeego/dropdown-menu' import StatusContext from './Context' const TimelineHeaderAndroid: React.FC = () => { - const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } = + const { queryKey, rootQueryKey, status, disableDetails, disableOnPress, rawContent } = useContext(StatusContext) if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null @@ -21,7 +21,8 @@ const TimelineHeaderAndroid: React.FC = () => { const mShare = menuShare({ visibility: status.visibility, type: 'status', - url: status.url || status.uri + url: status.url || status.uri, + rawContent }) const mAccount = menuAccount({ type: 'status', diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index f4f699fa..44e28479 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -16,7 +16,7 @@ import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedVisibility from './HeaderShared/Visibility' const TimelineHeaderDefault: React.FC = () => { - const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } = + const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } = useContext(StatusContext) if (!status) return null @@ -28,7 +28,7 @@ const TimelineHeaderDefault: React.FC = () => { visibility: status.visibility, type: 'status', url: status.url || status.uri, - copiableContent + rawContent }) const mAccount = menuAccount({ type: 'status', diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index f8c62192..a4246612 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -13,38 +13,19 @@ import { Circle } from 'react-native-animated-spinkit' import StatusContext from './Context' const TimelineTranslate = () => { - const { status, highlighted, copiableContent, detectedLanguage } = useContext(StatusContext) - if (!status || !highlighted) return null + const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext) + if (!status || !highlighted || !rawContent?.current.length) return null const { t } = useTranslation('componentTimeline') const { colors } = useTheme() - const backupTextProcessing = (): string[] => { - const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content] - - for (const i in text) { - for (const emoji of status.emojis) { - text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ') - } - text[i] = text[i] - .replace(/(<([^>]+)>)/gi, ' ') - .replace(/@.*? /gi, ' ') - .replace(/#.*? /gi, ' ') - .replace(/http(s):\/\/.*? /gi, ' ') - } - return text - } - const text = copiableContent?.current.content - ? [copiableContent?.current.content] - : backupTextProcessing() - const [detected, setDetected] = useState<{ language: string confidence: number }>({ language: status.language || '', confidence: 0 }) useEffect(() => { const detect = async () => { - const result = await detectLanguage(text.join('\n\n')) + const result = await detectLanguage(rawContent.current.join('\n\n')) if (result) { setDetected(result) if (detectedLanguage) { @@ -64,7 +45,7 @@ const TimelineTranslate = () => { const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({ source: detected.language, target: targetLanguage, - text, + text: rawContent.current, options: { enabled } }) diff --git a/src/components/contextMenu/share.ts b/src/components/contextMenu/share.ts index 8d907040..1f6d63d6 100644 --- a/src/components/contextMenu/share.ts +++ b/src/components/contextMenu/share.ts @@ -7,10 +7,7 @@ const menuShare = ( params: | { visibility?: Mastodon.Status['visibility'] - copiableContent?: React.MutableRefObject<{ - content?: string | undefined - complete: boolean - }> + rawContent?: React.MutableRefObject type: 'status' url?: string } @@ -48,17 +45,17 @@ const menuShare = ( icon: 'square.and.arrow.up' }) } - if (params.type === 'status' && Platform.OS === 'ios') + if (params.type === 'status') menus[0].push({ key: 'copy', item: { onSelect: () => { - Clipboard.setString(params.copiableContent?.current.content || '') + Clipboard.setString(params.rawContent?.current.join(`\n\n`) || '') displayMessage({ type: 'success', message: t(`copy.succeed`) }) }, disabled: false, destructive: false, - hidden: !params.copiableContent?.current.content?.length + hidden: !params.rawContent?.current.length }, title: t('copy.action'), icon: 'doc.on.doc' diff --git a/src/helpers/features.json b/src/helpers/features.json index 28f74239..e23774ff 100644 --- a/src/helpers/features.json +++ b/src/helpers/features.json @@ -42,5 +42,9 @@ { "feature": "notification_type_admin_report", "version": 4.0 + }, + { + "feature": "filter_server_side", + "version": 4.0 } ] \ No newline at end of file diff --git a/src/helpers/removeHTML.ts b/src/helpers/removeHTML.ts index 8690061a..e8a3d4c1 100644 --- a/src/helpers/removeHTML.ts +++ b/src/helpers/removeHTML.ts @@ -6,6 +6,9 @@ const removeHTML = (text: string): string => { const parser = new htmlparser2.Parser({ ontext: (text: string) => { raw = raw + text + }, + onclosetag: (tag: string) => { + if (['p', 'br'].includes(tag)) raw = raw + `\n` } }) diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index 639c4a40..2e948368 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -91,7 +91,12 @@ "content": { "expandHint": "Hidden content" }, - "filtered": "Filtered: {{phrase}}.", + "filtered": { + "reveal": "Show anyway", + "match_v1": "Filtered: {{phrase}}.", + "match_v2_one": "Filtered by {{filters}}.", + "match_v2_other": "Filtered by {{count}} filters, {{filters}}." + }, "fullConversation": "Read conversations", "translate": { "default": "Translate", diff --git a/src/utils/migrations/instances/v10.ts b/src/utils/migrations/instances/v10.ts index db44a516..d0565ec2 100644 --- a/src/utils/migrations/instances/v10.ts +++ b/src/utils/migrations/instances/v10.ts @@ -19,7 +19,7 @@ export type InstanceV10 = { } version: string configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean follow_request: boolean diff --git a/src/utils/migrations/instances/v11.ts b/src/utils/migrations/instances/v11.ts index dffb36eb..21e924cb 100644 --- a/src/utils/migrations/instances/v11.ts +++ b/src/utils/migrations/instances/v11.ts @@ -18,7 +18,7 @@ export type InstanceV11 = { } version: string configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean follow_request: boolean diff --git a/src/utils/migrations/instances/v5.ts b/src/utils/migrations/instances/v5.ts index a9f70a45..25faaab4 100644 --- a/src/utils/migrations/instances/v5.ts +++ b/src/utils/migrations/instances/v5.ts @@ -17,7 +17,7 @@ type Instance = { avatarStatic: Mastodon.Account['avatar_static'] preferences: Mastodon.Preferences } - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean diff --git a/src/utils/migrations/instances/v6.ts b/src/utils/migrations/instances/v6.ts index cb80c7bc..964fcac7 100644 --- a/src/utils/migrations/instances/v6.ts +++ b/src/utils/migrations/instances/v6.ts @@ -18,7 +18,7 @@ type Instance = { } max_toot_chars?: number // To be deprecated in v4 configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean diff --git a/src/utils/migrations/instances/v7.ts b/src/utils/migrations/instances/v7.ts index 02c4862d..fb590736 100644 --- a/src/utils/migrations/instances/v7.ts +++ b/src/utils/migrations/instances/v7.ts @@ -19,7 +19,7 @@ type Instance = { } max_toot_chars?: number // To be deprecated in v4 configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean diff --git a/src/utils/migrations/instances/v8.ts b/src/utils/migrations/instances/v8.ts index 675c6628..48440027 100644 --- a/src/utils/migrations/instances/v8.ts +++ b/src/utils/migrations/instances/v8.ts @@ -19,7 +19,7 @@ type Instance = { } max_toot_chars?: number // To be deprecated in v4 configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean diff --git a/src/utils/migrations/instances/v9.ts b/src/utils/migrations/instances/v9.ts index 2e2d47b5..061d6094 100644 --- a/src/utils/migrations/instances/v9.ts +++ b/src/utils/migrations/instances/v9.ts @@ -19,7 +19,7 @@ export type InstanceV9 = { } version: string configuration?: Mastodon.Instance['configuration'] - filters: Mastodon.Filter[] + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean diff --git a/src/utils/slices/instances/add.ts b/src/utils/slices/instances/add.ts index b8944f8e..989b596b 100644 --- a/src/utils/slices/instances/add.ts +++ b/src/utils/slices/instances/add.ts @@ -52,7 +52,7 @@ const addInstance = createAsyncThunk( headers: { Authorization: `Bearer ${token}` } }) - const { body: filters } = await apiGeneral({ + const { body: filters } = await apiGeneral[]>({ method: 'get', domain, url: `api/v1/filters`, diff --git a/src/utils/slices/instances/updateFilters.ts b/src/utils/slices/instances/updateFilters.ts index 5c722ac8..7b23192e 100644 --- a/src/utils/slices/instances/updateFilters.ts +++ b/src/utils/slices/instances/updateFilters.ts @@ -3,8 +3,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit' export const updateFilters = createAsyncThunk( 'instances/updateFilters', - async (): Promise => { - return apiInstance({ + async (): Promise[]> => { + return apiInstance[]>({ method: 'get', url: `filters` }).then(res => res.body)