feat: muted words

This commit is contained in:
codytseng 2026-01-08 22:53:11 +08:00
parent 3c74c8c5db
commit 603bd35b4a
25 changed files with 282 additions and 87 deletions

View file

@ -80,7 +80,7 @@ const NoteList = forwardRef<
const { startLogin } = useNostr() const { startLogin } = useNostr()
const { isSpammer, meetsMinTrustScore } = useUserTrust() const { isSpammer, meetsMinTrustScore } = useUserTrust()
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideContentMentioningMutedUsers, mutedWords } = useContentPolicy()
const { isEventDeleted } = useDeletedEvent() const { isEventDeleted } = useDeletedEvent()
const [storedEvents, setStoredEvents] = useState<Event[]>([]) const [storedEvents, setStoredEvents] = useState<Event[]>([])
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
@ -131,10 +131,18 @@ const NoteList = forwardRef<
if (filterFn && !filterFn(evt)) { if (filterFn && !filterFn(evt)) {
return true return true
} }
if (mutedWords.length > 0) {
const contentLower = evt.content.toLowerCase()
for (const word of mutedWords) {
if (contentLower.includes(word)) {
return true
}
}
}
return false return false
}, },
[mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn] [mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn, mutedWords]
) )
useEffect(() => { useEffect(() => {

View file

@ -43,6 +43,7 @@ export const StorageKey = {
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy', NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
MIN_TRUST_SCORE: 'minTrustScore', MIN_TRUST_SCORE: 'minTrustScore',
DEFAULT_RELAY_URLS: 'defaultRelayUrls', DEFAULT_RELAY_URLS: 'defaultRelayUrls',
MUTED_WORDS: 'mutedWords',
ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated

View file

@ -648,16 +648,20 @@ export default {
'trust-filter.quick-presets': 'إعدادات سريعة', 'trust-filter.quick-presets': 'إعدادات سريعة',
'trust-filter.show-all-content': 'إظهار جميع المحتويات', 'trust-filter.show-all-content': 'إظهار جميع المحتويات',
'trust-filter.only-show-wot': 'إظهار شبكة الثقة الخاصة بك فقط (المتابَعون + متابَعوهم)', 'trust-filter.only-show-wot': 'إظهار شبكة الثقة الخاصة بك فقط (المتابَعون + متابَعوهم)',
'trust-filter.hide-bottom-percent': 'trust-filter.hide-bottom-percent': 'تصفية أدنى {{score}}٪ من المستخدمين حسب تصنيف الثقة',
'تصفية أدنى {{score}}٪ من المستخدمين حسب تصنيف الثقة', 'trust-filter.trust-score-description':
'trust-filter.trust-score-description': 'محسوبة بناءً على سمعة المستخدم والنسبة المئوية للشبكة الاجتماعية', 'محسوبة بناءً على سمعة المستخدم والنسبة المئوية للشبكة الاجتماعية',
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا', 'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا',
'Disable live feed': 'تعطيل التغذية المباشرة', 'Disable live feed': 'تعطيل التغذية المباشرة',
'Enable live feed': 'تفعيل التغذية المباشرة', 'Enable live feed': 'تفعيل التغذية المباشرة',
'Default relays': 'المرحلات الافتراضية', 'Default relays': 'المرحلات الافتراضية',
'Reset to default': 'إعادة تعيين إلى الافتراضي', 'Reset to default': 'إعادة تعيين إلى الافتراضي',
'Default relays description': 'تُستخدم للاستعلام عن تكوينات المرحلات للمستخدمين الآخرين وكبديل احتياطي عندما لا يكون لدى المستخدمين مرحلات مكوّنة.', 'Default relays description':
'Default relays warning': 'تحذير: يرجى عدم تعديل هذه الإعدادات بشكل عشوائي، فقد يؤثر ذلك على تجربتك الأساسية.', 'تُستخدم للاستعلام عن تكوينات المرحلات للمستخدمين الآخرين وكبديل احتياطي عندما لا يكون لدى المستخدمين مرحلات مكوّنة.',
'Invalid relay URL': 'عنوان URL للمرحل غير صالح' 'Default relays warning':
'تحذير: يرجى عدم تعديل هذه الإعدادات بشكل عشوائي، فقد يؤثر ذلك على تجربتك الأساسية.',
'Invalid relay URL': 'عنوان URL للمرحل غير صالح',
'Muted words': 'الكلمات المحظورة',
'Add muted word': 'إضافة كلمة محظورة'
} }
} }

View file

@ -678,8 +678,12 @@ export default {
'Enable live feed': 'Live-Feed aktivieren', 'Enable live feed': 'Live-Feed aktivieren',
'Default relays': 'Standard-Relays', 'Default relays': 'Standard-Relays',
'Reset to default': 'Auf Standard zurücksetzen', 'Reset to default': 'Auf Standard zurücksetzen',
'Default relays description': 'Werden verwendet, um die Relay-Konfigurationen anderer Benutzer abzufragen und als Fallback, wenn Benutzer keine Relays konfiguriert haben.', 'Default relays description':
'Default relays warning': 'Warnung: Ändern Sie diese Einstellungen nicht leichtfertig, da dies Ihre grundlegende Erfahrung beeinträchtigen kann.', 'Werden verwendet, um die Relay-Konfigurationen anderer Benutzer abzufragen und als Fallback, wenn Benutzer keine Relays konfiguriert haben.',
'Invalid relay URL': 'Ungültige Relay-URL' 'Default relays warning':
'Warnung: Ändern Sie diese Einstellungen nicht leichtfertig, da dies Ihre grundlegende Erfahrung beeinträchtigen kann.',
'Invalid relay URL': 'Ungültige Relay-URL',
'Muted words': 'Stummgeschaltete Wörter',
'Add muted word': 'Stummgeschaltetes Wort hinzufügen'
} }
} }

