render reaction
This commit is contained in:
@@ -59,6 +59,9 @@ pub struct ChatPanel {
|
||||
/// All messages (sorted by created_at)
|
||||
messages: Vec<Message>,
|
||||
|
||||
/// All reactions
|
||||
reactions: BTreeMap<EventId, Vec<(SharedString, PublicKey)>>,
|
||||
|
||||
/// Mapping message ids to their rendered texts
|
||||
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
||||
|
||||
@@ -163,6 +166,7 @@ impl ChatPanel {
|
||||
focus_handle: cx.focus_handle(),
|
||||
id,
|
||||
messages,
|
||||
reactions: BTreeMap::new(),
|
||||
room,
|
||||
list_state,
|
||||
input,
|
||||
@@ -262,7 +266,11 @@ impl ChatPanel {
|
||||
move |this, _room, event, window, cx| {
|
||||
match event {
|
||||
RoomEvent::Incoming(message) => {
|
||||
this.insert_message(message, false, cx);
|
||||
if message.rumor.kind == Kind::Reaction {
|
||||
this.insert_reaction(&message.rumor, cx);
|
||||
} else {
|
||||
this.insert_message(message, false, cx);
|
||||
}
|
||||
}
|
||||
RoomEvent::Reload => {
|
||||
// Defer to avoid re-entrant read on Room while
|
||||
@@ -384,8 +392,10 @@ impl ChatPanel {
|
||||
let Some(room_entity) = room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Create rumor and send task
|
||||
let (rumor, send_task) = match room_entity.read_with(cx, |room, cx| {
|
||||
let rumor = room.rumor(content.clone(), replies, reaction, cx)?;
|
||||
let rumor = room.rumor(content.clone(), replies.clone(), reaction, cx)?;
|
||||
let send_task = room.send(rumor.clone(), cx)?;
|
||||
Some((rumor, send_task))
|
||||
}) {
|
||||
@@ -399,9 +409,15 @@ impl ChatPanel {
|
||||
let id = rumor.id.expect("rumor must have an id");
|
||||
|
||||
// Insert optimistic message and clear input
|
||||
self.insert_message(&rumor, true, cx);
|
||||
if rumor.kind != Kind::Reaction {
|
||||
self.insert_message(&rumor, true, cx);
|
||||
self.clear(window, cx);
|
||||
} else {
|
||||
self.insert_reaction(&rumor, cx);
|
||||
}
|
||||
|
||||
// Update reports
|
||||
self.insert_reports(id, vec![], cx);
|
||||
self.clear(window, cx);
|
||||
|
||||
// Spawn a single task to await the send and update reports
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -472,11 +488,31 @@ impl ChatPanel {
|
||||
/// Convert and insert a vector of nostr events into the chat panel
|
||||
fn insert_messages(&mut self, events: &[UnsignedEvent], cx: &mut Context<Self>) {
|
||||
for event in events.iter() {
|
||||
if event.kind == Kind::Reaction {
|
||||
self.insert_reaction(event, cx);
|
||||
continue;
|
||||
}
|
||||
// Bulk inserting messages, so no need to scroll to the latest message
|
||||
self.insert_message(event, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a reaction into the chat panel
|
||||
fn insert_reaction(&mut self, event: &UnsignedEvent, cx: &mut Context<Self>) {
|
||||
if event.kind != Kind::Reaction {
|
||||
return;
|
||||
}
|
||||
|
||||
for id in event.tags.event_ids() {
|
||||
self.reactions
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.push((SharedString::from(&event.content), event.pubkey));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Check if a message has any reports
|
||||
fn has_reports(&self, id: &EventId, _cx: &App) -> bool {
|
||||
self.reports_by_id.read().unwrap().get(id).is_some()
|
||||
@@ -491,6 +527,16 @@ impl ChatPanel {
|
||||
self.messages.iter().find(|msg| &msg.id == id)
|
||||
}
|
||||
|
||||
/// Get a reaction by its target ID
|
||||
fn reaction(&self, id: &EventId) -> Vec<(SharedString, PublicKey)> {
|
||||
self.reactions.get(id).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Check if a message has any reactions
|
||||
fn has_reaction(&self, id: &EventId) -> bool {
|
||||
self.reactions.contains_key(id)
|
||||
}
|
||||
|
||||
/// Scroll to a message by its ID
|
||||
fn scroll_to(&self, id: &EventId) {
|
||||
if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
|
||||
@@ -865,6 +911,7 @@ impl ChatPanel {
|
||||
|
||||
let replies = message.replies_to.as_slice();
|
||||
let has_replies = !replies.is_empty();
|
||||
let has_reactions = self.has_reaction(&id);
|
||||
let has_reports = self.has_reports(&id, cx);
|
||||
|
||||
// Hide avatar setting
|
||||
@@ -910,12 +957,7 @@ impl ChatPanel {
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.child(author.name()),
|
||||
)
|
||||
.child(div().font_semibold().child(author.name()))
|
||||
.child(message.created_at.to_human_time())
|
||||
.when(has_reports, |this| {
|
||||
this.child(self.render_sent_reports(&id, cx))
|
||||
@@ -926,7 +968,10 @@ impl ChatPanel {
|
||||
this.children(self.render_message_replies(replies, cx))
|
||||
})
|
||||
.child(rendered_text)
|
||||
.child(self.render_media(&message.media, cx)),
|
||||
.child(self.render_media(&message.media, cx))
|
||||
.when(has_reactions, |this| {
|
||||
this.child(self.render_reactions(&id, cx))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -1023,7 +1068,7 @@ impl ChatPanel {
|
||||
.border_l_2()
|
||||
.border_color(cx.theme().element_active)
|
||||
.text_sm()
|
||||
.child(div().child(author.name()))
|
||||
.child(div().font_semibold().child(author.name()))
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
@@ -1044,6 +1089,43 @@ impl ChatPanel {
|
||||
items
|
||||
}
|
||||
|
||||
fn render_reactions(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
||||
let current_user = NostrRegistry::global(cx).read(cx).current_user();
|
||||
let reactions = self.reaction(id);
|
||||
|
||||
// Group reactions by emoji and collect authors for each
|
||||
let mut grouped: BTreeMap<SharedString, Vec<PublicKey>> = BTreeMap::new();
|
||||
for (emoji, author) in &reactions {
|
||||
grouped.entry(emoji.clone()).or_default().push(*author);
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.children(grouped.into_iter().map(|(emoji, authors)| {
|
||||
let count = authors.len();
|
||||
let has_reacted = current_user
|
||||
.map(|pk| authors.contains(&pk))
|
||||
.unwrap_or(false);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.border_1()
|
||||
.when(has_reacted, |this| {
|
||||
this.text_color(cx.theme().secondary_foreground)
|
||||
.bg(cx.theme().secondary_background)
|
||||
.border_color(cx.theme().secondary_active)
|
||||
})
|
||||
.when(!has_reacted, |this| this.border_color(cx.theme().border))
|
||||
.child(emoji)
|
||||
.child(SharedString::from(count.to_string()))
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
||||
let reports = self.sent_reports(id, cx);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user