diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json
index e31d8443..216dbb94 100644
--- a/apps/desktop2/package.json
+++ b/apps/desktop2/package.json
@@ -19,6 +19,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
+ "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.29.0",
"@tanstack/react-query": "^5.29.0",
"@tanstack/react-query-persist-client": "^5.29.0",
diff --git a/apps/desktop2/src/routes/$account.home.tsx b/apps/desktop2/src/routes/$account.home.tsx
index 9f9dad30..47e7c9d8 100644
--- a/apps/desktop2/src/routes/$account.home.tsx
+++ b/apps/desktop2/src/routes/$account.home.tsx
@@ -10,7 +10,7 @@ import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react";
-import { useDebounce, useDebouncedCallback } from "use-debounce";
+import { useDebouncedCallback } from "use-debounce";
import { VList, VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({
diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx
index bc3f3b4b..a4111c79 100644
--- a/apps/desktop2/src/routes/__root.tsx
+++ b/apps/desktop2/src/routes/__root.tsx
@@ -2,8 +2,15 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { type Ark } from "@lume/ark";
import { type QueryClient } from "@tanstack/react-query";
import { type Platform } from "@tauri-apps/plugin-os";
-import { Account, Interests, Settings } from "@lume/types";
+import type { Account, Interests, Settings } from "@lume/types";
import { Spinner } from "@lume/ui";
+import { type Descendant } from "slate";
+
+type EditorElement = {
+ type: string;
+ children: Descendant[];
+ eventId?: string;
+};
interface RouterContext {
ark: Ark;
@@ -13,6 +20,7 @@ interface RouterContext {
settings?: Settings;
interests?: Interests;
accounts?: Account[];
+ initialValue?: EditorElement[];
}
export const Route = createRootRouteWithContext()({
diff --git a/apps/desktop2/src/routes/auth/settings.tsx b/apps/desktop2/src/routes/auth/settings.tsx
index 16676347..3eccb622 100644
--- a/apps/desktop2/src/routes/auth/settings.tsx
+++ b/apps/desktop2/src/routes/auth/settings.tsx
@@ -69,6 +69,13 @@ function Screen() {
}));
};
+ const toggleNsfw = () => {
+ setNewSettings((prev) => ({
+ ...prev,
+ nsfw: !newSettings.nsfw,
+ }));
+ };
+
const submit = async () => {
try {
// start loading
@@ -167,18 +174,28 @@ function Screen() {
-
-
- There are many more settings you can configure from the 'Settings'
- Screen. Be sure to visit it later.
-
+
+
toggleNsfw()}
+ className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
+ >
+
+
+
+
Filter sensitive content
+
+ By default, Lume will display all content which have Content
+ Warning tag, it's may include NSFW content.
+
+
diff --git a/apps/desktop2/src/routes/editor/-components/media.tsx b/apps/desktop2/src/routes/editor/-components/media.tsx
index bfcbddaf..e367d7cf 100644
--- a/apps/desktop2/src/routes/editor/-components/media.tsx
+++ b/apps/desktop2/src/routes/editor/-components/media.tsx
@@ -7,6 +7,7 @@ import { getCurrent } from "@tauri-apps/api/window";
import { UnlistenFn } from "@tauri-apps/api/event";
import { useRouteContext } from "@tanstack/react-router";
import { Spinner } from "@lume/ui";
+import * as Tooltip from "@radix-ui/react-tooltip";
export function MediaButton({ className }: { className?: string }) {
const { ark } = useRouteContext({ strict: false });
@@ -16,14 +17,13 @@ export function MediaButton({ className }: { className?: string }) {
const uploadToNostrBuild = async () => {
try {
+ // start loading
setLoading(true);
const image = await ark.upload();
+ insertImage(editor, image);
- if (image) {
- insertImage(editor, image);
- }
-
+ // reset loading
setLoading(false);
} catch (e) {
setLoading(false);
@@ -63,17 +63,29 @@ export function MediaButton({ className }: { className?: string }) {
}, []);
return (
-
+
+
+
+
+
+
+
+ Upload media
+
+
+
+
+
);
}
diff --git a/apps/desktop2/src/routes/editor/-components/nsfw.tsx b/apps/desktop2/src/routes/editor/-components/nsfw.tsx
new file mode 100644
index 00000000..079ed5e5
--- /dev/null
+++ b/apps/desktop2/src/routes/editor/-components/nsfw.tsx
@@ -0,0 +1,40 @@
+import { NsfwIcon } from "@lume/icons";
+import { cn } from "@lume/utils";
+import * as Tooltip from "@radix-ui/react-tooltip";
+import { Dispatch, SetStateAction } from "react";
+
+export function NsfwToggle({
+ nsfw,
+ setNsfw,
+ className,
+}: {
+ nsfw: boolean;
+ setNsfw: Dispatch>;
+ className?: string;
+}) {
+ return (
+
+
+
+
+
+
+
+ Mark as sensitive content
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop2/src/routes/editor/index.tsx b/apps/desktop2/src/routes/editor/index.tsx
index c8576602..8b24edb4 100644
--- a/apps/desktop2/src/routes/editor/index.tsx
+++ b/apps/desktop2/src/routes/editor/index.tsx
@@ -1,4 +1,4 @@
-import { LoaderIcon, TrashIcon } from "@lume/icons";
+import { ComposeFilledIcon, NsfwIcon, TrashIcon } from "@lume/icons";
import {
Portal,
cn,
@@ -35,11 +35,11 @@ import { Spinner, User } from "@lume/ui";
import { nip19 } from "nostr-tools";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
+import { NsfwToggle } from "./-components/nsfw";
-type EditorElement = {
- type: string;
- children: Descendant[];
- eventId?: string;
+type EditorSearch = {
+ reply_to: string;
+ quote: boolean;
};
const contactQueryOptions = queryOptions({
@@ -51,46 +51,48 @@ const contactQueryOptions = queryOptions({
});
export const Route = createFileRoute("/editor/")({
- loader: ({ context }) =>
- context.queryClient.ensureQueryData(contactQueryOptions),
+ validateSearch: (search: Record): EditorSearch => {
+ return {
+ reply_to: search.reply_to,
+ quote: search.quote === "true" ?? false,
+ };
+ },
+ beforeLoad: async ({ search }) => {
+ return {
+ initialValue: search.quote
+ ? [
+ {
+ type: "paragraph",
+ children: [{ text: "" }],
+ },
+ {
+ type: "event",
+ eventId: `nostr:${nip19.noteEncode(search.reply_to)}`,
+ children: [{ text: "" }],
+ },
+ {
+ type: "paragraph",
+ children: [{ text: "" }],
+ },
+ ]
+ : [
+ {
+ type: "paragraph",
+ children: [{ text: "" }],
+ },
+ ],
+ };
+ },
+ loader: ({ context }) => {
+ context.queryClient.ensureQueryData(contactQueryOptions);
+ },
component: Screen,
pendingComponent: Pending,
});
function Screen() {
- // @ts-ignore, useless
const { reply_to, quote } = Route.useSearch();
- const { ark } = Route.useRouteContext();
-
- let initialValue: EditorElement[];
-
- if (quote) {
- initialValue = [
- {
- type: "paragraph",
- children: [{ text: "" }],
- },
- {
- type: "event",
- eventId: `nostr:${nip19.noteEncode(reply_to)}`,
- children: [{ text: "" }],
- },
- {
- type: "paragraph",
- children: [{ text: "" }],
- },
- ];
- } else {
- initialValue = [
- {
- type: "paragraph",
- children: [{ text: "" }],
- },
- ];
- }
-
- const ref = useRef();
- const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
+ const { ark, initialValue } = Route.useRouteContext();
const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue);
@@ -98,10 +100,14 @@ function Screen() {
const [index, setIndex] = useState(0);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
+ const [nsfw, setNsfw] = useState(false);
const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
+ const ref = useRef();
+ const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
+
const filters = contacts
?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
@@ -204,15 +210,25 @@ function Screen() {
>
-
+
+
diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts
index 7e37c8ad..ac109d2b 100644
--- a/packages/ark/src/ark.ts
+++ b/packages/ark/src/ark.ts
@@ -23,9 +23,11 @@ enum NSTORE_KEYS {
export class Ark {
public windows: WebviewWindow[];
+ public settings: Settings;
constructor() {
this.windows = [];
+ this.settings = undefined;
}
public async get_all_accounts() {
@@ -144,7 +146,6 @@ export class Ark {
if (asOf && asOf > 0) until = asOf.toString();
- const dedup = true;
const seenIds = new Set
();
const dedupQueue = new Set();
@@ -155,31 +156,37 @@ export class Ark {
global: isGlobal,
});
- if (dedup) {
- for (const event of nostrEvents) {
- const tags = event.tags
- .filter((el) => el[0] === "e")
- ?.map((item) => item[1]);
+ for (const event of nostrEvents) {
+ const tags = event.tags
+ .filter((el) => el[0] === "e")
+ ?.map((item) => item[1]);
- if (tags.length) {
- for (const tag of tags) {
- if (seenIds.has(tag)) {
- dedupQueue.add(event.id);
- break;
- }
- seenIds.add(tag);
+ if (tags.length) {
+ for (const tag of tags) {
+ if (seenIds.has(tag)) {
+ dedupQueue.add(event.id);
+ break;
}
+ seenIds.add(tag);
}
}
-
- return nostrEvents
- .filter((event) => !dedupQueue.has(event.id))
- .sort((a, b) => b.created_at - a.created_at);
}
- return nostrEvents;
+ const events = nostrEvents
+ .filter((event) => !dedupQueue.has(event.id))
+ .sort((a, b) => b.created_at - a.created_at);
+
+ if (this.settings?.nsfw) {
+ return events.filter(
+ (event) =>
+ event.tags.filter((event) => event[0] === "content-warning")
+ .length > 0,
+ );
+ }
+
+ return events;
} catch (e) {
- console.error(String(e));
+ console.info(String(e));
return [];
}
}
@@ -229,7 +236,12 @@ export class Ark {
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
}
- public async publish(content: string, reply_to?: string, quote?: boolean) {
+ public async publish(
+ content: string,
+ reply_to?: string,
+ quote?: boolean,
+ nsfw?: boolean,
+ ) {
try {
const g = await generateContentTags(content);
@@ -238,26 +250,34 @@ export class Ark {
if (reply_to) {
const replyEvent = await this.get_event(reply_to);
+ const relayHint =
+ replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
if (quote) {
- eventTags.push([
- "e",
- replyEvent.id,
- replyEvent.relay || "",
- "mention",
- ]);
+ eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
} else {
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
if (rootEvent) {
- eventTags.push(["e", rootEvent[1], rootEvent[2] || "", "root"]);
+ eventTags.push([
+ "e",
+ rootEvent[1],
+ rootEvent[2] || relayHint,
+ "root",
+ ]);
}
- eventTags.push(["e", replyEvent.id, replyEvent.relay || "", "reply"]);
+ eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
eventTags.push(["p", replyEvent.pubkey]);
}
}
+ if (nsfw) {
+ eventTags.push(["L", "content-warning"]);
+ eventTags.push(["l", "reason", "content-warning"]);
+ eventTags.push(["content-warning", "nsfw"]);
+ }
+
const cmd: string = await invoke("publish", {
content: eventContent,
tags: eventTags,
@@ -605,6 +625,7 @@ export class Ark {
key: NSTORE_KEYS.settings,
});
const settings: Settings = cmd ? JSON.parse(cmd) : null;
+ this.settings = settings;
return settings;
} catch {
const defaultSettings: Settings = {
@@ -612,7 +633,9 @@ export class Ark {
enhancedPrivacy: false,
notification: false,
zap: false,
+ nsfw: false,
};
+ this.settings = defaultSettings;
return defaultSettings;
}
}
diff --git a/packages/icons/index.ts b/packages/icons/index.ts
index d0a5f4ea..516179e4 100644
--- a/packages/icons/index.ts
+++ b/packages/icons/index.ts
@@ -123,3 +123,4 @@ export * from "./src/laurel";
export * from "./src/quote";
export * from "./src/key";
export * from "./src/remote";
+export * from "./src/nsfw";
diff --git a/packages/icons/src/addMedia.tsx b/packages/icons/src/addMedia.tsx
index 29884065..71f20f80 100644
--- a/packages/icons/src/addMedia.tsx
+++ b/packages/icons/src/addMedia.tsx
@@ -1,20 +1,13 @@
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
- return (
-
- );
+ return (
+
+ );
}
diff --git a/packages/icons/src/nsfw.tsx b/packages/icons/src/nsfw.tsx
new file mode 100644
index 00000000..8bf8d9d5
--- /dev/null
+++ b/packages/icons/src/nsfw.tsx
@@ -0,0 +1,13 @@
+export function NsfwIcon(props: JSX.IntrinsicElements["svg"]) {
+ return (
+
+ );
+}
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 98d5dd2d..21f056ac 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -3,6 +3,8 @@ export interface Settings {
enhancedPrivacy: boolean;
autoUpdate: boolean;
zap: boolean;
+ nsfw: boolean;
+ [key: string]: string | number | boolean;
}
export interface Keys {
diff --git a/packages/ui/src/note/index.ts b/packages/ui/src/note/index.ts
index ac3d0de3..c4ffd9c7 100644
--- a/packages/ui/src/note/index.ts
+++ b/packages/ui/src/note/index.ts
@@ -20,7 +20,6 @@ export const Note = {
Pin: NotePin,
Content: NoteContent,
Zap: NoteZap,
- Pin: NotePin,
Child: NoteChild,
Thread: NoteThread,
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 47b3262d..e2087896 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,6 +87,9 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.0.7
+ version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/query-sync-storage-persister':
specifier: ^5.29.0
version: 5.29.0
@@ -259,7 +262,7 @@ importers:
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
- version: 1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
+ version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.29.0
version: 5.29.0(react@18.2.0)
@@ -410,7 +413,7 @@ importers:
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
- version: 1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
+ version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.29.0
version: 5.29.0(react@18.2.0)
@@ -2283,7 +2286,7 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
- /@radix-ui/react-tooltip@1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
+ /@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
peerDependencies:
'@types/react': '*'
@@ -2308,8 +2311,9 @@ packages:
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.75)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.75)(react@18.2.0)
- '@radix-ui/react-visually-hidden': 1.0.3(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.75
+ '@types/react-dom': 18.2.24
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@@ -2416,7 +2420,7 @@ packages:
react: 18.2.0
dev: false
- /@radix-ui/react-visually-hidden@1.0.3(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
+ /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
peerDependencies:
'@types/react': '*'
@@ -2432,6 +2436,7 @@ packages:
'@babel/runtime': 7.24.4
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.75
+ '@types/react-dom': 18.2.24
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index f4ca0210..a6f413e6 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -126,8 +126,6 @@ fn main() {
nostr::event::get_event_thread,
nostr::event::publish,
nostr::event::repost,
- nostr::event::upvote,
- nostr::event::downvote,
commands::folder::show_in_folder,
commands::folder::get_accounts,
commands::opg::fetch_opg,
diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs
index 3de7fa4d..c8f38a04 100644
--- a/src-tauri/src/nostr/event.rs
+++ b/src-tauri/src/nostr/event.rs
@@ -222,16 +222,15 @@ pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result>,
+ tags: Vec>,
state: State<'_, Nostr>,
) -> Result {
let client = &state.client;
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
- if let Ok(event_id) = client.publish_text_note(content, final_tags).await {
- Ok(event_id.to_bech32().unwrap())
- } else {
- Err("Publish text note failed".into())
+ match client.publish_text_note(content, final_tags).await {
+ Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
+ Err(err) => Err(err.to_string()),
}
}
@@ -246,27 +245,3 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result) -> Result {
- let client = &state.client;
- let event = Event::from_json(raw).unwrap();
-
- if let Ok(event_id) = client.like(&event).await {
- Ok(event_id)
- } else {
- Err("Upvote failed".into())
- }
-}
-
-#[tauri::command]
-pub async fn downvote(raw: &str, state: State<'_, Nostr>) -> Result {
- let client = &state.client;
- let event = Event::from_json(raw).unwrap();
-
- if let Ok(event_id) = client.dislike(&event).await {
- Ok(event_id)
- } else {
- Err("Downvote failed".into())
- }
-}