View file

@ -653,16 +653,20 @@ export default {
'trust-filter.quick-presets': 'Quick presets', 'trust-filter.quick-presets': 'Quick presets',
'trust-filter.show-all-content': 'Show all content', 'trust-filter.show-all-content': 'Show all content',
'trust-filter.only-show-wot': 'Only show your Web of Trust (follows + their follows)', 'trust-filter.only-show-wot': 'Only show your Web of Trust (follows + their follows)',
'trust-filter.hide-bottom-percent': 'trust-filter.hide-bottom-percent': 'Filter out bottom {{score}}% of users by trust rank',
'Filter out bottom {{score}}% of users by trust rank', 'trust-filter.trust-score-description':
'trust-filter.trust-score-description': 'Calculated based on user reputation and social network percentile', 'Calculated based on user reputation and social network percentile',
'Auto-load profile pictures': 'Auto-load profile pictures', 'Auto-load profile pictures': 'Auto-load profile pictures',
'Disable live feed': 'Disable live feed', 'Disable live feed': 'Disable live feed',
'Enable live feed': 'Enable live feed', 'Enable live feed': 'Enable live feed',
'Default relays': 'Default relays', 'Default relays': 'Default relays',
'Reset to default': 'Reset to default', 'Reset to default': 'Reset to default',
'Default relays description': 'Used to query other users\' relay configurations and as a fallback when users have no relays configured.', 'Default relays description':
'Default relays warning': 'Warning: Please do not modify these settings casually, as it may affect your basic experience.', "Used to query other users' relay configurations and as a fallback when users have no relays configured.",
'Invalid relay URL': 'Invalid relay URL' 'Default relays warning':
'Warning: Please do not modify these settings casually, as it may affect your basic experience.',
'Invalid relay URL': 'Invalid relay URL',
'Muted words': 'Muted words',
'Add muted word': 'Add muted word'
} }
} }

View file

@ -672,8 +672,12 @@ export default {
'Enable live feed': 'Activar feed en vivo', 'Enable live feed': 'Activar feed en vivo',
'Default relays': 'Relés predeterminados', 'Default relays': 'Relés predeterminados',
'Reset to default': 'Restablecer valores predeterminados', 'Reset to default': 'Restablecer valores predeterminados',
'Default relays description': 'Se utilizan para consultar las configuraciones de relés de otros usuarios y como respaldo cuando los usuarios no tienen relés configurados.', 'Default relays description':
'Default relays warning': 'Advertencia: No modifiques estas configuraciones a la ligera, ya que puede afectar tu experiencia básica.', 'Se utilizan para consultar las configuraciones de relés de otros usuarios y como respaldo cuando los usuarios no tienen relés configurados.',
'Invalid relay URL': 'URL de relé no válida' 'Default relays warning':
'Advertencia: No modifiques estas configuraciones a la ligera, ya que puede afectar tu experiencia básica.',
'Invalid relay URL': 'URL de relé no válida',
'Muted words': 'Palabras silenciadas',
'Add muted word': 'Agregar palabra silenciada'
} }
} }

View file

