Skip to content

Commit 1d6f7bd

Browse files
Add image attachment support with fast-thumbhash library. Update dependencies and refactor image handling logic
1 parent a00f3a8 commit 1d6f7bd

25 files changed

Lines changed: 747 additions & 83 deletions

File tree

Cargo.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

binaries/nullspace-egui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ arboard = { version = "3.6.1", features = [
5656
"image-data",
5757
"wayland-data-control",
5858
] }
59+
fast-thumbhash = "0.2.0"
5960
fastrand = "2.3.0"
6061
either = "1.15.0"
6162
futures-util = "0.3.32"

binaries/nullspace-egui/src/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ use clap::Parser;
77

88
use egui::Modal;
99
use egui_file_dialog::FileDialog as EguiFileDialog;
10-
use nullspace_client::{Client, Config, internal::Event};
10+
use nullspace_client::{
11+
Client, Config,
12+
internal::{Event, UploadedRoot},
13+
};
1114
use nullspace_crypt::hash::Hash;
1215
use nullspace_crypt::signing::SigningPublic;
13-
use nullspace_structs::fragment::Attachment;
1416
use smol::channel::Receiver;
1517
use url::Url;
1618

@@ -80,10 +82,12 @@ struct AppState {
8082
attach_updates: u64,
8183

8284
upload_progress: BTreeMap<i64, (u64, u64)>,
83-
upload_done: BTreeMap<i64, Attachment>,
85+
upload_done: BTreeMap<i64, UploadedRoot>,
8486
upload_error: BTreeMap<i64, String>,
8587
download_progress: BTreeMap<Hash, (u64, u64)>,
8688
download_error: BTreeMap<Hash, String>,
89+
90+
image_viewer: Option<PathBuf>,
8791
}
8892

