chore: fix crash when failing to parse message (#202)

* clean up

* .

* fix rich text component

* clean up
This commit is contained in:
reya
2025-11-03 19:04:16 +07:00
committed by GitHub
parent 4ebe590f8a
commit a4067d2c00
12 changed files with 200 additions and 366 deletions

10
Cargo.lock generated
View File

@@ -1065,7 +1065,6 @@ dependencies = [
"gpui_tokio", "gpui_tokio",
"indexset", "indexset",
"itertools 0.13.0", "itertools 0.13.0",
"linkify",
"log", "log",
"nostr", "nostr",
"nostr-sdk", "nostr-sdk",
@@ -3508,15 +3507,6 @@ dependencies = [
"rust-ini", "rust-ini",
] ]
[[package]]
name = "linkify"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.15"

View File

@@ -197,7 +197,11 @@ impl AutoUpdater {
}); });
} }
Err(e) => { Err(e) => {
log::warn!("{e}") _ = this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Idle, cx);
});
log::warn!("{e}");
} }
} }
}), }),

View File

@@ -371,6 +371,7 @@ impl Room {
continue; continue;
}; };
// Construct a filter for messaging relays
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(member) .author(member)
@@ -379,6 +380,7 @@ impl Room {
// Subscribe to get members messaging relays // Subscribe to get members messaging relays
client.subscribe(filter, Some(opts)).await?; client.subscribe(filter, Some(opts)).await?;
// Construct a filter for encryption keys announcement
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::Custom(10044)) .kind(Kind::Custom(10044))
.author(member) .author(member)
@@ -392,42 +394,6 @@ impl Room {
}) })
} }
pub fn verify_connections(&self, cx: &App) -> Task<Result<HashMap<PublicKey, bool>, Error>> {
let members = self.members();
cx.background_spawn(async move {
let client = app_state().client();
let mut result = HashMap::default();
for member in members.into_iter() {
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(member)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first() {
let urls: Vec<&RelayUrl> = nip17::extract_relay_list(event).collect();
if urls.is_empty() {
result.insert(member, false);
continue;
}
for url in urls {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
result.insert(member, true);
} else {
result.insert(member, false);
}
}
Ok(result)
})
}
/// Get all messages belonging to the room /// Get all messages belonging to the room
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> { pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
let conversation_id = self.id.to_string(); let conversation_id = self.id.to_string();

View File

@@ -30,5 +30,4 @@ serde_json.workspace = true
indexset = "0.12.3" indexset = "0.12.3"
emojis = "0.6.4" emojis = "0.6.4"
once_cell = "1.19.0" once_cell = "1.19.0"
linkify = "0.10.0"
regex = "1" regex = "1"

View File

@@ -22,7 +22,7 @@ use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use states::{app_state, SignerKind, QUERY_TIMEOUT}; use states::{app_state, SignerKind};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -45,9 +45,6 @@ mod emoji;
mod subject; mod subject;
mod text; mod text;
const NIP17_WARN: &str = "has not set up Messaging Relays, they cannot receive your message.";
const EMPTY_WARN: &str = "Something is wrong. Coop cannot display this message";
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> { pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx)) cx.new(|cx| ChatPanel::new(room, window, cx))
} }
@@ -77,7 +74,7 @@ pub struct ChatPanel {
image_cache: Entity<RetainAllImageCache>, image_cache: Entity<RetainAllImageCache>,
_subscriptions: SmallVec<[Subscription; 3]>, _subscriptions: SmallVec<[Subscription; 3]>,
_tasks: SmallVec<[Task<()>; 3]>, _tasks: SmallVec<[Task<()>; 2]>,
} }
impl ChatPanel { impl ChatPanel {
@@ -99,21 +96,11 @@ impl ChatPanel {
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
let connect = room.read(cx).connect(cx); let connect = room.read(cx).connect(cx);
let verify_connections = room.read(cx).verify_connections(cx);
let get_messages = room.read(cx).get_messages(cx); let get_messages = room.read(cx).get_messages(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![]; let mut tasks = smallvec![];
tasks.push(
// Get messaging relays and encryption keys announcement for all members
cx.background_spawn(async move {
if let Err(e) = connect.await {
log::error!("Failed to initialize room: {e}");
}
}),
);
tasks.push( tasks.push(
// Load all messages belonging to this room // Load all messages belonging to this room
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
@@ -134,35 +121,11 @@ impl ChatPanel {
); );
tasks.push( tasks.push(
// Connect and verify all members messaging relays // Get messaging relays and encryption keys announcement for each member
cx.spawn_in(window, async move |this, cx| { cx.background_spawn(async move {
// Wait for 5 seconds before connecting and verifying if let Err(e) = connect.await {
cx.background_executor() log::error!("Failed to initialize room: {}", e);
.timer(Duration::from_secs(QUERY_TIMEOUT)) }
.await;
let result = verify_connections.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(data) => {
let persons = PersonRegistry::global(cx);
for (public_key, status) in data.into_iter() {
if !status {
let profile = persons.read(cx).get_person(&public_key, cx);
let name = profile.display_name();
this.insert_warning(format!("{NIP17_WARN} {name}"), cx);
}
}
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
}), }),
); );
@@ -663,6 +626,33 @@ impl ChatPanel {
} }
fn render_message( fn render_message(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
if let Some(message) = self.messages.get_index(ix) {
match message {
Message::User(rendered) => {
let text = self
.rendered_texts_by_id
.entry(rendered.id)
.or_insert_with(|| RenderedText::new(&rendered.content, cx))
.element(ix.into(), window, cx);
self.render_text_message(ix, rendered, text, cx)
}
Message::Warning(content, _timestamp) => {
self.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => self.render_announcement(ix, cx),
}
} else {
self.render_warning(ix, SharedString::from("Message not found"), cx)
}
}
fn render_text_message(
&self, &self,
ix: usize, ix: usize,
message: &RenderedMessage, message: &RenderedMessage,
@@ -1358,26 +1348,9 @@ impl Render for ChatPanel {
.child( .child(
list( list(
self.list_state.clone(), self.list_state.clone(),
cx.processor(move |this, ix: usize, window, cx| { cx.processor(|this, ix, window, cx| {
if let Some(message) = this.messages.get_index(ix) { // Get and render message by index
match message { this.render_message(ix, window, cx)
Message::User(rendered) => {
let text = this
.rendered_texts_by_id
.entry(rendered.id)
.or_insert_with(|| RenderedText::new(&rendered.content, cx))
.element(ix.into(), window, cx);
this.render_message(ix, rendered, text, cx)
}
Message::Warning(content, _timestamp) => {
this.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => this.render_announcement(ix, cx),
}
} else {
this.render_warning(ix, SharedString::from(EMPTY_WARN), cx)
}
}), }),
) )
.flex_1(), .flex_1(),

View File

@@ -3,10 +3,9 @@ use std::sync::Arc;
use common::display::RenderedProfile; use common::display::RenderedProfile;
use gpui::{ use gpui::{
AnyElement, AnyView, App, ElementId, HighlightStyle, InteractiveText, IntoElement, AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
SharedString, StyledText, UnderlineStyle, Window, StyledText, UnderlineStyle, Window,
}; };
use linkify::{LinkFinder, LinkKind};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use person::PersonRegistry; use person::PersonRegistry;
@@ -16,7 +15,7 @@ use theme::ActiveTheme;
use crate::actions::OpenPublicKey; use crate::actions::OpenPublicKey;
static URL_REGEX: Lazy<Regex> = Lazy::new(|| { static URL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(?:[a-zA-Z]+://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$").unwrap() Regex::new(r"(?i)(?:^|\s)(?:https?://)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d+)?(?:/[^\s]*)?(?:\s|$)").unwrap()
}); });
static NOSTR_URI_REGEX: Lazy<Regex> = static NOSTR_URI_REGEX: Lazy<Regex> =
@@ -24,43 +23,16 @@ static NOSTR_URI_REGEX: Lazy<Regex> =
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Highlight { pub enum Highlight {
Link(HighlightStyle), Link,
Nostr, Nostr,
} }
impl Highlight {
fn link() -> Self {
Self::Link(HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
})
}
fn nostr() -> Self {
Self::Nostr
}
}
impl From<HighlightStyle> for Highlight {
fn from(style: HighlightStyle) -> Self {
Self::Link(style)
}
}
type CustomRangeTooltipFn =
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>;
#[derive(Default)] #[derive(Default)]
pub struct RenderedText { pub struct RenderedText {
pub text: SharedString, pub text: SharedString,
pub highlights: Vec<(Range<usize>, Highlight)>, pub highlights: Vec<(Range<usize>, Highlight)>,
pub link_ranges: Vec<Range<usize>>, pub link_ranges: Vec<Range<usize>>,
pub link_urls: Arc<[String]>, pub link_urls: Arc<[String]>,
pub custom_ranges: Vec<Range<usize>>,
custom_ranges_tooltip_fn: CustomRangeTooltipFn,
} }
impl RenderedText { impl RenderedText {
@@ -86,19 +58,9 @@ impl RenderedText {
link_urls: link_urls.into(), link_urls: link_urls.into(),
link_ranges, link_ranges,
highlights, highlights,
custom_ranges: Vec::new(),
custom_ranges_tooltip_fn: None,
} }
} }
#[allow(dead_code)]
pub fn set_tooltip_builder_for_custom_ranges<F>(&mut self, f: F)
where
F: Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
{
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
}
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement { pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
let link_color = cx.theme().text_accent; let link_color = cx.theme().text_accent;
@@ -110,17 +72,11 @@ impl RenderedText {
( (
range.clone(), range.clone(),
match highlight { match highlight {
Highlight::Link(highlight) => { Highlight::Link => HighlightStyle {
// Check if this is a link highlight by seeing if it has an underline color: Some(link_color),
if highlight.underline.is_some() { underline: Some(UnderlineStyle::default()),
// It's a link, so apply the link color ..Default::default()
let mut link_style = *highlight; },
link_style.color = Some(link_color);
link_style
} else {
*highlight
}
}
Highlight::Nostr => HighlightStyle { Highlight::Nostr => HighlightStyle {
color: Some(link_color), color: Some(link_color),
..Default::default() ..Default::default()
@@ -135,49 +91,22 @@ impl RenderedText {
move |ix, window, cx| { move |ix, window, cx| {
let token = link_urls[ix].as_str(); let token = link_urls[ix].as_str();
if token.starts_with("nostr:") { if let Some(clean_url) = token.strip_prefix("nostr:") {
let clean_url = token.replace("nostr:", ""); if let Ok(public_key) = PublicKey::parse(clean_url) {
let Ok(public_key) = PublicKey::parse(&clean_url) else { window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
log::error!("Failed to parse public key from: {clean_url}");
return;
};
window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
} else if is_url(token) {
if !token.starts_with("http") {
cx.open_url(&format!("https://{token}"));
} else {
cx.open_url(token);
} }
} else if is_url(token) {
let url = if token.starts_with("http") {
token.to_string()
} else {
format!("https://{token}")
};
cx.open_url(&url);
} else { } else {
log::warn!("Unrecognized token {token}") log::warn!("Unrecognized token {token}")
} }
} }
}) })
.tooltip({
let link_ranges = self.link_ranges.clone();
let link_urls = self.link_urls.clone();
let custom_tooltip_ranges = self.custom_ranges.clone();
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
move |idx, window, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
let url = &link_urls[ix];
if url.starts_with("http") {
// return Some(LinkPreview::new(url, cx));
}
// You can add custom tooltip handling for mentions here
}
}
for range in &custom_tooltip_ranges {
if range.contains(&idx) {
if let Some(f) = &custom_tooltip_fn {
return f(idx, range.clone(), window, cx);
}
}
}
None
}
})
.into_any_element() .into_any_element()
} }
} }
@@ -193,20 +122,15 @@ fn render_plain_text_mut(
// Copy the content directly // Copy the content directly
text.push_str(content); text.push_str(content);
// Initialize the link finder
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
finder.kinds(&[LinkKind::Url]);
// Collect all URLs // Collect all URLs
let mut url_matches: Vec<(Range<usize>, String)> = Vec::new(); let mut url_matches: Vec<(Range<usize>, String)> = Vec::new();
for link in finder.links(content) { for link in URL_REGEX.find_iter(content) {
let start = link.start(); let range = link.start()..link.end();
let end = link.end();
let range = start..end;
let url = link.as_str().to_string(); let url = link.as_str().to_string();
log::info!("Found URL: {}", url);
url_matches.push((range, url)); url_matches.push((range, url));
} }
@@ -214,9 +138,7 @@ fn render_plain_text_mut(
let mut nostr_matches: Vec<(Range<usize>, String)> = Vec::new(); let mut nostr_matches: Vec<(Range<usize>, String)> = Vec::new();
for nostr_match in NOSTR_URI_REGEX.find_iter(content) { for nostr_match in NOSTR_URI_REGEX.find_iter(content) {
let start = nostr_match.start(); let range = nostr_match.start()..nostr_match.end();
let end = nostr_match.end();
let range = start..end;
let nostr_uri = nostr_match.as_str().to_string(); let nostr_uri = nostr_match.as_str().to_string();
// Check if this nostr URI overlaps with any already processed URL // Check if this nostr URI overlaps with any already processed URL
@@ -240,12 +162,9 @@ fn render_plain_text_mut(
for (range, entity) in all_matches { for (range, entity) in all_matches {
// Handle URL token // Handle URL token
if is_url(&entity) { if is_url(&entity) {
// Add underline highlight highlights.push((range.clone(), Highlight::Link));
highlights.push((range.clone(), Highlight::link()));
// Make it clickable
link_ranges.push(range); link_ranges.push(range);
link_urls.push(entity); link_urls.push(entity);
continue; continue;
}; };
@@ -306,75 +225,6 @@ fn render_plain_text_mut(
} }
} }
} }
fn render_pubkey(
public_key: PublicKey,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
cx: &App,
) {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let display_name = format!("@{}", profile.display_name());
// Replace token with display name
text.replace_range(range.clone(), &display_name);
// Adjust ranges
let new_length = display_name.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
// New range for the replacement
let new_range = range.start..(range.start + new_length);
// Add highlight for the profile name
highlights.push((new_range.clone(), Highlight::nostr()));
// Make it clickable
link_ranges.push(new_range);
link_urls.push(format!("nostr:{}", profile.public_key().to_hex()));
// Adjust subsequent ranges if needed
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
fn render_bech32(
bech32: String,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) {
let njump_url = format!("https://njump.me/{bech32}");
// Create a shortened display format for the URL
let shortened_entity = format_shortened_entity(&bech32);
let display_text = format!("https://njump.me/{shortened_entity}");
// Replace the original entity with the shortened display version
text.replace_range(range.clone(), &display_text);
// Adjust the ranges
let new_length = display_text.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
// New range for the replacement
let new_range = range.start..(range.start + new_length);
// Add underline highlight
highlights.push((new_range.clone(), Highlight::link()));
// Make it clickable
link_ranges.push(new_range);
link_urls.push(njump_url);
// Adjust subsequent ranges if needed
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
} }
/// Check if a string is a URL /// Check if a string is a URL
@@ -396,6 +246,61 @@ fn format_shortened_entity(entity: &str) -> String {
} }
} }
fn render_pubkey(
public_key: PublicKey,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
cx: &App,
) {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let display_name = format!("@{}", profile.display_name());
text.replace_range(range.clone(), &display_name);
let new_length = display_name.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
let new_range = range.start..(range.start + new_length);
highlights.push((new_range.clone(), Highlight::Nostr));
link_ranges.push(new_range);
link_urls.push(format!("nostr:{}", profile.public_key().to_hex()));
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
fn render_bech32(
bech32: String,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) {
let njump_url = format!("https://njump.me/{bech32}");
let shortened_entity = format_shortened_entity(&bech32);
let display_text = format!("https://njump.me/{shortened_entity}");
text.replace_range(range.clone(), &display_text);
let new_length = display_text.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
let new_range = range.start..(range.start + new_length);
highlights.push((new_range.clone(), Highlight::Link));
link_ranges.push(new_range);
link_urls.push(njump_url);
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
// Helper function to adjust ranges when text length changes // Helper function to adjust ranges when text length changes
fn adjust_ranges( fn adjust_ranges(
highlights: &mut [(Range<usize>, Highlight)], highlights: &mut [(Range<usize>, Highlight)],

View File

@@ -6,7 +6,6 @@ use gpui::{Image, ImageFormat, SharedString, SharedUri};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use qrcode::render::svg; use qrcode::render::svg;
use qrcode::QrCode; use qrcode::QrCode;
use states::IMAGE_RESIZE_SERVICE;
const NOW: &str = "now"; const NOW: &str = "now";
const SECONDS_IN_MINUTE: i64 = 60; const SECONDS_IN_MINUTE: i64 = 60;
@@ -14,6 +13,7 @@ const MINUTES_IN_HOUR: i64 = 60;
const HOURS_IN_DAY: i64 = 24; const HOURS_IN_DAY: i64 = 24;
const DAYS_IN_MONTH: i64 = 30; const DAYS_IN_MONTH: i64 = 30;
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png"; const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
pub trait RenderedProfile { pub trait RenderedProfile {
fn avatar(&self, proxy: bool) -> SharedUri; fn avatar(&self, proxy: bool) -> SharedUri;

View File

@@ -1167,42 +1167,41 @@ impl ChatSpace {
let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore(); let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore();
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let auth_requests = self.auth_requests.read(cx).len(); let auth_requests = self.auth_requests.read(cx).len();
let auto_update = AutoUpdater::global(cx);
h_flex() h_flex()
.gap_1() .gap_1()
.map( .map(|this| match auto_update.read(cx).status.as_ref() {
|this| match AutoUpdater::global(cx).read(cx).status.as_ref() { AutoUpdateStatus::Checking => this.child(
AutoUpdateStatus::Checking => this.child( div()
div() .text_xs()
.text_xs() .text_color(cx.theme().text_muted)
.text_color(cx.theme().text_muted) .child(SharedString::from("Checking for Coop updates...")),
.child(SharedString::from("Checking for Coop updates...")), ),
), AutoUpdateStatus::Installing => this.child(
AutoUpdateStatus::Installing => this.child( div()
div() .text_xs()
.text_xs() .text_color(cx.theme().text_muted)
.text_color(cx.theme().text_muted) .child(SharedString::from("Installing updates...")),
.child(SharedString::from("Installing updates...")), ),
), AutoUpdateStatus::Errored { msg } => this.child(
AutoUpdateStatus::Errored { msg } => this.child( div()
div() .text_xs()
.text_xs() .text_color(cx.theme().text_muted)
.text_color(cx.theme().text_muted) .child(SharedString::from(msg.as_ref())),
.child(SharedString::from(msg.as_ref())), ),
), AutoUpdateStatus::Updated => this.child(
AutoUpdateStatus::Updated => this.child( div()
div() .id("restart")
.id("restart") .text_xs()
.text_xs() .text_color(cx.theme().text_muted)
.text_color(cx.theme().text_muted) .child(SharedString::from("Updated. Click to restart"))
.child(SharedString::from("Updated. Click to restart")) .on_click(|_ev, _window, cx| {
.on_click(|_ev, _window, cx| { cx.restart();
cx.restart(); }),
}), ),
), _ => this.child(div()),
_ => this.child(div()), })
},
)
.when(file_keystore, |this| { .when(file_keystore, |this| {
this.child( this.child(
Button::new("keystore-warning") Button::new("keystore-warning")

View File

@@ -6,7 +6,7 @@ use gpui::{
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions, WindowOptions,
}; };
use states::{app_state, APP_ID, CLIENT_NAME}; use states::{app_state, APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
use ui::Root; use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit}; use crate::actions::{load_embedded_fonts, quit, Quit};
@@ -21,14 +21,34 @@ fn main() {
// Initialize logging // Initialize logging
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Initialize the coop simple storage
let _app_state = app_state();
// Initialize the Application // Initialize the Application
let app = Application::new() let app = Application::new()
.with_assets(Assets) .with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new())); .with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
// Initialize app state
let app_state = app_state();
// Connect to relays
app.background_executor()
.spawn(async move {
let client = app_state.client();
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
client.add_relay(url).await.ok();
}
// Establish connection to relays
client.connect().await;
})
.detach();
// Run application // Run application
app.run(move |cx| { app.run(move |cx| {
// Load embedded fonts in assets/fonts // Load embedded fonts in assets/fonts

View File

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Error};
use chat::room::{Room, RoomKind}; use chat::room::{Room, RoomKind};
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::debounced_delay::DebouncedDelay; use common::debounced_delay::DebouncedDelay;
use common::display::{RenderedProfile, RenderedTimestamp, TextUtils}; use common::display::{RenderedTimestamp, TextUtils};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
@@ -628,8 +628,8 @@ impl Sidebar {
items.push( items.push(
RoomListItem::new(ix) RoomListItem::new(ix)
.room_id(room_id) .room_id(room_id)
.name(member.display_name()) .name(this.display_name(cx))
.avatar(member.avatar(proxy)) .avatar(this.display_image(proxy, cx))
.public_key(member.public_key()) .public_key(member.public_key())
.kind(this.kind) .kind(this.kind)
.created_at(this.created_at.to_ago()) .created_at(this.created_at.to_ago())

View File

@@ -42,9 +42,3 @@ pub const METADATA_BATCH_TIMEOUT: u64 = 300;
/// Default width of the sidebar. /// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.; pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
/// Image Resize Service
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
/// Default NIP96 Media Server.
pub const NIP96_SERVER: &str = "https://nostrmedia.com";

View File

@@ -12,7 +12,7 @@ use nostr_sdk::prelude::*;
use smol::lock::RwLock; use smol::lock::RwLock;
use crate::constants::{ use crate::constants::{
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT, SEARCH_RELAYS, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT,
}; };
use crate::paths::config_dir; use crate::paths::config_dir;
use crate::state::ingester::Ingester; use crate::state::ingester::Ingester;
@@ -210,19 +210,6 @@ impl AppState {
/// Handles events from the nostr client /// Handles events from the nostr client
pub async fn handle_notifications(&self) -> Result<(), Error> { pub async fn handle_notifications(&self) -> Result<(), Error> {
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
self.client.add_relay(url).await?;
}
// Establish connection to relays
self.client.connect().await;
let mut processed_events: HashSet<EventId> = HashSet::new(); let mut processed_events: HashSet<EventId> = HashSet::new();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new(); let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
let mut notifications = self.client.notifications(); let mut notifications = self.client.notifications();
@@ -345,9 +332,7 @@ impl AppState {
self.signal.send(SignalKind::NewProfile(profile)).await; self.signal.send(SignalKind::NewProfile(profile)).await;
} }
Kind::GiftWrap => { Kind::GiftWrap => {
if let Err(e) = self.extract_rumor(&event).await { self.extract_rumor(&event).await.ok();
log::error!("Failed to extract rumor: {e}");
}
} }
_ => {} _ => {}
} }
@@ -997,6 +982,8 @@ impl AppState {
.subscribe_with_id_to(&urls, id, filter, None) .subscribe_with_id_to(&urls, id, filter, None)
.await?; .await?;
log::info!("Subscribed to gift wrap events");
Ok(()) Ok(())
} }
@@ -1027,15 +1014,15 @@ impl AppState {
} }
/// Stores an unwrapped event in local database with reference to original /// Stores an unwrapped event in local database with reference to original
async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> { async fn set_rumor(&self, gift_wrap: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?; let rumor_id = rumor.id.context("Rumor is missing an event id")?;
let author = rumor.pubkey; let author = rumor.pubkey;
let conversation = self.conversation_id(rumor).to_string(); let conversation = self.conversation_id(rumor);
let mut tags = rumor.tags.clone().to_vec(); let mut tags = rumor.tags.clone().to_vec();
// Add a unique identifier // Add a unique identifier
tags.push(Tag::identifier(id)); tags.push(Tag::identifier(gift_wrap));
// Add a reference to the rumor's author // Add a reference to the rumor's author
tags.push(Tag::custom( tags.push(Tag::custom(
@@ -1046,7 +1033,7 @@ impl AppState {
// Add a conversation id // Add a conversation id
tags.push(Tag::custom( tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)), TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
[conversation], [conversation.to_string()],
)); ));
// Add a reference to the rumor's id // Add a reference to the rumor's id
@@ -1063,21 +1050,23 @@ impl AppState {
// Convert rumor to json // Convert rumor to json
let content = rumor.as_json(); let content = rumor.as_json();
// Construct the event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content) let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tags(tags) .tags(tags)
.sign(&Keys::generate()) .sign(&Keys::generate())
.await?; .await?;
// Save the event to the database
self.client.database().save_event(&event).await?; self.client.database().save_event(&event).await?;
Ok(()) Ok(())
} }
/// Retrieves a previously unwrapped event from local database /// Retrieves a previously unwrapped event from local database
async fn get_rumor(&self, id: EventId) -> Result<UnsignedEvent, Error> { async fn get_rumor(&self, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.identifier(id) .identifier(gift_wrap)
.limit(1); .limit(1);
if let Some(event) = self.client.database().query(filter).await?.first_owned() { if let Some(event) = self.client.database().query(filter).await?.first_owned() {
@@ -1118,20 +1107,15 @@ impl AppState {
// Helper method to try unwrapping with different signers // Helper method to try unwrapping with different signers
async fn try_unwrap_gift_wrap(&self, gift_wrap: &Event) -> Result<UnwrappedGift, Error> { async fn try_unwrap_gift_wrap(&self, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
// Try to unwrap with the encryption key first // Try to unwrap with the encryption key if available
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
if let Some(signer) = self.device.read().await.encryption.as_ref() { if let Some(signer) = self.device.read().await.encryption.as_ref() {
match UnwrappedGift::from_gift_wrap(signer, gift_wrap).await { if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await {
Ok(unwrapped) => { return Ok(unwrapped);
return Ok(unwrapped);
}
Err(e) => {
log::warn!("Failed to unwrap with the encryption key: {e}")
}
} }
} }
// Try to unwrap with the user's signer // Fallback to unwrap with the user's signer
let signer = self.client.signer().await?; let signer = self.client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?; let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;