@ -661,15 +661,18 @@ export default {
'فقط شبکه اعتماد شما را نشان دهید (دنبال‌شوندگان + دنبال‌شوندگان آنها)', 'فقط شبکه اعتماد شما را نشان دهید (دنبال‌شوندگان + دنبال‌شوندگان آنها)',
'trust-filter.hide-bottom-percent': 'trust-filter.hide-bottom-percent':
'فیلتر کردن {{score}}٪ پایین‌ترین کاربران بر اساس رتبه اعتماد', 'فیلتر کردن {{score}}٪ پایین‌ترین کاربران بر اساس رتبه اعتماد',
'trust-filter.trust-score-description': 'trust-filter.trust-score-description': 'بر اساس شهرت کاربر و صدک شبکه اجتماعی محاسبه می‌شود',
'بر اساس شهرت کاربر و صدک شبکه اجتماعی محاسبه می‌شود',
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل', 'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل',
'Disable live feed': 'غیرفعال کردن فید زنده', 'Disable live feed': 'غیرفعال کردن فید زنده',
'Enable live feed': 'فعال کردن فید زنده', 'Enable live feed': 'فعال کردن فید زنده',
'Default relays': 'رله‌های پیش‌فرض', 'Default relays': 'رله‌های پیش‌فرض',
'Reset to default': 'بازنشانی به پیش‌فرض', 'Reset to default': 'بازنشانی به پیش‌فرض',
'Default relays description': 'برای پرس‌وجو از پیکربندی‌های رله کاربران دیگر و به عنوان جایگزین زمانی که کاربران رله پیکربندی نکرده‌اند استفاده می‌شود.', 'Default relays description':
'Default relays warning': 'هشدار: لطفاً این تنظیمات را به صورت تصادفی تغییر ندهید، ممکن است بر تجربه اولیه شما تأثیر بگذارد.', 'برای پرس‌وجو از پیکربندی‌های رله کاربران دیگر و به عنوان جایگزین زمانی که کاربران رله پیکربندی نکرده‌اند استفاده می‌شود.',
'Invalid relay URL': 'آدرس URL رله نامعتبر است' 'Default relays warning':
'هشدار: لطفاً این تنظیمات را به صورت تصادفی تغییر ندهید، ممکن است بر تجربه اولیه شما تأثیر بگذارد.',
'Invalid relay URL': 'آدرس URL رله نامعتبر است',
'Muted words': 'کلمات بی‌صدا شده',
'Add muted word': 'افزودن کلمه بی‌صدا'
} }
} }

View file

@ -676,8 +676,12 @@ export default {
'Enable live feed': 'Activer le flux en direct', 'Enable live feed': 'Activer le flux en direct',
'Default relays': 'Relais par défaut', 'Default relays': 'Relais par défaut',
'Reset to default': 'Réinitialiser par défaut', 'Reset to default': 'Réinitialiser par défaut',
'Default relays description': 'Utilisés pour interroger les configurations de relais d\'autres utilisateurs et comme solution de secours lorsque les utilisateurs n\'ont pas de relais configurés.', 'Default relays description':
'Default relays warning': 'Attention : Ne modifiez pas ces paramètres à la légère, car cela pourrait affecter votre expérience de base.', "Utilisés pour interroger les configurations de relais d'autres utilisateurs et comme solution de secours lorsque les utilisateurs n'ont pas de relais configurés.",
'Invalid relay URL': 'URL de relais non valide' 'Default relays warning':
'Attention : Ne modifiez pas ces paramètres à la légère, car cela pourrait affecter votre expérience de base.',
'Invalid relay URL': 'URL de relais non valide',
'Muted words': 'Mots masqués',
'Add muted word': 'Ajouter un mot masqué'
} }
} }

View file

@ -668,8 +668,12 @@ export default {
'Enable live feed': 'लाइव फ़ीड सक्षम करें', 'Enable live feed': 'लाइव फ़ीड सक्षम करें',
'Default relays': 'डिफ़ॉल्ट रिले', 'Default relays': 'डिफ़ॉल्ट रिले',
'Reset to default': 'डिफ़ॉल्ट पर रीसेट करें', 'Reset to default': 'डिफ़ॉल्ट पर रीसेट करें',
'Default relays description': 'अन्य उपयोगकर्ताओं के रिले कॉन्फ़िगरेशन की जांच करने के लिए उपयोग किया जाता है और जब उपयोगकर्ताओं के पास रिले कॉन्फ़िगर नहीं है तो फ़ॉलबैक के रूप में।', 'Default relays description':
'Default relays warning': 'चेतावनी: कृपया इन सेटिंग्स को बेतरतीब ढंग से संशोधित न करें, क्योंकि यह आपके बुनियादी अनुभव को प्रभावित कर सकता है।', 'अन्य उपयोगकर्ताओं के रिले कॉन्फ़िगरेशन की जांच करने के लिए उपयोग किया जाता है और जब उपयोगकर्ताओं के पास रिले कॉन्फ़िगर नहीं है तो फ़ॉलबैक के रूप में।',
'Invalid relay URL': 'अमान्य रिले URL' 'Default relays warning':
'चेतावनी: कृपया इन सेटिंग्स को बेतरतीब ढंग से संशोधित न करें, क्योंकि यह आपके बुनियादी अनुभव को प्रभावित कर सकता है।',
'Invalid relay URL': 'अमान्य रिले URL',
'Muted words': 'म्यूट किए गए शब्द',
'Add muted word': 'म्यूट शब्द जोड़ें'
} }
} }