8993
impl NullspaceApp {
@@ -174,6 +178,7 @@ impl NullspaceApp {
174178
upload_error: BTreeMap::new(),
175179
download_progress: BTreeMap::new(),
176180
download_error: BTreeMap::new(),
181+
image_viewer: None,
177182
},
178183
}
179184
}
@@ -297,6 +302,7 @@ impl eframe::App for NullspaceApp {
297302
}
298303
}
299304
egui::CentralPanel::default().show(ctx, |ui| {
305+
ui.add(widgets::image_viewer::ImageViewer(&mut self.state.image_viewer));
300306
if let Some(e) = self.state.error_dialog.clone() {
301307
Modal::new("error_modal".into()).show(ctx, |ui| {
302308
ui.heading("Error");

binaries/nullspace-egui/src/notify.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub async fn show_notification(
5050
let body = match &message.body {
5151
MessageContent::PlainText(text) => text.clone(),
5252
MessageContent::Attachment { .. } => "Attachment".to_string(),
53+
MessageContent::ImageAttachment { .. } => "Image".to_string(),
5354
MessageContent::GroupInvite { .. } => "Group invite".to_string(),
5455
};
5556
let title = format!("Message from {}", message.sender);

binaries/nullspace-egui/src/screens/profile.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use egui::{Color32, RichText};
55
use egui_hooks::UseHookExt;
66
use egui_hooks::hook::state::{State, Var};
77
use egui_taffy::{Tui, TuiBuilderLogic, tui};
8-
use nullspace_client::internal::UserDetails;
9-
use nullspace_structs::fragment::Attachment;
8+
use nullspace_client::internal::{UploadedRoot, UserDetails};
9+
use nullspace_structs::fragment::ImageAttachment;
1010
use pollster::FutureExt;
1111
use taffy::style_helpers::{auto, fr, length};
1212
use taffy::{AlignItems, Dimension, Display, FlexDirection, Size as TaffySize, Style};
@@ -21,13 +21,13 @@ use crate::widgets::avatar::Avatar;
2121
enum AvatarChoice {
2222
Keep,
2323
Clear,
24-
Set(Attachment),
24+
Set(ImageAttachment),
2525
}
2626

2727
#[derive(Clone)]
2828
struct SaveRequest {
2929
display_name: Option<String>,
30-
avatar: Option<Attachment>,
30+
avatar: Option<ImageAttachment>,
3131
}
3232

3333
enum SaveOutcome {
@@ -247,7 +247,15 @@ impl Widget for ProfileInner<'_> {
247247
ui.add(eframe::egui::ProgressBar::new(progress).text("Uploading..."));
248248
} else if let Some(done) = self.app.state.upload_done.get(&upload_id) {
249249
let root = done.clone();
250-
avatar_choice.set_next(AvatarChoice::Set(root));
250+
match root {
251+
UploadedRoot::ImageAttachment(root) => {
252+
avatar_choice.set_next(AvatarChoice::Set(root));
253+
}
254+
UploadedRoot::Attachment(_) => {
255+
self.app.state.error_dialog =
256+
Some("avatar upload produced non-image payload".to_string());
257+
}
258+
}
251259
avatar_upload_id.set_next(None);
252260
self.app.state.upload_done.remove(&upload_id);
253261
self.app.state.upload_progress.remove(&upload_id);
@@ -336,7 +344,7 @@ fn start_avatar_upload(app: &mut NullspaceApp, upload_id: &State<Option<i64>>, p
336344
app.state.error_dialog = Some("avatar must be an image".to_string());
337345
return;
338346
}
339-
let Ok(id) = flatten_rpc(get_rpc().attachment_upload(path, mime).block_on()) else {
347+
let Ok(id) = flatten_rpc(get_rpc().image_attachment_upload(path).block_on()) else {
340348
return;
341349
};
342350
upload_id.set_next(Some(id));

binaries/nullspace-egui/src/widgets/avatar.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::PathBuf;
33
use eframe::egui::{Response, Widget};
44
use egui_hooks::UseHookExt;
55
use nullspace_crypt::hash::BcsHashExt;
6-
use nullspace_structs::fragment::Attachment;
6+
use nullspace_structs::fragment::ImageAttachment;
77
use nullspace_structs::username::UserName;
88

99
use crate::promises::flatten_rpc;
@@ -13,7 +13,7 @@ use crate::widgets::smooth::SmoothImage;
1313

1414
pub struct Avatar {
1515
pub sender: UserName,
16-
pub attachment: Option<Attachment>,
16+
pub attachment: Option<ImageAttachment>,
1717
pub size: f32,
1818
}
1919

@@ -49,7 +49,7 @@ impl Widget for Avatar {
4949
smol::spawn(async move {
5050
let _ = flatten_rpc(
5151
get_rpc()
52-
.attachment_download_oneshot(sender, attachment, save_to)
52+
.attachment_download_oneshot(sender, attachment.inner, save_to)
5353
.await,
5454
);
5555
})
@@ -60,6 +60,7 @@ impl Widget for Avatar {
6060
let size = eframe::egui::vec2(self.size, self.size);
6161
ui.add(
6262
SmoothImage::new(path.as_path())
63+
.thumbhash(Some(attachment.thumbhash.as_str()))
6364
.fit_to_size(size)
6465
.corner_radius(circle_corner_radius)
6566
.preserve_aspect_ratio(false)
@@ -98,8 +99,8 @@ fn paint_avatar_placeholder(ui: &eframe::egui::Ui, rect: eframe::egui::Rect, use
9899
);
99100
}
100101

101-
fn avatar_cache_path(attachment: &Attachment) -> Option<PathBuf> {
102+
fn avatar_cache_path(attachment: &ImageAttachment) -> Option<PathBuf> {
102103
let base = dirs::cache_dir()?;
103-
let filename = attachment.bcs_hash().to_string();
104+
let filename = attachment.inner.bcs_hash().to_string();
104105
Some(base.join("nullspace").join("avatars").join(filename))
105106
}

binaries/nullspace-egui/src/widgets/convo.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use egui::{Button, Color32, Image, Label, Modal, ProgressBar, ScrollArea, TextEd
33
use egui_hooks::UseHookExt;
44
use egui_hooks::hook::state::Var;
55
use egui_infinite_scroll::InfiniteScroll;
6-
use nullspace_client::internal::{ConvoId, ConvoMessage, OutgoingMessage};
6+
use nullspace_client::internal::{ConvoId, ConvoMessage, OutgoingMessage, UploadedRoot};
77
use nullspace_structs::username::UserName;
88
use pollster::block_on;
99
use smol_str::SmolStr;
@@ -318,7 +318,12 @@ fn start_upload(attachment: &mut Var<Option<i64>>, path: PathBuf) {
318318
"picked an attachment, starting upload..."
319319
);
320320
let mime = infer_mime(&path);
321-
let Ok(upload_id) = flatten_rpc(block_on(get_rpc().attachment_upload(path, mime))) else {
321+
let result = if mime.starts_with("image/") {
322+
flatten_rpc(block_on(get_rpc().image_attachment_upload(path)))
323+
} else {
324+
flatten_rpc(block_on(get_rpc().attachment_upload(path, mime)))
325+
};
326+
let Ok(upload_id) = result else {
322327
return;
323328
};
324329
attachment.replace(upload_id);
@@ -348,11 +353,11 @@ fn render_composer(ui: &mut egui::Ui, app: &mut NullspaceApp, convo_id: ConvoId)
348353
let root = done.clone();
349354
let convo_id = convo_id.clone();
350355
smol::spawn(async move {
351-
let _ = flatten_rpc(
352-
get_rpc()
353-
.convo_send(convo_id, OutgoingMessage::Attachment(root))
354-
.await,
355-
);
356+
let message = match root {
357+
UploadedRoot::Attachment(root) => OutgoingMessage::Attachment(root),
358+
UploadedRoot::ImageAttachment(root) => OutgoingMessage::ImageAttachment(root),
359+
};
360+
let _ = flatten_rpc(get_rpc().convo_send(convo_id, message).await);
356361
})
357362
.detach();
358363
*attachment = None;

binaries/nullspace-egui/src/widgets/convo/cluster.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ pub struct MessageRenderMeta {
1515
enum MessageKind {
1616
Text,
1717
Attachment,
18+
ImageAttachment,
1819
GroupInvite,
1920
}
2021

2122
fn message_kind(message: &ConvoMessage) -> MessageKind {
2223
match message.body {
2324
MessageContent::PlainText(_) => MessageKind::Text,
2425
MessageContent::Attachment { .. } => MessageKind::Attachment,
26+
MessageContent::ImageAttachment { .. } => MessageKind::ImageAttachment,
2527
MessageContent::GroupInvite { .. } => MessageKind::GroupInvite,
2628
}
2729
}

0 commit comments

Comments
 (0)