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

Merged
reya merged 4 commits from feat/render-media into master 2026-04-10 02:00:18 +00:00
2 changed files with 55 additions and 5 deletions
Showing only changes of commit 2bc50f07c4 - Show all commits

View File

@@ -9,9 +9,9 @@ use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
Styled, StyledImage, Subscription, Task, WeakEntity, Window, deferred, div, img, list, px, red, StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
relative, svg, white, deferred, div, img, list, px, red, relative, svg, white,
}; };
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -914,7 +914,8 @@ impl ChatPanel {
.when(has_replies, |this| { .when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx)) this.children(self.render_message_replies(replies, cx))
}) })
.child(rendered_text), .child(rendered_text)
.child(self.render_media(&message.media, cx)),
), ),
) )
.child( .child(
@@ -941,6 +942,55 @@ impl ChatPanel {
.into_any_element() .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( fn render_message_replies(
&self, &self,
replies: &[EventId], replies: &[EventId],

View File

@@ -67,7 +67,7 @@ impl MediaExtractor {
// Remove multiple consecutive spaces // Remove multiple consecutive spaces
let re = Regex::new(r"\s+").unwrap(); let re = Regex::new(r"\s+").unwrap();
re.replace_all(text, " ").to_string() re.replace_all(text, " ").trim().to_string()
} }
/// Validates if a URL is a valid media URL /// Validates if a URL is a valid media URL