View file

@ -661,8 +661,12 @@ export default {
'Enable live feed': 'Élő hírfolyam engedélyezése', 'Enable live feed': 'Élő hírfolyam engedélyezése',
'Default relays': 'Alapértelmezett továbbítók', 'Default relays': 'Alapértelmezett továbbítók',
'Reset to default': 'Visszaállítás alapértelmezettre', 'Reset to default': 'Visszaállítás alapértelmezettre',
'Default relays description': 'Más felhasználók továbbító konfigurációinak lekérdezésére használatos, és tartalékként szolgál, ha a felhasználóknak nincsenek továbbítóik beállítva.', 'Default relays description':
'Default relays warning': 'Figyelmeztetés: Ne módosítsa ezeket a beállításokat meggondolatlanul, mert ez befolyásolhatja az alapvető élményt.', 'Más felhasználók továbbító konfigurációinak lekérdezésére használatos, és tartalékként szolgál, ha a felhasználóknak nincsenek továbbítóik beállítva.',
'Invalid relay URL': 'Érvénytelen továbbító URL' 'Default relays warning':
'Figyelmeztetés: Ne módosítsa ezeket a beállításokat meggondolatlanul, mert ez befolyásolhatja az alapvető élményt.',
'Invalid relay URL': 'Érvénytelen továbbító URL',
'Muted words': 'Némított szavak',
'Add muted word': 'Némított szó hozzáadása'
} }
} }

View file

@ -672,8 +672,12 @@ export default {
'Enable live feed': 'Attiva feed live', 'Enable live feed': 'Attiva feed live',
'Default relays': 'Relay predefiniti', 'Default relays': 'Relay predefiniti',
'Reset to default': 'Ripristina predefiniti', 'Reset to default': 'Ripristina predefiniti',
'Default relays description': 'Utilizzati per interrogare le configurazioni dei relay di altri utenti e come fallback quando gli utenti non hanno relay configurati.', 'Default relays description':
'Default relays warning': 'Attenzione: Non modificare queste impostazioni alla leggera, potrebbe influire sull\'esperienza di base.', 'Utilizzati per interrogare le configurazioni dei relay di altri utenti e come fallback quando gli utenti non hanno relay configurati.',
'Invalid relay URL': 'URL relay non valido' 'Default relays warning':
"Attenzione: Non modificare queste impostazioni alla leggera, potrebbe influire sull'esperienza di base.",
'Invalid relay URL': 'URL relay non valido',
'Muted words': 'Parole silenziate',
'Add muted word': 'Aggiungi parola silenziata'
} }
} }

View file

@ -666,8 +666,12 @@ export default {
'Enable live feed': 'ライブフィードを有効にする', 'Enable live feed': 'ライブフィードを有効にする',
'Default relays': 'デフォルトリレー', 'Default relays': 'デフォルトリレー',
'Reset to default': 'デフォルトにリセット', 'Reset to default': 'デフォルトにリセット',
'Default relays description': '他のユーザーのリレー設定を照会するために使用され、ユーザーがリレーを設定していない場合のフォールバックとして機能します。', 'Default relays description':
'Default relays warning': '警告:これらの設定を無闇に変更しないでください。基本的な体験に影響を与える可能性があります。', '他のユーザーのリレー設定を照会するために使用され、ユーザーがリレーを設定していない場合のフォールバックとして機能します。',
'Invalid relay URL': '無効なリレーURL' 'Default relays warning':
'警告:これらの設定を無闇に変更しないでください。基本的な体験に影響を与える可能性があります。',
'Invalid relay URL': '無効なリレーURL',
'Muted words': 'ミュートワード',
'Add muted word': 'ミュートワードを追加'
} }
} }

