feat: Add support for NIP-51 (#236)

* feat: and follow and interest sets

* feat: improve query

* feat: improve
This commit is contained in:
雨宮蓮
2024-10-07 14:33:20 +07:00
committed by GitHub
parent d841163ba7
commit 090a815f99
37 changed files with 1500 additions and 765 deletions

View File

@@ -14,7 +14,8 @@
"zap-*",
"event-*",
"user-*",
"editor-*"
"editor-*",
"popup-*"
],
"permissions": [
"core:path:default",

View File

@@ -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"]}}

View File

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

View File

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

View File

@@ -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()),
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[]>([]);

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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