feat: re add group column

This commit is contained in:
2024-04-09 14:05:50 +07:00
parent 420be77b5c
commit 5e6692cd6d
10 changed files with 200 additions and 78 deletions

View File

@@ -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);
} }
} }

View File

@@ -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;

View File

@@ -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>

View 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>
);
}

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/group/create')({
component: () => <div>Hello /group/create!</div>
})

View File

@@ -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() {

View File

@@ -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}`, {

View File

@@ -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>
); );
} }

View File

@@ -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";

View File

@@ -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;