View file

@ -655,14 +655,19 @@ export default {
'trust-filter.show-all-content': '모든 콘텐츠 표시', 'trust-filter.show-all-content': '모든 콘텐츠 표시',
'trust-filter.only-show-wot': '신뢰 네트워크만 표시 (팔로우 + 팔로우의 팔로우)', 'trust-filter.only-show-wot': '신뢰 네트워크만 표시 (팔로우 + 팔로우의 팔로우)',
'trust-filter.hide-bottom-percent': '신뢰도 하위 {{score}}% 사용자 필터링', 'trust-filter.hide-bottom-percent': '신뢰도 하위 {{score}}% 사용자 필터링',
'trust-filter.trust-score-description': '사용자의 평판과 소셜 네트워크를 기반으로 신뢰도 백분위수 계산', 'trust-filter.trust-score-description':
'사용자의 평판과 소셜 네트워크를 기반으로 신뢰도 백분위수 계산',
'Auto-load profile pictures': '프로필 사진 자동 로드', 'Auto-load profile pictures': '프로필 사진 자동 로드',
'Disable live feed': '라이브 피드 비활성화', 'Disable live feed': '라이브 피드 비활성화',
'Enable live feed': '라이브 피드 활성화', 'Enable live feed': '라이브 피드 활성화',
'Default relays': '기본 릴레이', 'Default relays': '기본 릴레이',
'Reset to default': '기본값으로 재설정', 'Reset to default': '기본값으로 재설정',
'Default relays description': '다른 사용자의 릴레이 구성을 조회하는 데 사용되며, 사용자가 릴레이를 구성하지 않은 경우 대체 수단으로 사용됩니다.', 'Default relays description':
'Default relays warning': '경고: 이러한 설정을 임의로 수정하지 마십시오. 기본 경험에 영향을 줄 수 있습니다.', '다른 사용자의 릴레이 구성을 조회하는 데 사용되며, 사용자가 릴레이를 구성하지 않은 경우 대체 수단으로 사용됩니다.',
'Invalid relay URL': '유효하지 않은 릴레이 URL' 'Default relays warning':
'경고: 이러한 설정을 임의로 수정하지 마십시오. 기본 경험에 영향을 줄 수 있습니다.',
'Invalid relay URL': '유효하지 않은 릴레이 URL',
'Muted words': '차단 단어',
'Add muted word': '차단 단어 추가'
} }
} }

View file

@ -673,8 +673,12 @@ export default {
'Enable live feed': 'Włącz kanał na żywo', 'Enable live feed': 'Włącz kanał na żywo',
'Default relays': 'Domyślne przekaźniki', 'Default relays': 'Domyślne przekaźniki',
'Reset to default': 'Przywróć domyślne', 'Reset to default': 'Przywróć domyślne',
'Default relays description': 'Używane do odpytywania konfiguracji przekaźników innych użytkowników i jako rozwiązanie awaryjne, gdy użytkownicy nie mają skonfigurowanych przekaźników.', 'Default relays description':
'Default relays warning': 'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.', 'Używane do odpytywania konfiguracji przekaźników innych użytkowników i jako rozwiązanie awaryjne, gdy użytkownicy nie mają skonfigurowanych przekaźników.',
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika' 'Default relays warning':
'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.',
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika',
'Muted words': 'Wyciszone słowa',
'Add muted word': 'Dodaj wyciszone słowo'
} }
} }

View file

@ -669,8 +669,12 @@ export default {
'Enable live feed': 'Ativar feed ao vivo', 'Enable live feed': 'Ativar feed ao vivo',
'Default relays': 'Relays padrão', 'Default relays': 'Relays padrão',
'Reset to default': 'Redefinir para padrão', 'Reset to default': 'Redefinir para padrão',
'Default relays description': 'Usados para consultar as configurações de relays de outros usuários e como alternativa quando os usuários não têm relays configurados.', 'Default relays description':
'Default relays warning': 'Aviso: Não modifique essas configurações casualmente, pois pode afetar sua experiência básica.', 'Usados para consultar as configurações de relays de outros usuários e como alternativa quando os usuários não têm relays configurados.',
'Invalid relay URL': 'URL de relay inválida' 'Default relays warning':
'Aviso: Não modifique essas configurações casualmente, pois pode afetar sua experiência básica.',
'Invalid relay URL': 'URL de relé inválida',
'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada'
} }
} }

View file

