feat: add support for rendering images in chat messages (#29)

Reviewed-on: #29
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
This commit was merged in pull request #29.
This commit is contained in:
Ren Amamiya
2026-04-10 02:00:18 +00:00
committed by reya
parent 9ff18aae35
commit c239e351b8
11 changed files with 410 additions and 71 deletions

View File

@@ -4,14 +4,14 @@ use std::sync::Arc;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus};
use common::TimestampExt;
use common::{TimestampExt, coop_cache};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement,
Styled, StyledImage, Subscription, Task, WeakEntity, Window, deferred, div, img, list, px, red,
relative, svg, white,
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
deferred, div, img, list, px, red, relative, svg, white,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
@@ -914,7 +914,8 @@ impl ChatPanel {
.when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx))
})
.child(rendered_text),
.child(rendered_text)
.child(self.render_media(&message.media, cx)),
),
)
.child(
@@ -941,6 +942,55 @@ impl ChatPanel {
.into_any_element()
}
fn render_media(&self, media: &[SharedUri], cx: &Context<Self>) -> impl IntoElement {
// No media: return empty div
if media.is_empty() {
return div();
};
// Single media item: render full-width image
if media.len() == 1 {
return div().child(
img(media[0].clone())
.border_1()
.border_color(cx.theme().border_variant)
.h(px(250.))
.object_fit(ObjectFit::Cover)
.rounded(cx.theme().radius),
);
}
// Multiple media items: render in a row
div()
.w_full()
.flex_1()
.flex()
.flex_row()
.flex_wrap()
.gap_2()
.children({
let mut items = vec![];
for (ix, item) in media.iter().enumerate() {
items.push(
div()
.id(format!("media-{ix}"))
.flex_grow_0()
.flex_shrink_0()
.child(
img(item.clone())
.h_32()
.border_1()
.border_color(cx.theme().border_variant)
.rounded(cx.theme().radius),
),
);
}
items
})
}
fn render_message_replies(
&self,
replies: &[EventId],
@@ -1435,6 +1485,7 @@ impl Focusable for ChatPanel {
impl Render for ChatPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.image_cache(coop_cache(self.id.clone(), 100))
.on_action(cx.listener(Self::on_command))
.size_full()
.when(*self.subject_bar.read(cx), |this| {

View File

@@ -69,6 +69,7 @@ impl RenderedText {
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
let code_background = cx.theme().elevated_surface_background;
let color = cx.theme().text_accent;
InteractiveText::new(
id,
@@ -100,6 +101,7 @@ impl RenderedText {
}
}
Highlight::Mention => HighlightStyle {
color: Some(color),
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()