mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 12:00:32 +02:00
Merge branch 'next' into nip-72
This commit is contained in:
5
.changeset/gold-shoes-type.md
Normal file
5
.changeset/gold-shoes-type.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Small fix for url RegExp
|
5
.changeset/unlucky-cooks-help.md
Normal file
5
.changeset/unlucky-cooks-help.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add Muted words option in display settings
|
@@ -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
|
||||
|
@@ -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() {
|
||||
|
20
src/hooks/use-client-side-mute-filter.ts
Normal file
20
src/hooks/use-client-side-mute-filter.ts
Normal file
@@ -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],
|
||||
);
|
||||
}
|
25
src/hooks/use-mute-word-filter.ts
Normal file
25
src/hooks/use-mute-word-filter.ts
Normal file
@@ -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],
|
||||
);
|
||||
}
|
@@ -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;
|
||||
|
@@ -22,14 +22,21 @@ export type AppSettingsV0 = {
|
||||
redditRedirect?: string;
|
||||
youtubeRedirect?: string;
|
||||
};
|
||||
export type AppSettingsV1 = Omit<AppSettingsV0, "version"> & {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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 (
|
||||
<Flex direction="column" gap="2">
|
||||
{isMuted && !alwaysShow ? (
|
||||
<Alert status="warning">
|
||||
<AlertIcon />
|
||||
Muted user
|
||||
Muted user or note
|
||||
<Button size="xs" ml="auto" onClick={() => setAlwaysShow(true)}>
|
||||
Show anyway
|
||||
</Button>
|
||||
|
@@ -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() {
|
||||
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="muted-words" mb="0">
|
||||
Muted words
|
||||
</FormLabel>
|
||||
<Textarea id="muted-words" {...register("mutedWords")} placeholder="Broccoli, Spinach, Artichoke..." />
|
||||
<FormHelperText>
|
||||
<span>
|
||||
Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)
|
||||
</span>
|
||||
<br />
|
||||
<span>Be careful its easy to hide all notes if you add common words.</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
@@ -15,14 +16,14 @@ import TimelineActionAndStatus from "../../components/timeline-page/timeline-act
|
||||
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||
import { NostrRequestFilter } from "../../types/nostr-query";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
|
||||
function StreamsPage() {
|
||||
useAppTitle("Streams");
|
||||
const relays = useRelaySelectionRelays();
|
||||
const userMuteFilter = useUserMuteFilter();
|
||||
const userMuteFilter = useClientSideMuteFilter();
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
|
@@ -6,21 +6,20 @@ import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../hel
|
||||
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
||||
import { useCurrentAccount } from "../../../../hooks/use-current-account";
|
||||
import useStreamGoal from "../../../../hooks/use-stream-goal";
|
||||
import { NostrQuery } from "../../../../types/nostr-query";
|
||||
import useUserMuteFilter from "../../../../hooks/use-user-mute-filter";
|
||||
import useClientSideMuteFilter from "../../../../hooks/use-client-side-mute-filter";
|
||||
|
||||
export default function useStreamChatTimeline(stream: ParsedStream) {
|
||||
const account = useCurrentAccount();
|
||||
const streamRelays = useRelaySelectionRelays();
|
||||
|
||||
const hostMuteFilter = useUserMuteFilter(stream.host);
|
||||
const userMuteFilter = useUserMuteFilter(account?.pubkey);
|
||||
const muteFilter = useClientSideMuteFilter();
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => !(hostMuteFilter(event) || userMuteFilter(event)),
|
||||
[hostMuteFilter, userMuteFilter],
|
||||
(event: NostrEvent) => !(hostMuteFilter(event) || muteFilter(event)),
|
||||
[hostMuteFilter, muteFilter],
|
||||
);
|
||||
|
||||
const goal = useStreamGoal(stream);
|
||||
|
Reference in New Issue
Block a user