@ -672,8 +672,12 @@ export default {
'Enable live feed': 'Ativar feed ao vivo', 'Enable live feed': 'Ativar feed ao vivo',
'Default relays': 'Relays predefinidos', 'Default relays': 'Relays predefinidos',
'Reset to default': 'Repor predefinições', 'Reset to default': 'Repor predefinições',
'Default relays description': 'Utilizados para consultar as configurações de relays de outros utilizadores e como alternativa quando os utilizadores não têm relays configurados.', 'Default relays description':
'Default relays warning': 'Aviso: Não modifique estas configurações casualmente, pois pode afetar a sua experiência básica.', 'Utilizados para consultar as configurações de relays de outros utilizadores e como alternativa quando os utilizadores não têm relays configurados.',
'Invalid relay URL': 'URL de relay inválido' 'Default relays warning':
'Aviso: Não modifique estas configurações casualmente, pois pode afetar a sua experiência básica.',
'Invalid relay URL': 'URL de relay inválido',
'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada'
} }
} }

View file

@ -672,8 +672,12 @@ export default {
'Enable live feed': 'Включить прямую трансляцию', 'Enable live feed': 'Включить прямую трансляцию',
'Default relays': 'Реле по умолчанию', 'Default relays': 'Реле по умолчанию',
'Reset to default': 'Сбросить по умолчанию', 'Reset to default': 'Сбросить по умолчанию',
'Default relays description': 'Используются для запроса конфигураций реле других пользователей и в качестве резервного варианта, когда у пользователей не настроены реле.', 'Default relays description':
'Default relays warning': 'Предупреждение: Не изменяйте эти настройки без необходимости, это может повлиять на базовый опыт использования.', 'Используются для запроса конфигураций реле других пользователей и в качестве резервного варианта, когда у пользователей не настроены реле.',
'Invalid relay URL': 'Неверный URL реле' 'Default relays warning':
'Предупреждение: Не изменяйте эти настройки без необходимости, это может повлиять на базовый опыт использования.',
'Invalid relay URL': 'Неверный URL реле',
'Muted words': 'Заблокированные слова',
'Add muted word': 'Добавить заблокированное слово'
} }
} }

View file

@ -649,8 +649,7 @@ export default {
'trust-filter.show-all-content': 'แสดงเนื้อหาทั้งหมด', 'trust-filter.show-all-content': 'แสดงเนื้อหาทั้งหมด',
'trust-filter.only-show-wot': 'trust-filter.only-show-wot':
'แสดงเฉพาะเครือข่ายความไว้วางใจของคุณ (ผู้ติดตาม + ผู้ติดตามของพวกเขา)', 'แสดงเฉพาะเครือข่ายความไว้วางใจของคุณ (ผู้ติดตาม + ผู้ติดตามของพวกเขา)',
'trust-filter.hide-bottom-percent': 'trust-filter.hide-bottom-percent': 'กรอง {{score}}% ล่างสุดของผู้ใช้ตามอันดับความไว้วางใจ',
'กรอง {{score}}% ล่างสุดของผู้ใช้ตามอันดับความไว้วางใจ',
'trust-filter.trust-score-description': 'trust-filter.trust-score-description':
'คำนวณจากชื่อเสียงของผู้ใช้และเปอร์เซ็นไทล์ของเครือข่ายสังคม', 'คำนวณจากชื่อเสียงของผู้ใช้และเปอร์เซ็นไทล์ของเครือข่ายสังคม',
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ', 'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ',
@ -658,8 +657,12 @@ export default {
'Enable live feed': 'เปิดฟีดสด', 'Enable live feed': 'เปิดฟีดสด',
'Default relays': 'รีเลย์เริ่มต้น', 'Default relays': 'รีเลย์เริ่มต้น',
'Reset to default': 'รีเซ็ตเป็นค่าเริ่มต้น', 'Reset to default': 'รีเซ็ตเป็นค่าเริ่มต้น',
'Default relays description': 'ใช้สำหรับสอบถามการกำหนดค่ารีเลย์ของผู้ใช้อื่นและเป็นทางเลือกสำรองเมื่อผู้ใช้ไม่ได้กำหนดค่ารีเลย์', 'Default relays description':
'Default relays warning': 'คำเตือน: กรุณาอย่าแก้ไขการตั้งค่าเหล่านี้โดยไม่ระมัดระวัง เพราะอาจส่งผลต่อประสบการณ์พื้นฐานของคุณ', 'ใช้สำหรับสอบถามการกำหนดค่ารีเลย์ของผู้ใช้อื่นและเป็นทางเลือกสำรองเมื่อผู้ใช้ไม่ได้กำหนดค่ารีเลย์',
'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง' 'Default relays warning':
'คำเตือน: กรุณาอย่าแก้ไขการตั้งค่าเหล่านี้โดยไม่ระมัดระวัง เพราะอาจส่งผลต่อประสบการณ์พื้นฐานของคุณ',
'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง',
'Muted words': 'คำที่ถูกปิดเสียง',
'Add muted word': 'เพิ่มคำที่ถูกปิดเสียง'
} }
} }

