fix: build

This commit is contained in:
kieran 2024-04-22 15:24:25 +01:00
parent 80a4b5d8e6
commit bfdcbca08b
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
20 changed files with 135 additions and 128 deletions

37
packages/app/.eslintrc.js Normal file
View File

@ -0,0 +1,37 @@
/* eslint-disable import/no-anonymous-default-export */
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
rules: {
"formatjs/enforce-id": [
"error",
{
idInterpolationPattern: "[sha512:contenthash:base64:6]",
},
],
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"react-refresh/only-export-components": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"@typescript-eslint/no-unused-vars": "error",
"max-lines": ["warn", { max: 300, skipBlankLines: true, skipComments: true }],
},
overrides: [
{
files: ["*.tsx"],
rules: {
"max-lines": ["warn", { max: 200, skipBlankLines: true, skipComments: true }],
},
},
],
root: true,
ignorePatterns: ["build/", "*.test.ts", "*.js"],
env: {
browser: true,
worker: true,
commonjs: true,
node: false,
},
};

View File

@ -1,44 +0,0 @@
/* eslint-disable import/no-anonymous-default-export */
export default [
{
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
rules: {
"formatjs/enforce-id": [
"error",
{
idInterpolationPattern: "[sha512:contenthash:base64:6]",
},
],
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"react-refresh/only-export-components": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"@typescript-eslint/no-unused-vars": "error",
"max-lines": ["warn", { max: 300, skipBlankLines: true, skipComments: true }],
},
overrides: [
{
files: ["*.tsx"],
rules: {
"max-lines": ["warn", { max: 200, skipBlankLines: true, skipComments: true }],
},
},
],
root: true,
ignores: ["build/", "*.test.ts", "*.js"],
env: {
browser: true,
worker: true,
commonjs: true,
node: false,
},
},
];

View File

