mirror of
https://github.com/lumehq/lume.git
synced 2025-08-06 22:26:25 +02:00
feat: re add group column
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.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 existed = columns.find((item) => item.label === column.label);
|
||||
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",
|
||||
});
|
||||
setColumns((state) => [...state, column]);
|
||||
};
|
||||
|
||||
const remove = (label: string) => {
|
||||
const newColumns = columns.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);
|
||||
setColumns((state) => state.filter((t) => t.label !== label));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ark.set_columns(columns);
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn = undefined;
|
||||
|
||||
|
@@ -1,30 +1,44 @@
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { Suggest } from "@/components/suggest";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { useEvents } from "@lume/ark";
|
||||
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 { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { id, name, account } = Route.useSearch();
|
||||
const { label, name, account } = Route.useSearch();
|
||||
const { ark } = Route.useRouteContext();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useEvents("local", account);
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: [name, account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const events = await ark.get_events(20, pageParam);
|
||||
return events;
|
||||
},
|
||||
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) => {
|
||||
if (!event) return;
|
||||
@@ -38,9 +52,9 @@ export function Screen() {
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header id={id} name={name} />
|
||||
<Column.Header label={label} name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
{isLoading ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</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";
|
||||
|
||||
export const Route = createFileRoute("/group")({
|
||||
component: Screen,
|
||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
@@ -18,14 +17,23 @@ export const Route = createFileRoute("/group")({
|
||||
name: search.name,
|
||||
};
|
||||
},
|
||||
beforeLoad: async ({ context }) => {
|
||||
beforeLoad: async ({ search, context }) => {
|
||||
const ark = context.ark;
|
||||
if (!ark) {
|
||||
const groups = await ark.get_nstore(`lume_group_${search.label}`);
|
||||
|
||||
if (!groups) {
|
||||
throw redirect({
|
||||
to: "/group/create",
|
||||
to: "/create-group",
|
||||
replace: false,
|
||||
search,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
groups,
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
|
@@ -19,7 +19,6 @@ enum NSTORE_KEYS {
|
||||
settings = "lume_user_settings",
|
||||
interests = "lume_user_interests",
|
||||
columns = "lume_user_columns",
|
||||
group = "lume_group_",
|
||||
}
|
||||
|
||||
export class Ark {
|
||||
@@ -654,11 +653,36 @@ export class Ark {
|
||||
content: JSON.stringify(interests),
|
||||
});
|
||||
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 {
|
||||
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) {
|
||||
try {
|
||||
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 (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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"
|
||||
></path>
|
||||
d="M5.75 12.75v3.5a2 2 0 0 0 2 2h3.5m1.5-12.5h3.5a2 2 0 0 1 2 2v3.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
||||
import { CancelIcon, ExpandIcon, RefreshIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { ReactNode } from "react";
|
||||
|
@@ -56,15 +56,11 @@ fn main() {
|
||||
client
|
||||
.add_relay("wss://relay.nostr.band")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
client
|
||||
.add_relay("wss://relay.damus.io")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
.expect("Cannot connect to relay.nostr.band, please try again later.");
|
||||
client
|
||||
.add_relay("wss://purplepag.es")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
.expect("Cannot connect to purplepag.es, please try again later.");
|
||||
|
||||
// Connect
|
||||
client.connect().await;
|
||||
|
Reference in New Issue
Block a user