Skip to content

Commit 8c0b0bf

Browse files
committed
Bump version to 0.2.1 and improve image handling
- Added LRU caching for images in the client to optimize rendering. - Enhanced image resizing and display logic with `ratatui_image`. - Updated `Setting` widget to display server address type with warnings. - Introduced `ConversationState` and `ExploreState` for better state management. - Added `ring` dependency for SHA256 hashing.
1 parent 56562bc commit 8c0b0bf

9 files changed

Lines changed: 104 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
99
-->
1010

1111
## [Unreleased]
12+
## [0.2.1] - 2025-04-27
13+
14+
- improve image display
15+
1216
## [0.2.0] - 2025-04-27
1317

1418
- TLS support

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2ee_chat_client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ ratatui-image = "5.0"
2929
regex = "1.11"
3030
lru = "0.14.0"
3131
image = "0.25.6"
32+
ring = "0.17"

e2ee_chat_client/src/widget/app.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020

2121
use super::{
2222
contacts::Contacts,
23-
conversation::Conversation,
23+
conversation::{Conversation, ConversationState},
2424
explore::{Explore, ExploreState},
2525
footer::Footer,
2626
form::Form,
@@ -48,6 +48,7 @@ pub struct AppState {
4848
pub screen: Screen,
4949

5050
pub explore_state: ExploreState,
51+
conversation_state: ConversationState,
5152

5253
picker: Picker,
5354
}
@@ -73,6 +74,7 @@ impl AppState {
7374
current_activated_textarea: 0,
7475
screen: Screen::default(),
7576
explore_state: Default::default(),
77+
conversation_state: Default::default(),
7678
picker: Picker::from_query_stdio()?,
7779
})
7880
}
@@ -133,9 +135,10 @@ impl StatefulWidget for App {
133135
{
134136
Conversation {
135137
messages,
138+
current: state.current_activated_message,
136139
picker: &state.picker,
137140
}
138-
.render(conversation, buf, &mut state.current_activated_message);
141+
.render(conversation, buf, &mut state.conversation_state);
139142
}
140143
}
141144

@@ -153,7 +156,7 @@ impl StatefulWidget for App {
153156
area,
154157
buf,
155158
&mut SettingState {
156-
server_addr: state.server_addr.to_string(),
159+
server_addr: state.server_addr.clone(),
157160
public_identity_key: state.x3dh_client.public_identity_key(),
158161
},
159162
),
@@ -173,7 +176,12 @@ impl StatefulWidget for App {
173176
.render(area, buf, &mut state.current_activated_textarea);
174177
}
175178
Screen::SelectFile => {
176-
PopUpStateful { inner: Explore {} }.render(area, buf, &mut state.explore_state);
179+
PopUpStateful {
180+
inner: Explore {
181+
picker: &state.picker,
182+
},
183+
}
184+
.render(area, buf, &mut state.explore_state);
177185
}
178186
}
179187
}

e2ee_chat_client/src/widget/bubble.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use image::load_from_memory;
2+
use lru::LruCache;
23
use ratatui::{
34
prelude::*,
45
widgets::{Block, Padding, Paragraph, Wrap},
56
};
6-
use ratatui_image::{FilterType, Resize, StatefulImage, picker::Picker};
7+
use ratatui_image::{
8+
FilterType, Resize, StatefulImage, picker::Picker, protocol::StatefulProtocol,
9+
};
10+
use ring::digest::{SHA256, digest};
711

812
use crate::message::{Message, MessageContent, MessageState};
913

@@ -12,8 +16,10 @@ pub struct Bubble<'a> {
1216
pub message: &'a Message,
1317
}
1418