View file

@ -641,8 +641,11 @@ export default {
'Enable live feed': '啟用即時推送', 'Enable live feed': '啟用即時推送',
'Default relays': '預設中繼', 'Default relays': '預設中繼',
'Reset to default': '重置為預設', 'Reset to default': '重置為預設',
'Default relays description': '用於查詢其他使用者的中繼配置,並在使用者沒有配置中繼時作為回退策略。', 'Default relays description':
'用於查詢其他使用者的中繼配置,並在使用者沒有配置中繼時作為回退策略。',
'Default relays warning': '警告:請不要隨意修改這些設定,可能會影響基礎體驗。', 'Default relays warning': '警告:請不要隨意修改這些設定,可能會影響基礎體驗。',
'Invalid relay URL': '無效的中繼地址' 'Invalid relay URL': '無效的中繼地址',
'Muted words': '屏蔽詞',
'Add muted word': '添加屏蔽詞'
} }
} }

View file

@ -646,8 +646,11 @@ export default {
'Enable live feed': '启用实时推送', 'Enable live feed': '启用实时推送',
'Default relays': '默认中继', 'Default relays': '默认中继',
'Reset to default': '重置为默认', 'Reset to default': '重置为默认',
'Default relays description': '用于查询其他用户的中继配置,并在用户没有配置中继时作为回退策略。', 'Default relays description':
'Default relays warning': '警告:请不要随意修改这些设置,可能会影响基础体验。', '用于查询其他用户的中继配置,以及当用户没有配置中继时的备用选项。',
'Invalid relay URL': '无效的中继地址' 'Default relays warning': '警告:请不要随意修改这些设置,可能会影响您的基本体验。',
'Invalid relay URL': '无效的中继地址',
'Muted words': '屏蔽词',
'Add muted word': '添加屏蔽词'
} }
} }

View file

@ -0,0 +1,78 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { Plus, X } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import SettingItem from './SettingItem'
export default function MutedWords() {
const { t } = useTranslation()
const { mutedWords, setMutedWords } = useContentPolicy()
const [newMutedWord, setNewMutedWord] = useState('')
const handleAddMutedWord = () => {
const word = newMutedWord.trim().toLowerCase()
if (word && !mutedWords.includes(word)) {
setMutedWords([...mutedWords, word])
setNewMutedWord('')
}
}
const handleRemoveMutedWord = (word: string) => {
setMutedWords(mutedWords.filter((w) => w !== word))
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddMutedWord()
}
}
return (
<SettingItem className="flex-col items-start gap-2">
<Label className="text-base font-normal">{t('Muted words')}</Label>
<div className="w-full space-y-2">
<div className="flex gap-2">
<Input
placeholder={t('Add muted word')}
value={newMutedWord}
onChange={(e) => setNewMutedWord(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1"
/>
<Button
variant="ghost"
size="icon"
onClick={handleAddMutedWord}
disabled={!newMutedWord.trim() || mutedWords.includes(newMutedWord.trim())}
>
<Plus />
</Button>
</div>
{mutedWords.length > 0 && (
<div className="flex flex-wrap gap-2">
{mutedWords.map((word) => (
<div
key={word}
className="flex items-center gap-1 bg-muted px-2 py-1 rounded-md text-sm"
>
<span>{word}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-transparent"
onClick={() => handleRemoveMutedWord(word)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
</SettingItem>
)
}

View file

@ -0,0 +1,21 @@
import { cn } from '@/lib/utils'
import { forwardRef, HTMLProps } from 'react'
const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
({ children, className, ...props }, ref) => {
return (
<div
className={cn(
'flex justify-between select-none items-center px-4 min-h-9 [&_svg]:size-4 [&_svg]:shrink-0',
className
)}
{...props}
ref={ref}
>
{children}
</div>
)
}
)
SettingItem.displayName = 'SettingItem'
export default SettingItem

View file

@ -11,14 +11,16 @@ import {
} from '@/constants' } from '@/constants'
import { LocalizedLanguageNames, TLanguage } from '@/i18n' import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn, isSupportCheckConnectionType } from '@/lib/utils' import { isSupportCheckConnectionType } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy, TProfilePictureAutoLoadPolicy } from '@/types' import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy, TProfilePictureAutoLoadPolicy } from '@/types'
import { SelectValue } from '@radix-ui/react-select' import { SelectValue } from '@radix-ui/react-select'
import { RotateCcw } from 'lucide-react' import { RotateCcw } from 'lucide-react'
import { forwardRef, HTMLProps, useState } from 'react' import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MutedWords from './MutedWords'
import SettingItem from './SettingItem'
const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
@ -188,27 +190,10 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
</div> </div>
</SettingItem> </SettingItem>
)} )}
<MutedWords />
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>
) )
}) })
GeneralSettingsPage.displayName = 'GeneralSettingsPage' GeneralSettingsPage.displayName = 'GeneralSettingsPage'
export default GeneralSettingsPage export default GeneralSettingsPage
const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
({ children, className, ...props }, ref) => {
return (
<div
className={cn(
'flex justify-between select-none items-center px-4 min-h-9 [&_svg]:size-4 [&_svg]:shrink-0',
className
)}
{...props}
ref={ref}
>
{children}
</div>
)
}
)
SettingItem.displayName = 'SettingItem'

