This commit is contained in:
reya 2024-10-27 15:57:32 +07:00
parent eb6e3e52df
commit 0518389f50
17 changed files with 306 additions and 349 deletions

4
src-tauri/Cargo.lock generated
View File

@ -3081,7 +3081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@ -7223,7 +7223,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@ -11,11 +11,10 @@ rust-version = "1.70"
tauri-build = { version = "2.0.0", features = [] } tauri-build = { version = "2.0.0", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0", features = [ tauri = { version = "2.0.0", features = [ "protocol-asset",
"unstable", "unstable",
"tray-icon", "tray-icon",
"macos-private-api", "macos-private-api"
"protocol-asset",
] } ] }
tauri-plugin-window-state = "2.0.0" tauri-plugin-window-state = "2.0.0"
tauri-plugin-clipboard-manager = "2.0.0" tauri-plugin-clipboard-manager = "2.0.0"

View File

@ -281,18 +281,33 @@ pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result<String, St
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_all_groups(id: String, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> { pub async fn get_all_newsfeeds(
id: String,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client; let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
let filter = Filter::new().kind(Kind::FollowSet).author(public_key);
match client let groups = Filter::new().kind(Kind::FollowSet).author(public_key);
.fetch_events(vec![filter], Some(Duration::from_secs(3))) let contacts = Filter::new()
.kind(Kind::ContactList)
.author(public_key)
.limit(1);
let remote_events = client
.fetch_events(vec![groups], Some(Duration::from_secs(3)))
.await .await
{ .map_err(|e| e.to_string())?;
Ok(events) => Ok(process_event(client, events, false).await),
Err(e) => Err(e.to_string()), let contact_events = client
} .fetch_events(vec![contacts], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
let events = remote_events.merge(contact_events);
let alt_events = process_event(client, events, false).await;
Ok(alt_events)
} }
#[tauri::command] #[tauri::command]

View File

@ -47,7 +47,17 @@ pub async fn create_column(
main_window: Window, main_window: Window,
) -> Result<String, String> { ) -> Result<String, String> {
match app_handle.get_webview(&column.label) { match app_handle.get_webview(&column.label) {
Some(_) => Ok(column.label), Some(webview) => {
if let Err(e) = webview.set_size(LogicalSize::new(column.width, column.height)) {
return Err(e.to_string());
}
if let Err(e) = webview.set_position(LogicalPosition::new(column.x, column.y)) {
return Err(e.to_string());
}
Ok(column.label)
}
None => { None => {
let path = PathBuf::from(column.url); let path = PathBuf::from(column.url);
let webview_url = WebviewUrl::App(path); let webview_url = WebviewUrl::App(path);
@ -145,28 +155,15 @@ pub async fn close_column(label: String, app_handle: tauri::AppHandle) -> Result
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn update_column( pub async fn close_all_columns(app_handle: tauri::AppHandle) -> Result<(), String> {
label: String, let mut webviews = app_handle.webviews();
width: f32, webviews.remove("main");
height: f32,
x: f32,
y: f32,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
match app_handle.get_webview(&label) {
Some(webview) => {
if let Err(e) = webview.set_size(LogicalSize::new(width, height)) {
return Err(e.to_string());
}
if let Err(e) = webview.set_position(LogicalPosition::new(x, y)) { for webview in webviews.values() {
return Err(e.to_string()); webview.close().unwrap()
}
Ok(())
}
None => Err("Cannot update, column not found.".into()),
} }
Ok(())
} }
#[tauri::command] #[tauri::command]

View File

@ -97,7 +97,7 @@ fn main() {
get_all_profiles, get_all_profiles,
set_group, set_group,
get_group, get_group,
get_all_groups, get_all_newsfeeds,
set_interest, set_interest,
get_interest, get_interest,
get_all_interests, get_all_interests,
@ -129,9 +129,9 @@ fn main() {
event_to_bech32, event_to_bech32,
user_to_bech32, user_to_bech32,
create_column, create_column,
update_column,
reload_column, reload_column,
close_column, close_column,
close_all_columns,
open_window, open_window,
]); ]);
@ -288,48 +288,46 @@ fn main() {
let _ = client let _ = client
.handle_notifications(|notification| async { .handle_notifications(|notification| async {
#[allow(clippy::collapsible_match)] if let RelayPoolNotification::Event {
if let RelayPoolNotification::Message { message, .. } = notification { event,
if let RelayMessage::Event { subscription_id,
event, ..
subscription_id, } = notification
} = message {
{ // Handle events from notification subscription
// Handle events from notification subscription if subscription_id == notification_id {
if subscription_id == notification_id { // Send native notification
// Send native notification if allow_notification {
if allow_notification { let author = client
let author = client .database()
.database() .profile(event.pubkey)
.profile(event.pubkey) .await
.await .unwrap_or_else(|_| {
.unwrap_or_else(|_| { DatabaseProfile::new(event.pubkey, Metadata::new())
DatabaseProfile::new(event.pubkey, Metadata::new()) });
});
send_event_notification( send_event_notification(
&event, &event,
author.metadata(), author.metadata(),
&handle_clone, &handle_clone,
); );
} }
} else if event.kind != Kind::RelayList { } else if event.kind != Kind::RelayList {
let payload = RichEvent { let payload = RichEvent {
raw: event.as_json(), raw: event.as_json(),
parsed: if event.kind == Kind::TextNote { parsed: if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await) Some(parse_event(&event.content).await)
} else { } else {
None None
}, },
}; };
if let Err(e) = handle_clone.emit_to( if let Err(e) = handle_clone.emit_to(
EventTarget::labeled(subscription_id.to_string()), EventTarget::labeled(subscription_id.to_string()),
"event", "event",
payload, payload,
) { ) {
println!("Emitter error: {}", e) println!("Emitter error: {}", e)
}
} }
} }
} }

View File

@ -200,9 +200,9 @@ async getGroup(id: string) : Promise<Result<string, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getAllGroups(id: string) : Promise<Result<RichEvent[], string>> { async getAllNewsfeeds(id: string) : Promise<Result<RichEvent[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_all_groups", { id }) }; return { status: "ok", data: await TAURI_INVOKE("get_all_newsfeeds", { id }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
@ -456,14 +456,6 @@ async createColumn(column: Column) : Promise<Result<string, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async updateColumn(label: string, width: number, height: number, x: number, y: number) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("update_column", { label, width, height, x, y }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async reloadColumn(label: string) : Promise<Result<null, string>> { async reloadColumn(label: string) : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("reload_column", { label }) }; return { status: "ok", data: await TAURI_INVOKE("reload_column", { label }) };
@ -480,6 +472,14 @@ async closeColumn(label: string) : Promise<Result<boolean, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async closeAllColumns() : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("close_all_columns") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async openWindow(window: NewWindow) : Promise<Result<string, string>> { async openWindow(window: NewWindow) : Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) }; return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };

View File

@ -4,67 +4,32 @@ import type { LumeColumn } from "@/types";
import { CaretDown, Check } from "@phosphor-icons/react"; import { CaretDown, Check } from "@phosphor-icons/react";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { User } from "./user"; import { User } from "./user";
export function Column({ column }: { column: LumeColumn }) { export function Column({ column }: { column: LumeColumn }) {
const [rect, ref] = useRect(); const [rect, ref] = useRect();
const [_error, setError] = useState<string>(""); const [error, setError] = useState("");
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (rect) { if (rect) {
const res = await commands.updateColumn( const res = await commands.createColumn({
column.label, label: column.label,
rect.width, x: rect.x,
rect.height, y: rect.y,
rect.x, width: rect.width,
rect.y, height: rect.height,
); url: `${column.url}?label=${column.label}&name=${column.name}`,
});
if (res.status === "ok") { if (res.status === "error") {
console.log("webview is updated: ", column.label); setError(res.error);
} else {
console.log("webview error: ", res.error);
} }
} }
})(); })();
}, [rect]); }, [rect]);
useLayoutEffect(() => {
console.log(column.label);
if (ref.current) {
const initialRect = ref.current.getBoundingClientRect();
commands
.createColumn({
label: column.label,
x: initialRect.x,
y: initialRect.y,
width: initialRect.width,
height: initialRect.height,
url: `${column.url}?label=${column.label}&name=${column.name}`,
})
.then((res) => {
if (res.status === "ok") {
console.log("webview is created: ", column.label);
} else {
setError(res.error);
}
});
return () => {
commands.closeColumn(column.label).then((res) => {
if (res.status === "ok") {
console.log("webview is closed: ", column.label);
} else {
console.log("webview error: ", res.error);
}
});
};
}
}, []);
return ( return (
<div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5"> <div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5">
<div className="flex flex-col gap-px size-full"> <div className="flex flex-col gap-px size-full">
@ -73,7 +38,13 @@ export function Column({ column }: { column: LumeColumn }) {
name={column.name} name={column.name}
account={column.account} account={column.account}
/> />
<div ref={ref} className="flex-1 size-full" /> <div ref={ref} className="flex-1 size-full">
<div className="size-full flex flex-col items-center justify-center">
<div className="text-red-500 text-sm break-all">
{error?.length ? error : null}
</div>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -13,14 +13,14 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as SetInterestImport } from './routes/set-interest'
import { Route as SetGroupImport } from './routes/set-group'
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
import { Route as AppImport } from './routes/_app' import { Route as AppImport } from './routes/_app'
import { Route as NewPostIndexImport } from './routes/new-post/index' import { Route as NewPostIndexImport } from './routes/new-post/index'
import { Route as AppIndexImport } from './routes/_app/index' import { Route as AppIndexImport } from './routes/_app/index'
import { Route as ZapIdImport } from './routes/zap.$id' import { Route as ZapIdImport } from './routes/zap.$id'
import { Route as ColumnsLayoutImport } from './routes/columns/_layout' import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
import { Route as IdSetInterestImport } from './routes/$id.set-interest'
import { Route as IdSetGroupImport } from './routes/$id.set-group'
import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet' import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay' import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile' import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile'
@ -79,16 +79,6 @@ const NewLazyRoute = NewLazyImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const SetInterestRoute = SetInterestImport.update({
path: '/set-interest',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/set-interest.lazy').then((d) => d.Route))
const SetGroupRoute = SetGroupImport.update({
path: '/set-group',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/set-group.lazy').then((d) => d.Route))
const BootstrapRelaysRoute = BootstrapRelaysImport.update({ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
path: '/bootstrap-relays', path: '/bootstrap-relays',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@ -149,6 +139,18 @@ const ColumnsLayoutRoute = ColumnsLayoutImport.update({
getParentRoute: () => ColumnsRoute, getParentRoute: () => ColumnsRoute,
} as any) } as any)
const IdSetInterestRoute = IdSetInterestImport.update({
path: '/$id/set-interest',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/$id.set-interest.lazy').then((d) => d.Route),
)
const IdSetGroupRoute = IdSetGroupImport.update({
path: '/$id/set-group',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/$id.set-group.lazy').then((d) => d.Route))
const ColumnsLayoutTrendingLazyRoute = ColumnsLayoutTrendingLazyImport.update({ const ColumnsLayoutTrendingLazyRoute = ColumnsLayoutTrendingLazyImport.update({
path: '/trending', path: '/trending',
getParentRoute: () => ColumnsLayoutRoute, getParentRoute: () => ColumnsLayoutRoute,
@ -309,20 +311,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BootstrapRelaysImport preLoaderRoute: typeof BootstrapRelaysImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/set-group': {
id: '/set-group'
path: '/set-group'
fullPath: '/set-group'
preLoaderRoute: typeof SetGroupImport
parentRoute: typeof rootRoute
}
'/set-interest': {
id: '/set-interest'
path: '/set-interest'
fullPath: '/set-interest'
preLoaderRoute: typeof SetInterestImport
parentRoute: typeof rootRoute
}
'/new': { '/new': {
id: '/new' id: '/new'
path: '/new' path: '/new'
@ -330,6 +318,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof NewLazyImport preLoaderRoute: typeof NewLazyImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/$id/set-group': {
id: '/$id/set-group'
path: '/$id/set-group'
fullPath: '/$id/set-group'
preLoaderRoute: typeof IdSetGroupImport
parentRoute: typeof rootRoute
}
'/$id/set-interest': {
id: '/$id/set-interest'
path: '/$id/set-interest'
fullPath: '/$id/set-interest'
preLoaderRoute: typeof IdSetInterestImport
parentRoute: typeof rootRoute
}
'/columns': { '/columns': {
id: '/columns' id: '/columns'
path: '/columns' path: '/columns'
@ -636,9 +638,9 @@ const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren(
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'': typeof AppRouteWithChildren '': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute '/bootstrap-relays': typeof BootstrapRelaysRoute
'/set-group': typeof SetGroupRoute
'/set-interest': typeof SetInterestRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/columns': typeof ColumnsLayoutRouteWithChildren '/columns': typeof ColumnsLayoutRouteWithChildren
'/zap/$id': typeof ZapIdRoute '/zap/$id': typeof ZapIdRoute
'/new-account/connect': typeof NewAccountConnectLazyRoute '/new-account/connect': typeof NewAccountConnectLazyRoute
@ -671,9 +673,9 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/bootstrap-relays': typeof BootstrapRelaysRoute '/bootstrap-relays': typeof BootstrapRelaysRoute
'/set-group': typeof SetGroupRoute
'/set-interest': typeof SetInterestRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/columns': typeof ColumnsLayoutRouteWithChildren '/columns': typeof ColumnsLayoutRouteWithChildren
'/zap/$id': typeof ZapIdRoute '/zap/$id': typeof ZapIdRoute
'/new-account/connect': typeof NewAccountConnectLazyRoute '/new-account/connect': typeof NewAccountConnectLazyRoute
@ -708,9 +710,9 @@ export interface FileRoutesById {
__root__: typeof rootRoute __root__: typeof rootRoute
'/_app': typeof AppRouteWithChildren '/_app': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute '/bootstrap-relays': typeof BootstrapRelaysRoute
'/set-group': typeof SetGroupRoute
'/set-interest': typeof SetInterestRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/columns': typeof ColumnsRouteWithChildren '/columns': typeof ColumnsRouteWithChildren
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren '/columns/_layout': typeof ColumnsLayoutRouteWithChildren
'/zap/$id': typeof ZapIdRoute '/zap/$id': typeof ZapIdRoute
@ -747,9 +749,9 @@ export interface FileRouteTypes {
fullPaths: fullPaths:
| '' | ''
| '/bootstrap-relays' | '/bootstrap-relays'
| '/set-group'
| '/set-interest'
| '/new' | '/new'
| '/$id/set-group'
| '/$id/set-interest'
| '/columns' | '/columns'
| '/zap/$id' | '/zap/$id'
| '/new-account/connect' | '/new-account/connect'
@ -781,9 +783,9 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/bootstrap-relays' | '/bootstrap-relays'
| '/set-group'
| '/set-interest'
| '/new' | '/new'
| '/$id/set-group'
| '/$id/set-interest'
| '/columns' | '/columns'
| '/zap/$id' | '/zap/$id'
| '/new-account/connect' | '/new-account/connect'
@ -816,9 +818,9 @@ export interface FileRouteTypes {
| '__root__' | '__root__'
| '/_app' | '/_app'
| '/bootstrap-relays' | '/bootstrap-relays'
| '/set-group'
| '/set-interest'
| '/new' | '/new'
| '/$id/set-group'
| '/$id/set-interest'
| '/columns' | '/columns'
| '/columns/_layout' | '/columns/_layout'
| '/zap/$id' | '/zap/$id'
@ -854,9 +856,9 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
AppRoute: typeof AppRouteWithChildren AppRoute: typeof AppRouteWithChildren
BootstrapRelaysRoute: typeof BootstrapRelaysRoute BootstrapRelaysRoute: typeof BootstrapRelaysRoute
SetGroupRoute: typeof SetGroupRoute
SetInterestRoute: typeof SetInterestRoute
NewLazyRoute: typeof NewLazyRoute NewLazyRoute: typeof NewLazyRoute
IdSetGroupRoute: typeof IdSetGroupRoute
IdSetInterestRoute: typeof IdSetInterestRoute
ColumnsRoute: typeof ColumnsRouteWithChildren ColumnsRoute: typeof ColumnsRouteWithChildren
ZapIdRoute: typeof ZapIdRoute ZapIdRoute: typeof ZapIdRoute
NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute
@ -869,9 +871,9 @@ export interface RootRouteChildren {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRouteWithChildren, AppRoute: AppRouteWithChildren,
BootstrapRelaysRoute: BootstrapRelaysRoute, BootstrapRelaysRoute: BootstrapRelaysRoute,
SetGroupRoute: SetGroupRoute,
SetInterestRoute: SetInterestRoute,
NewLazyRoute: NewLazyRoute, NewLazyRoute: NewLazyRoute,
IdSetGroupRoute: IdSetGroupRoute,
IdSetInterestRoute: IdSetInterestRoute,
ColumnsRoute: ColumnsRouteWithChildren, ColumnsRoute: ColumnsRouteWithChildren,
ZapIdRoute: ZapIdRoute, ZapIdRoute: ZapIdRoute,
NewAccountConnectLazyRoute: NewAccountConnectLazyRoute, NewAccountConnectLazyRoute: NewAccountConnectLazyRoute,
@ -895,9 +897,9 @@ export const routeTree = rootRoute
"children": [ "children": [
"/_app", "/_app",
"/bootstrap-relays", "/bootstrap-relays",
"/set-group",
"/set-interest",
"/new", "/new",
"/$id/set-group",
"/$id/set-interest",
"/columns", "/columns",
"/zap/$id", "/zap/$id",
"/new-account/connect", "/new-account/connect",
@ -916,15 +918,15 @@ export const routeTree = rootRoute
"/bootstrap-relays": { "/bootstrap-relays": {
"filePath": "bootstrap-relays.tsx" "filePath": "bootstrap-relays.tsx"
}, },
"/set-group": {
"filePath": "set-group.tsx"
},
"/set-interest": {
"filePath": "set-interest.tsx"
},
"/new": { "/new": {
"filePath": "new.lazy.tsx" "filePath": "new.lazy.tsx"
}, },
"/$id/set-group": {
"filePath": "$id.set-group.tsx"
},
"/$id/set-interest": {
"filePath": "$id.set-interest.tsx"
},
"/columns": { "/columns": {
"filePath": "columns", "filePath": "columns",
"children": [ "children": [

View File

@ -8,13 +8,13 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
export const Route = createLazyFileRoute("/set-group")({ export const Route = createLazyFileRoute("/$id/set-group")({
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const contacts = Route.useLoaderData(); const contacts = Route.useLoaderData();
const { account } = Route.useSearch(); const { id } = Route.useParams();
const { queryClient } = Route.useRouteContext(); const { queryClient } = Route.useRouteContext();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@ -40,11 +40,11 @@ function Screen() {
const submit = () => { const submit = () => {
startTransition(async () => { startTransition(async () => {
const signer = await commands.hasSigner(account); const signer = await commands.hasSigner(id);
if (signer.status === "ok") { if (signer.status === "ok") {
if (!signer.data) { if (!signer.data) {
const res = await commands.setSigner(account); const res = await commands.setSigner(id);
if (res.status === "error") { if (res.status === "error") {
await message(res.error, { kind: "error" }); await message(res.error, { kind: "error" });
@ -63,7 +63,7 @@ function Screen() {
// Invalidate cache // Invalidate cache
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["mygroups", account], queryKey: ["others", "newsfeeds", id],
}); });
// Create column in the main window // Create column in the main window
@ -72,6 +72,7 @@ function Screen() {
column: { column: {
label: res.data, label: res.data,
name: title, name: title,
account: id,
url: `/columns/groups/${res.data}`, url: `/columns/groups/${res.data}`,
}, },
}); });

View File

@ -0,0 +1,14 @@
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/$id/set-group")({
loader: async ({ params }) => {
const res = await commands.getContactList(params.id);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
});

View File

@ -23,7 +23,7 @@ const TOPICS = [
}, },
]; ];
export const Route = createLazyFileRoute("/set-interest")({ export const Route = createLazyFileRoute("/$id/set-interest")({
component: Screen, component: Screen,
}); });
@ -33,7 +33,7 @@ function Screen() {
const [hashtags, setHashtags] = useState<string[]>([]); const [hashtags, setHashtags] = useState<string[]>([]);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const { account } = Route.useSearch(); const { id } = Route.useParams();
const { queryClient } = Route.useRouteContext(); const { queryClient } = Route.useRouteContext();
const toggleHashtag = (tag: string) => { const toggleHashtag = (tag: string) => {
@ -52,11 +52,11 @@ function Screen() {
const submit = () => { const submit = () => {
startTransition(async () => { startTransition(async () => {
const signer = await commands.hasSigner(account); const signer = await commands.hasSigner(id);
if (signer.status === "ok") { if (signer.status === "ok") {
if (!signer.data) { if (!signer.data) {
const res = await commands.setSigner(account); const res = await commands.setSigner(id);
if (res.status === "error") { if (res.status === "error") {
await message(res.error, { kind: "error" }); await message(res.error, { kind: "error" });
@ -79,7 +79,7 @@ function Screen() {
// Invalidate cache // Invalidate cache
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["myinterests", account], queryKey: ["interests", id],
}); });
// Create column in the main window // Create column in the main window
@ -88,6 +88,7 @@ function Screen() {
column: { column: {
label: res.data, label: res.data,
name: title, name: title,
account: id,
url: `/columns/interests/${res.data}`, url: `/columns/interests/${res.data}`,
}, },
}); });

View File

@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/$id/set-interest")({});

View File

@ -4,11 +4,12 @@ import { Column, Spinner } from "@/components";
import { LumeWindow } from "@/system"; import { LumeWindow } from "@/system";
import type { ColumnEvent, LumeColumn, Metadata } from "@/types"; import type { ColumnEvent, LumeColumn, Metadata } from "@/types";
import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react"; import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute, useRouter } from "@tanstack/react-router";
import { useStore } from "@tanstack/react-store"; import { useStore } from "@tanstack/react-store";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog";
import useEmblaCarousel from "embla-carousel-react"; import useEmblaCarousel from "embla-carousel-react";
import { import {
type ReactNode, type ReactNode,
@ -26,6 +27,7 @@ export const Route = createLazyFileRoute("/_app/")({
function Screen() { function Screen() {
const initialAppColumns = Route.useLoaderData(); const initialAppColumns = Route.useLoaderData();
const router = useRouter();
const columns = useStore(appColumns, (state) => state); const columns = useStore(appColumns, (state) => state);
const [emblaRef, emblaApi] = useEmblaCarousel({ const [emblaRef, emblaApi] = useEmblaCarousel({
@ -45,6 +47,19 @@ function Screen() {
getCurrentWindow().emit("scrolling", {}); getCurrentWindow().emit("scrolling", {});
}, []); }, []);
const remove = useCallback(
async (label: string) => {
const res = await commands.closeColumn(label);
if (res.status === "ok") {
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
} else {
await message(res.error, { kind: "errror" });
}
},
[columns],
);
const add = useDebouncedCallback((column: LumeColumn) => { const add = useDebouncedCallback((column: LumeColumn) => {
const exist = columns.find((col) => col.label === column.label); const exist = columns.find((col) => col.label === column.label);
@ -57,10 +72,6 @@ function Screen() {
} }
}, 150); }, 150);
const remove = useDebouncedCallback((label: string) => {
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
}, 150);
const move = useDebouncedCallback( const move = useDebouncedCallback(
(label: string, direction: "left" | "right") => { (label: string, direction: "left" | "right") => {
const newCols = [...columns]; const newCols = [...columns];
@ -122,6 +133,16 @@ function Screen() {
}; };
}, []); }, []);
useEffect(() => {
const unsubscribeFn = router.subscribe("onBeforeNavigate", async () => {
await commands.closeAllColumns();
});
return () => {
unsubscribeFn();
};
}, []);
useEffect(() => { useEffect(() => {
if (initialAppColumns) { if (initialAppColumns) {
appColumns.setState(() => initialAppColumns); appColumns.setState(() => initialAppColumns);

View File

@ -3,7 +3,7 @@ import { cn, toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components"; import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system"; import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types"; import type { LumeColumn, NostrEvent } from "@/types";
import { ArrowClockwise, Plus } from "@phosphor-icons/react"; import { ArrowClockwise, ArrowRight, Plus } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
@ -24,7 +24,7 @@ function Screen() {
className="overflow-hidden size-full" className="overflow-hidden size-full"
> >
<ScrollArea.Viewport className="relative h-full px-3 pb-3"> <ScrollArea.Viewport className="relative h-full px-3 pb-3">
<Groups /> <Newsfeeds />
<Interests /> <Interests />
<Core /> <Core />
</ScrollArea.Viewport> </ScrollArea.Viewport>
@ -39,80 +39,12 @@ function Screen() {
); );
} }
/* function Newsfeeds() {
function SyncProgress() {
const { id } = Route.useParams();
const { queryClient } = Route.useRouteContext();
const [error, setError] = useState("");
const [progress, setProgress] = useState(0);
useEffect(() => {
(async () => {
if (progress >= 100) {
await queryClient.invalidateQueries();
}
})();
}, [progress]);
useEffect(() => {
const channel = new Channel<number>();
channel.onmessage = (message) => {
setProgress(message);
};
(async () => {
const res = await commands.syncAccount(id, channel);
if (res.status === "error") {
setError(res.error);
}
})();
}, []);
return (
<div className="size-full">
<div className="flex flex-col gap-3">
<div className="h-32 flex flex-col items-center justify-center rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="w-2/3 flex flex-col gap-2">
<Progress.Root
className="relative overflow-hidden bg-black/20 dark:bg-white/20 rounded-full w-full h-1"
style={{
transform: "translateZ(0)",
}}
value={progress}
>
<Progress.Indicator
className="bg-blue-500 size-full transition-transform duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]"
style={{ transform: `translateX(-${100 - progress}%)` }}
/>
</Progress.Root>
<span className="text-center text-xs">
{error ? error : "Syncing in Progress..."}
</span>
</div>
</div>
<a
href="https://github.com/hoytech/strfry/blob/nextneg/docs/negentropy.md"
target="_blank"
className="text-center !underline text-xs font-medium text-blue-500"
rel="noreferrer"
>
Learn more about Negentropy
</a>
</div>
</div>
);
}
*/
function Groups() {
const { id } = Route.useParams(); const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({ const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
queryKey: ["others", "groups", id], queryKey: ["others", "newsfeeds", id],
queryFn: async () => { queryFn: async () => {
const res = await commands.getAllGroups(id); const res = await commands.getAllNewsfeeds(id);
if (res.status === "ok") { if (res.status === "ok") {
const data = toLumeEvents(res.data); const data = toLumeEvents(res.data);
@ -131,8 +63,14 @@ function Groups() {
const renderItem = useCallback( const renderItem = useCallback(
(item: NostrEvent) => { (item: NostrEvent) => {
const name = const name =
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed"; item.kind === 3
const label = item.tags.find((tag) => tag[0] === "d")?.[1] || nanoid(); ? "Contacts"
: item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
const label =
item.kind === 3
? `newsfeed-${id.slice(0, 5)}`
: item.tags.find((tag) => tag[0] === "d")?.[1] || nanoid();
return ( return (
<div <div
@ -140,19 +78,34 @@ function Groups() {
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800" className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
> >
<div className="px-2 pt-2"> <div className="px-2 pt-2">
<div className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg flex flex-wrap items-center justify-center gap-2 overflow-y-auto"> <ScrollArea.Root
{item.tags type={"scroll"}
.filter((tag) => tag[0] === "p") scrollHideDelay={300}
.map((tag) => ( className="overflow-hidden size-full"
<div key={tag[1]}> >
<User.Provider pubkey={tag[1]}> <ScrollArea.Viewport className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<User.Root> <div className="flex flex-wrap items-center justify-center gap-2">
<User.Avatar className="size-8 rounded-full" /> {item.tags
</User.Root> .filter((tag) => tag[0] === "p")
</User.Provider> .map((tag) => (
</div> <div key={tag[1]}>
))} <User.Provider pubkey={tag[1]}>
</div> <User.Root>
<User.Avatar className="size-8 rounded-full" />
</User.Root>
</User.Provider>
</div>
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div> </div>
<div className="p-2 flex items-center justify-between"> <div className="p-2 flex items-center justify-between">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
@ -170,10 +123,14 @@ function Groups() {
LumeWindow.openColumn({ LumeWindow.openColumn({
label, label,
name, name,
url: `/columns/groups/${item.id}`, account: id,
url:
item.kind === 3
? `/columns/newsfeed/${id}`
: `/columns/groups/${item.id}`,
}) })
} }
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white" className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-100 group-hover:bg-blue-600 dark:group-hover:bg-blue-400 group-hover:text-white"
> >
Add Add
</button> </button>
@ -188,7 +145,7 @@ function Groups() {
return ( return (
<div className="mb-12 flex flex-col gap-3"> <div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Groups</h3> <h3 className="font-semibold">Newsfeeds</h3>
<div className="inline-flex items-center justify-center gap-2"> <div className="inline-flex items-center justify-center gap-2">
<button <button
type="button" type="button"
@ -202,7 +159,7 @@ function Groups() {
</button> </button>
<button <button
type="button" type="button"
onClick={() => LumeWindow.openPopup("/set-group", "New group")} onClick={() => LumeWindow.openPopup(`${id}/set-group`, "New group")}
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white" className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
> >
<Plus className="size-3" weight="bold" /> <Plus className="size-3" weight="bold" />
@ -227,6 +184,10 @@ function Groups() {
) : ( ) : (
data?.map((item) => renderItem(item)) data?.map((item) => renderItem(item))
)} )}
<div className="h-12 px-3 flex items-center justify-between items-betwe bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400">
<span className="text-sm font-medium">Discover newsfeeds</span>
<ArrowRight className="size-4" weight="bold" />
</div>
</div> </div>
</div> </div>
); );
@ -266,15 +227,30 @@ function Interests() {
className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800" className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
> >
<div className="px-2 pt-2"> <div className="px-2 pt-2">
<div className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg flex flex-wrap items-center justify-center gap-4 overflow-y-auto"> <ScrollArea.Root
{item.tags type={"scroll"}
.filter((tag) => tag[0] === "t") scrollHideDelay={300}
.map((tag) => ( className="overflow-hidden size-full"
<div key={tag[1]} className="text-sm font-medium"> >
{tag[1]} <ScrollArea.Viewport className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
</div> <div className="flex flex-wrap items-center justify-center gap-2">
))} {item.tags
</div> .filter((tag) => tag[0] === "t")
.map((tag) => (
<div key={tag[1]} className="text-sm font-medium">
{tag[1].includes("#") ? tag[1] : `#${tag[1]}`}
</div>
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div> </div>
<div className="p-3 flex items-center justify-between"> <div className="p-3 flex items-center justify-between">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
@ -292,10 +268,11 @@ function Interests() {
LumeWindow.openColumn({ LumeWindow.openColumn({
label, label,
name, name,
account: id,
url: `/columns/interests/${item.id}`, url: `/columns/interests/${item.id}`,
}) })
} }
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white" className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-100 group-hover:bg-blue-600 dark:group-hover:bg-blue-400 group-hover:text-white"
> >
Add Add
</button> </button>
@ -325,7 +302,7 @@ function Interests() {
<button <button
type="button" type="button"
onClick={() => onClick={() =>
LumeWindow.openPopup("/set-interest", "New interest") LumeWindow.openPopup(`${id}/set-interest`, "New interest")
} }
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white" className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
> >
@ -351,6 +328,10 @@ function Interests() {
) : ( ) : (
data?.map((item) => renderItem(item)) data?.map((item) => renderItem(item))
)} )}
<div className="h-12 px-3 flex items-center justify-between items-betwe bg-neutral-200/50 rounded-xl text-blue-600 dark:text-blue-400">
<span className="text-sm font-medium">Discover interests</span>
<ArrowRight className="size-4" weight="bold" />
</div>
</div> </div>
</div> </div>
); );
@ -380,16 +361,6 @@ function Core() {
</div> </div>
<div className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"> <div className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="flex flex-col gap-2 p-2"> <div className="flex flex-col gap-2 p-2">
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
<div className="text-sm font-medium">Newsfeed</div>
<button
type="button"
onClick={() => LumeWindow.openNewsfeed(id)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800"> <div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
<div className="text-sm font-medium">Stories</div> <div className="text-sm font-medium">Stories</div>
<button <button

View File

@ -1,23 +0,0 @@
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
type RouteSearch = {
account: string;
};
export const Route = createFileRoute("/set-group")({
validateSearch: (search: Record<string, string>): RouteSearch => {
return {
account: search.account,
};
},
loader: async () => {
const res = await commands.getContactList();
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
});

View File

@ -1,13 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
type RouteSearch = {
account: string;
};
export const Route = createFileRoute("/set-interest")({
validateSearch: (search: Record<string, string>): RouteSearch => {
return {
account: search.account,
};
},
});

View File

@ -15,7 +15,7 @@ export const LumeWindow = {
await getCurrentWindow().emit("columns", { await getCurrentWindow().emit("columns", {
type: "add", type: "add",
column: { column: {
label: "launchpad", label: `launchpad-${account.slice(0, 5)}`,
name: "Launchpad", name: "Launchpad",
url: `/columns/launchpad/${account}`, url: `/columns/launchpad/${account}`,
account, account,