diff --git a/.changeset/gold-shoes-type.md b/.changeset/gold-shoes-type.md new file mode 100644 index 000000000..708b832e1 --- /dev/null +++ b/.changeset/gold-shoes-type.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Small fix for url RegExp diff --git a/.changeset/unlucky-cooks-help.md b/.changeset/unlucky-cooks-help.md new file mode 100644 index 000000000..54e6faac3 --- /dev/null +++ b/.changeset/unlucky-cooks-help.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add Muted words option in display settings diff --git a/src/helpers/regexp.ts b/src/helpers/regexp.ts index dfa02f27a..0f0dbbd34 100644 --- a/src/helpers/regexp.ts +++ b/src/helpers/regexp.ts @@ -2,7 +2,7 @@ export const getMatchNostrLink = () => /(nostr:|@)?((npub|note|nprofile|nevent|nrelay|naddr)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/gi; export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu; export const getMatchLink = () => - /https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:]*)/gu; + /https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:!]*)/gu; export const getMatchEmoji = () => /:([a-zA-Z0-9_]+):/gi; // read more https://www.regular-expressions.info/unicode.html#category diff --git a/src/hooks/use-app-settings.ts b/src/hooks/use-app-settings.ts index 835ece9dd..17887e82a 100644 --- a/src/hooks/use-app-settings.ts +++ b/src/hooks/use-app-settings.ts @@ -1,7 +1,8 @@ import { useCallback } from "react"; +import { useToast } from "@chakra-ui/react"; + import appSettings, { replaceSettings } from "../services/settings/app-settings"; import useSubject from "./use-subject"; -import { useToast } from "@chakra-ui/react"; import { AppSettings } from "../services/settings/migrations"; export default function useAppSettings() { diff --git a/src/hooks/use-client-side-mute-filter.ts b/src/hooks/use-client-side-mute-filter.ts new file mode 100644 index 000000000..2b7934088 --- /dev/null +++ b/src/hooks/use-client-side-mute-filter.ts @@ -0,0 +1,20 @@ +import { useCallback } from "react"; + +import { useCurrentAccount } from "./use-current-account"; +import useWordMuteFilter from "./use-mute-word-filter"; +import useUserMuteFilter from "./use-user-mute-filter"; +import { NostrEvent } from "../types/nostr-event"; + +export default function useClientSideMuteFilter() { + const account = useCurrentAccount(); + + const wordMuteFilter = useWordMuteFilter(); + const mustListFilter = useUserMuteFilter(account?.pubkey); + + return useCallback( + (event: NostrEvent) => { + return wordMuteFilter(event) || mustListFilter(event); + }, + [wordMuteFilter, mustListFilter], + ); +} diff --git a/src/hooks/use-mute-word-filter.ts b/src/hooks/use-mute-word-filter.ts new file mode 100644 index 000000000..b283973ea --- /dev/null +++ b/src/hooks/use-mute-word-filter.ts @@ -0,0 +1,25 @@ +import { useCallback, useMemo } from "react"; + +import { NostrEvent } from "../types/nostr-event"; +import useAppSettings from "./use-app-settings"; + +export default function useWordMuteFilter() { + const { mutedWords } = useAppSettings(); + + const regexp = useMemo(() => { + if (!mutedWords) return; + const words = mutedWords + .split(/[,\n]/g) + .map((s) => s.trim()) + .filter(Boolean); + return new RegExp(`(?:^|\\s|#)(?:${words.join("|")})(?:\\s|$)`, "i"); + }, [mutedWords]); + + return useCallback( + (event: NostrEvent) => { + if (!regexp) return false; + return event.content.match(regexp) !== null; + }, + [mutedWords], + ); +} diff --git a/src/providers/notification-timeline.tsx b/src/providers/notification-timeline.tsx index 460069178..e52bfc30e 100644 --- a/src/providers/notification-timeline.tsx +++ b/src/providers/notification-timeline.tsx @@ -5,8 +5,8 @@ import { useReadRelayUrls } from "../hooks/use-client-relays"; import { useCurrentAccount } from "../hooks/use-current-account"; import { TimelineLoader } from "../classes/timeline-loader"; import timelineCacheService from "../services/timeline-cache"; -import useUserMuteFilter from "../hooks/use-user-mute-filter"; import { NostrEvent } from "../types/nostr-event"; +import useClientSideMuteFilter from "../hooks/use-client-side-mute-filter"; type NotificationTimelineContextType = { timeline?: TimelineLoader; @@ -31,7 +31,7 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil : undefined; }, [account?.pubkey]); - const userMuteFilter = useUserMuteFilter(account?.pubkey); + const userMuteFilter = useClientSideMuteFilter(); const eventFilter = useCallback( (event: NostrEvent) => { if (userMuteFilter(event)) return false; diff --git a/src/services/settings/migrations.ts b/src/services/settings/migrations.ts index b69aa3c5e..c876e29c9 100644 --- a/src/services/settings/migrations.ts +++ b/src/services/settings/migrations.ts @@ -22,14 +22,21 @@ export type AppSettingsV0 = { redditRedirect?: string; youtubeRedirect?: string; }; +export type AppSettingsV1 = Omit & { + version: 1; + mutedWords?: string; +}; export function isV0(settings: { version: number }): settings is AppSettingsV0 { return settings.version === undefined || settings.version === 0; } +export function isV1(settings: { version: number }): settings is AppSettingsV1 { + return settings.version === 1; +} -export type AppSettings = AppSettingsV0; +export type AppSettings = AppSettingsV1; export const defaultSettings: AppSettings = { - version: 0, + version: 1, colorMode: "system", blurImages: true, autoShowMedia: true, @@ -49,8 +56,9 @@ export const defaultSettings: AppSettings = { youtubeRedirect: undefined, }; -export function upgradeSettings(settings: { version: number }) { - if (isV0(settings)) return settings; +export function upgradeSettings(settings: { version: number }): AppSettings | null { + if (isV0(settings)) return { ...settings, version: 1 }; + if (isV1(settings)) return settings; return null; } diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index 7921417fb..4169d9e05 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -12,12 +12,12 @@ import RelaySelectionButton from "../../components/relay-selection/relay-selecti import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider"; import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider"; import { NostrRequestFilter } from "../../types/nostr-query"; -import useUserMuteFilter from "../../hooks/use-user-mute-filter"; +import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter"; function HomePage() { const timelinePageEventFilter = useTimelinePageEventFilter(); const showReplies = useDisclosure(); - const muteFilter = useUserMuteFilter(); + const muteFilter = useClientSideMuteFilter(); const eventFilter = useCallback( (event: NostrEvent) => { if (muteFilter(event)) return false; diff --git a/src/views/note/components/thread-post.tsx b/src/views/note/components/thread-post.tsx index 60272b6b5..f55b06c5a 100644 --- a/src/views/note/components/thread-post.tsx +++ b/src/views/note/components/thread-post.tsx @@ -6,7 +6,7 @@ import { Note } from "../../../components/note"; import { countReplies, ThreadItem } from "../../../helpers/thread"; import { TrustProvider } from "../../../providers/trust"; import ReplyForm from "./reply-form"; -import useUserMuteFilter from "../../../hooks/use-user-mute-filter"; +import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter"; export type ThreadItemProps = { post: ThreadItem; @@ -19,18 +19,20 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) const toggle = () => setShowReplies((v) => !v); const showReplyForm = useDisclosure(); - const muteFilter = useUserMuteFilter(); + const muteFilter = useClientSideMuteFilter(); const [alwaysShow, setAlwaysShow] = useState(false); const numberOfReplies = countReplies(post); const isMuted = muteFilter(post.event); + if (isMuted && numberOfReplies === 0) return null; + return ( {isMuted && !alwaysShow ? ( - Muted user + Muted user or note diff --git a/src/views/settings/display-settings.tsx b/src/views/settings/display-settings.tsx index 35424bde2..823f690b2 100644 --- a/src/views/settings/display-settings.tsx +++ b/src/views/settings/display-settings.tsx @@ -12,6 +12,7 @@ import { FormHelperText, Input, Select, + Textarea, } from "@chakra-ui/react"; import { AppSettings } from "../../services/settings/migrations"; import { AppearanceIcon } from "../../components/icons"; @@ -75,6 +76,19 @@ export default function DisplaySettings() { Enabled: shows a warning for notes with NIP-36 Content Warning + + + Muted words + +