feat: add support for NIP-09

This commit is contained in:
2024-10-08 16:30:26 +07:00
parent d2b5ae0507
commit c40762cc04
13 changed files with 270 additions and 436 deletions

View File

@@ -325,8 +325,8 @@ pub async fn login(
.reconcile( .reconcile(
Filter::new() Filter::new()
.authors(authors.clone()) .authors(authors.clone())
.kinds(vec![Kind::Metadata, Kind::ContactList]) .kinds(vec![Kind::Metadata, Kind::ContactList, Kind::EventDeletion])
.limit(authors.len() * 10), .limit(authors.len() * 20),
NegentropyOptions::default(), NegentropyOptions::default(),
) )
.await .await
@@ -409,8 +409,13 @@ pub async fn login(
.reconcile( .reconcile(
Filter::new() Filter::new()
.authors(trusted) .authors(trusted)
.kinds(vec![Kind::Metadata, Kind::TextNote, Kind::Repost]) .kinds(vec![
.limit(20000), Kind::Metadata,
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
])
.limit(30000),
NegentropyOptions::default(), NegentropyOptions::default(),
) )
.await .await

View File

@@ -5,7 +5,7 @@ use specta::Type;
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use tauri::State; use tauri::State;
use crate::common::{create_event_tags, filter_converstation, parse_event, Meta}; use crate::common::{create_tags, parse_event, process_event, Meta};
use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT}; use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT};
#[derive(Debug, Clone, Serialize, Type)] #[derive(Debug, Clone, Serialize, Type)]
@@ -215,22 +215,7 @@ pub async fn get_all_events_by_author(
.get_events_of(vec![filter], EventSource::Database) .get_events_of(vec![filter], EventSource::Database)
.await .await
{ {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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()), Err(err) => Err(err.to_string()),
} }
} }
@@ -264,23 +249,7 @@ pub async fn get_all_events_by_authors(
.get_events_of(vec![filter], EventSource::Database) .get_events_of(vec![filter], EventSource::Database)
.await .await
{ {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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()), Err(err) => Err(err.to_string()),
} }
} }
@@ -312,22 +281,7 @@ pub async fn get_all_events_by_hashtags(
) )
.await .await
{ {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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()), Err(err) => Err(err.to_string()),
} }
} }
@@ -351,22 +305,7 @@ pub async fn get_local_events(
.until(as_of); .until(as_of);
match client.database().query(vec![filter]).await { match client.database().query(vec![filter]).await {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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()), Err(err) => Err(err.to_string()),
} }
} }
@@ -396,22 +335,7 @@ pub async fn get_global_events(
) )
.await .await
{ {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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()), Err(err) => Err(err.to_string()),
} }
} }
@@ -427,7 +351,7 @@ pub async fn publish(
let client = &state.client; let client = &state.client;
// Create tags from content // Create tags from content
let mut tags = create_event_tags(&content); let mut tags = create_tags(&content);
// Add client tag // Add client tag
// TODO: allow user config this setting // TODO: allow user config this setting
@@ -464,10 +388,8 @@ pub async fn reply(
let client = &state.client; let client = &state.client;
let database = client.database(); let database = client.database();
// Create tags from content
let mut tags = create_event_tags(&content);
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?; let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
let mut tags = create_tags(&content);
match database.query(vec![Filter::new().id(reply_id)]).await { match database.query(vec![Filter::new().id(reply_id)]).await {
Ok(events) => { Ok(events) => {
@@ -651,23 +573,7 @@ pub async fn search(
) )
.await .await
{ {
Ok(events) => { Ok(events) => Ok(process_event(client, events).await),
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(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
} }
} }
@@ -689,3 +595,16 @@ pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<boo
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
} }
} }
#[tauri::command]
#[specta::specta]
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
let builder = EventBuilder::delete(vec![event_id]);
match client.send_event_builder(builder).await {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}

View File

@@ -1,3 +1,4 @@
use futures::future::join_all;
use linkify::LinkFinder; use linkify::LinkFinder;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use reqwest::Client as ReqClient; use reqwest::Client as ReqClient;
@@ -7,7 +8,7 @@ use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use crate::Settings; use crate::RichEvent;
#[derive(Debug, Clone, Serialize, Type)] #[derive(Debug, Clone, Serialize, Type)]
pub struct Meta { pub struct Meta {
@@ -18,6 +19,7 @@ pub struct Meta {
pub hashtags: Vec<String>, pub hashtags: Vec<String>,
} }
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
const NOSTR_EVENTS: [&str; 10] = [ const NOSTR_EVENTS: [&str; 10] = [
"@nevent1", "@nevent1",
"@note1", "@note1",
@@ -42,135 +44,12 @@ const NOSTR_MENTIONS: [&str; 10] = [
"Nostr:nprofile1", "Nostr:nprofile1",
"Nostr:naddr1", "Nostr:naddr1",
]; ];
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
pub fn get_latest_event(events: &[Event]) -> Option<&Event> { pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
events.iter().next() events.iter().next()
} }
pub fn filter_converstation(events: Vec<Event>) -> Vec<Event> { pub fn create_tags(content: &str) -> Vec<Tag> {
events
.into_iter()
.filter_map(|ev| {
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)
} else {
None
}
} else {
Some(ev)
}
})
.collect::<Vec<Event>>()
}
pub fn dedup_event(events: &[Event]) -> Vec<Event> {
let mut seen_ids = HashSet::new();
events
.iter()
.filter(|&event| {
let e = TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E));
let e_tags: Vec<&Tag> = event.tags.iter().filter(|el| el.kind() == e).collect();
let ids: Vec<&str> = e_tags.iter().filter_map(|tag| tag.content()).collect();
let is_dup = ids.iter().any(|id| seen_ids.contains(*id));
for id in &ids {
seen_ids.insert(*id);
}
!is_dup
})
.cloned()
.collect()
}
pub async fn parse_event(content: &str) -> Meta {
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
// Get urls
let urls: Vec<_> = finder.links(content).collect();
// Get words
let words: Vec<_> = content.split_whitespace().collect();
let hashtags = words
.iter()
.filter(|&&word| word.starts_with('#'))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let events = words
.iter()
.filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el)))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let mentions = words
.iter()
.filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el)))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let mut images = Vec::new();
let mut text = content.to_string();
if !urls.is_empty() {
let client = ReqClient::new();
for url in urls {
let url_str = url.as_str();
if let Ok(parsed_url) = Url::from_str(url_str) {
if let Some(ext) = parsed_url
.path_segments()
.and_then(|segments| segments.last().and_then(|s| s.split('.').last()))
{
if IMAGES.contains(&ext) {
text = text.replace(url_str, "");
images.push(url_str.to_string());
// Process the next item.
continue;
}
}
// Check the content type of URL via HEAD request
if let Ok(res) = client.head(url_str).send().await {
if let Some(content_type) = res.headers().get("Content-Type") {
if content_type.to_str().unwrap_or("").starts_with("image") {
text = text.replace(url_str, "");
images.push(url_str.to_string());
// Process the next item.
continue;
}
}
}
}
}
}
// Clean up the resulting content string to remove extra spaces
let cleaned_text = text.trim().to_string();
Meta {
content: cleaned_text,
events,
mentions,
hashtags,
images,
}
}
pub fn create_event_tags(content: &str) -> Vec<Tag> {
let mut tags: Vec<Tag> = vec![]; let mut tags: Vec<Tag> = vec![];
let mut tag_set: HashSet<String> = HashSet::new(); let mut tag_set: HashSet<String> = HashSet::new();
@@ -251,6 +130,72 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
tags tags
} }
pub async fn process_event(client: &Client, events: Vec<Event>) -> Vec<RichEvent> {
// Remove event thread if event is TextNote
let events: Vec<Event> = events
.into_iter()
.filter_map(|ev| {
if ev.kind == Kind::TextNote {
let tags = ev
.tags
.iter()
.filter(|t| t.is_reply() || t.is_root())
.filter_map(|t| t.content())
.collect::<Vec<_>>();
if tags.is_empty() {
Some(ev)
} else {
None
}
} else {
Some(ev)
}
})
.collect();
// Get deletion request by event's authors
let ids: Vec<EventId> = events.iter().map(|ev| ev.id).collect();
let filter = Filter::new().events(ids).kind(Kind::EventDeletion);
let mut final_events: Vec<Event> = events.clone();
if let Ok(requests) = client.database().query(vec![filter]).await {
if !requests.is_empty() {
let ids: Vec<&str> = requests
.iter()
.flat_map(|ev| ev.get_tags_content(TagKind::e()))
.collect();
// Remove event if event is deleted by author
final_events = events
.into_iter()
.filter_map(|ev| {
if ids.iter().any(|&i| i == ev.id.to_hex()) {
None
} else {
Some(ev)
}
})
.collect();
}
};
// Convert raw event to rich event
let futures = final_events.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 }
});
join_all(futures).await
}
pub async fn init_nip65(client: &Client, public_key: &str) { pub async fn init_nip65(client: &Client, public_key: &str) {
let author = PublicKey::from_str(public_key).unwrap(); let author = PublicKey::from_str(public_key).unwrap();
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1); let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
@@ -286,73 +231,78 @@ pub async fn init_nip65(client: &Client, public_key: &str) {
} }
} }
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> { pub async fn parse_event(content: &str) -> Meta {
let ident = "lume_v4:settings"; let mut finder = LinkFinder::new();
let signer = client finder.url_must_have_scheme(false);
.signer()
.await
.map_err(|e| format!("Failed to get signer: {:?}", e))?;
let public_key = signer
.public_key()
.await
.map_err(|e| format!("Failed to get public key: {:?}", e))?;
let filter = Filter::new() // Get urls
.author(public_key) let urls: Vec<_> = finder.links(content).collect();
.kind(Kind::ApplicationSpecificData) // Get words
.identifier(ident) let words: Vec<_> = content.split_whitespace().collect();
.limit(1);
match client let hashtags = words
.get_events_of( .iter()
vec![filter], .filter(|&&word| word.starts_with('#'))
EventSource::both(Some(Duration::from_secs(5))), .map(|&s| s.to_string())
) .collect::<Vec<_>>();
.await
{ let events = words
Ok(events) => { .iter()
if let Some(event) = events.first() { .filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el)))
match signer.nip44_decrypt(&public_key, &event.content).await { .map(|&s| s.to_string())
Ok(decrypted) => match serde_json::from_str(&decrypted) { .collect::<Vec<_>>();
Ok(parsed) => Ok(parsed),
Err(_) => Err("Could not parse settings payload".into()), let mentions = words
}, .iter()
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)), .filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el)))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let mut images = Vec::new();
let mut text = content.to_string();
if !urls.is_empty() {
let client = ReqClient::new();
for url in urls {
let url_str = url.as_str();
if let Ok(parsed_url) = Url::from_str(url_str) {
if let Some(ext) = parsed_url
.path_segments()
.and_then(|segments| segments.last().and_then(|s| s.split('.').last()))
{
if IMAGES.contains(&ext) {
text = text.replace(url_str, "");
images.push(url_str.to_string());
// Process the next item.
continue;
}
}
// Check the content type of URL via HEAD request
if let Ok(res) = client.head(url_str).send().await {
if let Some(content_type) = res.headers().get("Content-Type") {
if content_type.to_str().unwrap_or("").starts_with("image") {
text = text.replace(url_str, "");
images.push(url_str.to_string());
// Process the next item.
continue;
}
}
} }
} else {
Err("Settings not found.".into())
} }
} }
Err(e) => Err(format!( }
"Failed to get events for ApplicationSpecificData: {:?}",
e // Clean up the resulting content string to remove extra spaces
)), let cleaned_text = text.trim().to_string();
}
} Meta {
content: cleaned_text,
#[cfg(test)] events,
mod tests { mentions,
use super::*; hashtags,
images,
#[tokio::test]
async fn test_parse_event() {
let content = "Check this image: https://example.com/image.jpg #cool @npub1";
let meta = parse_event(content).await;
assert_eq!(meta.content, "Check this image: #cool @npub1");
assert_eq!(meta.images, vec!["https://example.com/image.jpg"]);
assert_eq!(meta.hashtags, vec!["#cool"]);
assert_eq!(meta.mentions, vec!["@npub1"]);
}
#[tokio::test]
async fn test_parse_video() {
let content = "Check this video: https://example.com/video.mp4 #cool @npub1";
let meta = parse_event(content).await;
assert_eq!(meta.content, "Check this video: #cool @npub1");
assert_eq!(meta.images, Vec::<String>::new());
assert_eq!(meta.hashtags, vec!["#cool"]);
assert_eq!(meta.mentions, vec!["@npub1"]);
} }
} }