15-
impl Widget for Bubble<'_> {
16-
fn render(self, area: Rect, buf: &mut Buffer) {
19+
impl StatefulWidget for Bubble<'_> {
20+
type State = LruCache<Vec<u8>, Option<StatefulProtocol>>;
21+
22+
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
1723
let chunk = match self.message.state {
1824
MessageState::Sent => {
1925
let chunks = Layout::default()
@@ -64,10 +70,16 @@ impl Widget for Bubble<'_> {
6470
p.render(inner, buf);
6571
}
6672
MessageContent::Image(bytes) => {
67-
if let Ok(image) = load_from_memory(bytes) {
68-
let mut image = self.picker.new_resize_protocol(image);
69-
image.resize_encode(&Resize::Fit(Some(FilterType::CatmullRom)), inner);
70-
StatefulImage::default().render(inner, buf, &mut image);
73+
if let Some(image) =
74+
state.get_or_insert_mut(digest(&SHA256, bytes).as_ref().to_vec(), || {
75+
load_from_memory(bytes).ok().map(|image| {
76+
let mut image = self.picker.new_resize_protocol(image);
77+
image.resize_encode(&Resize::Fit(Some(FilterType::CatmullRom)), inner);
78+
image
79+
})
80+
})
81+
{
82+
StatefulImage::default().render(inner, buf, image);
7183
}
7284
}
7385
}

e2ee_chat_client/src/widget/conversation.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1+
use std::num::NonZero;
2+
3+
use lru::LruCache;
14
use ratatui::{
25
prelude::*,
36
widgets::{Scrollbar, ScrollbarState},
47
};
5-
use ratatui_image::picker::Picker;
8+
use ratatui_image::{picker::Picker, protocol::StatefulProtocol};
69

710
use crate::message::Message;
811

912
use super::bubble::Bubble;
1013

14+
pub struct ConversationState {
15+
lru_image: LruCache<Vec<u8>, Option<StatefulProtocol>>,
16+
}
17+
18+
impl Default for ConversationState {
19+
fn default() -> Self {
20+
Self {
21+
lru_image: LruCache::new(NonZero::new(16).unwrap()),
22+
}
23+
}
24+
}
25+
1126
pub struct Conversation<'a> {
1227
pub messages: &'a Vec<Message>,
28+
pub current: usize,
1329
pub picker: &'a Picker,
1430
}
1531

1632
impl StatefulWidget for Conversation<'_> {
17-
type State = usize;
33+
type State = ConversationState;
1834

1935
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
2036
let chunks = Layout::default()
@@ -24,11 +40,11 @@ impl StatefulWidget for Conversation<'_> {
2440
Scrollbar::default().render(
2541
chunks[1],
2642
buf,
27-
&mut ScrollbarState::new(self.messages.len()).position(*state),
43+
&mut ScrollbarState::new(self.messages.len()).position(self.current),
2844
);
2945
let lines = chunks[0].height;
3046
let mut used = 0;
31-
for message in self.messages[..=*state].iter().rev() {
47+
for message in self.messages[..=self.current].iter().rev() {
3248
let line_num = message.line_num((chunks[0].width * 3 / 5).max(7) - 6) + 2; // border and padding
3349
if used + line_num >= lines {
3450
break;
@@ -45,7 +61,7 @@ impl StatefulWidget for Conversation<'_> {
4561
message,
4662
picker: self.picker,
4763
}
48-
.render(chunks[1], buf);
64+
.render(chunks[1], buf, &mut state.lru_image);
4965
used += line_num;
5066
}
5167
}

e2ee_chat_client/src/widget/explore.rs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,26 @@ use ratatui::{
1212
prelude::*,
1313
widgets::{Block, Borders, Padding, Paragraph, Scrollbar, ScrollbarState},
1414
};
15-
use ratatui_image::{FilterType, Resize, StatefulImage, picker::Picker};
15+
use ratatui_image::{
16+
FilterType, Resize, StatefulImage, picker::Picker, protocol::StatefulProtocol,
17+
};
1618
use regex::Regex;
1719
use tui_textarea::{Input, TextArea};
1820
use unicode_width::UnicodeWidthStr;
1921

2022
use crate::message::MessageContent;
2123

22-
pub struct Explore {}
24+
pub struct Explore<'a> {
25+
pub picker: &'a Picker,
26+
}
2327

2428
#[derive(Debug)]
2529
pub struct ExploreState {
2630
pub selected: usize,
2731
pub paths: Vec<PathBuf>,
2832
textarea: TextArea<'static>,
29-
lru: LruCache<PathBuf, Vec<u8>>,
30-
picker: Picker,
33+
lru_bytes: LruCache<PathBuf, Option<Vec<u8>>>,
34+
lru_image: LruCache<PathBuf, Option<StatefulProtocol>>,
3135
}
3236

