mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-26 19:47:25 +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;
|
/(nostr:|@)?((npub|note|nprofile|nevent|nrelay|naddr)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/gi;
|
||||||
export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
|
export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
|
||||||
export const getMatchLink = () =>
|
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;
|
export const getMatchEmoji = () => /:([a-zA-Z0-9_]+):/gi;
|
||||||
|
|
||||||
// read more https://www.regular-expressions.info/unicode.html#category
|
// read more https://www.regular-expressions.info/unicode.html#category
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useToast } from "@chakra-ui/react";
|
||||||
|
|
||||||
import appSettings, { replaceSettings } from "../services/settings/app-settings";
|
import appSettings, { replaceSettings } from "../services/settings/app-settings";
|
||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
import { useToast } from "@chakra-ui/react";
|
|
||||||
import { AppSettings } from "../services/settings/migrations";
|
import { AppSettings } from "../services/settings/migrations";
|
||||||
|
|
||||||
export default function useAppSettings() {
|
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 { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import { TimelineLoader } from "../classes/timeline-loader";
|
import { TimelineLoader } from "../classes/timeline-loader";
|
||||||
import timelineCacheService from "../services/timeline-cache";
|
import timelineCacheService from "../services/timeline-cache";
|
||||||
import useUserMuteFilter from "../hooks/use-user-mute-filter";
|
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import useClientSideMuteFilter from "../hooks/use-client-side-mute-filter";
|
||||||
|
|
||||||
type NotificationTimelineContextType = {
|
type NotificationTimelineContextType = {
|
||||||
timeline?: TimelineLoader;
|
timeline?: TimelineLoader;
|
||||||
@@ -31,7 +31,7 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil
|
|||||||
: undefined;
|
: undefined;
|
||||||
}, [account?.pubkey]);
|
}, [account?.pubkey]);
|
||||||
|
|
||||||
const userMuteFilter = useUserMuteFilter(account?.pubkey);
|
const userMuteFilter = useClientSideMuteFilter();
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
if (userMuteFilter(event)) return false;
|
if (userMuteFilter(event)) return false;
|
||||||
|
@@ -22,14 +22,21 @@ export type AppSettingsV0 = {
|
|||||||
redditRedirect?: string;
|
redditRedirect?: string;
|
||||||
youtubeRedirect?: string;
|
youtubeRedirect?: string;
|
||||||
};
|
};
|
||||||
|
export type AppSettingsV1 = Omit<AppSettingsV0, "version"> & {
|
||||||
|
version: 1;
|
||||||
|
mutedWords?: string;
|
||||||
|
};
|
||||||
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
||||||
return settings.version === undefined || settings.version === 0;
|
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 = {
|
export const defaultSettings: AppSettings = {
|
||||||
version: 0,
|
version: 1,
|
||||||
colorMode: "system",
|
colorMode: "system",
|
||||||
blurImages: true,
|
blurImages: true,
|
||||||
autoShowMedia: true,
|
autoShowMedia: true,
|
||||||
@@ -49,8 +56,9 @@ export const defaultSettings: AppSettings = {
|
|||||||
youtubeRedirect: undefined,
|
youtubeRedirect: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function upgradeSettings(settings: { version: number }) {
|
export function upgradeSettings(settings: { version: number }): AppSettings | null {
|
||||||
if (isV0(settings)) return settings;
|
if (isV0(settings)) return { ...settings, version: 1 };
|
||||||
|
if (isV1(settings)) return settings;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,12 +12,12 @@ import RelaySelectionButton from "../../components/relay-selection/relay-selecti
|
|||||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||||
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||||
import { NostrRequestFilter } from "../../types/nostr-query";
|
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() {
|
function HomePage() {
|
||||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||||
const showReplies = useDisclosure();
|
const showReplies = useDisclosure();
|
||||||
const muteFilter = useUserMuteFilter();
|
const muteFilter = useClientSideMuteFilter();
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
if (muteFilter(event)) return false;
|
if (muteFilter(event)) return false;
|
||||||
|
@@ -6,7 +6,7 @@ import { Note } from "../../../components/note";
|
|||||||
import { countReplies, ThreadItem } from "../../../helpers/thread";
|
import { countReplies, ThreadItem } from "../../../helpers/thread";
|
||||||
import { TrustProvider } from "../../../providers/trust";
|
import { TrustProvider } from "../../../providers/trust";
|
||||||
import ReplyForm from "./reply-form";
|
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 = {
|
export type ThreadItemProps = {
|
||||||
post: ThreadItem;
|
post: ThreadItem;
|
||||||
@@ -19,18 +19,20 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
|
|||||||
const toggle = () => setShowReplies((v) => !v);
|
const toggle = () => setShowReplies((v) => !v);
|
||||||
const showReplyForm = useDisclosure();
|
const showReplyForm = useDisclosure();
|
||||||
|
|
||||||
const muteFilter = useUserMuteFilter();
|
const muteFilter = useClientSideMuteFilter();
|
||||||
const [alwaysShow, setAlwaysShow] = useState(false);
|
const [alwaysShow, setAlwaysShow] = useState(false);
|
||||||
|
|
||||||
const numberOfReplies = countReplies(post);
|
const numberOfReplies = countReplies(post);
|
||||||
const isMuted = muteFilter(post.event);
|
const isMuted = muteFilter(post.event);
|
||||||
|
|
||||||
|
if (isMuted && numberOfReplies === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
{isMuted && !alwaysShow ? (
|
{isMuted && !alwaysShow ? (
|
||||||
<Alert status="warning">
|
<Alert status="warning">
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
Muted user
|
Muted user or note
|
||||||
<Button size="xs" ml="auto" onClick={() => setAlwaysShow(true)}>
|
<Button size="xs" ml="auto" onClick={() => setAlwaysShow(true)}>
|
||||||
Show anyway
|
Show anyway
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -12,6 +12,7 @@ import {
|
|||||||
FormHelperText,
|
FormHelperText,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
Textarea,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { AppSettings } from "../../services/settings/migrations";
|
import { AppSettings } from "../../services/settings/migrations";
|
||||||
import { AppearanceIcon } from "../../components/icons";
|
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>
|
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</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>
|
</Flex>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||||
|
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
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 useParsedStreams from "../../hooks/use-parsed-streams";
|
||||||
import { NostrRequestFilter } from "../../types/nostr-query";
|
import { NostrRequestFilter } from "../../types/nostr-query";
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||||
|
|
||||||
function StreamsPage() {
|
function StreamsPage() {
|
||||||
useAppTitle("Streams");
|
useAppTitle("Streams");
|
||||||
const relays = useRelaySelectionRelays();
|
const relays = useRelaySelectionRelays();
|
||||||
const userMuteFilter = useUserMuteFilter();
|
const userMuteFilter = useClientSideMuteFilter();
|
||||||
|
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
|
@@ -6,21 +6,20 @@ import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../hel
|
|||||||
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../../../types/nostr-event";
|
import { NostrEvent } from "../../../../types/nostr-event";
|
||||||
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
||||||
import { useCurrentAccount } from "../../../../hooks/use-current-account";
|
|
||||||
import useStreamGoal from "../../../../hooks/use-stream-goal";
|
import useStreamGoal from "../../../../hooks/use-stream-goal";
|
||||||
import { NostrQuery } from "../../../../types/nostr-query";
|
import { NostrQuery } from "../../../../types/nostr-query";
|
||||||
import useUserMuteFilter from "../../../../hooks/use-user-mute-filter";
|
import useUserMuteFilter from "../../../../hooks/use-user-mute-filter";
|
||||||
|
import useClientSideMuteFilter from "../../../../hooks/use-client-side-mute-filter";
|
||||||
|
|
||||||
export default function useStreamChatTimeline(stream: ParsedStream) {
|
export default function useStreamChatTimeline(stream: ParsedStream) {
|
||||||
const account = useCurrentAccount();
|
|
||||||
const streamRelays = useRelaySelectionRelays();
|
const streamRelays = useRelaySelectionRelays();
|
||||||
|
|
||||||
const hostMuteFilter = useUserMuteFilter(stream.host);
|
const hostMuteFilter = useUserMuteFilter(stream.host);
|
||||||
const userMuteFilter = useUserMuteFilter(account?.pubkey);
|
const muteFilter = useClientSideMuteFilter();
|
||||||
|
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => !(hostMuteFilter(event) || userMuteFilter(event)),
|
(event: NostrEvent) => !(hostMuteFilter(event) || muteFilter(event)),
|
||||||
[hostMuteFilter, userMuteFilter],
|
[hostMuteFilter, muteFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goal = useStreamGoal(stream);
|
const goal = useStreamGoal(stream);
|
||||||
|
Reference in New Issue
Block a user