mirror of
https://github.com/lumehq/lume.git
synced 2025-03-28 18:52:33 +01:00
feat: support nip-36
This commit is contained in:
parent
09b143cb08
commit
94d400cab2
@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/query-sync-storage-persister": "^5.29.0",
|
"@tanstack/query-sync-storage-persister": "^5.29.0",
|
||||||
"@tanstack/react-query": "^5.29.0",
|
"@tanstack/react-query": "^5.29.0",
|
||||||
"@tanstack/react-query-persist-client": "^5.29.0",
|
"@tanstack/react-query-persist-client": "^5.29.0",
|
||||||
|
@ -10,7 +10,7 @@ import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
|||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDebounce, useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { VList, VListHandle } from "virtua";
|
import { VList, VListHandle } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/home")({
|
export const Route = createFileRoute("/$account/home")({
|
||||||
|
@ -2,8 +2,15 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
|||||||
import { type Ark } from "@lume/ark";
|
import { type Ark } from "@lume/ark";
|
||||||
import { type QueryClient } from "@tanstack/react-query";
|
import { type QueryClient } from "@tanstack/react-query";
|
||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
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 { Spinner } from "@lume/ui";
|
||||||
|
import { type Descendant } from "slate";
|
||||||
|
|
||||||
|
type EditorElement = {
|
||||||
|
type: string;
|
||||||
|
children: Descendant[];
|
||||||
|
eventId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface RouterContext {
|
interface RouterContext {
|
||||||
ark: Ark;
|
ark: Ark;
|
||||||
@ -13,6 +20,7 @@ interface RouterContext {
|
|||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
interests?: Interests;
|
interests?: Interests;
|
||||||
accounts?: Account[];
|
accounts?: Account[];
|
||||||
|
initialValue?: EditorElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
|
@ -69,6 +69,13 @@ function Screen() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleNsfw = () => {
|
||||||
|
setNewSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
nsfw: !newSettings.nsfw,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
// start loading
|
// start loading
|
||||||
@ -167,18 +174,28 @@ function Screen() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-50 px-5 py-4 dark:bg-neutral-950">
|
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
<Switch.Root
|
||||||
There are many more settings you can configure from the 'Settings'
|
checked={newSettings.nsfw}
|
||||||
Screen. Be sure to visit it later.
|
onClick={() => toggleNsfw()}
|
||||||
</p>
|
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"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold">Filter sensitive content</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
By default, Lume will display all content which have Content
|
||||||
|
Warning tag, it's may include NSFW content.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{t("global.continue")}
|
{t("global.continue")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,6 +7,7 @@ import { getCurrent } from "@tauri-apps/api/window";
|
|||||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
export function MediaButton({ className }: { className?: string }) {
|
export function MediaButton({ className }: { className?: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
const { ark } = useRouteContext({ strict: false });
|
||||||
@ -16,14 +17,13 @@ export function MediaButton({ className }: { className?: string }) {
|
|||||||
|
|
||||||
const uploadToNostrBuild = async () => {
|
const uploadToNostrBuild = async () => {
|
||||||
try {
|
try {
|
||||||
|
// start loading
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const image = await ark.upload();
|
const image = await ark.upload();
|
||||||
|
insertImage(editor, image);
|
||||||
|
|
||||||
if (image) {
|
// reset loading
|
||||||
insertImage(editor, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -63,17 +63,29 @@ export function MediaButton({ className }: { className?: string }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Tooltip.Provider>
|
||||||
type="button"
|
<Tooltip.Root delayDuration={150}>
|
||||||
onClick={() => uploadToNostrBuild()}
|
<Tooltip.Trigger asChild>
|
||||||
disabled={loading}
|
<button
|
||||||
className={cn("inline-flex items-center justify-center", className)}
|
type="button"
|
||||||
>
|
onClick={() => uploadToNostrBuild()}
|
||||||
{loading ? (
|
disabled={loading}
|
||||||
<Spinner className="size-5" />
|
className={cn("inline-flex items-center justify-center", className)}
|
||||||
) : (
|
>
|
||||||
<AddMediaIcon className="size-5" />
|
{loading ? (
|
||||||
)}
|
<Spinner className="size-4" />
|
||||||
</button>
|
) : (
|
||||||
|
<AddMediaIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Upload media
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
40
apps/desktop2/src/routes/editor/-components/nsfw.tsx
Normal file
40
apps/desktop2/src/routes/editor/-components/nsfw.tsx
Normal file
@ -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<SetStateAction<boolean>>;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root delayDuration={150}>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNsfw((prev) => !prev)}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center",
|
||||||
|
className,
|
||||||
|
nsfw ? "bg-blue-500 text-white" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NsfwIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Mark as sensitive content
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { LoaderIcon, TrashIcon } from "@lume/icons";
|
import { ComposeFilledIcon, NsfwIcon, TrashIcon } from "@lume/icons";
|
||||||
import {
|
import {
|
||||||
Portal,
|
Portal,
|
||||||
cn,
|
cn,
|
||||||
@ -35,11 +35,11 @@ import { Spinner, User } from "@lume/ui";
|
|||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { NsfwToggle } from "./-components/nsfw";
|
||||||
|
|
||||||
type EditorElement = {
|
type EditorSearch = {
|
||||||
type: string;
|
reply_to: string;
|
||||||
children: Descendant[];
|
quote: boolean;
|
||||||
eventId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const contactQueryOptions = queryOptions({
|
const contactQueryOptions = queryOptions({
|
||||||
@ -51,46 +51,48 @@ const contactQueryOptions = queryOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/editor/")({
|
export const Route = createFileRoute("/editor/")({
|
||||||
loader: ({ context }) =>
|
validateSearch: (search: Record<string, string>): EditorSearch => {
|
||||||
context.queryClient.ensureQueryData(contactQueryOptions),
|
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,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
pendingComponent: Pending,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
// @ts-ignore, useless
|
|
||||||
const { reply_to, quote } = Route.useSearch();
|
const { reply_to, quote } = Route.useSearch();
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark, initialValue } = 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<HTMLDivElement | null>();
|
|
||||||
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
const [editorValue, setEditorValue] = useState(initialValue);
|
||||||
@ -98,10 +100,14 @@ function Screen() {
|
|||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [nsfw, setNsfw] = useState(false);
|
||||||
const [editor] = useState(() =>
|
const [editor] = useState(() =>
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement | null>();
|
||||||
|
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
|
||||||
|
|
||||||
const filters = contacts
|
const filters = contacts
|
||||||
?.filter((c) =>
|
?.filter((c) =>
|
||||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
||||||
@ -204,15 +210,25 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
|
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2"
|
||||||
>
|
>
|
||||||
<MediaButton className="size-9 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
|
<NsfwToggle
|
||||||
|
nsfw={nsfw}
|
||||||
|
setNsfw={setNsfw}
|
||||||
|
className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||||
|
/>
|
||||||
|
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={publish}
|
onClick={publish}
|
||||||
className="inline-flex h-9 w-24 items-center justify-center rounded-full bg-blue-500 px-3 font-medium text-white hover:bg-blue-600"
|
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{loading ? <Spinner className="size-5" /> : t("global.post")}
|
{loading ? (
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ComposeFilledIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
{t("global.post")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
|
@ -23,9 +23,11 @@ enum NSTORE_KEYS {
|
|||||||
|
|
||||||
export class Ark {
|
export class Ark {
|
||||||
public windows: WebviewWindow[];
|
public windows: WebviewWindow[];
|
||||||
|
public settings: Settings;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
|
this.settings = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_all_accounts() {
|
public async get_all_accounts() {
|
||||||
@ -144,7 +146,6 @@ export class Ark {
|
|||||||
|
|
||||||
if (asOf && asOf > 0) until = asOf.toString();
|
if (asOf && asOf > 0) until = asOf.toString();
|
||||||
|
|
||||||
const dedup = true;
|
|
||||||
const seenIds = new Set<string>();
|
const seenIds = new Set<string>();
|
||||||
const dedupQueue = new Set<string>();
|
const dedupQueue = new Set<string>();
|
||||||
|
|
||||||
@ -155,31 +156,37 @@ export class Ark {
|
|||||||
global: isGlobal,
|
global: isGlobal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dedup) {
|
for (const event of nostrEvents) {
|
||||||
for (const event of nostrEvents) {
|
const tags = event.tags
|
||||||
const tags = event.tags
|
.filter((el) => el[0] === "e")
|
||||||
.filter((el) => el[0] === "e")
|
?.map((item) => item[1]);
|
||||||
?.map((item) => item[1]);
|
|
||||||
|
|
||||||
if (tags.length) {
|
if (tags.length) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (seenIds.has(tag)) {
|
if (seenIds.has(tag)) {
|
||||||
dedupQueue.add(event.id);
|
dedupQueue.add(event.id);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
seenIds.add(tag);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} catch (e) {
|
||||||
console.error(String(e));
|
console.info(String(e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,7 +236,12 @@ export class Ark {
|
|||||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
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 {
|
try {
|
||||||
const g = await generateContentTags(content);
|
const g = await generateContentTags(content);
|
||||||
|
|
||||||
@ -238,26 +250,34 @@ export class Ark {
|
|||||||
|
|
||||||
if (reply_to) {
|
if (reply_to) {
|
||||||
const replyEvent = await this.get_event(reply_to);
|
const replyEvent = await this.get_event(reply_to);
|
||||||
|
const relayHint =
|
||||||
|
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
|
||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
eventTags.push([
|
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||||
"e",
|
|
||||||
replyEvent.id,
|
|
||||||
replyEvent.relay || "",
|
|
||||||
"mention",
|
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||||
|
|
||||||
if (rootEvent) {
|
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]);
|
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", {
|
const cmd: string = await invoke("publish", {
|
||||||
content: eventContent,
|
content: eventContent,
|
||||||
tags: eventTags,
|
tags: eventTags,
|
||||||
@ -605,6 +625,7 @@ export class Ark {
|
|||||||
key: NSTORE_KEYS.settings,
|
key: NSTORE_KEYS.settings,
|
||||||
});
|
});
|
||||||
const settings: Settings = cmd ? JSON.parse(cmd) : null;
|
const settings: Settings = cmd ? JSON.parse(cmd) : null;
|
||||||
|
this.settings = settings;
|
||||||
return settings;
|
return settings;
|
||||||
} catch {
|
} catch {
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
@ -612,7 +633,9 @@ export class Ark {
|
|||||||
enhancedPrivacy: false,
|
enhancedPrivacy: false,
|
||||||
notification: false,
|
notification: false,
|
||||||
zap: false,
|
zap: false,
|
||||||
|
nsfw: false,
|
||||||
};
|
};
|
||||||
|
this.settings = defaultSettings;
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,3 +123,4 @@ export * from "./src/laurel";
|
|||||||
export * from "./src/quote";
|
export * from "./src/quote";
|
||||||
export * from "./src/key";
|
export * from "./src/key";
|
||||||
export * from "./src/remote";
|
export * from "./src/remote";
|
||||||
|
export * from "./src/nsfw";
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
|
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path
|
||||||
width="24"
|
stroke="currentColor"
|
||||||
height="24"
|
strokeLinecap="round"
|
||||||
fill="none"
|
strokeLinejoin="round"
|
||||||
viewBox="0 0 24 24"
|
strokeWidth="1.5"
|
||||||
{...props}
|
d="M15.25 8.75v-4a2 2 0 0 0-2-2h-8.5a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2h4M3.1 11.9l1.794-1.176a2 2 0 0 1 2.206.01l1.279.852M6 6.25h.5m8 8.75h.5M6.75 6.25a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Zm7 6.95v3.6l2.8-1.8-2.8-1.8Zm5.5 8.05h-8.5a2 2 0 0 1-2-2v-8.5a2 2 0 0 1 2-2h8.5a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2Z"
|
||||||
>
|
/>
|
||||||
<path
|
</svg>
|
||||||
stroke="currentColor"
|
);
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M19 22v-3m0 0v-3m0 3h-3m3 0h3m0-5.648V11l-.001-1m-9.464 11H10c-.756 0-1.41 0-1.983-.01M22 10H21c-1.393 0-2.09 0-2.676.06A11.5 11.5 0 008.06 20.324c-.02.2-.034.415-.043.665M22 10c-.008-2.15-.068-3.336-.544-4.27a5 5 0 00-2.185-2.185C18.2 3 16.8 3 14 3h-4c-2.8 0-4.2 0-5.27.545A5 5 0 002.545 5.73C2 6.8 2 8.2 2 11v2c0 2.8 0 4.2.545 5.27a5 5 0 002.185 2.185c.78.398 1.738.505 3.287.534M7.5 9.5a1 1 0 110-2 1 1 0 010 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
13
packages/icons/src/nsfw.tsx
Normal file
13
packages/icons/src/nsfw.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export function NsfwIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M4.75 18.75v1.5a1 1 0 0 0 1 1h12.5a1 1 0 0 0 1-1v-1.5a2 2 0 0 0-2-2H6.75a2 2 0 0 0-2 2Zm2-2V12a5.25 5.25 0 0 1 10.5 0v4.75M12 1.75v1.025M21.225 12h1.025M2.775 12H1.75m16.773-6.523.725-.725m-13.771.725-.725-.725M12 16.75v-3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@ -3,6 +3,8 @@ export interface Settings {
|
|||||||
enhancedPrivacy: boolean;
|
enhancedPrivacy: boolean;
|
||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
zap: boolean;
|
zap: boolean;
|
||||||
|
nsfw: boolean;
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Keys {
|
export interface Keys {
|
||||||
|
@ -20,7 +20,6 @@ export const Note = {
|
|||||||
Pin: NotePin,
|
Pin: NotePin,
|
||||||
Content: NoteContent,
|
Content: NoteContent,
|
||||||
Zap: NoteZap,
|
Zap: NoteZap,
|
||||||
Pin: NotePin,
|
|
||||||
Child: NoteChild,
|
Child: NoteChild,
|
||||||
Thread: NoteThread,
|
Thread: NoteThread,
|
||||||
};
|
};
|
||||||
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -87,6 +87,9 @@ importers:
|
|||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.0.3
|
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)
|
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':
|
'@tanstack/query-sync-storage-persister':
|
||||||
specifier: ^5.29.0
|
specifier: ^5.29.0
|
||||||
version: 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)
|
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':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
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':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.29.0
|
specifier: ^5.29.0
|
||||||
version: 5.29.0(react@18.2.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)
|
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':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
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':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.29.0
|
specifier: ^5.29.0
|
||||||
version: 5.29.0(react@18.2.0)
|
version: 5.29.0(react@18.2.0)
|
||||||
@ -2283,7 +2286,7 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@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-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-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-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': 18.2.75
|
||||||
|
'@types/react-dom': 18.2.24
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
@ -2416,7 +2420,7 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
@ -2432,6 +2436,7 @@ packages:
|
|||||||
'@babel/runtime': 7.24.4
|
'@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)
|
'@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': 18.2.75
|
||||||
|
'@types/react-dom': 18.2.24
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -126,8 +126,6 @@ fn main() {
|
|||||||
nostr::event::get_event_thread,
|
nostr::event::get_event_thread,
|
||||||
nostr::event::publish,
|
nostr::event::publish,
|
||||||
nostr::event::repost,
|
nostr::event::repost,
|
||||||
nostr::event::upvote,
|
|
||||||
nostr::event::downvote,
|
|
||||||
commands::folder::show_in_folder,
|
commands::folder::show_in_folder,
|
||||||
commands::folder::get_accounts,
|
commands::folder::get_accounts,
|
||||||
commands::opg::fetch_opg,
|
commands::opg::fetch_opg,
|
||||||
|
@ -222,16 +222,15 @@ pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<E
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn publish(
|
pub async fn publish(
|
||||||
content: &str,
|
content: &str,
|
||||||
tags: Vec<Vec<String>>,
|
tags: Vec<Vec<&str>>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
|
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 {
|
match client.publish_text_note(content, final_tags).await {
|
||||||
Ok(event_id.to_bech32().unwrap())
|
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
||||||
} else {
|
Err(err) => Err(err.to_string()),
|
||||||
Err("Publish text note failed".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,27 +245,3 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
|
|||||||
Err("Repost failed".into())
|
Err("Repost failed".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn upvote(raw: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
|
||||||
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<EventId, String> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user