feat: Add support for NIP-51 (#236)
* feat: and follow and interest sets * feat: improve query * feat: improve
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
"zap-*",
|
||||
"event-*",
|
||||
"user-*",
|
||||
"editor-*"
|
||||
"editor-*",
|
||||
"popup-*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"column":{"identifier":"column","description":"Capability for the column","local":true,"windows":["column-*"],"permissions":["core:resources:default","core:tray:default","os:allow-locale","os:allow-os-type","clipboard-manager:allow-write-text","dialog:allow-open","dialog:allow-ask","dialog:allow-message","fs:allow-read-file","core:menu:default","core:menu:allow-new","core:menu:allow-popup","http:default","shell:allow-open","store:allow-get","store:allow-set","store:allow-delete",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]},"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","store:default","prevent-default:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
||||
{"column":{"identifier":"column","description":"Capability for the column","local":true,"windows":["column-*"],"permissions":["core:resources:default","core:tray:default","os:allow-locale","os:allow-os-type","clipboard-manager:allow-write-text","dialog:allow-open","dialog:allow-ask","dialog:allow-message","fs:allow-read-file","core:menu:default","core:menu:allow-new","core:menu:allow-popup","http:default","shell:allow-open","store:allow-get","store:allow-set","store:allow-delete",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]},"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*","popup-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","store:default","prevent-default:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
||||
@@ -20,9 +20,9 @@
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "local_feeds",
|
||||
"name": "Local Feeds",
|
||||
"description": "All notes from your follows.",
|
||||
"label": "newsfeed",
|
||||
"name": "Newsfeed",
|
||||
"description": "All notes from you're following.",
|
||||
"url": "/columns/newsfeed",
|
||||
"picture": ""
|
||||
},
|
||||
@@ -58,19 +58,10 @@
|
||||
"official": true,
|
||||
"label": "global_feeds",
|
||||
"name": "Global Feeds",
|
||||
"description": "Discover all global notes.",
|
||||
"description": "All global notes from all connected relays.",
|
||||
"url": "/columns/global",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "group_feeds",
|
||||
"name": "Group",
|
||||
"description": "Custom feeds for group of people.",
|
||||
"url": "/columns/group",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
|
||||
@@ -271,6 +271,15 @@ pub async fn login(
|
||||
// NIP-65: Connect to user's relay list
|
||||
init_nip65(client, &public_key).await;
|
||||
|
||||
// NIP-03: Get user's contact list
|
||||
let contact_list = {
|
||||
let contacts = client.get_contact_list(None).await.unwrap();
|
||||
// Update app's state
|
||||
state.contact_list.lock().await.clone_from(&contacts);
|
||||
// Return
|
||||
contacts
|
||||
};
|
||||
|
||||
// Run seperate thread for syncing data
|
||||
let pk = public_key.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -292,7 +301,10 @@ pub async fn login(
|
||||
Kind::MuteList,
|
||||
Kind::Bookmarks,
|
||||
Kind::Interests,
|
||||
Kind::InterestSet,
|
||||
Kind::FollowSet,
|
||||
Kind::PinList,
|
||||
Kind::EventDeletion,
|
||||
])
|
||||
.limit(1000),
|
||||
NegentropyOptions::default(),
|
||||
@@ -355,15 +367,6 @@ pub async fn login(
|
||||
println!("Subscribed: {}", e.success.len())
|
||||
}
|
||||
|
||||
// Get user's contact list
|
||||
let contact_list = {
|
||||
let contacts = client.get_contact_list(None).await.unwrap();
|
||||
// Update app's state
|
||||
state.contact_list.lock().await.clone_from(&contacts);
|
||||
// Return
|
||||
contacts
|
||||
};
|
||||
|
||||
// Get events from contact list
|
||||
if !contact_list.is_empty() {
|
||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
@@ -198,7 +198,7 @@ pub async fn subscribe_to(id: String, state: State<'_, Nostr>) -> Result<(), Str
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_events_by(
|
||||
pub async fn get_all_events_by_author(
|
||||
public_key: String,
|
||||
limit: i32,
|
||||
state: State<'_, Nostr>,
|
||||
@@ -237,14 +237,111 @@ pub async fn get_events_by(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_local_events(
|
||||
until: Option<&str>,
|
||||
pub async fn get_all_events_by_authors(
|
||||
public_keys: Vec<String>,
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = public_keys
|
||||
.iter()
|
||||
.map(|pk| PublicKey::from_str(pk).map_err(|err| err.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of)
|
||||
.authors(authors);
|
||||
|
||||
match client
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_all_events_by_hashtags(
|
||||
hashtags: Vec<String>,
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of)
|
||||
.hashtags(hashtags);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_local_events(
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
@@ -274,75 +371,22 @@ pub async fn get_local_events(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_group_events(
|
||||
public_keys: Vec<&str>,
|
||||
until: Option<&str>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = public_keys
|
||||
.iter()
|
||||
.map(|p| PublicKey::from_str(p).map_err(|err| err.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(20)
|
||||
.until(as_of)
|
||||
.authors(authors);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_global_events(
|
||||
until: Option<&str>,
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(20)
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of);
|
||||
|
||||
match client
|
||||
@@ -372,51 +416,6 @@ pub async fn get_global_events(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_hashtag_events(
|
||||
hashtags: Vec<&str>,
|
||||
until: Option<&str>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(20)
|
||||
.until(as_of)
|
||||
.hashtags(hashtags);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let fils = filter_converstation(events);
|
||||
let futures = fils.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RichEvent { raw, parsed }
|
||||
});
|
||||
let rich_events = join_all(futures).await;
|
||||
|
||||
Ok(rich_events)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn publish(
|
||||
@@ -534,7 +533,7 @@ pub async fn reply(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event = Event::from_json(raw).map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -587,7 +586,7 @@ pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<Stri
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn user_to_bech32(user: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(user).map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -672,3 +671,21 @@ pub async fn search(
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.event(event_id)
|
||||
.kind(Kind::EventDeletion);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
use tauri::{Emitter, Manager, State};
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::{common::get_latest_event, NewSettings, Nostr, Settings};
|
||||
@@ -105,18 +105,14 @@ pub async fn set_contact_list(
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let contact_list = state.contact_list.lock().await.clone();
|
||||
println!("Total contacts: {}", contact_list.len());
|
||||
let vec: Vec<String> = contact_list
|
||||
.into_iter()
|
||||
.map(|f| f.public_key.to_hex())
|
||||
.collect();
|
||||
|
||||
match client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||
Ok(contact_list) => {
|
||||
let list = contact_list
|
||||
.into_iter()
|
||||
.map(|f| f.public_key.to_hex())
|
||||
.collect();
|
||||
Ok(list)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -200,6 +196,194 @@ pub async fn toggle_contact(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_group(
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
image: Option<String>,
|
||||
users: Vec<String>,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_keys: Vec<PublicKey> = users
|
||||
.iter()
|
||||
.map(|u| PublicKey::from_str(u).unwrap())
|
||||
.collect();
|
||||
let label = title.to_lowercase().replace(" ", "-");
|
||||
let mut tags: Vec<Tag> = vec![Tag::title(title)];
|
||||
|
||||
if let Some(desc) = description {
|
||||
tags.push(Tag::description(desc))
|
||||
};
|
||||
|
||||
if let Some(img) = image {
|
||||
let url = UncheckedUrl::new(img);
|
||||
tags.push(Tag::image(url, None));
|
||||
}
|
||||
|
||||
let builder = EventBuilder::follow_set(label, public_keys.clone()).add_tags(tags);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(report) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(public_keys)
|
||||
.limit(500);
|
||||
|
||||
if let Ok(report) = client.reconcile(filter, NegentropyOptions::default()).await {
|
||||
println!("Received: {}", report.received.len());
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
};
|
||||
});
|
||||
|
||||
Ok(report.id().to_hex())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new().kind(Kind::FollowSet).id(event_id);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => match get_latest_event(&events) {
|
||||
Some(ev) => Ok(ev.as_json()),
|
||||
None => Err("Not found.".to_string()),
|
||||
},
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_all_groups(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new().kind(Kind::FollowSet).author(public_key);
|
||||
|
||||
match client
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let data: Vec<String> = events.iter().map(|ev| ev.as_json()).collect();
|
||||
Ok(data)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_interest(
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
image: Option<String>,
|
||||
hashtags: Vec<String>,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let label = title.to_lowercase().replace(" ", "-");
|
||||
let mut tags: Vec<Tag> = vec![Tag::title(title)];
|
||||
|
||||
if let Some(desc) = description {
|
||||
tags.push(Tag::description(desc))
|
||||
};
|
||||
|
||||
if let Some(img) = image {
|
||||
let url = UncheckedUrl::new(img);
|
||||
tags.push(Tag::image(url, None));
|
||||
}
|
||||
|
||||
let builder = EventBuilder::interest_set(label, hashtags.clone()).add_tags(tags);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(report) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.hashtags(hashtags)
|
||||
.limit(500);
|
||||
|
||||
if let Ok(report) = client.reconcile(filter, NegentropyOptions::default()).await {
|
||||
println!("Received: {}", report.received.len());
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
};
|
||||
});
|
||||
|
||||
Ok(report.id().to_hex())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_interest(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::Interests, Kind::InterestSet])
|
||||
.id(event_id);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => match get_latest_event(&events) {
|
||||
Some(ev) => Ok(ev.as_json()),
|
||||
None => Err("Not found.".to_string()),
|
||||
},
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::InterestSet, Kind::Interests])
|
||||
.author(public_key);
|
||||
|
||||
match client
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let data: Vec<String> = events.iter().map(|ev| ev.as_json()).collect();
|
||||
Ok(data)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
||||
@@ -230,61 +414,6 @@ pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, S
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_lume_store(
|
||||
key: String,
|
||||
content: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let encrypted = signer
|
||||
.nip44_encrypt(&public_key, content)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let tag = Tag::identifier(key);
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(event_id) => Ok(event_id.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(key)
|
||||
.limit(10);
|
||||
|
||||
match client
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = get_latest_event(&events) {
|
||||
match signer.nip44_decrypt(&public_key, &event.content).await {
|
||||
Ok(decrypted) => Ok(decrypted),
|
||||
Err(_) => Err(event.content.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("Not found".into())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
|
||||
@@ -113,15 +113,15 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let window = WebviewWindowBuilder::new(
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
WebviewUrl::App(PathBuf::from(window.url)),
|
||||
@@ -129,7 +129,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.title(&window.title)
|
||||
.min_inner_size(window.width, window.height)
|
||||
.inner_size(window.width, window.height)
|
||||
.hidden_title(true)
|
||||
.hidden_title(window.hidden_title)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
@@ -144,7 +144,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.unwrap();
|
||||
|
||||
// Restore native border
|
||||
window.add_border(None);
|
||||
new_window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -217,7 +217,7 @@ pub fn reopen_lume(app: tauri::AppHandle) {
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
window.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = window.clone();
|
||||
@@ -225,7 +225,7 @@ pub fn reopen_lume(app: tauri::AppHandle) {
|
||||
#[cfg(target_os = "macos")]
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
win.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,14 +52,23 @@ pub fn filter_converstation(events: Vec<Event>) -> Vec<Event> {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|ev| {
|
||||
let tags = ev.get_tags_content(TagKind::SingleLetter(SingleLetterTag::lowercase(
|
||||
Alphabet::E,
|
||||
)));
|
||||
if ev.kind == Kind::TextNote {
|
||||
let tags: Vec<&str> = ev
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E))
|
||||
})
|
||||
.filter_map(|t| t.content())
|
||||
.collect();
|
||||
|
||||
if tags.is_empty() {
|
||||
Some(ev)
|
||||
if tags.is_empty() {
|
||||
Some(ev)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Some(ev)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Event>>()
|
||||
|
||||
@@ -116,8 +116,12 @@ fn main() {
|
||||
check_contact,
|
||||
toggle_contact,
|
||||
get_mention_list,
|
||||
get_lume_store,
|
||||
set_lume_store,
|
||||
set_group,
|
||||
get_group,
|
||||
get_all_groups,
|
||||
set_interest,
|
||||
get_interest,
|
||||
get_all_interests,
|
||||
set_wallet,
|
||||
load_wallet,
|
||||
remove_wallet,
|
||||
@@ -134,11 +138,12 @@ fn main() {
|
||||
get_event_from,
|
||||
get_replies,
|
||||
subscribe_to,
|
||||
get_events_by,
|
||||
get_all_events_by_author,
|
||||
get_all_events_by_authors,
|
||||
get_all_events_by_hashtags,
|
||||
get_local_events,
|
||||
get_group_events,
|
||||
get_global_events,
|
||||
get_hashtag_events,
|
||||
is_deleted_event,
|
||||
search,
|
||||
publish,
|
||||
reply,
|
||||
|
||||
@@ -13,8 +13,8 @@ import { createStore } from "@tauri-apps/plugin-store";
|
||||
import { routeTree } from "./routes.gen"; // auto generated file
|
||||
|
||||
const platform = type();
|
||||
// @ts-ignore, https://github.com/tauri-apps/plugins-workspace/pull/1860
|
||||
const store = await createStore(".cache", { autoSave: 100 });
|
||||
// @ts-expect-error, required: https://github.com/tauri-apps/plugins-workspace/pull/1860
|
||||
const store = await createStore(".cache", { autoSave: 250 });
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -163,17 +163,49 @@ async getMentionList() : Promise<Result<Mention[], string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getLumeStore(key: string) : Promise<Result<string, string>> {
|
||||
async setGroup(title: string, description: string | null, image: string | null, users: string[]) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_lume_store", { key }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_group", { title, description, image, users }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setLumeStore(key: string, content: string) : Promise<Result<string, string>> {
|
||||
async getGroup(id: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_lume_store", { key, content }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_group", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAllGroups() : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_all_groups") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setInterest(title: string, description: string | null, image: string | null, hashtags: string[]) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_interest", { title, description, image, hashtags }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getInterest(id: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_interest", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAllInterests() : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_all_interests") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -307,9 +339,25 @@ async subscribeTo(id: string) : Promise<Result<null, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getEventsBy(publicKey: string, limit: number) : Promise<Result<RichEvent[], string>> {
|
||||
async getAllEventsByAuthor(publicKey: string, limit: number) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, limit }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_author", { publicKey, limit }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAllEventsByAuthors(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_authors", { publicKeys, until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAllEventsByHashtags(hashtags: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_hashtags", { hashtags, until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -323,14 +371,6 @@ async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getGroupEvents(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_global_events", { until }) };
|
||||
@@ -339,9 +379,9 @@ async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getHashtagEvents(hashtags: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
async isDeletedEvent(id: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
|
||||
@@ -18,7 +18,7 @@ import { decode } from "light-bolt11-decoder";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { RichEvent, Settings } from "./commands.gen";
|
||||
import { LumeEvent } from "./system";
|
||||
import type { NostrEvent } from "./types";
|
||||
import type { LumeColumn, NostrEvent } from "./types";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
@@ -154,6 +154,7 @@ export function decodeZapInvoice(tags?: string[][]) {
|
||||
(s: { name: string }) => s.name === "amount",
|
||||
);
|
||||
|
||||
// @ts-ignore, its fine.
|
||||
const amount = Number.parseInt(amountSection.value);
|
||||
const displayValue = getBitcoinDisplayValues(amount);
|
||||
|
||||
@@ -289,3 +290,5 @@ export const appSettings = new Store<Settings>({
|
||||
display_media: true,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
export const appColumns = new Store<LumeColumn[]>([]);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appColumns } from "@/commons";
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { CaretDown, Check } from "@phosphor-icons/react";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
@@ -88,7 +90,7 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
||||
return (
|
||||
<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">
|
||||
<Header label={column.label} name={column.name} />
|
||||
<Header label={column.label} />
|
||||
<div ref={container} className="flex-1 size-full">
|
||||
{!isCreated ? (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
@@ -101,10 +103,14 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
||||
);
|
||||
});
|
||||
|
||||
function Header({ label, name }: { label: string; name: string }) {
|
||||
function Header({ label }: { label: string }) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
const column = useStore(appColumns, (state) =>
|
||||
state.find((col) => col.label === label),
|
||||
);
|
||||
|
||||
const saveNewTitle = async () => {
|
||||
const mainWindow = getCurrentWindow();
|
||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
||||
@@ -186,7 +192,7 @@ function Header({ label, name }: { label: string; name: string }) {
|
||||
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
||||
className="text-[12px] font-semibold focus:outline-none"
|
||||
>
|
||||
{name}
|
||||
{column.name}
|
||||
</div>
|
||||
{isChanged ? (
|
||||
<button
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { LumeWindow } from "@/system";
|
||||
|
||||
export function Hashtag({ tag }: { tag: string }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openHashtag(tag)}
|
||||
className="leading-normal cursor-default text-blue-500 hover:text-blue-600 font-normal"
|
||||
>
|
||||
<span className="leading-normal cursor-default text-blue-500 hover:text-blue-600 font-normal">
|
||||
{tag}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
// Import Routes
|
||||
|
||||
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 LoadingImport } from './routes/loading'
|
||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
@@ -22,17 +24,17 @@ import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
|
||||
import { Route as AccountBackupImport } from './routes/$account/backup'
|
||||
import { Route as AccountAppImport } from './routes/$account/_app'
|
||||
import { Route as ColumnsLayoutStoriesImport } from './routes/columns/_layout/stories'
|
||||
import { Route as ColumnsLayoutGroupImport } from './routes/columns/_layout/group'
|
||||
import { Route as ColumnsLayoutNewsfeedImport } from './routes/columns/_layout/newsfeed'
|
||||
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
||||
import { Route as ColumnsLayoutGalleryImport } from './routes/columns/_layout/gallery'
|
||||
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
||||
import { Route as ColumnsLayoutCreateGroupImport } from './routes/columns/_layout/create-group'
|
||||
import { Route as AccountSettingsWalletImport } from './routes/$account/_settings/wallet'
|
||||
import { Route as AccountSettingsRelayImport } from './routes/$account/_settings/relay'
|
||||
import { Route as AccountSettingsProfileImport } from './routes/$account/_settings/profile'
|
||||
import { Route as AccountSettingsGeneralImport } from './routes/$account/_settings/general'
|
||||
import { Route as AccountSettingsBitcoinConnectImport } from './routes/$account/_settings/bitcoin-connect'
|
||||
import { Route as AccountAppHomeImport } from './routes/$account/_app/home'
|
||||
import { Route as ColumnsLayoutInterestsIdImport } from './routes/columns/_layout/interests.$id'
|
||||
import { Route as ColumnsLayoutGroupsIdImport } from './routes/columns/_layout/groups.$id'
|
||||
import { Route as ColumnsLayoutCreateNewsfeedUsersImport } from './routes/columns/_layout/create-newsfeed.users'
|
||||
import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/_layout/create-newsfeed.f2f'
|
||||
|
||||
@@ -58,8 +60,8 @@ const ColumnsLayoutOnboardingLazyImport = createFileRoute(
|
||||
const ColumnsLayoutNotificationLazyImport = createFileRoute(
|
||||
'/columns/_layout/notification',
|
||||
)()
|
||||
const ColumnsLayoutNewsfeedLazyImport = createFileRoute(
|
||||
'/columns/_layout/newsfeed',
|
||||
const ColumnsLayoutGalleryLazyImport = createFileRoute(
|
||||
'/columns/_layout/gallery',
|
||||
)()
|
||||
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
|
||||
'/columns/_layout/users/$id',
|
||||
@@ -67,9 +69,6 @@ const ColumnsLayoutUsersIdLazyImport = createFileRoute(
|
||||
const ColumnsLayoutRepliesIdLazyImport = createFileRoute(
|
||||
'/columns/_layout/replies/$id',
|
||||
)()
|
||||
const ColumnsLayoutHashtagsContentLazyImport = createFileRoute(
|
||||
'/columns/_layout/hashtags/$content',
|
||||
)()
|
||||
const ColumnsLayoutEventsIdLazyImport = createFileRoute(
|
||||
'/columns/_layout/events/$id',
|
||||
)()
|
||||
@@ -96,6 +95,16 @@ const NewLazyRoute = NewLazyImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} 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 LoadingRoute = LoadingImport.update({
|
||||
path: '/loading',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -190,11 +199,11 @@ const ColumnsLayoutNotificationLazyRoute =
|
||||
import('./routes/columns/_layout/notification.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutNewsfeedLazyRoute = ColumnsLayoutNewsfeedLazyImport.update({
|
||||
path: '/newsfeed',
|
||||
const ColumnsLayoutGalleryLazyRoute = ColumnsLayoutGalleryLazyImport.update({
|
||||
path: '/gallery',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/newsfeed.lazy').then((d) => d.Route),
|
||||
import('./routes/columns/_layout/gallery.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
|
||||
@@ -204,11 +213,11 @@ const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
|
||||
import('./routes/columns/_layout/stories.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutGroupRoute = ColumnsLayoutGroupImport.update({
|
||||
path: '/group',
|
||||
const ColumnsLayoutNewsfeedRoute = ColumnsLayoutNewsfeedImport.update({
|
||||
path: '/newsfeed',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/group.lazy').then((d) => d.Route),
|
||||
import('./routes/columns/_layout/newsfeed.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
|
||||
@@ -216,26 +225,12 @@ const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const ColumnsLayoutGalleryRoute = ColumnsLayoutGalleryImport.update({
|
||||
path: '/gallery',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/gallery.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutCreateNewsfeedRoute =
|
||||
ColumnsLayoutCreateNewsfeedImport.update({
|
||||
path: '/create-newsfeed',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const ColumnsLayoutCreateGroupRoute = ColumnsLayoutCreateGroupImport.update({
|
||||
path: '/create-group',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/create-group.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountSettingsWalletRoute = AccountSettingsWalletImport.update({
|
||||
path: '/wallet',
|
||||
getParentRoute: () => AccountSettingsLazyRoute,
|
||||
@@ -297,16 +292,6 @@ const ColumnsLayoutRepliesIdLazyRoute = ColumnsLayoutRepliesIdLazyImport.update(
|
||||
import('./routes/columns/_layout/replies.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutHashtagsContentLazyRoute =
|
||||
ColumnsLayoutHashtagsContentLazyImport.update({
|
||||
path: '/hashtags/$content',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/hashtags.$content.lazy').then(
|
||||
(d) => d.Route,
|
||||
),
|
||||
)
|
||||
|
||||
const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
|
||||
path: '/events/$id',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
@@ -314,6 +299,20 @@ const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
|
||||
import('./routes/columns/_layout/events.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutInterestsIdRoute = ColumnsLayoutInterestsIdImport.update({
|
||||
path: '/interests/$id',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/interests.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutGroupsIdRoute = ColumnsLayoutGroupsIdImport.update({
|
||||
path: '/groups/$id',
|
||||
getParentRoute: () => ColumnsLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/columns/_layout/groups.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ColumnsLayoutCreateNewsfeedUsersRoute =
|
||||
ColumnsLayoutCreateNewsfeedUsersImport.update({
|
||||
path: '/users',
|
||||
@@ -351,6 +350,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoadingImport
|
||||
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': {
|
||||
id: '/new'
|
||||
path: '/new'
|
||||
@@ -484,13 +497,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AccountSettingsWalletImport
|
||||
parentRoute: typeof AccountSettingsLazyImport
|
||||
}
|
||||
'/columns/_layout/create-group': {
|
||||
id: '/columns/_layout/create-group'
|
||||
path: '/create-group'
|
||||
fullPath: '/columns/create-group'
|
||||
preLoaderRoute: typeof ColumnsLayoutCreateGroupImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/create-newsfeed': {
|
||||
id: '/columns/_layout/create-newsfeed'
|
||||
path: '/create-newsfeed'
|
||||
@@ -498,13 +504,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/gallery': {
|
||||
id: '/columns/_layout/gallery'
|
||||
path: '/gallery'
|
||||
fullPath: '/columns/gallery'
|
||||
preLoaderRoute: typeof ColumnsLayoutGalleryImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/global': {
|
||||
id: '/columns/_layout/global'
|
||||
path: '/global'
|
||||
@@ -512,11 +511,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ColumnsLayoutGlobalImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/group': {
|
||||
id: '/columns/_layout/group'
|
||||
path: '/group'
|
||||
fullPath: '/columns/group'
|
||||
preLoaderRoute: typeof ColumnsLayoutGroupImport
|
||||
'/columns/_layout/newsfeed': {
|
||||
id: '/columns/_layout/newsfeed'
|
||||
path: '/newsfeed'
|
||||
fullPath: '/columns/newsfeed'
|
||||
preLoaderRoute: typeof ColumnsLayoutNewsfeedImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/stories': {
|
||||
@@ -526,11 +525,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ColumnsLayoutStoriesImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/newsfeed': {
|
||||
id: '/columns/_layout/newsfeed'
|
||||
path: '/newsfeed'
|
||||
fullPath: '/columns/newsfeed'
|
||||
preLoaderRoute: typeof ColumnsLayoutNewsfeedLazyImport
|
||||
'/columns/_layout/gallery': {
|
||||
id: '/columns/_layout/gallery'
|
||||
path: '/gallery'
|
||||
fullPath: '/columns/gallery'
|
||||
preLoaderRoute: typeof ColumnsLayoutGalleryLazyImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/notification': {
|
||||
@@ -575,6 +574,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedUsersImport
|
||||
parentRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
||||
}
|
||||
'/columns/_layout/groups/$id': {
|
||||
id: '/columns/_layout/groups/$id'
|
||||
path: '/groups/$id'
|
||||
fullPath: '/columns/groups/$id'
|
||||
preLoaderRoute: typeof ColumnsLayoutGroupsIdImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/interests/$id': {
|
||||
id: '/columns/_layout/interests/$id'
|
||||
path: '/interests/$id'
|
||||
fullPath: '/columns/interests/$id'
|
||||
preLoaderRoute: typeof ColumnsLayoutInterestsIdImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/events/$id': {
|
||||
id: '/columns/_layout/events/$id'
|
||||
path: '/events/$id'
|
||||
@@ -582,13 +595,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/hashtags/$content': {
|
||||
id: '/columns/_layout/hashtags/$content'
|
||||
path: '/hashtags/$content'
|
||||
fullPath: '/columns/hashtags/$content'
|
||||
preLoaderRoute: typeof ColumnsLayoutHashtagsContentLazyImport
|
||||
parentRoute: typeof ColumnsLayoutImport
|
||||
}
|
||||
'/columns/_layout/replies/$id': {
|
||||
id: '/columns/_layout/replies/$id'
|
||||
path: '/replies/$id'
|
||||
@@ -672,38 +678,36 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren =
|
||||
)
|
||||
|
||||
interface ColumnsLayoutRouteChildren {
|
||||
ColumnsLayoutCreateGroupRoute: typeof ColumnsLayoutCreateGroupRoute
|
||||
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
ColumnsLayoutGalleryRoute: typeof ColumnsLayoutGalleryRoute
|
||||
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
|
||||
ColumnsLayoutGroupRoute: typeof ColumnsLayoutGroupRoute
|
||||
ColumnsLayoutNewsfeedRoute: typeof ColumnsLayoutNewsfeedRoute
|
||||
ColumnsLayoutStoriesRoute: typeof ColumnsLayoutStoriesRoute
|
||||
ColumnsLayoutNewsfeedLazyRoute: typeof ColumnsLayoutNewsfeedLazyRoute
|
||||
ColumnsLayoutGalleryLazyRoute: typeof ColumnsLayoutGalleryLazyRoute
|
||||
ColumnsLayoutNotificationLazyRoute: typeof ColumnsLayoutNotificationLazyRoute
|
||||
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
|
||||
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
|
||||
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
|
||||
ColumnsLayoutGroupsIdRoute: typeof ColumnsLayoutGroupsIdRoute
|
||||
ColumnsLayoutInterestsIdRoute: typeof ColumnsLayoutInterestsIdRoute
|
||||
ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute
|
||||
ColumnsLayoutHashtagsContentLazyRoute: typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||
ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute
|
||||
ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute
|
||||
}
|
||||
|
||||
const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
|
||||
ColumnsLayoutCreateGroupRoute: ColumnsLayoutCreateGroupRoute,
|
||||
ColumnsLayoutCreateNewsfeedRoute:
|
||||
ColumnsLayoutCreateNewsfeedRouteWithChildren,
|
||||
ColumnsLayoutGalleryRoute: ColumnsLayoutGalleryRoute,
|
||||
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
|
||||
ColumnsLayoutGroupRoute: ColumnsLayoutGroupRoute,
|
||||
ColumnsLayoutNewsfeedRoute: ColumnsLayoutNewsfeedRoute,
|
||||
ColumnsLayoutStoriesRoute: ColumnsLayoutStoriesRoute,
|
||||
ColumnsLayoutNewsfeedLazyRoute: ColumnsLayoutNewsfeedLazyRoute,
|
||||
ColumnsLayoutGalleryLazyRoute: ColumnsLayoutGalleryLazyRoute,
|
||||
ColumnsLayoutNotificationLazyRoute: ColumnsLayoutNotificationLazyRoute,
|
||||
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
|
||||
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
|
||||
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
|
||||
ColumnsLayoutGroupsIdRoute: ColumnsLayoutGroupsIdRoute,
|
||||
ColumnsLayoutInterestsIdRoute: ColumnsLayoutInterestsIdRoute,
|
||||
ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute,
|
||||
ColumnsLayoutHashtagsContentLazyRoute: ColumnsLayoutHashtagsContentLazyRoute,
|
||||
ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute,
|
||||
ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute,
|
||||
}
|
||||
@@ -727,6 +731,8 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/loading': typeof LoadingRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||
@@ -743,21 +749,20 @@ export interface FileRoutesByFullPath {
|
||||
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/columns/group': typeof ColumnsLayoutGroupRoute
|
||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||
'/columns/gallery': typeof ColumnsLayoutGalleryLazyRoute
|
||||
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||
'/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||
}
|
||||
@@ -766,6 +771,8 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/loading': typeof LoadingRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||
@@ -782,21 +789,20 @@ export interface FileRoutesByTo {
|
||||
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/columns/group': typeof ColumnsLayoutGroupRoute
|
||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||
'/columns/gallery': typeof ColumnsLayoutGalleryLazyRoute
|
||||
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||
'/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||
}
|
||||
@@ -806,6 +812,8 @@ export interface FileRoutesById {
|
||||
'/': typeof IndexRoute
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/loading': typeof LoadingRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
'/$account': typeof AccountRouteWithChildren
|
||||
@@ -825,21 +833,20 @@ export interface FileRoutesById {
|
||||
'/$account/_settings/profile': typeof AccountSettingsProfileRoute
|
||||
'/$account/_settings/relay': typeof AccountSettingsRelayRoute
|
||||
'/$account/_settings/wallet': typeof AccountSettingsWalletRoute
|
||||
'/columns/_layout/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/_layout/gallery': typeof ColumnsLayoutGalleryRoute
|
||||
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/columns/_layout/group': typeof ColumnsLayoutGroupRoute
|
||||
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||
'/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute
|
||||
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||
'/columns/_layout/gallery': typeof ColumnsLayoutGalleryLazyRoute
|
||||
'/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
|
||||
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||
'/columns/_layout/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||
'/columns/_layout/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||
'/columns/_layout/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||
'/columns/_layout/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||
'/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||
'/columns/_layout/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||
'/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||
'/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||
}
|
||||
@@ -850,6 +857,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/bootstrap-relays'
|
||||
| '/loading'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/new'
|
||||
| '/reset'
|
||||
| '/$account'
|
||||
@@ -866,21 +875,20 @@ export interface FileRouteTypes {
|
||||
| '/$account/profile'
|
||||
| '/$account/relay'
|
||||
| '/$account/wallet'
|
||||
| '/columns/create-group'
|
||||
| '/columns/create-newsfeed'
|
||||
| '/columns/gallery'
|
||||
| '/columns/global'
|
||||
| '/columns/group'
|
||||
| '/columns/stories'
|
||||
| '/columns/newsfeed'
|
||||
| '/columns/stories'
|
||||
| '/columns/gallery'
|
||||
| '/columns/notification'
|
||||
| '/columns/onboarding'
|
||||
| '/columns/search'
|
||||
| '/columns/trending'
|
||||
| '/columns/create-newsfeed/f2f'
|
||||
| '/columns/create-newsfeed/users'
|
||||
| '/columns/groups/$id'
|
||||
| '/columns/interests/$id'
|
||||
| '/columns/events/$id'
|
||||
| '/columns/hashtags/$content'
|
||||
| '/columns/replies/$id'
|
||||
| '/columns/users/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
@@ -888,6 +896,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/bootstrap-relays'
|
||||
| '/loading'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/new'
|
||||
| '/reset'
|
||||
| '/$account'
|
||||
@@ -904,21 +914,20 @@ export interface FileRouteTypes {
|
||||
| '/$account/profile'
|
||||
| '/$account/relay'
|
||||
| '/$account/wallet'
|
||||
| '/columns/create-group'
|
||||
| '/columns/create-newsfeed'
|
||||
| '/columns/gallery'
|
||||
| '/columns/global'
|
||||
| '/columns/group'
|
||||
| '/columns/stories'
|
||||
| '/columns/newsfeed'
|
||||
| '/columns/stories'
|
||||
| '/columns/gallery'
|
||||
| '/columns/notification'
|
||||
| '/columns/onboarding'
|
||||
| '/columns/search'
|
||||
| '/columns/trending'
|
||||
| '/columns/create-newsfeed/f2f'
|
||||
| '/columns/create-newsfeed/users'
|
||||
| '/columns/groups/$id'
|
||||
| '/columns/interests/$id'
|
||||
| '/columns/events/$id'
|
||||
| '/columns/hashtags/$content'
|
||||
| '/columns/replies/$id'
|
||||
| '/columns/users/$id'
|
||||
id:
|
||||
@@ -926,6 +935,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/bootstrap-relays'
|
||||
| '/loading'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/new'
|
||||
| '/reset'
|
||||
| '/$account'
|
||||
@@ -945,21 +956,20 @@ export interface FileRouteTypes {
|
||||
| '/$account/_settings/profile'
|
||||
| '/$account/_settings/relay'
|
||||
| '/$account/_settings/wallet'
|
||||
| '/columns/_layout/create-group'
|
||||
| '/columns/_layout/create-newsfeed'
|
||||
| '/columns/_layout/gallery'
|
||||
| '/columns/_layout/global'
|
||||
| '/columns/_layout/group'
|
||||
| '/columns/_layout/stories'
|
||||
| '/columns/_layout/newsfeed'
|
||||
| '/columns/_layout/stories'
|
||||
| '/columns/_layout/gallery'
|
||||
| '/columns/_layout/notification'
|
||||
| '/columns/_layout/onboarding'
|
||||
| '/columns/_layout/search'
|
||||
| '/columns/_layout/trending'
|
||||
| '/columns/_layout/create-newsfeed/f2f'
|
||||
| '/columns/_layout/create-newsfeed/users'
|
||||
| '/columns/_layout/groups/$id'
|
||||
| '/columns/_layout/interests/$id'
|
||||
| '/columns/_layout/events/$id'
|
||||
| '/columns/_layout/hashtags/$content'
|
||||
| '/columns/_layout/replies/$id'
|
||||
| '/columns/_layout/users/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
@@ -969,6 +979,8 @@ export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
||||
LoadingRoute: typeof LoadingRoute
|
||||
SetGroupRoute: typeof SetGroupRoute
|
||||
SetInterestRoute: typeof SetInterestRoute
|
||||
NewLazyRoute: typeof NewLazyRoute
|
||||
ResetLazyRoute: typeof ResetLazyRoute
|
||||
AccountRoute: typeof AccountRouteWithChildren
|
||||
@@ -984,6 +996,8 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
||||
LoadingRoute: LoadingRoute,
|
||||
SetGroupRoute: SetGroupRoute,
|
||||
SetInterestRoute: SetInterestRoute,
|
||||
NewLazyRoute: NewLazyRoute,
|
||||
ResetLazyRoute: ResetLazyRoute,
|
||||
AccountRoute: AccountRouteWithChildren,
|
||||
@@ -1010,6 +1024,8 @@ export const routeTree = rootRoute
|
||||
"/",
|
||||
"/bootstrap-relays",
|
||||
"/loading",
|
||||
"/set-group",
|
||||
"/set-interest",
|
||||
"/new",
|
||||
"/reset",
|
||||
"/$account",
|
||||
@@ -1030,6 +1046,12 @@ export const routeTree = rootRoute
|
||||
"/loading": {
|
||||
"filePath": "loading.tsx"
|
||||
},
|
||||
"/set-group": {
|
||||
"filePath": "set-group.tsx"
|
||||
},
|
||||
"/set-interest": {
|
||||
"filePath": "set-interest.tsx"
|
||||
},
|
||||
"/new": {
|
||||
"filePath": "new.lazy.tsx"
|
||||
},
|
||||
@@ -1065,19 +1087,18 @@ export const routeTree = rootRoute
|
||||
"filePath": "columns/_layout.tsx",
|
||||
"parent": "/columns",
|
||||
"children": [
|
||||
"/columns/_layout/create-group",
|
||||
"/columns/_layout/create-newsfeed",
|
||||
"/columns/_layout/gallery",
|
||||
"/columns/_layout/global",
|
||||
"/columns/_layout/group",
|
||||
"/columns/_layout/stories",
|
||||
"/columns/_layout/newsfeed",
|
||||
"/columns/_layout/stories",
|
||||
"/columns/_layout/gallery",
|
||||
"/columns/_layout/notification",
|
||||
"/columns/_layout/onboarding",
|
||||
"/columns/_layout/search",
|
||||
"/columns/_layout/trending",
|
||||
"/columns/_layout/groups/$id",
|
||||
"/columns/_layout/interests/$id",
|
||||
"/columns/_layout/events/$id",
|
||||
"/columns/_layout/hashtags/$content",
|
||||
"/columns/_layout/replies/$id",
|
||||
"/columns/_layout/users/$id"
|
||||
]
|
||||
@@ -1132,10 +1153,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "$account/_settings/wallet.tsx",
|
||||
"parent": "/$account/_settings"
|
||||
},
|
||||
"/columns/_layout/create-group": {
|
||||
"filePath": "columns/_layout/create-group.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/create-newsfeed": {
|
||||
"filePath": "columns/_layout/create-newsfeed.tsx",
|
||||
"parent": "/columns/_layout",
|
||||
@@ -1144,24 +1161,20 @@ export const routeTree = rootRoute
|
||||
"/columns/_layout/create-newsfeed/users"
|
||||
]
|
||||
},
|
||||
"/columns/_layout/gallery": {
|
||||
"filePath": "columns/_layout/gallery.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/global": {
|
||||
"filePath": "columns/_layout/global.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/group": {
|
||||
"filePath": "columns/_layout/group.tsx",
|
||||
"/columns/_layout/newsfeed": {
|
||||
"filePath": "columns/_layout/newsfeed.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/stories": {
|
||||
"filePath": "columns/_layout/stories.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/newsfeed": {
|
||||
"filePath": "columns/_layout/newsfeed.lazy.tsx",
|
||||
"/columns/_layout/gallery": {
|
||||
"filePath": "columns/_layout/gallery.lazy.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/notification": {
|
||||
@@ -1188,12 +1201,16 @@ export const routeTree = rootRoute
|
||||
"filePath": "columns/_layout/create-newsfeed.users.tsx",
|
||||
"parent": "/columns/_layout/create-newsfeed"
|
||||
},
|
||||
"/columns/_layout/events/$id": {
|
||||
"filePath": "columns/_layout/events.$id.lazy.tsx",
|
||||
"/columns/_layout/groups/$id": {
|
||||
"filePath": "columns/_layout/groups.$id.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/hashtags/$content": {
|
||||
"filePath": "columns/_layout/hashtags.$content.lazy.tsx",
|
||||
"/columns/_layout/interests/$id": {
|
||||
"filePath": "columns/_layout/interests.$id.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/events/$id": {
|
||||
"filePath": "columns/_layout/events.$id.lazy.tsx",
|
||||
"parent": "/columns/_layout"
|
||||
},
|
||||
"/columns/_layout/replies/$id": {
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
export const Route = createFileRoute("/$account/_app")({
|
||||
beforeLoad: async () => {
|
||||
const systemPath = "resources/columns.json";
|
||||
const resourcePath = await resolveResource(systemPath);
|
||||
const resourceFile = await readTextFile(resourcePath);
|
||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||
|
||||
return { systemColumns };
|
||||
},
|
||||
});
|
||||
export const Route = createFileRoute("/$account/_app")();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appColumns } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { Column } from "@/components/column";
|
||||
import { LumeWindow } from "@/system";
|
||||
import type { ColumnEvent, LumeColumn } from "@/types";
|
||||
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
@@ -25,9 +28,8 @@ export const Route = createLazyFileRoute("/$account/_app/home")({
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { initialColumns } = Route.useRouteContext();
|
||||
const columns = useStore(appColumns, (state) => state);
|
||||
|
||||
const [columns, setColumns] = useState<LumeColumn[]>([]);
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
watchDrag: false,
|
||||
loop: false,
|
||||
@@ -51,11 +53,11 @@ function Screen() {
|
||||
|
||||
const add = useDebouncedCallback((column: LumeColumn) => {
|
||||
column.label = `${column.label}-${nanoid()}`; // update col label
|
||||
setColumns((prev) => [column, ...prev]);
|
||||
appColumns.setState((prev) => [column, ...prev]);
|
||||
}, 150);
|
||||
|
||||
const remove = useDebouncedCallback((label: string) => {
|
||||
setColumns((prev) => prev.filter((t) => t.label !== label));
|
||||
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
|
||||
}, 150);
|
||||
|
||||
const move = useDebouncedCallback(
|
||||
@@ -70,12 +72,12 @@ function Screen() {
|
||||
if (direction === "left") newCols.splice(colIndex - 1, 0, col);
|
||||
if (direction === "right") newCols.splice(colIndex + 1, 0, col);
|
||||
|
||||
setColumns(newCols);
|
||||
appColumns.setState(() => newCols);
|
||||
},
|
||||
150,
|
||||
);
|
||||
|
||||
const updateName = useDebouncedCallback((label: string, title: string) => {
|
||||
const update = useDebouncedCallback((label: string, title: string) => {
|
||||
const currentColIndex = columns.findIndex((col) => col.label === label);
|
||||
|
||||
const updatedCol = Object.assign({}, columns[currentColIndex]);
|
||||
@@ -84,10 +86,10 @@ function Screen() {
|
||||
const newCols = columns.slice();
|
||||
newCols[currentColIndex] = updatedCol;
|
||||
|
||||
setColumns(newCols);
|
||||
appColumns.setState(() => newCols);
|
||||
}, 150);
|
||||
|
||||
const reset = useDebouncedCallback(() => setColumns([]), 150);
|
||||
const reset = useDebouncedCallback(() => appColumns.setState(() => []), 150);
|
||||
|
||||
const handleKeyDown = useDebouncedCallback((event) => {
|
||||
if (event.defaultPrevented) return;
|
||||
@@ -106,18 +108,6 @@ function Screen() {
|
||||
event.preventDefault();
|
||||
}, 150);
|
||||
|
||||
const saveAllColumns = useDebouncedCallback(async () => {
|
||||
const key = "lume_v4:columns";
|
||||
const content = JSON.stringify(columns);
|
||||
const res = await commands.setLumeStore(key, content);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
console.log(res.error);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
if (emblaApi) {
|
||||
emblaApi.on("scroll", emitScrollEvent);
|
||||
@@ -132,14 +122,6 @@ function Screen() {
|
||||
};
|
||||
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) saveAllColumns();
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(initialColumns);
|
||||
}, [initialColumns]);
|
||||
|
||||
// Listen for keyboard event
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
@@ -158,7 +140,7 @@ function Screen() {
|
||||
if (data.payload.type === "move")
|
||||
move(data.payload.label, data.payload.direction);
|
||||
if (data.payload.type === "set_title")
|
||||
updateName(data.payload.label, data.payload.title);
|
||||
update(data.payload.label, data.payload.title);
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -166,6 +148,21 @@ function Screen() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function getSystemColumns() {
|
||||
const systemPath = "resources/columns.json";
|
||||
const resourcePath = await resolveResource(systemPath);
|
||||
const resourceFile = await readTextFile(resourcePath);
|
||||
const cols: LumeColumn[] = JSON.parse(resourceFile);
|
||||
|
||||
appColumns.setState(() => cols.filter((col) => col.default));
|
||||
}
|
||||
|
||||
if (!columns.length) {
|
||||
getSystemColumns();
|
||||
}
|
||||
}, [columns.length]);
|
||||
|
||||
return (
|
||||
<div className="size-full">
|
||||
<div ref={emblaRef} className="overflow-hidden size-full">
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/$account/_app/home")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
const key = "lume_v4:columns";
|
||||
const defaultColumns = context.systemColumns.filter((col) => col.default);
|
||||
const query = await commands.getLumeStore(key);
|
||||
|
||||
let initialColumns: LumeColumn[] = defaultColumns;
|
||||
|
||||
if (query.status === "ok") {
|
||||
initialColumns = JSON.parse(query.data);
|
||||
return { initialColumns };
|
||||
}
|
||||
|
||||
return { initialColumns };
|
||||
},
|
||||
});
|
||||
export const Route = createFileRoute("/$account/_app/home")();
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner } from "@/components";
|
||||
import { User } from "@/components/user";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/create-group")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
const REYA_NPUB =
|
||||
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445";
|
||||
|
||||
function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
const navigate = Route.useNavigate();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [npub, setNpub] = useState("");
|
||||
const [users, setUsers] = useState<string[]>([REYA_NPUB]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const toggleUser = (pubkey: string) => {
|
||||
setUsers((prev) =>
|
||||
prev.includes(pubkey)
|
||||
? prev.filter((i) => i !== pubkey)
|
||||
: [...prev, pubkey],
|
||||
);
|
||||
};
|
||||
|
||||
const addUser = () => {
|
||||
if (!npub.startsWith("npub1")) return;
|
||||
if (users.includes(npub)) return;
|
||||
|
||||
setUsers((prev) => [...prev, npub]);
|
||||
setNpub("");
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
const key = `lume_v4:group:${search.label}`;
|
||||
const res = await commands.setLumeStore(key, JSON.stringify(users));
|
||||
|
||||
if (res.status === "ok") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [search.label, search.account],
|
||||
});
|
||||
// @ts-ignore, tanstack router bug.
|
||||
navigate({ to: search.redirect, search: { ...search, name: title } });
|
||||
} else {
|
||||
await message(res.error, {
|
||||
title: "Create Group",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-4">
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<h1 className="font-serif text-2xl font-medium">Create a group</h1>
|
||||
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
For the people that you want to keep up.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col w-4/5 max-w-full gap-3">
|
||||
<div className="flex items-center w-full rounded-lg h-9 shrink-0 bg-neutral-200 dark:bg-neutral-800">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter a name for this group"
|
||||
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-full gap-3">
|
||||
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-neutral-200 dark:bg-neutral-900 rounded-xl">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
name="npub"
|
||||
value={npub}
|
||||
onChange={(e) => setNpub(e.target.value)}
|
||||
placeholder="npub1..."
|
||||
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-300 dark:bg-neutral-700 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addUser()}
|
||||
className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-300 dark:bg-neutral-700 shrink-0 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-semibold">Added</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
{users.length ? (
|
||||
users.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleUser(item)}
|
||||
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||
>
|
||||
<User.Provider pubkey={item}>
|
||||
<User.Root className="flex items-center gap-2.5">
|
||||
<User.Avatar className="rounded-full size-8" />
|
||||
<div className="flex items-center gap-1">
|
||||
<User.Name className="text-sm font-medium" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-300 dark:bg-neutral-700 h-14">
|
||||
Empty.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-semibold">Contacts</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
{contacts.length ? (
|
||||
contacts.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleUser(item)}
|
||||
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||
>
|
||||
<User.Provider pubkey={item}>
|
||||
<User.Root className="flex items-center gap-2.5">
|
||||
<User.Avatar className="rounded-full size-8" />
|
||||
<div className="flex items-center gap-1">
|
||||
<User.Name className="text-sm font-medium" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
|
||||
<p>
|
||||
Find more user at{" "}
|
||||
<a
|
||||
href="https://www.nostr.directory/"
|
||||
target="_blank"
|
||||
className="text-blue-600 after:content-['_↗']"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Nostr Directory
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending || users.length < 1}
|
||||
className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner /> : "Confirm"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +1,30 @@
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner, User } from "@/components";
|
||||
import { LumeWindow } from "@/system";
|
||||
import type { LumeColumn, NostrEvent } from "@/types";
|
||||
import { ArrowClockwise, Plus } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/gallery")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { columns } = Route.useRouteContext();
|
||||
|
||||
const install = async (column: LumeColumn) => {
|
||||
const mainWindow = getCurrentWindow();
|
||||
await mainWindow.emit("columns", { type: "add", column });
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
scrollHideDelay={300}
|
||||
className="overflow-hidden size-full"
|
||||
>
|
||||
<ScrollArea.Viewport className="relative h-full px-3">
|
||||
{columns.map((column) => (
|
||||
<div
|
||||
key={column.label}
|
||||
className="mb-3 group flex px-4 items-center justify-between h-16 rounded-xl bg-white dark:bg-black border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<div className="mb-px leading-tight font-semibold">
|
||||
{column.name}
|
||||
</div>
|
||||
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
|
||||
{column.description}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => install(column)}
|
||||
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||
<MyGroups />
|
||||
<MyInterests />
|
||||
<Core />
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||
@@ -55,3 +36,276 @@ function Screen() {
|
||||
</ScrollArea.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function Core() {
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["core"],
|
||||
queryFn: async () => {
|
||||
const systemPath = "resources/columns.json";
|
||||
const resourcePath = await resolveResource(systemPath);
|
||||
const resourceFile = await readTextFile(resourcePath);
|
||||
|
||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||
const columns = systemColumns.filter((col) => !col.default);
|
||||
|
||||
return columns;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h3 className="font-semibold">Core</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading ? (
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<Spinner className="size-4" />
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
data.map((column) => (
|
||||
<div
|
||||
key={column.label}
|
||||
className="group flex px-4 items-center justify-between h-16 rounded-xl bg-white dark:bg-black border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<div className="mb-px leading-tight font-semibold">
|
||||
{column.name}
|
||||
</div>
|
||||
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
|
||||
{column.description}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openColumn(column)}
|
||||
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyGroups() {
|
||||
const { account } = Route.useSearch();
|
||||
const { isLoading, data, refetch } = useQuery({
|
||||
queryKey: ["mygroups", account],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllGroups();
|
||||
|
||||
if (res.status === "ok") {
|
||||
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
select: (data) =>
|
||||
data.filter(
|
||||
(item) => item.tags.filter((tag) => tag[0] === "p")?.length > 0,
|
||||
),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const renderItem = useCallback(
|
||||
(item: NostrEvent) => {
|
||||
const name = item.tags.filter((tag) => tag[0] === "d")[0][1] ?? "unnamed";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
|
||||
{item.tags
|
||||
.filter((tag) => tag[0] === "p")
|
||||
.map((tag) => (
|
||||
<div key={tag[1]}>
|
||||
<User.Provider pubkey={tag[1]}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-8 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openColumn({
|
||||
label: name,
|
||||
name,
|
||||
url: `/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"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-12 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h3 className="font-semibold">My groups</h3>
|
||||
<div className="inline-flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="size-7 inline-flex items-center justify-center rounded-full"
|
||||
>
|
||||
<ArrowClockwise className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openPopup("New group", `/set-group?account=${account}`)
|
||||
}
|
||||
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" />
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading ? (
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<Spinner className="size-4" />
|
||||
Loading...
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
|
||||
<p className="text-center">You don't have any groups yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => renderItem(item))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyInterests() {
|
||||
const { account } = Route.useSearch();
|
||||
const { isLoading, data, refetch } = useQuery({
|
||||
queryKey: ["myinterests", account],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAllInterests();
|
||||
|
||||
if (res.status === "ok") {
|
||||
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
select: (data) =>
|
||||
data.filter(
|
||||
(item) => item.tags.filter((tag) => tag[0] === "t")?.length > 0,
|
||||
),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const renderItem = useCallback(
|
||||
(item: NostrEvent) => {
|
||||
const name = item.tags.filter((tag) => tag[0] === "d")[0][1] ?? "unnamed";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
|
||||
{item.tags
|
||||
.filter((tag) => tag[0] === "t")
|
||||
.map((tag) => (
|
||||
<div key={tag[1]} className="text-sm font-medium">
|
||||
{tag[1]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openColumn({
|
||||
label: name,
|
||||
name,
|
||||
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"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-12 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h3 className="font-semibold">My interests</h3>
|
||||
<div className="inline-flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="size-7 inline-flex items-center justify-center rounded-full"
|
||||
>
|
||||
<ArrowClockwise className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openPopup(
|
||||
"New interest",
|
||||
`/set-interest?account=${account}`,
|
||||
)
|
||||
}
|
||||
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" />
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading ? (
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<Spinner className="size-4" />
|
||||
Loading...
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
|
||||
<p className="text-center">You don't have any interests yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => renderItem(item))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/gallery")({
|
||||
beforeLoad: async () => {
|
||||
const systemPath = "resources/columns.json";
|
||||
const resourcePath = await resolveResource(systemPath);
|
||||
const resourceFile = await readTextFile(resourcePath);
|
||||
|
||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||
const columns = systemColumns.filter((col) => !col.default);
|
||||
|
||||
return {
|
||||
columns,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/group")({
|
||||
beforeLoad: async ({ search }) => {
|
||||
const key = `lume_v4:group:${search.label}`;
|
||||
const res = await commands.getLumeStore(key);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const groups: string[] = JSON.parse(res.data);
|
||||
|
||||
if (groups.length) {
|
||||
return { groups };
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
to: "/columns/create-group",
|
||||
search: {
|
||||
...search,
|
||||
redirect: "/columns/group",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -7,16 +7,19 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/group")({
|
||||
export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
const { label, account } = Route.useSearch();
|
||||
const { groups } = Route.useRouteContext();
|
||||
const group = Route.useLoaderData();
|
||||
const params = Route.useParams();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -25,11 +28,11 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [label, account],
|
||||
queryKey: ["groups", params.id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const res = await commands.getGroupEvents(groups, until);
|
||||
const res = await commands.getAllEventsByAuthors(group, until);
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
@@ -39,6 +42,7 @@ export function Screen() {
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
select: (data) => data?.pages.flat(),
|
||||
enabled: group?.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
@@ -80,6 +84,16 @@ export function Screen() {
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("synchronized", async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["groups", params.id] });
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
20
src/routes/columns/_layout/groups.$id.tsx
Normal file
20
src/routes/columns/_layout/groups.$id.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { NostrEvent } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/groups/$id")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.getGroup(params.id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const event: NostrEvent = JSON.parse(res.data);
|
||||
const tag = event.tags
|
||||
.filter((tag) => tag[0] === "p")
|
||||
.map((tag) => tag[1]);
|
||||
|
||||
return tag;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,16 +7,19 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/hashtags/$content")({
|
||||
export const Route = createLazyFileRoute("/columns/_layout/interests/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
const { label, account } = Route.useSearch();
|
||||
const { content } = Route.useParams();
|
||||
const hashtags = Route.useLoaderData();
|
||||
const params = Route.useParams();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -25,12 +28,12 @@ export function Screen() {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [label, account],
|
||||
queryKey: ["hashtags", params.id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const hashtags = content.split("_");
|
||||
const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", ""));
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const res = await commands.getHashtagEvents(hashtags, until);
|
||||
const res = await commands.getAllEventsByHashtags(tags, until);
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
@@ -81,6 +84,18 @@ export function Screen() {
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("synchronized", async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["hashtags", params.id],
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
20
src/routes/columns/_layout/interests.$id.tsx
Normal file
20
src/routes/columns/_layout/interests.$id.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { NostrEvent } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/interests/$id")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.getInterest(params.id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const event: NostrEvent = JSON.parse(res.data);
|
||||
const tag = event.tags
|
||||
.filter((tag) => tag[0] === "t")
|
||||
.map((tag) => tag[1]);
|
||||
|
||||
return tag;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -6,11 +6,7 @@ import { Kind, type Meta } from "@/types";
|
||||
import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Navigate,
|
||||
createLazyFileRoute,
|
||||
useLocation,
|
||||
} from "@tanstack/react-router";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
@@ -33,12 +29,12 @@ export const Route = createLazyFileRoute("/columns/_layout/newsfeed")({
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
const { label, account } = Route.useSearch();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
@@ -48,7 +44,7 @@ export function Screen() {
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const res = await commands.getLocalEvents(until);
|
||||
const res = await commands.getAllEventsByAuthors(contacts, until);
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
@@ -58,9 +54,9 @@ export function Screen() {
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1,
|
||||
select: (data) => data?.pages.flat(),
|
||||
enabled: contacts?.length > 0,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderItem = useCallback(
|
||||
@@ -109,16 +105,6 @@ export function Screen() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/columns/create-newsfeed/users"
|
||||
// @ts-ignore, tanstack router bug.
|
||||
search={{ label, account, redirect: location.href }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/create-group")({
|
||||
export const Route = createFileRoute("/columns/_layout/newsfeed")({
|
||||
loader: async () => {
|
||||
const res = await commands.getContactList();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type ReactNode, useEffect, useRef } from "react";
|
||||
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/notification")({
|
||||
@@ -180,17 +180,7 @@ function Screen() {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{events.map((event) => (
|
||||
<User.Provider
|
||||
key={event.id}
|
||||
pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
|
||||
>
|
||||
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
||||
<User.Avatar className="rounded-full size-6" />
|
||||
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
||||
₿ {decodeZapInvoice(event.tags).bitcoinFormatted}
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<ZapReceipt key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,3 +301,36 @@ function TextNote({ event }: { event: LumeEvent }) {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ZapReceipt({ event }: { event: LumeEvent }) {
|
||||
const amount = useMemo(
|
||||
() => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0",
|
||||
[event.id],
|
||||
);
|
||||
const sender = useMemo(
|
||||
() => event.tags.find((tag) => tag[0] === "P")?.[1],
|
||||
[event.id],
|
||||
);
|
||||
|
||||
if (!sender) {
|
||||
return (
|
||||
<div className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
||||
<div className="rounded-full size-6 bg-blue-500" />
|
||||
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
||||
₿ {amount}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<User.Provider pubkey={sender}>
|
||||
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
||||
<User.Avatar className="rounded-full size-6" />
|
||||
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
||||
₿ {amount}
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
} = useQuery({
|
||||
queryKey: ["stories", contact],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getEventsBy(contact, 10);
|
||||
const res = await commands.getAllEventsByAuthor(contact, 10);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const data = toLumeEvents(res.data);
|
||||
|
||||
@@ -22,7 +22,7 @@ function Screen() {
|
||||
const { isLoading, data: events } = useQuery({
|
||||
queryKey: ["stories", id],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getEventsBy(id, 100);
|
||||
const res = await commands.getAllEventsByAuthor(id, 100);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const data = toLumeEvents(res.data);
|
||||
|
||||
@@ -21,25 +21,21 @@ function Screen() {
|
||||
href="/auth/connect"
|
||||
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-900 ring-1 ring-black/5 dark:ring-white/5"
|
||||
>
|
||||
<h3 className="mb-1.5 font-medium">Continue with Nostr Connect</h3>
|
||||
<div className="text-sm">
|
||||
<p className="text-neutral-500 dark:text-neutral-600">
|
||||
Your account will be handled by a remote signer. Lume will not
|
||||
store your account keys.
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="mb-1 font-medium">Continue with Nostr Connect</h3>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-600">
|
||||
Your account will be handled by a remote signer. Lume will not
|
||||
store your account keys.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
href="/auth/import"
|
||||
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-900 ring-1 ring-black/5 dark:ring-white/5"
|
||||
>
|
||||
<h3 className="mb-1.5 font-medium">Continue with Secret Key</h3>
|
||||
<div className="text-sm">
|
||||
<p className="text-neutral-500 dark:text-neutral-600">
|
||||
Lume will store your keys in secure storage. You can provide a
|
||||
password to add extra security.
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="mb-1 font-medium">Continue with Secret Key</h3>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-600">
|
||||
Lume will store your keys in secure storage. You can provide a
|
||||
password to add extra security.
|
||||
</p>
|
||||
</a>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1 h-px bg-black/5 dark:bg-white/5" />
|
||||
|
||||
205
src/routes/set-group.lazy.tsx
Normal file
205
src/routes/set-group.lazy.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner } from "@/components";
|
||||
import { User } from "@/components/user";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/set-group")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const { account } = Route.useSearch();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [npub, setNpub] = useState("");
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const toggleUser = (pubkey: string) => {
|
||||
setUsers((prev) =>
|
||||
prev.includes(pubkey)
|
||||
? prev.filter((i) => i !== pubkey)
|
||||
: [...prev, pubkey],
|
||||
);
|
||||
};
|
||||
|
||||
const addUser = () => {
|
||||
if (!npub.startsWith("npub1")) return;
|
||||
if (users.includes(npub)) return;
|
||||
|
||||
setUsers((prev) => [...prev, npub]);
|
||||
setNpub("");
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
const res = await commands.setGroup(title, null, null, users);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const window = getCurrentWindow();
|
||||
|
||||
// Invalidate cache
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["mygroups", account],
|
||||
});
|
||||
|
||||
// Create column in the main window
|
||||
await window.emitTo("main", "columns", {
|
||||
type: "add",
|
||||
column: {
|
||||
label: res.data,
|
||||
name: title,
|
||||
url: `/columns/groups/${res.data}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Close current popup
|
||||
await window.close();
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col size-full">
|
||||
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||
<div className="shrink-0 h-14 px-3 flex items-center gap-2 justify-between border-b border-black/5 dark:border-white/5">
|
||||
<div className="flex items-center flex-1 rounded-lg h-9 shrink-0 bg-black/10 dark:bg-white/10">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="family, bff, devs,..."
|
||||
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending || users.length < 1}
|
||||
className="shrink-0 inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-lg w-20 h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner /> : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
scrollHideDelay={300}
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold">Added</h3>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
name="npub"
|
||||
value={npub}
|
||||
onChange={(e) => setNpub(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") addUser();
|
||||
}}
|
||||
placeholder="npub1..."
|
||||
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-100 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addUser()}
|
||||
className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-200 dark:bg-neutral-800 shrink-0 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{users.length ? (
|
||||
users.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleUser(item)}
|
||||
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||
>
|
||||
<User.Provider pubkey={item}>
|
||||
<User.Root className="flex items-center gap-2.5">
|
||||
<User.Avatar className="rounded-full size-8" />
|
||||
<div className="flex items-center gap-1">
|
||||
<User.Name className="text-sm font-medium" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-100 dark:bg-neutral-900 h-14">
|
||||
Please add some user to your group.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold">Contacts</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{contacts.length ? (
|
||||
contacts
|
||||
.filter((c) => !users.includes(c))
|
||||
.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleUser(item)}
|
||||
className="inline-flex items-center justify-between px-3 py-2 rounded-lg border-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||
>
|
||||
<User.Provider pubkey={item}>
|
||||
<User.Root className="flex items-center gap-2.5">
|
||||
<User.Avatar className="rounded-full size-8" />
|
||||
<div className="flex items-center gap-1">
|
||||
<User.Name className="text-sm font-medium" />
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
|
||||
<p>
|
||||
Find more user at{" "}
|
||||
<a
|
||||
href="https://www.nostr.directory/"
|
||||
target="_blank"
|
||||
className="text-blue-600 after:content-['_↗']"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Nostr Directory
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
23
src/routes/set-group.tsx
Normal file
23
src/routes/set-group.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
198
src/routes/set-interest.lazy.tsx
Normal file
198
src/routes/set-interest.lazy.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { cn } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
const TOPICS = [
|
||||
{
|
||||
title: "Popular",
|
||||
content: [
|
||||
"#nostr",
|
||||
"#introductions",
|
||||
"#grownostr",
|
||||
"#zap",
|
||||
"#meme",
|
||||
"#asknostr",
|
||||
"#bitcoin",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Route = createLazyFileRoute("/set-interest")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [hashtag, setHashtag] = useState("");
|
||||
const [hashtags, setHashtags] = useState<string[]>([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { account } = Route.useSearch();
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const toggleHashtag = (tag: string) => {
|
||||
setHashtags((prev) =>
|
||||
prev.includes(tag) ? prev.filter((i) => i !== tag) : [...prev, tag],
|
||||
);
|
||||
};
|
||||
|
||||
const addHashtag = () => {
|
||||
if (!hashtag.startsWith("#")) return;
|
||||
if (hashtags.includes(hashtag)) return;
|
||||
|
||||
setHashtags((prev) => [...prev, hashtag]);
|
||||
setHashtag("");
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
const content = hashtags.map((tag) =>
|
||||
tag.toLowerCase().replace(" ", "-").replace("#", ""),
|
||||
);
|
||||
const res = await commands.setInterest(title, null, null, content);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const window = getCurrentWindow();
|
||||
|
||||
// Invalidate cache
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["myinterests", account],
|
||||
});
|
||||
|
||||
// Create column in the main window
|
||||
await window.emitTo("main", "columns", {
|
||||
type: "add",
|
||||
column: {
|
||||
label: res.data,
|
||||
name: title,
|
||||
url: `/columns/interests/${res.data}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Close current popup
|
||||
await window.close();
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col size-full">
|
||||
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||
<div className="shrink-0 h-14 px-3 flex items-center gap-2 justify-between border-b border-black/5 dark:border-white/5">
|
||||
<div className="flex items-center flex-1 rounded-lg h-9 shrink-0 bg-black/10 dark:bg-white/10">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="anime, sport, art,..."
|
||||
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="shrink-0 inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-lg w-20 h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner /> : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
scrollHideDelay={300}
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
<span className="text-sm font-semibold">Added</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
{hashtags.length ? (
|
||||
hashtags.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleHashtag(item)}
|
||||
className="inline-flex items-center justify-between p-3 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||
>
|
||||
<p>{item}</p>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-100 dark:bg-neutral-900 h-14">
|
||||
Please add some hashtag.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-semibold">Hashtags</span>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
name="hashtag"
|
||||
placeholder="#nostr"
|
||||
onChange={(e) => setHashtag(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") addHashtag();
|
||||
}}
|
||||
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-100 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addHashtag()}
|
||||
className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-200 dark:bg-neutral-800 shrink-0 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
{TOPICS.map((topic) => (
|
||||
<div key={topic.title} className="flex flex-col gap-2">
|
||||
<div className="text-sm font-semibold">{topic.title}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{topic.content.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleHashtag(item)}
|
||||
className={cn(
|
||||
"text-sm p-2 rounded-full",
|
||||
hashtags.includes(item)
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 dark:bg-neutral-900",
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
13
src/routes/set-interest.tsx
Normal file
13
src/routes/set-interest.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { LumeColumn, NostrEvent } from "@/types";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { LumeEvent } from "./event";
|
||||
|
||||
export const LumeWindow = {
|
||||
@@ -150,8 +151,22 @@ export const LumeWindow = {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
openMainWindow: async () => {
|
||||
const query = await commands.reopenLume();
|
||||
return query;
|
||||
openPopup: async (title: string, url: string) => {
|
||||
const query = await commands.openWindow({
|
||||
label: `popup-${nanoid()}`,
|
||||
url,
|
||||
title,
|
||||
width: 400,
|
||||
height: 500,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
hidden_title: false,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user