View File

@@ -143,6 +143,7 @@ fn main() {
get_local_events, get_local_events,
get_global_events, get_global_events,
is_deleted_event, is_deleted_event,
request_delete,
search, search,
publish, publish,
reply, reply,

View File

@@ -390,6 +390,14 @@ async isDeletedEvent(id: string) : Promise<Result<boolean, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async requestDelete(id: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async search(query: string, until: string | null) : Promise<Result<RichEvent[], string>> { async search(query: string, until: string | null) : Promise<Result<RichEvent[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("search", { query, until }) }; return { status: "ok", data: await TAURI_INVOKE("search", { query, until }) };

View File

@@ -13,14 +13,17 @@ export const MentionNote = memo(function MentionNote({
return ( return (
<div className="relative my-2"> <div className="relative my-2">
<div className="pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10"> <div className="min-h-[64px] pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10">
{isLoading ? ( {isLoading ? (
<Spinner /> <div className="h-[64px] flex items-center">
<Spinner />
</div>
) : isError || !event ? ( ) : isError || !event ? (
<p className="text-sm font-medium text-red-500"> <div className="h-[64px] flex items-center">
{error.message || <p className="text-sm font-medium text-red-500">
"Quoted note is not found with your current relay set"} {error.message || "Note can be found with your current relay set"}
</p> </p>
</div>
) : ( ) : (
<Note.Provider event={event}> <Note.Provider event={event}>
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>

View File

@@ -1,51 +1,68 @@
import { commands } from "@/commands.gen";
import { DotsThree } from "@phosphor-icons/react"; import { DotsThree } from "@phosphor-icons/react";
import { useSearch } from "@tanstack/react-router";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { nip19 } from "nostr-tools";
import { useCallback } from "react"; import { useCallback } from "react";
import { useNoteContext } from "./provider"; import { useNoteContext } from "./provider";
export function NoteMenu() { export function NoteMenu() {
const event = useNoteContext(); const event = useNoteContext();
const { account }: { account: string } = useSearch({ strict: false });
const showContextMenu = useCallback(async (e: React.MouseEvent) => { const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
const menuItems = await Promise.all([ const list = [
MenuItem.new({ MenuItem.new({
text: "Copy Sharable Link", text: "Copy ID",
action: async () => {
const eventId = await event.idAsBech32();
await writeText(`https://njump.me/${eventId}`);
},
}),
MenuItem.new({
text: "Copy Event ID",
action: async () => { action: async () => {
const eventId = await event.idAsBech32(); const eventId = await event.idAsBech32();
await writeText(eventId); await writeText(eventId);
}, },
}), }),
MenuItem.new({ MenuItem.new({
text: "Copy Public Key", text: "Copy author",
action: async () => { action: async () => {
const pubkey = await event.pubkeyAsBech32(); const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey); await writeText(pubkey);
}, },
}), }),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({ MenuItem.new({
text: "Copy Raw Event", text: "Copy sharable link",
action: async () => { action: async () => {
event.meta = undefined; const eventId = await event.idAsBech32();
const raw = JSON.stringify(event); await writeText(`https://njump.me/${eventId}`);
await writeText(raw);
}, },
}), }),
]); PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Copy raw event",
action: async () => {
event.meta = undefined;
await writeText(JSON.stringify(event));
},
}),
];
const menu = await Menu.new({ if (account?.length) {
items: menuItems, const pubkey = nip19.decode(account).data;
});
if (event.pubkey === pubkey) {
list.push(
MenuItem.new({
text: "Request delete",
action: async () => {
await commands.requestDelete(event.id);
},
}),
);
}
}
const items = await Promise.all(list);
const menu = await Menu.new({ items });
await menu.popup().catch((e) => console.error(e)); await menu.popup().catch((e) => console.error(e));
}, []); }, []);

View File

@@ -1,6 +1,6 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons"; import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowDown } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
@@ -55,25 +55,14 @@ export function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) { return (
return ( <TextNote
<Quote key={event.id}
key={event.id} event={event}
event={event} className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" />
/> );
);
} else {
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
}
} }
}, },
[data], [data],

View File

@@ -1,6 +1,6 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons"; import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowDown } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
@@ -58,25 +58,14 @@ export function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) { return (
return ( <TextNote
<Quote key={event.id}
key={event.id} event={event}
event={event} className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" />
/> );
);
} else {
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
}
} }
}, },
[data], [data],

