feat: re add group column
This commit is contained in:
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-primary {
|
.shadow-primary {
|
||||||
filter: drop-shadow(0px 0px 4px rgba(66, 65, 73, 0.14));
|
box-shadow: 0px 0px 4px rgba(66, 65, 73, 0.14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,43 +54,17 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const add = (column: LumeColumn) => {
|
const add = (column: LumeColumn) => {
|
||||||
const existed = columns.find((item) => item.label === column.label);
|
setColumns((state) => [...state, column]);
|
||||||
if (!existed) {
|
|
||||||
const lastColIndex = columns.findIndex((item) => item.label === "open");
|
|
||||||
const newColumns = [
|
|
||||||
...columns.slice(0, lastColIndex),
|
|
||||||
column,
|
|
||||||
...columns.slice(lastColIndex),
|
|
||||||
];
|
|
||||||
|
|
||||||
// update state
|
|
||||||
setColumns(newColumns);
|
|
||||||
setSelectedIndex(newColumns.length - 1);
|
|
||||||
|
|
||||||
// save state
|
|
||||||
ark.set_columns(newColumns);
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to new column
|
|
||||||
vlistRef.current.scrollToIndex(columns.length - 1, {
|
|
||||||
align: "center",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = (label: string) => {
|
const remove = (label: string) => {
|
||||||
const newColumns = columns.filter((t) => t.label !== label);
|
setColumns((state) => state.filter((t) => t.label !== label));
|
||||||
|
|
||||||
// update state
|
|
||||||
setColumns(newColumns);
|
|
||||||
setSelectedIndex(newColumns.length - 1);
|
|
||||||
vlistRef.current.scrollToIndex(newColumns.length - 1, {
|
|
||||||
align: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
// save state
|
|
||||||
ark.set_columns(newColumns);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ark.set_columns(columns);
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: UnlistenFn = undefined;
|
let unlisten: UnlistenFn = undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,44 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { Suggest } from "@/components/suggest";
|
import { Suggest } from "@/components/suggest";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { useEvents } from "@lume/ark";
|
|
||||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||||
import { Event, Kind } from "@lume/types";
|
import { ColumnRouteSearch, Event, Kind } from "@lume/types";
|
||||||
import { Column } from "@lume/ui";
|
import { Column } from "@lume/ui";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/antenas")({
|
export const Route = createFileRoute("/antenas")({
|
||||||
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
|
return {
|
||||||
|
account: search.account,
|
||||||
|
label: search.label,
|
||||||
|
name: search.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
// @ts-ignore, just work!!!
|
const { label, name, account } = Route.useSearch();
|
||||||
const { id, name, account } = Route.useSearch();
|
const { ark } = Route.useRouteContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||||
data,
|
useInfiniteQuery({
|
||||||
hasNextPage,
|
queryKey: [name, account],
|
||||||
isLoading,
|
initialPageParam: 0,
|
||||||
isRefetching,
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
isFetchingNextPage,
|
const events = await ark.get_events(20, pageParam);
|
||||||
fetchNextPage,
|
return events;
|
||||||
} = useEvents("local", account);
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage?.at(-1);
|
||||||
|
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: Event) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
@@ -38,9 +52,9 @@ export function Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Column.Root>
|
<Column.Root>
|
||||||
<Column.Header id={id} name={name} />
|
<Column.Header label={label} name={name} />
|
||||||
<Column.Content>
|
<Column.Content>
|
||||||
{isLoading || isRefetching ? (
|
{isLoading ? (
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
116
apps/desktop2/src/routes/create-group.tsx
Normal file
116
apps/desktop2/src/routes/create-group.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { CheckCircleIcon } from "@lume/icons";
|
||||||
|
import { ColumnRouteSearch } from "@lume/types";
|
||||||
|
import { Column, User } from "@lume/ui";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/create-group")({
|
||||||
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
|
return {
|
||||||
|
account: search.account,
|
||||||
|
label: search.label,
|
||||||
|
name: search.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
const contacts = await ark.get_contact_list();
|
||||||
|
return contacts;
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const contacts = Route.useLoaderData();
|
||||||
|
|
||||||
|
const { ark } = Route.useRouteContext();
|
||||||
|
const { label, name } = Route.useSearch();
|
||||||
|
|
||||||
|
const [title, setTitle] = useState<string>("Just a new group");
|
||||||
|
const [users, setUsers] = useState<Array<string>>([]);
|
||||||
|
const [isDone, setIsDone] = useState(false);
|
||||||
|
|
||||||
|
const toggleUser = (pubkey: string) => {
|
||||||
|
const arr = users.includes(pubkey)
|
||||||
|
? users.filter((i) => i !== pubkey)
|
||||||
|
: [...users, pubkey];
|
||||||
|
setUsers(arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
if (isDone) return history.back();
|
||||||
|
|
||||||
|
const groups = await ark.set_nstore(
|
||||||
|
`lume_group_${label}`,
|
||||||
|
JSON.stringify(users),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groups) setIsDone(true);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column.Root>
|
||||||
|
<Column.Header label={label} name={name} />
|
||||||
|
<Column.Content>
|
||||||
|
<div className="flex flex-col gap-5 p-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor="name" className="font-medium">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Nostrichs..."
|
||||||
|
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="inline-flex items-center justify-between">
|
||||||
|
<span className="font-medium">Pick user</span>
|
||||||
|
<span className="text-xs text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{contacts.map((item: string) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleUser(item)}
|
||||||
|
className="inline-flex items-center justify-between px-3 py-2 rounded-xl bg-neutral-50 dark:bg-neutral-950 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={item}>
|
||||||
|
<User.Root className="flex items-center gap-2.5">
|
||||||
|
<User.Avatar className="size-10 rounded-full object-cover" />
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<User.Name className="font-medium" />
|
||||||
|
<User.NIP05 className="text-neutral-700 dark:text-neutral-300" />
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
{users.includes(item) ? (
|
||||||
|
<CheckCircleIcon className="size-5 text-teal-500" />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="fixed z-10 flex items-center justify-center w-full bottom-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={submit}
|
||||||
|
disabled={users.length < 1}
|
||||||
|
className="inline-flex items-center justify-center px-4 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isDone ? "Back" : "Update"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Column.Content>
|
||||||
|
</Column.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/group/create')({
|
|
||||||
component: () => <div>Hello /group/create!</div>
|
|
||||||
})
|
|
||||||
@@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/group")({
|
export const Route = createFileRoute("/group")({
|
||||||
component: Screen,
|
|
||||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
return {
|
return {
|
||||||
account: search.account,
|
account: search.account,
|
||||||
@@ -18,14 +17,23 @@ export const Route = createFileRoute("/group")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
if (!ark) {
|
const groups = await ark.get_nstore(`lume_group_${search.label}`);
|
||||||
|
|
||||||
|
if (!groups) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/group/create",
|
to: "/create-group",
|
||||||
|
replace: false,
|
||||||
|
search,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ enum NSTORE_KEYS {
|
|||||||
settings = "lume_user_settings",
|
settings = "lume_user_settings",
|
||||||
interests = "lume_user_interests",
|
interests = "lume_user_interests",
|
||||||
columns = "lume_user_columns",
|
columns = "lume_user_columns",
|
||||||
group = "lume_group_",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Ark {
|
export class Ark {
|
||||||
@@ -654,11 +653,36 @@ export class Ark {
|
|||||||
content: JSON.stringify(interests),
|
content: JSON.stringify(interests),
|
||||||
});
|
});
|
||||||
return cmd;
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get_nstore(key: string) {
|
||||||
|
try {
|
||||||
|
const cmd: string = await invoke("get_nstore", {
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
const parse: string | string[] = cmd ? JSON.parse(cmd) : null;
|
||||||
|
if (!parse.length) return null;
|
||||||
|
return parse;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async set_nstore(key: string, content: string) {
|
||||||
|
try {
|
||||||
|
const cmd: string = await invoke("set_nstore", {
|
||||||
|
key,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public open_thread(id: string) {
|
public open_thread(id: string) {
|
||||||
try {
|
try {
|
||||||
const window = new WebviewWindow(`event-${id}`, {
|
const window = new WebviewWindow(`event-${id}`, {
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function ExpandIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function ExpandIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M13.75 3.75h5.75a.75.75 0 01.75.75v5.75m-16.5 3.5v5.75c0 .414.336.75.75.75h5.75M19.5 4.5L14 10m-4 4l-5.5 5.5"
|
d="M5.75 12.75v3.5a2 2 0 0 0 2 2h3.5m1.5-12.5h3.5a2 2 0 0 1 2 2v3.5"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
import { CancelIcon, ExpandIcon, RefreshIcon } from "@lume/icons";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|||||||
@@ -56,15 +56,11 @@ fn main() {
|
|||||||
client
|
client
|
||||||
.add_relay("wss://relay.nostr.band")
|
.add_relay("wss://relay.nostr.band")
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.expect("Cannot connect to relay.nostr.band, please try again later.");
|
||||||
client
|
|
||||||
.add_relay("wss://relay.damus.io")
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
client
|
client
|
||||||
.add_relay("wss://purplepag.es")
|
.add_relay("wss://purplepag.es")
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.expect("Cannot connect to purplepag.es, please try again later.");
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user