From 1913174e1c6e146cc13b91bcc8e8afb56bc9407d Mon Sep 17 00:00:00 2001 From: Tianrong Zhang Date: Thu, 11 Jun 2026 23:33:34 -0400 Subject: [PATCH] Add AyuLike settings screen with filter import/export/clear - New SettingsScreens.AyuLikeSettings screen wired into Settings.tsx - SettingsAyuLike: toggle hideSponsoredMessages, import JSON from AyuGram Desktop, export back to same format, clear all rules with confirmation - messageFilters util: importFromAyuGram / exportToAyuGram supporting AyuGram v2 export format (text=regex, dialogId, caseInsensitive, reversed) - Entry in SettingsMain under existing menu section --- src/components/left/settings/Settings.tsx | 5 + .../left/settings/SettingsAyuLike.tsx | 154 ++++++++++++++++++ src/components/left/settings/SettingsMain.tsx | 7 + src/types/index.ts | 1 + src/util/ayuLike/messageFilters.ts | 44 +++++ 5 files changed, 211 insertions(+) create mode 100644 src/components/left/settings/SettingsAyuLike.tsx diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 752a75895..4d14f4003 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -23,6 +23,7 @@ import SettingsCustomEmoji from './SettingsCustomEmoji'; import SettingsDataStorage from './SettingsDataStorage'; import SettingsDoNotTranslate from './SettingsDoNotTranslate'; import SettingsEditProfile from './SettingsEditProfile'; +import SettingsAyuLike from './SettingsAyuLike'; import SettingsExperimental from './SettingsExperimental'; import SettingsGeneral from './SettingsGeneral'; import SettingsGeneralBackground from './SettingsGeneralBackground'; @@ -307,6 +308,10 @@ const Settings: FC = ({ return ( ); + case SettingsScreens.AyuLikeSettings: + return ( + + ); case SettingsScreens.Experimental: return ( diff --git a/src/components/left/settings/SettingsAyuLike.tsx b/src/components/left/settings/SettingsAyuLike.tsx new file mode 100644 index 000000000..46e55c3dc --- /dev/null +++ b/src/components/left/settings/SettingsAyuLike.tsx @@ -0,0 +1,154 @@ +import { memo, useRef } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { MessageFilterRule } from '../../../global/types/sharedState'; +import { selectSharedSettings } from '../../../global/selectors/sharedState'; +import download from '../../../util/download'; +import { exportToAyuGram, importFromAyuGram } from '../../../util/ayuLike/messageFilters'; + +import useHistoryBack from '../../../hooks/useHistoryBack'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Island from '../../gili/layout/Island'; +import Checkbox from '../../ui/Checkbox'; +import ListItem from '../../ui/ListItem'; +import ConfirmDialog from '../../ui/ConfirmDialog'; +import useFlag from '../../../hooks/useFlag'; + +type OwnProps = { + isActive?: boolean; + onReset: () => void; +}; + +type StateProps = { + hideSponsoredMessages: boolean; + messageFilters: MessageFilterRule[]; +}; + +const EXPORT_FILENAME = 'ayu-filters.json'; + +const SettingsAyuLike = ({ + isActive, + hideSponsoredMessages, + messageFilters, + onReset, +}: OwnProps & StateProps) => { + const { setSharedSettingOption } = getActions(); + const fileInputRef = useRef(); + const [isClearConfirmOpen, openClearConfirm, closeClearConfirm] = useFlag(false); + + useHistoryBack({ isActive, onBack: onReset }); + + const handleExport = useLastCallback(() => { + const json = exportToAyuGram(messageFilters); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + download(url, EXPORT_FILENAME); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }); + + const handleImportClick = useLastCallback(() => { + fileInputRef.current?.click(); + }); + + const handleFileChange = useLastCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = () => { + try { + const imported = importFromAyuGram(JSON.parse(reader.result as string)); + setSharedSettingOption({ + ayuLike: { hideSponsoredMessages, messageFilters: imported }, + }); + } catch { + // Silently ignore malformed files — user will see no change + } + // Reset so the same file can be re-imported + e.target.value = ''; + }; + reader.readAsText(file); + }); + + const handleClearConfirmed = useLastCallback(() => { + setSharedSettingOption({ + ayuLike: { hideSponsoredMessages, messageFilters: [] }, + }); + closeClearConfirm(); + }); + + return ( +
+
+

+ Local message filters and ad settings. Changes take effect immediately and are stored locally. +

+
+ + + setSharedSettingOption({ + ayuLike: { hideSponsoredMessages: checked, messageFilters }, + })} + /> + + + + +
Import Filters from AyuGram
+ {messageFilters.length > 0 && ( + {messageFilters.length} rules + )} +
+ +
Export Filters
+
+ +
Clear All Filters
+
+
+ + + + +
+ ); +}; + +export default memo(withGlobal( + (global): Complete => { + const { ayuLike } = selectSharedSettings(global); + return { + hideSponsoredMessages: ayuLike.hideSponsoredMessages, + messageFilters: ayuLike.messageFilters, + }; + }, +)(SettingsAyuLike)); diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 301b161c1..63c8e4d47 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -167,6 +167,13 @@ const SettingsMain: FC = ({ > {lang('MenuStickers')} + openSettingsScreen({ screen: SettingsScreens.AyuLikeSettings })} + > + Message Filters + {canBuyPremium && ( diff --git a/src/types/index.ts b/src/types/index.ts index dca9a5d02..6084138a6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -273,6 +273,7 @@ export enum SettingsScreens { PasscodeTurnOff, PasscodeCongratulations, Experimental, + AyuLikeSettings, Stickers, QuickReaction, CustomEmoji, diff --git a/src/util/ayuLike/messageFilters.ts b/src/util/ayuLike/messageFilters.ts index 5b496ed29..dbeff25ec 100644 --- a/src/util/ayuLike/messageFilters.ts +++ b/src/util/ayuLike/messageFilters.ts @@ -31,6 +31,50 @@ function getMessageMediaTypes(message: ApiMessage): string[] { ].filter(Boolean) as string[]; } +// AyuGram Desktop export format (version 2) +interface AyuGramExport { + filters: { + id: string; + text: string; + enabled: boolean; + reversed: boolean; + caseInsensitive: boolean; + dialogId: string | null; + }[]; + exclusions?: unknown[]; + version?: number; +} + +export function importFromAyuGram(json: unknown): MessageFilterRule[] { + const data = json as AyuGramExport; + if (!Array.isArray(data?.filters)) throw new Error('Invalid AyuGram export format'); + + return data.filters.map((f) => ({ + id: f.id, + enabled: Boolean(f.enabled), + reversed: Boolean(f.reversed), + caseInsensitive: Boolean(f.caseInsensitive), + regex: f.text, + chatIds: f.dialogId ? [String(f.dialogId)] : undefined, + })); +} + +export function exportToAyuGram(rules: MessageFilterRule[]): string { + const data: AyuGramExport = { + exclusions: [], + filters: rules.map((r) => ({ + id: r.id, + text: r.regex ?? r.keyword ?? '', + enabled: r.enabled, + reversed: Boolean(r.reversed), + caseInsensitive: Boolean(r.caseInsensitive), + dialogId: r.chatIds?.[0] ?? null, + })), + version: 2, + }; + return JSON.stringify(data, null, 2); +} + export function shouldHideMessageByRules( message: ApiMessage, rules: MessageFilterRule[] = [],