View File

@@ -1,6 +1,6 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons"; import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import { ArrowDown } from "@phosphor-icons/react"; import { ArrowDown } from "@phosphor-icons/react";
@@ -58,25 +58,14 @@ export function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) { return (
return ( <TextNote
<Quote key={event.id}
key={event.id} event={event}
event={event} className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" />
/> );
);
} else {
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
}
} }
}, },
[data], [data],

View File

@@ -1,6 +1,6 @@
import { events, commands } from "@/commands.gen"; import { events, commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons"; import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { RepostNote, Spinner, TextNote } from "@/components";
import { LumeEvent } from "@/system"; import { LumeEvent } from "@/system";
import { Kind, type Meta } from "@/types"; import { Kind, type Meta } from "@/types";
import { ArrowDown, ArrowUp } from "@phosphor-icons/react"; import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
@@ -69,25 +69,14 @@ export function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) { return (
return ( <TextNote
<Quote key={event.id}
key={event.id} event={event}
event={event} className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" />
/> );
);
} else {
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
}
} }
}, },
[data], [data],

View File

@@ -1,4 +1,4 @@
import { Quote, RepostNote, Spinner, TextNote } from "@/components"; import { RepostNote, Spinner, TextNote } from "@/components";
import { LumeEvent } from "@/system"; import { LumeEvent } from "@/system";
import { Kind, type NostrEvent } from "@/types"; import { Kind, type NostrEvent } from "@/types";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -50,25 +50,14 @@ function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) { return (
return ( <TextNote
<Quote key={event.id}
key={event.id} event={event}
event={event} className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" />
/> );
);
} else {
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
}
} }
}, },
[data], [data],

View File

@@ -1,10 +1,6 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons"; import { toLumeEvents } from "@/commons";
import { Spinner } from "@/components"; import { RepostNote, Spinner, TextNote, User } from "@/components";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { User } from "@/components/user";
import type { LumeEvent } from "@/system"; import type { LumeEvent } from "@/system";
import { Kind } from "@/types"; import { Kind } from "@/types";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -48,16 +44,7 @@ function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
default: { default:
if (event.isQuote) {
return (
<Quote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
return ( return (
<TextNote <TextNote
key={event.id} key={event.id}
@@ -65,7 +52,6 @@ function Screen() {
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700" className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/> />
); );
}
} }
}, },
[events], [events],