@ -1,4 +1,4 @@
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system"; import { EventKind, NostrEvent, NostrLink } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu"; import { Menu, MenuItem } from "@szhsin/react-menu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -90,16 +90,12 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
await navigator.clipboard.writeText(link); await navigator.clipboard.writeText(link);
} }
async function pin(id: HexKey) { async function pin(ev: NostrEvent) {
if (publisher) { await login.state.addToList(EventKind.PinList, NostrLink.fromEvent(ev), true);
//todo: PIN note
}
} }
async function bookmark(id: string) { async function bookmark(ev: NostrEvent) {
if (publisher) { await login.state.addToList(EventKind.BookmarksList, NostrLink.fromEvent(ev), true);
//todo: bookmark note
}
} }
async function copyEvent() { async function copyEvent() {
@ -129,13 +125,13 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
<FormattedMessage {...messages.Share} /> <FormattedMessage {...messages.Share} />
</MenuItem> </MenuItem>
{!login.state.isOnList(EventKind.PinList, link) && !login.readonly && ( {!login.state.isOnList(EventKind.PinList, link) && !login.readonly && (
<MenuItem onClick={() => pin(ev.id)}> <MenuItem onClick={() => pin(ev)}>
<Icon name="pin" /> <Icon name="pin" />
<FormattedMessage {...messages.Pin} /> <FormattedMessage {...messages.Pin} />
</MenuItem> </MenuItem>
)} )}
{!login.state.isOnList(EventKind.BookmarksList, link) && !login.readonly && ( {!login.state.isOnList(EventKind.BookmarksList, link) && !login.readonly && (
<MenuItem onClick={() => bookmark(ev.id)}> <MenuItem onClick={() => bookmark(ev)}>
<Icon name="bookmark" /> <Icon name="bookmark" />
<FormattedMessage {...messages.Bookmark} /> <FormattedMessage {...messages.Bookmark} />
</MenuItem> </MenuItem>

View File

@ -1,5 +1,7 @@
import "./RootTabs.css"; import "./RootTabs.css";
import { unwrap } from "@snort/shared";
import { EventKind } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu"; import { Menu, MenuItem } from "@szhsin/react-menu";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
@ -9,8 +11,6 @@ import Icon from "@/Components/Icons/Icon";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences"; import usePreferences from "@/Hooks/usePreferences";
import { RootTabRoutePath } from "@/Pages/Root/RootTabRoutes"; import { RootTabRoutePath } from "@/Pages/Root/RootTabRoutes";
import { EventKind } from "@snort/system";
import { unwrap } from "@snort/shared";
export function RootTabs({ base = "/" }: { base: string }) { export function RootTabs({ base = "/" }: { base: string }) {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -255,7 +255,7 @@ class IrisAccount extends Component<Props> {
} }
async declineReserved() { async declineReserved() {
if (!confirm(`Are you sure you want to decline iris.to/${name}?`)) { if (!window.confirm(`Are you sure you want to decline iris.to/${this.state.newUserName}?`)) {
return; return;
} }
const login = LoginStore.snapshot(); const login = LoginStore.snapshot();

View File

@ -1,36 +1,36 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
function useHistoryState(initialValue, key) { function useHistoryState<T>(initialValue: T, key: string) {
const currentHistoryState = history.state ? history.state[key] : undefined; const currentHistoryState = globalThis.history.state ? globalThis.history.state[key] : undefined;
const myInitialValue = currentHistoryState === undefined ? initialValue : currentHistoryState; const myInitialValue = currentHistoryState === undefined ? initialValue : currentHistoryState;
const [state, setState] = useState(myInitialValue); const [state, setState] = useState(myInitialValue);
const latestValue = useRef(state); const latestValue = useRef(state);
const setHistoryState = value => { const setHistoryState = (value: T) => {
const newHistoryState = { ...history.state, [key]: value }; const newHistoryState = { ...globalThis.history.state, [key]: value };
history.replaceState(newHistoryState, ""); globalThis.history.replaceState(newHistoryState, "");
latestValue.current = value; latestValue.current = value;
}; };
useEffect(() => { useEffect(() => {
if (state !== latestValue.current) { if (state !== latestValue.current) {
setHistoryState(state); setHistoryState(state);
const newHistoryState = { ...history.state, [key]: state }; const newHistoryState = { ...globalThis.history.state, [key]: state };
history.replaceState(newHistoryState, ""); globalThis.history.replaceState(newHistoryState, "");
latestValue.current = state; latestValue.current = state;
} }
// Cleanup logic // Cleanup logic
return () => { return () => {
if (state !== latestValue.current) { if (state !== latestValue.current) {
const newHistoryState = { ...history.state, [key]: state }; const newHistoryState = { ...globalThis.history.state, [key]: state };
history.replaceState(newHistoryState, ""); // Save the final state globalThis.history.replaceState(newHistoryState, ""); // Save the final state
} }
}; };
}, [state, key]); }, [state, key]);
const popStateListener = event => { const popStateListener = (event: PopStateEvent) => {
if (event.state && key in event.state) { if (event.state && key in event.state) {
setState(event.state[key]); setState(event.state[key]);
} }

View File

@ -1,7 +1,15 @@
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system"; import { dedupe } from "@snort/shared";
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent, ToNostrEventTag } from "@snort/system";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import { dedupe } from "@snort/shared";
export class MutedWordTag implements ToNostrEventTag {
constructor(readonly word: string) {}
toEventTag(): string[] | undefined {
return ["word", this.word.toLowerCase()];
}
}
export default function useModeration() { export default function useModeration() {
const state = useLogin(s => s.state); const state = useLogin(s => s.state);
@ -30,13 +38,33 @@ export default function useModeration() {
} }
function isMutedWord(word: string) { function isMutedWord(word: string) {
return false; const words = getMutedWords();
return words.includes(word);
}
async function addMutedWord(word: string) {
await state.addToList(EventKind.MuteList, new MutedWordTag(word.toLowerCase()));
}
async function removeMutedWord(word: string) {
await state.removeFromList(EventKind.MuteList, new MutedWordTag(word.toLowerCase()));
} }
function isEventMuted(ev: TaggedNostrEvent | NostrEvent) { function isEventMuted(ev: TaggedNostrEvent | NostrEvent) {
return isMuted(ev.pubkey) || false; return isMuted(ev.pubkey) || false;
} }
function getMutedWords() {
return state
.getList(EventKind.MuteList, o => {
if (o[0] === "word") {
return new MutedWordTag(o[1]);
}
})
.filter(a => a instanceof MutedWordTag)
.map(a => (a as MutedWordTag).word);
}
return { return {
muteList: state.muted, muteList: state.muted,
mute, mute,
@ -45,5 +73,8 @@ export default function useModeration() {
isMuted, isMuted,
isMutedWord, isMutedWord,
isEventMuted, isEventMuted,
addMutedWord,
removeMutedWord,
getMutedWords,
}; };
} }

View File

@ -11,7 +11,6 @@ export function ZapPoolDonateSection() {
if (!CONFIG.features.zapPool) { if (!CONFIG.features.zapPool) {
return; return;
} }
// eslint-disable-next-line react-hooks/rules-of-hooks
const zapPool = useSyncExternalStore( const zapPool = useSyncExternalStore(
c => unwrap(ZapPoolController).hook(c), c => unwrap(ZapPoolController).hook(c),
() => unwrap(ZapPoolController).snapshot(), () => unwrap(ZapPoolController).snapshot(),

View File

@ -9,9 +9,9 @@ import { Link, useParams } from "react-router-dom";
import AsyncButton from "@/Components/Button/AsyncButton"; import AsyncButton from "@/Components/Button/AsyncButton";
import Timeline from "@/Components/Feed/Timeline"; import Timeline from "@/Components/Feed/Timeline";
import ProfileImage from "@/Components/User/ProfileImage"; import ProfileImage from "@/Components/User/ProfileImage";
import { TimelineSubject } from "@/Feed/TimelineFeed";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import { formatShort } from "@/Utils/Number"; import { formatShort } from "@/Utils/Number";
import { TimelineSubject } from "@/Feed/TimelineFeed";
const HashTagsPage = () => { const HashTagsPage = () => {
const params = useParams(); const params = useParams();

View File

@ -1,3 +1,4 @@
import { unwrap } from "@snort/shared";
import { EventKind, NostrLink, NostrPrefix, parseNostrLink } from "@snort/system"; import { EventKind, NostrLink, NostrPrefix, parseNostrLink } from "@snort/system";
import { useEventFeed } from "@snort/system-react"; import { useEventFeed } from "@snort/system-react";
import classNames from "classnames"; import classNames from "classnames";
@ -13,7 +14,6 @@ import useLogin from "@/Hooks/useLogin";
import { LogoHeader } from "@/Pages/Layout/LogoHeader"; import { LogoHeader } from "@/Pages/Layout/LogoHeader";
import NotificationsHeader from "@/Pages/Layout/NotificationsHeader"; import NotificationsHeader from "@/Pages/Layout/NotificationsHeader";
import { bech32ToHex } from "@/Utils"; import { bech32ToHex } from "@/Utils";
import { unwrap } from "@snort/shared";
export function Header() { export function Header() {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -9,7 +9,7 @@ import useLogin from "@/Hooks/useLogin";
export default function RightColumn() { export default function RightColumn() {
const { pubkey } = useLogin(s => ({ pubkey: s.publicKey })); const { pubkey } = useLogin(s => ({ pubkey: s.publicKey }));
const hideRightColumnPaths = ["/login", "/new", "/messages"]; const hideRightColumnPaths = ["/login", "/new", "/messages"];
const show = !hideRightColumnPaths.some(path => location.pathname.startsWith(path)); const show = !hideRightColumnPaths.some(path => globalThis.location.pathname.startsWith(path));
const getTitleMessage = () => { const getTitleMessage = () => {
return pubkey ? ( return pubkey ? (

View File

@ -1,4 +1,4 @@
import { EventKind, NostrLink, TaggedNostrEvent, Nip10 } from "@snort/system"; import { EventKind, Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
export function getNotificationContext(ev: TaggedNostrEvent) { export function getNotificationContext(ev: TaggedNostrEvent) {
switch (ev.kind) { switch (ev.kind) {

View File

@ -1,10 +1,10 @@
import { unwrap } from "@snort/shared";
import { EventKind } from "@snort/system";
import { useMemo } from "react"; import { useMemo } from "react";
import Timeline from "@/Components/Feed/Timeline"; import Timeline from "@/Components/Feed/Timeline";
import useLogin from "@/Hooks/useLogin";
import { EventKind } from "@snort/system";
import { unwrap } from "@snort/shared";
import { TimelineSubject } from "@/Feed/TimelineFeed"; import { TimelineSubject } from "@/Feed/TimelineFeed";
import useLogin from "@/Hooks/useLogin";
export function TopicsPage() { export function TopicsPage() {
const { tags, pubKey } = useLogin(s => ({ const { tags, pubKey } = useLogin(s => ({

View File

@ -1,39 +1,15 @@
import { useState } from "react"; import { useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import useEventPublisher from "@/Hooks/useEventPublisher"; import AsyncButton from "@/Components/Button/AsyncButton";
import useLogin from "@/Hooks/useLogin"; import useModeration from "@/Hooks/useModeration";
import { appendDedupe } from "@/Utils";
import { SnortAppData, updateAppData } from "@/Utils/Login";
import { useAllPreferences } from "@/Hooks/usePreferences"; import { useAllPreferences } from "@/Hooks/usePreferences";
export default function ModerationSettingsPage() { export default function ModerationSettingsPage() {
const state = useAllPreferences(); const { addMutedWord, removeMutedWord, getMutedWords } = useModeration();
const preferences = useAllPreferences();
const [muteWord, setMuteWord] = useState(""); const [muteWord, setMuteWord] = useState("");
function addMutedWord() {
updateAppData(login.id, ad => ({
...ad,
mutedWords: appendDedupe(appData.mutedWords, [muteWord]),
}));
setMuteWord("");
}
const handleToggle = (setting: keyof SnortAppData) => {
updateAppData(login.id, ad => ({
...ad,
[setting]: !appData[setting],
}));
};
function removeMutedWord(word: string) {
updateAppData(login.id, ad => ({
...ad,
mutedWords: appData.mutedWords.filter(a => a !== word),
}));
setMuteWord("");
}
return ( return (
<> <>
<h2> <h2>
@ -44,8 +20,13 @@ export default function ModerationSettingsPage() {
<div className="flex items-center mb-2"> <div className="flex items-center mb-2">
<input <input
type="checkbox" type="checkbox"
checked={appData.showContentWarningPosts} checked={preferences.preferences.showContentWarningPosts}
onChange={() => handleToggle("showContentWarningPosts")} onChange={() =>
preferences.update({
...preferences.preferences,
showContentWarningPosts: !preferences.preferences.showContentWarningPosts,
})
}
className="mr-2" className="mr-2"
id="showContentWarningPosts" id="showContentWarningPosts"
/> />
@ -67,11 +48,11 @@ export default function ModerationSettingsPage() {
value={muteWord} value={muteWord}
onChange={e => setMuteWord(e.target.value.toLowerCase())} onChange={e => setMuteWord(e.target.value.toLowerCase())}
/> />
<button type="button" onClick={addMutedWord}> <AsyncButton type="button" onClick={() => addMutedWord(muteWord)}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" /> <FormattedMessage defaultMessage="Add" id="2/2yg+" />
</button> </AsyncButton>
</div> </div>
{appData.mutedWords.map(v => ( {getMutedWords().map(v => (
<div key={v} className="p br b flex items-center justify-between"> <div key={v} className="p br b flex items-center justify-between">
<div>{v}</div> <div>{v}</div>
<button type="button" onClick={() => removeMutedWord(v)}> <button type="button" onClick={() => removeMutedWord(v)}>

View File

@ -100,7 +100,7 @@ export async function subscribeToNotifications(publisher: EventPublisher) {
endpoint: sub.endpoint, endpoint: sub.endpoint,
p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))), p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))), auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
scope: `${location.protocol}//${location.hostname}`, scope: `${globalThis.location.protocol}//${globalThis.location.hostname}`,
}); });
} }
} }

View File

@ -134,12 +134,12 @@ class ZapPool extends ExternalStore<Array<ZapPoolRecipient>> {
} }
#save() { #save() {
self.localStorage.setItem("zap-pool", JSON.stringify(this.takeSnapshot())); globalThis.localStorage.setItem("zap-pool", JSON.stringify(this.takeSnapshot()));
self.localStorage.setItem("zap-pool-last-payout", this.#lastPayout.toString()); globalThis.localStorage.setItem("zap-pool-last-payout", this.#lastPayout.toString());
} }
#load() { #load() {
const existing = self.localStorage.getItem("zap-pool"); const existing = globalThis.localStorage.getItem("zap-pool");
if (existing) { if (existing) {
const arr = JSON.parse(existing) as Array<ZapPoolRecipient>; const arr = JSON.parse(existing) as Array<ZapPoolRecipient>;
this.#store = new Map(arr.map(a => [`${a.pubkey}-${a.type}`, a])); this.#store = new Map(arr.map(a => [`${a.pubkey}-${a.type}`, a]));
@ -157,7 +157,7 @@ class ZapPool extends ExternalStore<Array<ZapPoolRecipient>> {
]); ]);
} }
const lastPayout = self.localStorage.getItem("zap-pool-last-payout"); const lastPayout = globalThis.localStorage.getItem("zap-pool-last-payout");
if (lastPayout) { if (lastPayout) {
this.#lastPayout = Number(lastPayout); this.#lastPayout = Number(lastPayout);
} }

View File

@ -41,6 +41,7 @@ import { storeRefCode, unwrap } from "@/Utils";
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm"; import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
import { Wallets } from "@/Wallet"; import { Wallets } from "@/Wallet";
import { setupWebLNWalletConfig } from "@/Wallet"; import { setupWebLNWalletConfig } from "@/Wallet";
import { LoginStore } from "./Utils/Login"; import { LoginStore } from "./Utils/Login";
async function initSite() { async function initSite() {

View File

@ -1,7 +1,6 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { encodeTLVEntries, NostrLink, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system"; import { encodeTLVEntries, NostrLink, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { clientsClaim } from "workbox-core"; import { clientsClaim } from "workbox-core";
import { ExpirationPlugin } from "workbox-expiration"; import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, PrecacheEntry } from "workbox-precaching"; import { precacheAndRoute, PrecacheEntry } from "workbox-precaching";
@ -181,7 +180,7 @@ self.addEventListener("notificationclick", event => {
if (mention.event) { if (mention.event) {
return `/${new NostrLink(NostrPrefix.Note, mention.event).encode()}`; return `/${new NostrLink(NostrPrefix.Note, mention.event).encode()}`;
} }
} else if (ev.type == PushType.DirectMessage) { } else if (ev.type === PushType.DirectMessage) {
const reaction = ev.data as CompactReaction; const reaction = ev.data as CompactReaction;
return `/messages/${encodeTLVEntries("chat4" as NostrPrefix, { return `/messages/${encodeTLVEntries("chat4" as NostrPrefix, {
type: TLVEntryType.Author, type: TLVEntryType.Author,

View File

@ -24,6 +24,13 @@ export class NostrHashtagLink implements ToNostrEventTag {
} }
} }
export class UnknownTag implements ToNostrEventTag {
constructor(readonly value: Array<string>) {}
toEventTag(): string[] | undefined {
return this.value;
}
}
export class NostrLink implements ToNostrEventTag { export class NostrLink implements ToNostrEventTag {
constructor( constructor(
readonly type: NostrPrefix, readonly type: NostrPrefix,
@ -188,7 +195,7 @@ export class NostrLink implements ToNostrEventTag {
tag: Array<string>, tag: Array<string>,
author?: string, author?: string,
kind?: number, kind?: number,
fnOther?: (tag: Array<string>) => T, fnOther?: (tag: Array<string>) => T | undefined,
) { ) {
const relays = tag.length > 2 ? [tag[2]] : undefined; const relays = tag.length > 2 ? [tag[2]] : undefined;
switch (tag[0]) { switch (tag[0]) {
@ -203,15 +210,12 @@ export class NostrLink implements ToNostrEventTag {
return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, tag[3]); return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, tag[3]);
} }
default: { default: {
if (fnOther) { return fnOther?.(tag) ?? new UnknownTag(tag);
return fnOther(tag);
}
} }
} }
throw new Error(`Unknown tag kind ${tag[0]}`);
} }
static fromTags<T = NostrLink>(tags: ReadonlyArray<Array<string>>, fnOther?: (tag: Array<string>) => T) { static fromTags<T = NostrLink>(tags: ReadonlyArray<Array<string>>, fnOther?: (tag: Array<string>) => T | undefined) {
return removeUndefined( return removeUndefined(
tags.map(a => { tags.map(a => {
try { try {

View File

@ -396,9 +396,12 @@ export class UserState<TAppData> extends EventEmitter<UserStateEvents> {
return false; return false;
} }
getList(kind: EventKind): Array<ToNostrEventTag> { getList<T extends ToNostrEventTag>(
kind: EventKind,
fnOther?: (tag: Array<string>) => T | undefined,
): Array<ToNostrEventTag> {
const list = this.#standardLists.get(kind); const list = this.#standardLists.get(kind);
return NostrLink.fromTags(list?.tags ?? []); return NostrLink.fromTags<T>(list?.tags ?? [], fnOther);
} }
serialize(): UserStateObject<TAppData> { serialize(): UserStateObject<TAppData> {