View file

@ -23,6 +23,9 @@ type TContentPolicyContext = {
faviconUrlTemplate: string faviconUrlTemplate: string
setFaviconUrlTemplate: (template: string) => void setFaviconUrlTemplate: (template: string) => void
mutedWords: string[]
setMutedWords: (words: string[]) => void
} }
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined) const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
@ -46,6 +49,7 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
storage.getProfilePictureAutoLoadPolicy() storage.getProfilePictureAutoLoadPolicy()
) )
const [faviconUrlTemplate, setFaviconUrlTemplate] = useState(storage.getFaviconUrlTemplate()) const [faviconUrlTemplate, setFaviconUrlTemplate] = useState(storage.getFaviconUrlTemplate())
const [mutedWords, setMutedWords] = useState(storage.getMutedWords())
const [connectionType, setConnectionType] = useState((navigator as any).connection?.type) const [connectionType, setConnectionType] = useState((navigator as any).connection?.type)
useEffect(() => { useEffect(() => {
@ -115,6 +119,11 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setFaviconUrlTemplate(template) setFaviconUrlTemplate(template)
} }
const updateMutedWords = (words: string[]) => {
storage.setMutedWords(words)
setMutedWords(words)
}
return ( return (
<ContentPolicyContext.Provider <ContentPolicyContext.Provider
value={{ value={{
@ -131,7 +140,9 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
profilePictureAutoLoadPolicy, profilePictureAutoLoadPolicy,
setProfilePictureAutoLoadPolicy: updateProfilePictureAutoLoadPolicy, setProfilePictureAutoLoadPolicy: updateProfilePictureAutoLoadPolicy,
faviconUrlTemplate, faviconUrlTemplate,
setFaviconUrlTemplate: updateFaviconUrlTemplate setFaviconUrlTemplate: updateFaviconUrlTemplate,
mutedWords,
setMutedWords: updateMutedWords
}} }}
> >
{children} {children}

View file

@ -66,6 +66,7 @@ class LocalStorageService {
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40 private minTrustScore: number = 40
private defaultRelayUrls: string[] = BIG_RELAY_URLS private defaultRelayUrls: string[] = BIG_RELAY_URLS
private mutedWords: string[] = []
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@ -293,6 +294,18 @@ class LocalStorageService {
} }
} }
const mutedWordsStr = window.localStorage.getItem(StorageKey.MUTED_WORDS)
if (mutedWordsStr) {
try {
const words = JSON.parse(mutedWordsStr)
if (Array.isArray(words) && words.every((word) => typeof word === 'string')) {
this.mutedWords = words
}
} catch {
// Invalid JSON, use default
}
}
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS) window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@ -640,6 +653,15 @@ class LocalStorageService {
this.defaultRelayUrls = urls this.defaultRelayUrls = urls
window.localStorage.setItem(StorageKey.DEFAULT_RELAY_URLS, JSON.stringify(urls)) window.localStorage.setItem(StorageKey.DEFAULT_RELAY_URLS, JSON.stringify(urls))
} }
getMutedWords() {
return this.mutedWords
}
setMutedWords(words: string[]) {
this.mutedWords = words
window.localStorage.setItem(StorageKey.MUTED_WORDS, JSON.stringify(this.mutedWords))
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()