3337
impl Default for ExploreState {
@@ -36,8 +40,8 @@ impl Default for ExploreState {
3640
selected: 0,
3741
paths: collect_paths(std::env::current_dir().unwrap()),
3842
textarea: Default::default(),
39-
lru: LruCache::new(NonZeroUsize::new(16).unwrap()),
40-
picker: Picker::from_query_stdio().unwrap(),
43+
lru_bytes: LruCache::new(NonZeroUsize::new(16).unwrap()),
44+
lru_image: LruCache::new(NonZeroUsize::new(16).unwrap()),
4145
}
4246
}
4347
}
@@ -100,7 +104,7 @@ impl ExploreState {
100104
}
101105
}
102106

103-
impl StatefulWidget for Explore {
107+
impl StatefulWidget for Explore<'_> {
104108
type State = ExploreState;
105109

106110
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
@@ -168,7 +172,7 @@ impl StatefulWidget for Explore {
168172
if let Some(mime) = mime_guess::from_path(selected).first() {
169173
match mime.type_() {
170174
TEXT => {
171-
let bytes = state.lru.get_or_insert(selected.clone(), || {
175+
if let Some(bytes) = state.lru_bytes.get_or_insert(selected.clone(), || {
172176
fs::File::open(selected)
173177
.map(|mut file| {
174178
let mut lines = 0;
@@ -189,20 +193,28 @@ impl StatefulWidget for Explore {
189193
}
190194
bytes
191195
})
192-
.unwrap_or_default()
193-
});
194-
let text = String::from_utf8_lossy(bytes);
195-
let lines = text.lines().map(Into::into).collect::<Vec<_>>();
196-
Paragraph::new(lines).render(preview, buf);
196+
.ok()
197+
}) {
198+
let text = String::from_utf8_lossy(bytes);
199+
let lines = text.lines().map(Into::into).collect::<Vec<_>>();
200+
Paragraph::new(lines).render(preview, buf);
201+
}
197202
}
198203
IMAGE => {
199-
let bytes = state.lru.get_or_insert(selected.clone(), || {
200-
std::fs::read(selected).unwrap_or_default()
201-
});
202-
if let Ok(image) = load_from_memory(bytes) {
203-
let mut image = state.picker.new_resize_protocol(image);
204-
image.resize_encode(&Resize::Fit(Some(FilterType::CatmullRom)), preview);
205-
StatefulImage::default().render(preview, buf, &mut image);
204+
if let Some(image) = state.lru_image.get_or_insert_mut(selected.clone(), || {
205+
std::fs::read(selected)
206+
.ok()
207+
.and_then(|bytes| load_from_memory(&bytes).ok())
208+
.map(|image| {
209+
let mut image = self.picker.new_resize_protocol(image);
210+
image.resize_encode(
211+
&Resize::Fit(Some(FilterType::CatmullRom)),
212+
preview,
213+
);
214+
image
215+
})
216+
}) {
217+
StatefulImage::default().render(preview, buf, image);
206218
}
207219
}
208220
_ => {}

e2ee_chat_client/src/widget/setting.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ use ratatui::{
33
prelude::*,
44
widgets::{Block, Cell, Padding, Row, Table},
55
};
6+
use ratchetx2::Uri;
67

78
pub struct Setting {}
89

910
pub struct SettingState {
10-
pub server_addr: String,
11+
pub server_addr: Uri,
1112
pub public_identity_key: Vec<u8>,
1213
}
1314

@@ -33,8 +34,16 @@ impl StatefulWidget for Setting {
3334
)
3435
.rows([
3536
Row::new([
36-
Cell::from("Server Address"),
37-
Cell::from(state.server_addr.as_str()),
37+
{
38+
if state.server_addr.scheme_str() == Some("http") {
39+
Cell::from("Server Addr (HTTP⚠️)").red()
40+
} else if state.server_addr.scheme_str() == Some("https") {
41+
Cell::from("Server Addr (HTTPS)")
42+
} else {
43+
Cell::from("Server Addr (Unknown Type)").yellow()
44+
}
45+
},
46+
Cell::from(state.server_addr.to_string()),
3847
]),
3948
Row::new([
4049
Cell::from("Public Identity Key"),

e2ee_chat_server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "e2ee_chat_server"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2024"
55
description = "An E2EE chat server."
66
keywords = ["E2EE", "double-ratchet", "xeddsa", "XEdDSA", "signal"]

0 commit comments

Comments
 (0)