Muted words: phase 1
This commit is contained in:
@ -164,10 +164,6 @@
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.hidden-note button {
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
.expand-note {
|
||||
padding: 0 0 16px 0;
|
||||
font-weight: 400;
|
||||
|
@ -67,14 +67,14 @@ export interface NoteProps {
|
||||
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||
const [show, setShow] = useState(false);
|
||||
return show ? (
|
||||
<>{children}</>
|
||||
children
|
||||
) : (
|
||||
<div className="card note hidden-note">
|
||||
<div className="header">
|
||||
<p>
|
||||
<FormattedMessage {...messages.MutedAuthor} />
|
||||
<FormattedMessage defaultMessage="This note has been muted" />
|
||||
</p>
|
||||
<button onClick={() => setShow(true)}>
|
||||
<button type="button" onClick={() => setShow(true)}>
|
||||
<FormattedMessage {...messages.Show} />
|
||||
</button>
|
||||
</div>
|
||||
@ -116,8 +116,7 @@ export function NoteInner(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
|
||||
const { isMuted } = useModeration();
|
||||
const isOpMuted = isMuted(ev?.pubkey);
|
||||
const { isEventMuted } = useModeration();
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked } = login;
|
||||
@ -466,5 +465,5 @@ export function NoteInner(props: NoteProps) {
|
||||
</div>
|
||||
);
|
||||
|
||||
return !ignoreModeration && isOpMuted ? <HiddenNote>{note}</HiddenNote> : note;
|
||||
return !ignoreModeration && isEventMuted(ev) ? <HiddenNote>{note}</HiddenNote> : note;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { getMutedKeys } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login";
|
||||
import { SnortAppData, addSubscription, setAppData, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login";
|
||||
import { SnortPubKey } from "Const";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import useRelaysFeedFollows from "./RelaysFeedFollows";
|
||||
@ -38,7 +38,9 @@ export default function useLoginFeed() {
|
||||
leaveOpen: true,
|
||||
});
|
||||
b.withFilter().authors([pubKey]).kinds([EventKind.ContactList]);
|
||||
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
|
||||
b.withFilter()
|
||||
.relay("wss://relay.snort.social")
|
||||
.kinds([EventKind.SnortSubscriptions])
|
||||
.authors([bech32ToHex(SnortPubKey)])
|
||||
.tag("p", [pubKey])
|
||||
@ -97,6 +99,14 @@ export default function useLoginFeed() {
|
||||
}
|
||||
}),
|
||||
).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap)));
|
||||
|
||||
const appData = getNewest(loginFeed.data.filter(a => a.kind === EventKind.AppData));
|
||||
if(appData) {
|
||||
publisher.decryptGeneric(appData.content, appData.pubkey).then(d => {
|
||||
setAppData(login, JSON.parse(d) as SnortAppData, appData.created_at * 1000);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}, [loginFeed, publisher]);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HexKey } from "@snort/system";
|
||||
import { HexKey, TaggedNostrEvent } from "@snort/system";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBlocked, setMuted } from "Login";
|
||||
@ -7,7 +7,7 @@ import { System } from "index";
|
||||
|
||||
export default function useModeration() {
|
||||
const login = useLogin();
|
||||
const { muted, blocked } = login;
|
||||
const { muted, blocked, appData } = login;
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function setMutedList(pub: HexKey[], priv: HexKey[]) {
|
||||
@ -57,6 +57,14 @@ export default function useModeration() {
|
||||
setMuted(login, newMuted, ts);
|
||||
}
|
||||
|
||||
function isMutedWord(word: string) {
|
||||
return appData.item.mutedWords.includes(word.toLowerCase());
|
||||
}
|
||||
|
||||
function isEventMuted(ev: TaggedNostrEvent) {
|
||||
return isMuted(ev.pubkey) || appData.item.mutedWords.some(w => ev.content.toLowerCase().includes(w));
|
||||
}
|
||||
|
||||
return {
|
||||
muted: muted.item,
|
||||
mute,
|
||||
@ -67,5 +75,7 @@ export default function useModeration() {
|
||||
block,
|
||||
unblock,
|
||||
isBlocked,
|
||||
isMutedWord,
|
||||
isEventMuted
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import * as secp from "@noble/curves/secp256k1";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
|
||||
import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import { LoginStore, UserPreferences, LoginSession, LoginSessionType } from "Login";
|
||||
import { LoginStore, UserPreferences, LoginSession, LoginSessionType, SnortAppData } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from "SnortUtils";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
@ -157,6 +157,15 @@ export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setAppData(state: LoginSession, data: SnortAppData, ts: number) {
|
||||
if(state.appData.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.appData.item = data;
|
||||
state.appData.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
||||
const newSubs = dedupeById([...(state.subscriptions || []), ...subs]);
|
||||
if (newSubs.length !== state.subscriptions.length) {
|
||||
|
@ -18,6 +18,10 @@ export enum LoginSessionType {
|
||||
Nip7os = "nip7_os",
|
||||
}
|
||||
|
||||
export interface SnortAppData {
|
||||
mutedWords: Array<string>
|
||||
}
|
||||
|
||||
export interface LoginSession {
|
||||
/**
|
||||
* Unique ID to identify this session
|
||||
@ -114,4 +118,9 @@ export interface LoginSession {
|
||||
* Remote signer relays (NIP-46)
|
||||
*/
|
||||
remoteSignerRelays?: Array<string>;
|
||||
|
||||
/**
|
||||
* Snort application data
|
||||
*/
|
||||
appData: Newest<SnortAppData>;
|
||||
}
|
||||
|
@ -46,6 +46,12 @@ const LoggedOut = {
|
||||
latestNotification: 0,
|
||||
readNotifications: 0,
|
||||
subscriptions: [],
|
||||
appData: {
|
||||
item: {
|
||||
mutedWords: []
|
||||
},
|
||||
timestamp: 0,
|
||||
},
|
||||
} as LoginSession;
|
||||
|
||||
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
|
@ -9,6 +9,7 @@ import AccountsPage from "Pages/settings/Accounts";
|
||||
import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
|
||||
import { ManageHandleRoutes } from "Pages/settings/handle";
|
||||
import ExportKeys from "Pages/settings/Keys";
|
||||
import { ModerationSettings } from "./settings/Moderation";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -56,6 +57,10 @@ export const SettingsRoutes: RouteObject[] = [
|
||||
path: "keys",
|
||||
element: <ExportKeys />,
|
||||
},
|
||||
{
|
||||
path: "moderation",
|
||||
element: <ModerationSettings />,
|
||||
},
|
||||
...ManageHandleRoutes,
|
||||
...WalletSettingsRoutes,
|
||||
],
|
||||
|
44
packages/app/src/Pages/settings/Moderation.tsx
Normal file
44
packages/app/src/Pages/settings/Moderation.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { unixNowMs } from "@snort/shared";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setAppData } from "Login";
|
||||
import { appendDedupe } from "SnortUtils";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export function ModerationSettings() {
|
||||
const login = useLogin();
|
||||
const [muteWord, setMuteWord] = useState("");
|
||||
|
||||
function addMutedWord() {
|
||||
login.appData ??= {
|
||||
item: {
|
||||
mutedWords: []
|
||||
},
|
||||
timestamp: 0
|
||||
};
|
||||
setAppData(login, {
|
||||
...login.appData.item,
|
||||
mutedWords: appendDedupe(login.appData.item.mutedWords, [muteWord])
|
||||
}, unixNowMs());
|
||||
setMuteWord("");
|
||||
}
|
||||
return <>
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Muted Words" />
|
||||
</h2>
|
||||
<div className="flex-column g12">
|
||||
<div className="flex g8">
|
||||
<input type="text" placeholder="eg. crypto" className="w-max" value={muteWord} onChange={e => setMuteWord(e.target.value)} />
|
||||
<button type="button" onClick={addMutedWord}>
|
||||
<FormattedMessage defaultMessage="Add" />
|
||||
</button>
|
||||
</div>
|
||||
{login.appData.item.mutedWords.map(v => <div className="p br b flex f-space">
|
||||
<div>{v}</div>
|
||||
<button type="button">
|
||||
<FormattedMessage defaultMessage="Delete" />
|
||||
</button>
|
||||
</div>)}
|
||||
</div>
|
||||
</>
|
||||
}
|
@ -51,6 +51,11 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Export Keys" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("moderation")}>
|
||||
<Icon name="shield-tick" size={24} />
|
||||
<FormattedMessage defaultMessage="Moderation" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("handle")}>
|
||||
<Icon name="badge" size={24} />
|
||||
<FormattedMessage defaultMessage="Nostr Adddress" />
|
||||
|
@ -133,6 +133,10 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
.b {
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
Reference in New Issue
Block a user