refactor chats (#15)

* refactor

* update

* update

* update

* remove nostrprofile struct

* update

* refactor contacts

* prevent double login
This commit is contained in:
reya
2025-04-10 08:10:53 +07:00
committed by GitHub
parent f7610cc9c9
commit 3246abace1
27 changed files with 1166 additions and 909 deletions

View File

@@ -0,0 +1,5 @@
pub(crate) const NOW: &str = "now";
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
pub(crate) const HOURS_IN_DAY: i64 = 24;
pub(crate) const DAYS_IN_MONTH: i64 = 30;

View File

@@ -1,7 +1,7 @@
use std::{cmp::Reverse, collections::HashMap};
use anyhow::anyhow;
use common::{last_seen::LastSeen, utils::room_hash};
use anyhow::{anyhow, Error};
use common::room_hash;
use global::get_client;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use itertools::Itertools;
@@ -11,6 +11,7 @@ use smallvec::{smallvec, SmallVec};
use crate::room::Room;
mod constants;
pub mod message;
pub mod room;
@@ -22,35 +23,56 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {}
/// Main registry for managing chat rooms and user profiles
///
/// The ChatRegistry is responsible for:
/// - Managing chat rooms and their states
/// - Tracking user profiles
/// - Loading room data from the lmdb
/// - Handling messages and room creation
pub struct ChatRegistry {
/// Collection of all chat rooms
rooms: Vec<Entity<Room>>,
/// Map of user public keys to their profile metadata
profiles: Entity<HashMap<PublicKey, Option<Metadata>>>,
/// Indicates if rooms are currently being loaded
loading: bool,
/// Subscriptions for observing changes
#[allow(dead_code)]
subscriptions: SmallVec<[Subscription; 1]>,
}
impl ChatRegistry {
pub fn global(cx: &mut App) -> Entity<Self> {
/// Retrieve the global ChatRegistry instance
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalChatRegistry>().0.clone()
}
/// Set the global ChatRegistry instance
pub fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalChatRegistry(state));
}
/// Create a new ChatRegistry instance
fn new(cx: &mut Context<Self>) -> Self {
let profiles = cx.new(|_| HashMap::new());
let mut subscriptions = smallvec![];
// Observe new Room creations to collect profile metadata
subscriptions.push(cx.observe_new::<Room>(|this, _, cx| {
let load_metadata = this.load_metadata(cx);
let task = this.metadata(cx);
cx.spawn(async move |this, cx| {
if let Ok(profiles) = load_metadata.await {
cx.spawn(async move |_, cx| {
if let Ok(data) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.update_members(profiles, cx);
})
.ok();
for (public_key, metadata) in data.into_iter() {
Self::global(cx).update(cx, |this, cx| {
this.add_profile(public_key, metadata, cx);
})
}
})
.ok();
}
@@ -61,24 +83,73 @@ impl ChatRegistry {
Self {
rooms: vec![],
loading: true,
profiles,
subscriptions,
}
}
/// Get the global loading status
pub fn loading(&self) -> bool {
self.loading
}
/// Get a room by its ID.
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
.iter()
.find(|model| model.read(cx).id == *id)
.cloned()
}
/// Get all rooms grouped by their kind.
pub fn rooms(&self, cx: &App) -> HashMap<RoomKind, Vec<&Entity<Room>>> {
let mut groups = HashMap::new();
groups.insert(RoomKind::Ongoing, Vec::new());
groups.insert(RoomKind::Trusted, Vec::new());
groups.insert(RoomKind::Unknown, Vec::new());
for room in self.rooms.iter() {
let kind = room.read(cx).kind;
groups.entry(kind).or_insert_with(Vec::new).push(room);
}
groups
}
/// Get rooms by their kind.
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind == kind)
.collect()
}
/// Get the IDs of all rooms.
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
self.rooms.iter().map(|room| room.read(cx).id).collect()
}
/// Load all rooms from the lmdb.
///
/// This method:
/// 1. Fetches all private direct messages from the lmdb
/// 2. Groups them by ID
/// 3. Determines each room's type based on message frequency and trust status
/// 4. Creates Room entities for each unique room
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let room_ids = self.room_ids(cx);
type Rooms = Vec<(Event, usize, bool)>;
type LoadResult = Result<Vec<(Event, usize, bool)>, Error>;
let task: Task<LoadResult> = cx.background_spawn(async move {
let task: Task<Result<Rooms, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// Get messages sent by the user
let send = Filter::new()
.kind(Kind::PrivateDirectMessage)
.author(public_key);
// Get messages received by the user
let recv = Filter::new()
.kind(Kind::PrivateDirectMessage)
.pubkey(public_key);
@@ -89,26 +160,26 @@ impl ChatRegistry {
let mut room_map: HashMap<u64, (Event, usize, bool)> = HashMap::new();
// Process each event and group by room hash
for event in events
.into_iter()
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
{
let hash = room_hash(&event);
if !room_ids.iter().any(|id| id == &hash) {
let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey);
let is_trust = client.database().count(filter).await? >= 1;
let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey);
let is_trust = client.database().count(filter).await? >= 1;
room_map
.entry(hash)
.and_modify(|(_, count, trusted)| {
*count += 1;
*trusted = is_trust;
})
.or_insert((event, 1, is_trust));
}
room_map
.entry(hash)
.and_modify(|(_, count, trusted)| {
*count += 1;
*trusted = is_trust;
})
.or_insert((event, 1, is_trust));
}
// Sort rooms by creation date (newest first)
let result: Vec<(Event, usize, bool)> = room_map
.into_values()
.sorted_by_key(|(ev, _, _)| Reverse(ev.created_at))
@@ -119,26 +190,31 @@ impl ChatRegistry {
cx.spawn_in(window, async move |this, cx| {
if let Ok(events) = task.await {
let rooms: Vec<Entity<Room>> = events
.into_iter()
.map(|(event, count, trusted)| {
let kind = if count > 2 {
// If frequency count is greater than 2, mark this room as ongoing
RoomKind::Ongoing
} else if trusted {
RoomKind::Trusted
} else {
RoomKind::Unknown
};
cx.new(|_| Room::new(&event, kind)).unwrap()
})
.collect();
cx.update(|_, cx| {
this.update(cx, |this, cx| {
let ids = this.room_ids(cx);
let rooms: Vec<Entity<Room>> = events
.into_iter()
.filter_map(|(event, count, trusted)| {
let hash = room_hash(&event);
if !ids.iter().any(|this| this == &hash) {
let kind = if count > 2 {
// If frequency count is greater than 2, mark this room as ongoing
RoomKind::Ongoing
} else if trusted {
RoomKind::Trusted
} else {
RoomKind::Unknown
};
Some(cx.new(|_| Room::new(&event).kind(kind)))
} else {
None
}
})
.collect();
this.rooms.extend(rooms);
this.rooms.sort_by_key(|r| Reverse(r.read(cx).last_seen()));
this.rooms.sort_by_key(|r| Reverse(r.read(cx).created_at));
this.loading = false;
cx.notify();
@@ -151,48 +227,40 @@ impl ChatRegistry {
.detach();
}
/// Get the IDs of all rooms.
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
self.rooms.iter().map(|room| room.read(cx).id).collect()
/// Add a user profile to the registry
///
/// Only adds the profile if it doesn't already exist or is currently none
pub fn add_profile(
&mut self,
public_key: PublicKey,
metadata: Option<Metadata>,
cx: &mut Context<Self>,
) {
self.profiles.update(cx, |this, _cx| {
this.entry(public_key)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
});
}
/// Get all rooms.
pub fn rooms(&self, cx: &App) -> HashMap<RoomKind, Vec<&Entity<Room>>> {
let mut groups = HashMap::new();
groups.insert(RoomKind::Ongoing, Vec::new());
groups.insert(RoomKind::Trusted, Vec::new());
groups.insert(RoomKind::Unknown, Vec::new());
/// Get a user profile by public key
pub fn profile(&self, public_key: &PublicKey, cx: &App) -> Profile {
let metadata = if let Some(profile) = self.profiles.read(cx).get(public_key) {
profile.clone().unwrap_or_default()
} else {
Metadata::default()
};
for room in self.rooms.iter() {
let kind = room.read(cx).kind();
groups.entry(kind).or_insert_with(Vec::new).push(room);
}
groups
Profile::new(*public_key, metadata)
}
/// Get rooms by their kind.
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind() == kind)
.collect()
}
/// Get the loading status of the rooms.
pub fn loading(&self) -> bool {
self.loading
}
/// Get a room by its ID.
pub fn get(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
.iter()
.find(|model| model.read(cx).id == *id)
.cloned()
}
/// Push a room to the list.
/// Add a new room to the registry
///
/// Returns an error if the room already exists
pub fn push(&mut self, room: Room, cx: &mut Context<Self>) -> Result<(), anyhow::Error> {
let room = cx.new(|_| room);
@@ -210,21 +278,24 @@ impl ChatRegistry {
}
}
/// Push a message to a room.
/// Push a new message to a room
///
/// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages.
pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let id = room_hash(&event);
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
room.update(cx, |this, cx| {
this.set_last_seen(LastSeen(event.created_at), cx);
this.created_at(event.created_at, cx);
this.emit_message(event, window, cx);
});
// Re-sort rooms by last seen
self.rooms
.sort_by_key(|room| Reverse(room.read(cx).last_seen()));
.sort_by_key(|room| Reverse(room.read(cx).created_at));
} else {
let new_room = cx.new(|_| Room::new(&event, RoomKind::default()));
let new_room = cx.new(|_| Room::new(&event));
// Push the new room to the front of the list
self.rooms.insert(0, new_room);

View File

@@ -1,36 +1,101 @@
use common::{last_seen::LastSeen, profile::NostrProfile};
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*;
/// # Message
///
/// Represents a message in the application.
///
/// ## Fields
///
/// - `id`: The unique identifier for the message
/// - `content`: The text content of the message
/// - `author`: Profile information about who created the message
/// - `mentions`: List of profiles mentioned in the message
/// - `created_at`: Timestamp when the message was created
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub id: EventId,
pub content: String,
pub author: NostrProfile,
pub mentions: Vec<NostrProfile>,
pub created_at: LastSeen,
pub author: Profile,
pub mentions: Vec<Profile>,
pub created_at: Timestamp,
}
impl Message {
pub fn new(
id: EventId,
content: String,
author: NostrProfile,
mentions: Vec<NostrProfile>,
created_at: Timestamp,
) -> Self {
let created_at = LastSeen(created_at);
/// Creates a new message with the provided details
///
/// # Arguments
///
/// * `id` - Unique event identifier
/// * `content` - Message text content
/// * `author` - Profile of the message author
/// * `created_at` - When the message was created
///
/// # Returns
///
/// A new `Message` instance
pub fn new(id: EventId, content: String, author: Profile, created_at: Timestamp) -> Self {
Self {
id,
content,
author,
mentions,
created_at,
mentions: vec![],
}
}
/// Adds or replaces mentions in the message
///
/// # Arguments
///
/// * `mentions` - New list of mentioned profiles
///
/// # Returns
///
/// The same message with updated mentions
pub fn with_mentions(mut self, mentions: impl IntoIterator<Item = Profile>) -> Self {
self.mentions.extend(mentions);
self
}
/// Formats the message timestamp as a human-readable relative time
///
/// # Returns
///
/// A formatted string like "Today at 12:30 PM", "Yesterday at 3:45 PM",
/// or a date and time for older messages
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let now = Local::now();
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
.into()
}
}
/// # RoomMessage
///
/// Represents different types of messages that can appear in a room.
///
/// ## Variants
///
/// - `User`: A message sent by a user
/// - `System`: A message generated by the system
/// - `Announcement`: A special message type used for room announcements
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoomMessage {
/// User message
@@ -43,14 +108,37 @@ pub enum RoomMessage {
}
impl RoomMessage {
/// Creates a new user message
///
/// # Arguments
///
/// * `message` - The message content
///
/// # Returns
///
/// A `RoomMessage::User` variant
pub fn user(message: Message) -> Self {
Self::User(Box::new(message))
}
/// Creates a new system message
///
/// # Arguments
///
/// * `content` - The system message content
///
/// # Returns
///
/// A `RoomMessage::System` variant
pub fn system(content: SharedString) -> Self {
Self::System(content)
}
/// Creates a new announcement placeholder
///
/// # Returns
///
/// A `RoomMessage::Announcement` variant
pub fn announcement() -> Self {
Self::Announcement
}

View File

@@ -1,18 +1,19 @@
use std::{collections::HashSet, sync::Arc};
use std::sync::Arc;
use account::Account;
use anyhow::Error;
use common::{
last_seen::LastSeen,
profile::NostrProfile,
utils::{compare, room_hash},
};
use chrono::{Local, TimeZone};
use common::{compare, profile::SharedProfile, room_hash};
use global::get_client;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::SmallVec;
use crate::message::{Message, RoomMessage};
use crate::{
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE},
message::{Message, RoomMessage},
ChatRegistry,
};
#[derive(Debug, Clone)]
pub struct IncomingEvent {
@@ -29,15 +30,13 @@ pub enum RoomKind {
pub struct Room {
pub id: u64,
pub last_seen: LastSeen,
pub created_at: Timestamp,
/// Subject of the room
pub subject: Option<SharedString>,
/// All members of the room
pub members: Arc<SmallVec<[NostrProfile; 2]>>,
pub members: Arc<Vec<PublicKey>>,
/// Kind
pub kind: RoomKind,
/// All public keys of the room members
pubkeys: Vec<PublicKey>,
}
impl EventEmitter<IncomingEvent> for Room {}
@@ -49,10 +48,25 @@ impl PartialEq for Room {
}
impl Room {
/// Create a new room from an Nostr Event
pub fn new(event: &Event, kind: RoomKind) -> Self {
/// Creates a new Room instance from a Nostr event
///
/// # Arguments
///
/// * `event` - The Nostr event containing chat information
///
/// # Returns
///
/// A new Room instance with information extracted from the event
pub fn new(event: &Event) -> Self {
let id = room_hash(event);
let last_seen = LastSeen(event.created_at);
let created_at = event.created_at;
// Get all pubkeys from the event's tags
let mut pubkeys: Vec<PublicKey> = event.tags.public_keys().cloned().collect();
pubkeys.push(event.pubkey);
// Convert pubkeys into members
let members = Arc::new(pubkeys.into_iter().unique().sorted().collect());
// Get the subject from the event's tags
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
@@ -61,112 +75,264 @@ impl Room {
None
};
// Get all public keys from the event's tags
let mut pubkeys = vec![];
pubkeys.extend(event.tags.public_keys().collect::<HashSet<_>>());
pubkeys.push(event.pubkey);
Self {
id,
last_seen,
created_at,
subject,
kind,
members: Arc::new(SmallVec::with_capacity(pubkeys.len())),
pubkeys,
members,
kind: RoomKind::Unknown,
}
}
/// Get room's id
pub fn id(&self) -> u64 {
self.id
/// Sets the kind of the room
///
/// # Arguments
///
/// * `kind` - The kind of room to set
///
/// # Returns
///
/// The room with the updated kind
pub fn kind(mut self, kind: RoomKind) -> Self {
self.kind = kind;
self
}
/// Get room's member by public key
pub fn member(&self, public_key: &PublicKey) -> Option<NostrProfile> {
self.members
.iter()
.find(|m| &m.public_key == public_key)
.cloned()
}
/// Get room's first member's public key
pub fn first_member(&self) -> Option<&NostrProfile> {
self.members.first()
}
/// Collect room's member's public keys
pub fn public_keys(&self) -> Vec<PublicKey> {
self.pubkeys.clone()
}
/// Get room's display name
pub fn subject(&self) -> Option<SharedString> {
self.subject.clone()
}
/// Get room's kind
pub fn kind(&self) -> RoomKind {
self.kind
}
/// Determine if room is a group
pub fn is_group(&self) -> bool {
self.members.len() > 2
}
/// Get room's last seen
pub fn last_seen(&self) -> LastSeen {
self.last_seen
}
/// Set room's last seen
pub fn set_last_seen(&mut self, last_seen: LastSeen, cx: &mut Context<Self>) {
self.last_seen = last_seen;
cx.notify();
}
/// Get room's last seen as ago format
/// Calculates a human-readable representation of the time passed since room creation
///
/// # Returns
///
/// A SharedString representing the relative time since room creation:
/// - "now" for less than a minute
/// - "Xm" for minutes
/// - "Xh" for hours
/// - "Xd" for days
/// - Month and day (e.g. "Jan 15") for older dates
pub fn ago(&self) -> SharedString {
self.last_seen.ago()
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "1m".into(),
};
let now = Local::now();
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
.into()
}
pub fn update_members(&mut self, profiles: Vec<NostrProfile>, cx: &mut Context<Self>) {
// Update the room's name if it's not already set
if self.subject.is_none() {
// Merge all members into a single name
/// Gets the profile for a specific public key
///
/// # Arguments
///
/// * `public_key` - The public key to get the profile for
/// * `cx` - The App context
///
/// # Returns
///
/// The Profile associated with the given public key
pub fn profile_by_pubkey(&self, public_key: &PublicKey, cx: &App) -> Profile {
ChatRegistry::global(cx).read(cx).profile(public_key, cx)
}
/// Gets the first member in the room that isn't the current user
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// The Profile of the first member in the room
pub fn first_member(&self, cx: &App) -> Profile {
let account = Account::global(cx).read(cx);
let profile = account.profile.clone().unwrap();
if let Some(public_key) = self
.members
.iter()
.filter(|&pubkey| pubkey != &profile.public_key())
.collect::<Vec<_>>()
.first()
{
self.profile_by_pubkey(public_key, cx)
} else {
profile
}
}
/// Gets all avatars for members in the room
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A vector of SharedString containing all members' avatars
pub fn avatars(&self, cx: &App) -> Vec<SharedString> {
let profiles: Vec<Profile> = self
.members
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
profiles
.iter()
.map(|member| member.shared_avatar())
.collect()
}
/// Gets a formatted string of member names
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A SharedString containing formatted member names:
/// - For a group chat: "name1, name2, +X" where X is the number of additional members
/// - For a direct message: just the name of the other person
pub fn names(&self, cx: &App) -> SharedString {
if self.is_group() {
let profiles = self
.members
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect::<Vec<_>>();
let mut name = profiles
.iter()
.take(2)
.map(|profile| profile.name.to_string())
.map(|profile| profile.shared_name())
.collect::<Vec<_>>()
.join(", ");
// Create a specific name for group
if profiles.len() > 2 {
name = format!("{}, +{}", name, profiles.len() - 2);
}
self.subject = Some(name.into());
};
name.into()
} else {
self.first_member(cx).shared_name()
}
}
// Update the room's members
self.members = Arc::new(profiles.into());
/// Gets the display name for the room
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A SharedString representing the display name:
/// - The subject of the room if it exists
/// - Otherwise, the formatted names of the members
pub fn display_name(&self, cx: &App) -> SharedString {
if let Some(subject) = self.subject.as_ref() {
subject.clone()
} else {
self.names(cx)
}
}
/// Gets the display image for the room
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// An Option<SharedString> containing the avatar:
/// - For a direct message: the other person's avatar
/// - For a group chat: None
pub fn display_image(&self, cx: &App) -> Option<SharedString> {
if !self.is_group() {
Some(self.first_member(cx).shared_avatar())
} else {
None
}
}
/// Checks if the room is a group chat
///
/// # Returns
///
/// true if the room has more than 2 members, false otherwise
pub fn is_group(&self) -> bool {
self.members.len() > 2
}
/// Updates the creation timestamp of the room
///
/// # Arguments
///
/// * `created_at` - The new Timestamp to set
/// * `cx` - The context to notify about the update
pub fn created_at(&mut self, created_at: Timestamp, cx: &mut Context<Self>) {
self.created_at = created_at;
cx.notify();
}
/// Verify messaging_relays for all room's members
/// Fetches metadata for all members in the room
///
/// # Arguments
///
/// * `cx` - The context for the background task
///
/// # Returns
///
/// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error>
#[allow(clippy::type_complexity)]
pub fn metadata(
&self,
cx: &mut Context<Self>,
) -> Task<Result<Vec<(PublicKey, Option<Metadata>)>, Error>> {
let client = get_client();
let public_keys = self.members.clone();
cx.background_spawn(async move {
let mut output = vec![];
for public_key in public_keys.iter() {
let metadata = client.database().metadata(*public_key).await?;
output.push((*public_key, metadata));
}
Ok(output)
})
}
/// Checks which members have inbox relays set up
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where
/// the boolean indicates if the member has inbox relays configured
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
let pubkeys = Arc::clone(&self.members);
cx.background_spawn(async move {
let mut result = Vec::with_capacity(pubkeys.len());
for pubkey in pubkeys.into_iter() {
for pubkey in pubkeys.iter() {
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(pubkey)
.author(*pubkey)
.limit(1);
let is_ready = client
@@ -177,17 +343,27 @@ impl Room {
.and_then(|events| events.first_owned())
.is_some();
result.push((pubkey, is_ready));
result.push((*pubkey, is_ready));
}
Ok(result)
})
}
/// Send message to all room's members
/// Sends a message to all members in the room
///
/// # Arguments
///
/// * `content` - The content of the message to send
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<String>, Error> where the
/// strings contain error messages for any failed sends
pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
let pubkeys = self.members.clone();
cx.background_spawn(async move {
let signer = client.signer().await?;
@@ -218,48 +394,29 @@ impl Room {
})
}
/// Load metadata for all members
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<Vec<NostrProfile>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
cx.background_spawn(async move {
let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;
let mut profiles = Vec::with_capacity(pubkeys.len());
for public_key in pubkeys.into_iter() {
let metadata = client
.database()
.metadata(public_key)
.await?
.unwrap_or_default();
// Convert metadata to profile
let profile = NostrProfile::new(public_key, metadata);
if public_key == signer_pubkey {
// Room's owner always push to the end of the vector
profiles.push(profile);
} else {
profiles.insert(0, profile);
}
}
Ok(profiles)
})
}
/// Load room messages
/// Loads all messages for this room from the database
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing
/// all messages for this room
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
let members = Arc::clone(&self.members);
let pubkeys = Arc::clone(&self.members);
let profiles: Vec<Profile> = pubkeys
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
let filter = Filter::new()
.kind(Kind::PrivateDirectMessage)
.authors(pubkeys.clone())
.pubkeys(pubkeys.clone());
.authors(pubkeys.to_vec())
.pubkeys(pubkeys.to_vec());
cx.background_spawn(async move {
let mut messages = vec![];
@@ -282,14 +439,16 @@ impl Room {
for event in events.into_iter() {
let mut mentions = vec![];
let id = event.id;
let created_at = event.created_at;
let content = event.content.clone();
let tokens = parser.parse(&content);
let author = members
let author = profiles
.iter()
.find(|profile| profile.public_key == event.pubkey)
.find(|profile| profile.public_key() == event.pubkey)
.cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
.unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = tokens
.filter_map(|token| match token {
@@ -303,22 +462,16 @@ impl Room {
.collect::<Vec<_>>();
for pubkey in pubkey_tokens {
if let Some(profile) =
members.iter().find(|profile| profile.public_key == pubkey)
{
mentions.push(profile.clone());
} else {
let metadata = client
.database()
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
mentions.push(
profiles
.iter()
.find(|profile| profile.public_key() == pubkey)
.cloned()
.unwrap_or_else(|| Profile::new(pubkey, Metadata::default())),
);
}
let message = Message::new(event.id, content, author, mentions, event.created_at);
let message = Message::new(id, content, author, created_at).with_mentions(mentions);
let room_message = RoomMessage::user(message);
messages.push(room_message);
@@ -328,22 +481,37 @@ impl Room {
})
}
/// Emit message to GPUI
/// Emits a message event to the GPUI
///
/// # Arguments
///
/// * `event` - The Nostr event to emit
/// * `window` - The Window to emit the event to
/// * `cx` - The context for the room
///
/// # Effects
///
/// Processes the event and emits an IncomingEvent to the UI when complete
pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let members = Arc::clone(&self.members);
let pubkeys = self.members.clone();
let profiles: Vec<Profile> = pubkeys
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
let task: Task<Result<RoomMessage, Error>> = cx.background_spawn(async move {
let parser = NostrParser::new();
let id = event.id;
let created_at = event.created_at;
let content = event.content.clone();
let tokens = parser.parse(&content);
let mut mentions = vec![];
let author = members
let author = profiles
.iter()
.find(|profile| profile.public_key == event.pubkey)
.find(|profile| profile.public_key() == event.pubkey)
.cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
.unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = tokens
.filter_map(|token| match token {
@@ -357,23 +525,16 @@ impl Room {
.collect::<Vec<_>>();
for pubkey in pubkey_tokens {
if let Some(profile) = members
.iter()
.find(|profile| profile.public_key == event.pubkey)
{
mentions.push(profile.clone());
} else {
let metadata = client
.database()
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
mentions.push(
profiles
.iter()
.find(|profile| profile.public_key() == pubkey)
.cloned()
.unwrap_or_else(|| Profile::new(pubkey, Metadata::default())),
);
}
let message = Message::new(event.id, content, author, mentions, event.created_at);
let message = Message::new(id, content, author, created_at).with_mentions(mentions);
let room_message = RoomMessage::user(message);
Ok(room_message)