Skip to content

Feat/profile update and others#4

Merged
jgmontoya merged 5 commits into
masterfrom
feat/profile-update-and-others
Mar 8, 2026
Merged

Feat/profile update and others#4
jgmontoya merged 5 commits into
masterfrom
feat/profile-update-and-others

Conversation

@jgmontoya

@jgmontoya jgmontoya commented Mar 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Inline image rendering, media upload/download and previews, profile pictures, reactions, message deletion, reply previews, NSec export, logout, follow-checks, view user profiles, create groups with members
  • UI/UX Improvements

    • Expanded hints layout and clearer keyboard shortcuts, richer profile and message displays, selection and inline-image previews, improved composer placeholders
  • Documentation

    • Updated README project link and build instructions
  • Chores

    • Dependency updates and new image/terminal rendering deps

…up info, and account management

Add terminal image rendering for profile pictures using ratatui-image with
auto-detection of Kitty/Sixel/iTerm2/Halfblocks protocols. Images display on
both the profile screen and user profile popups. Includes a workaround for
iTerm2 misdetecting as Kitty by checking ITERM_SESSION_ID env var.

Extend profile editing with display_name, picture URL, NIP-05, and lightning
address fields. Use  instead of  for full metadata
including picture URLs.

Add user profile viewing from follows list (Enter) and search results (F2),
group info popup (i) on group detail screen, group picker for adding searched
users to existing groups (F4), and create-group-with-user flow (F3).

Add logout from profile screen (Q) and account select screen (d), create
new identity from main screen (C) and account select (c), and login with
nsec from account select (l).

Other improvements: log panel visible on all screens, Tab switches log tabs
globally, paste into popup text inputs, DM invite name resolution via
welcomer_pubkey lookup.

Deps: ratatui 0.29 -> 0.30, +ratatui-image 10, +image 0.25
Add react/unreact support with emoji popup (r/u keys in Messages panel),
selection-based message navigation with viewport-aware scrolling (j/k
moves cursor, g/G jumps to oldest/newest), and message kind filtering
to only display kind-9 chat messages from the subscription stream.

Remove left groups from the chat list immediately on leave, and clear
stale entries when re-subscribing after group actions.
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown

Walkthrough

Adds inline image rendering and media upload/download workflows, significantly expands Action/Effect enums for messaging, reactions, profiles and account operations, introduces a new util module for JSON→hex conversion, updates UI screens and widgets to support images and richer profile display, and bumps UI/image dependencies in Cargo.toml.

Changes

Cohort / File(s) Summary
Manifest / Dependencies
Cargo.toml
Bump ratatui 0.29 → 0.30; add ratatui-image = "10" (default-features = false, features = ["crossterm"]) and image = "0.25" (default-features = false, features = ["png","jpeg","gif","webp"]).
Docs / Build
README.md
Project reference changed to whitenoise-rs; updated UI demo, clone/build instructions and binary/feature flags.
Actions & Effects
src/action.rs
Large addition of public Action and Effect enum variants for reactions, message lifecycle (load, delete), media (upload/download/image/popup), profile/account flows (export NSEC, fetch profile image, show profile, logout), follow checks, and CreateGroup members.
Runtime / Effects Handling
src/main.rs
Add pub mod util, initialize image protocol detection, implement handling/dispatch for new effects (LoadMessages, React/Unreact, DeleteMessage, ExportNsec, FetchProfileImage, ShowUserProfile, Logout, media download/upload/image load/popup, CheckFollow, CreateGroup members), and extend profile/update argument handling.
Utility module
src/util.rs
New public module with pub fn value_to_hex(...) and pub fn extract_hash_hex(...) to normalize JSON value shapes into hex strings used for group/media IDs.
Main UI / Rendering
src/screen/main_screen.rs
Changed draw signature to take &mut App; added inline image rendering using StatefulImage, selection/media_downloads/inline_images plumbing to MessageListWidget, reply previews, composer placeholder logic, hint updates, and made draw_log_panel public.
Profile UI
src/screen/profile.rs
Changed draw signature to &mut App; added optional profile image rendering via StatefulImage, richer profile fields (display_name, picture, nip05, lud16, npub), two-row hints and layout adjustments.
Message list widget
src/widget/message_list.rs
Major refactor: add attachment/inline-image support and reply indicators; extend format_message signature, add public fields selected, media_downloads, inline_images, builders, image_positions() method, viewport visibility computation, and update tests.
Popup util
src/widget/popup.rs
Made centered_rect public (fnpub fn).
Chat list JSON parsing
src/widget/chat_list.rs
Replaced inline group_id parsing with crate::util::value_to_hex(...) call to centralize JSON→hex handling.
Screens — small UI hint changes
src/screen/group_detail.rs, src/screen/login.rs, src/screen/user_search.rs
Inserted/expanded hint rows and adjusted hint content/layout (Info hint, extra login hint row, split follow/new-group hints).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feat/profile update and others' is vague and overly broad. It uses the generic phrase 'and others' which obscures the actual scope of changes across multiple features including image rendering, message reactions, media handling, and UI improvements. Consider a more specific title that captures the main changes, such as 'Add media/image support and expand user profile functionality' or break into more focused pull requests.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 91.84% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/profile-update-and-others

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/screen/user_search.rs (1)

24-29: ⚠️ Potential issue | 🟡 Minor

Footer exceeds 80 columns and shows mode-inappropriate shortcuts.

The footer spans 107 characters when all hints are rendered, far exceeding the standard 80-column terminal width. More critically, F3 ("New group") and F4 ("Add to group") are shown unconditionally whenever results exist, despite SearchPurpose distinguishing between Browse and AddMember modes—in AddMember, "Add to group" appears twice (as the Enter action and as F4), while in Browse mode, the F4 shortcut is misleading. Split the footer into two rows and conditionally render shortcuts based on SearchPurpose to keep it readable and accurate. Verify at 80-column width before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screen/user_search.rs` around lines 24 - 29, The footer currently renders
as a single long row in user_search.rs (the Layout::vertical with Constraints
and the footer render code) and unconditionally shows F3/F4 hints; update the
footer to use two rows (split into two Constraint::Length(1) rows in the
vertical layout and render two separate hint lines) and make hint rendering
conditional based on SearchPurpose (check the enum value used in the search
screen, e.g., SearchPurpose::Browse vs SearchPurpose::AddMember) so that F3
("New group") and F4 ("Add to group") are only shown when appropriate (F4
omitted in Browse, avoid duplicating "Add to group" in AddMember since Enter
already performs that action). Ensure the total width of each row fits within 80
columns by truncating or omitting less important hints when necessary and test
at an 80-column terminal before merging.
src/widget/message_list.rs (1)

582-593: ⚠️ Potential issue | 🟠 Major

Keep at least one oversized message visible.

With INLINE_IMAGE_ROWS = 8, the newest message can easily be taller than visible_height. This loop then breaks before pushing anything, so the pane renders empty even though messages exist. Include the first candidate even if it overflows, and apply the same rule in compute_visible() so inline-image positions stay aligned.

💡 Suggested fix
-        for i in (0..end).rev() {
-            let h = message_height_with_images(&self.messages[i], width, self.inline_images);
-            if used_rows + h > visible_height {
-                break;
-            }
+        for i in (0..end).rev() {
+            let h = message_height_with_images(&self.messages[i], width, self.inline_images);
+            if used_rows + h > visible_height && !visible_msgs.is_empty() {
+                break;
+            }
             used_rows += h;
             visible_msgs.push(i);
+            if used_rows >= visible_height {
+                break;
+            }
         }

Apply the same condition in MessageListWidget::compute_visible().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widget/message_list.rs` around lines 582 - 593, The loop that builds
visible_msgs in MessageListWidget (using variables visible_msgs, used_rows,
used_rows + h, visible_height, skip_messages and calling
message_height_with_images) currently breaks before pushing any item if the
first candidate's height exceeds visible_height, leaving the pane empty; change
the logic so the first candidate index is always pushed into visible_msgs even
if h > visible_height (i.e., allow one oversized message), and mirror this same
rule inside compute_visible() so inline-image positioning stays aligned with
INLINE_IMAGE_ROWS handling. Ensure you still break out after adding the first
oversized message and continue preserving existing skip_messages behavior.
🧹 Nitpick comments (1)
src/widget/message_list.rs (1)

91-114: Extract extract_hash_hex into a shared helper.

This duplicates the existing attachment-hash parsing logic in src/app.rs. Keeping two copies of the same schema handling will drift the next time the CLI payload changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widget/message_list.rs` around lines 91 - 114, Move the extract_hash_hex
parsing logic into a single shared helper function (keep the current signature
fn extract_hash_hex(att: &Value, field: &str) -> Option<String>) in a common
module (e.g., a new util/mod or shared/helpers module), export it publicly, and
replace the duplicate implementation in src/widget/message_list.rs and the copy
in src/app.rs with calls to that shared function; update use/import paths where
needed and run tests/compile to ensure no behavioral change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 32-34: The README build instructions currently run git clone then
cargo build but never switch to the advertised feat/cli branch; update the
instructions to explicitly check out that branch (either by adding a git
checkout feat/cli after git clone or by cloning with the branch flag) before
running the cargo build command so the CLI targets/features referenced
(feat/cli) are present for the subsequent cargo build --release --bin wn
--features cli --bin wnd step.

In `@src/action.rs`:
- Line 36: The MessagesLoaded enum variant (and the analogous success variants
for FetchProfileImage and ShowUserProfile) currently drops the original request
key causing race conditions; update MessagesLoaded(Vec<Value>) to include the
request identity (e.g., MessagesLoaded { group_id: GroupId, messages: Vec<Value>
}) and do the same for the profile image and user profile success variants
(carry url or pubkey alongside payload), then update the action creators that
dispatch LoadMessages/FetchProfileImage/ShowUserProfile to include the same key
in their success dispatches and modify the reducer handlers to match on the key
before applying payloads so stale responses are ignored.

In `@src/main.rs`:
- Around line 953-960: The ShowUserProfile branch currently forwards the raw
wn::exec output, causing mismatched shape vs LoadProfile; update
Effect::ShowUserProfile (the tokio::spawn handling that calls wn::exec and
constructs Action::UserProfileLoaded) to mirror the flattening logic from
Effect::LoadProfile: if the returned value contains a metadata object, extract
its fields (name, display_name, about, picture, nip05, lud16, npub) and promote
them to top-level keys, and if top-level npub is missing set it to the
account/pubkey fallback used in LoadProfile before constructing
Action::UserProfileLoaded so the screen receives the same normalized shape.
- Around line 807-823: Update the curl invocation and response handling inside
the Effect::FetchProfileImage branch: add the "--fail" flag to the
tokio::process::Command::new("curl") args so curl returns non-zero on HTTP
4xx/5xx, and enforce a maximum response size (e.g. define a const
MAX_PROFILE_IMAGE_SIZE) by checking output.stdout.len() before sending
Event::Action(Action::ProfileImageFetched(...)); if output.status.success() is
false or the body is empty or exceeds MAX_PROFILE_IMAGE_SIZE, send the existing
Event::Action(Action::Log(...)) instead. Reference: Effect::FetchProfileImage,
tokio::process::Command::new("curl"), and
Event::Action(Action::ProfileImageFetched).

In `@src/screen/main_screen.rs`:
- Line 356: The current call to
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner)
uses Wrap which lets a single log entry expand into multiple visual rows, but
your windowing uses visible based on raw entry count so long entries hide newest
logs; fix by either removing the .wrap(Wrap { trim: false }) so each entry
occupies one row (keep Paragraph::new(lines) unwrapped) or make the windowing
logic row-aware: compute visual rows for each entry (e.g., measure wrapped line
count for each entry using the same wrapping rules) and adjust the code that
computes visible to use wrapped-row counts rather than entry count (update the
logic that selects entries for rendering using the wrapped row totals).
- Around line 184-190: The code calculates preview length and slices it by bytes
which can panic on multi-byte UTF‑8; in the block that builds placeholder
(matching on app.reply_to and using preview and the byte-slice &preview[..27]),
replace the byte-length check preview.len() and byte-slice with character-aware
logic: use preview.chars().count() to decide if truncation is needed and build
the truncated string with preview.chars().take(27).collect::<String>() (or
equivalent) so slicing never splits a UTF‑8 codepoint, then use that truncated
String in the format! for placeholder.

In `@src/screen/profile.rs`:
- Around line 87-95: When has_image is true the fixed 20-column image pane can
collapse the text pane on narrow terminals; update the logic that builds
(image_area, text_area) so it measures the available width from vertical[0]
before using Layout::horizontal with Constraint::Length(20) and
Constraint::Fill(1) and, if the total width is below a safe threshold (enough to
accommodate labels like "  Name:          "), fall back to the no-image branch
(set image_area to None and text_area to vertical[0]) instead of splitting;
apply this check where (image_area, text_area) is computed so the code avoids
creating a too-narrow cols[1].

---

Outside diff comments:
In `@src/screen/user_search.rs`:
- Around line 24-29: The footer currently renders as a single long row in
user_search.rs (the Layout::vertical with Constraints and the footer render
code) and unconditionally shows F3/F4 hints; update the footer to use two rows
(split into two Constraint::Length(1) rows in the vertical layout and render two
separate hint lines) and make hint rendering conditional based on SearchPurpose
(check the enum value used in the search screen, e.g., SearchPurpose::Browse vs
SearchPurpose::AddMember) so that F3 ("New group") and F4 ("Add to group") are
only shown when appropriate (F4 omitted in Browse, avoid duplicating "Add to
group" in AddMember since Enter already performs that action). Ensure the total
width of each row fits within 80 columns by truncating or omitting less
important hints when necessary and test at an 80-column terminal before merging.

In `@src/widget/message_list.rs`:
- Around line 582-593: The loop that builds visible_msgs in MessageListWidget
(using variables visible_msgs, used_rows, used_rows + h, visible_height,
skip_messages and calling message_height_with_images) currently breaks before
pushing any item if the first candidate's height exceeds visible_height, leaving
the pane empty; change the logic so the first candidate index is always pushed
into visible_msgs even if h > visible_height (i.e., allow one oversized
message), and mirror this same rule inside compute_visible() so inline-image
positioning stays aligned with INLINE_IMAGE_ROWS handling. Ensure you still
break out after adding the first oversized message and continue preserving
existing skip_messages behavior.

---

Nitpick comments:
In `@src/widget/message_list.rs`:
- Around line 91-114: Move the extract_hash_hex parsing logic into a single
shared helper function (keep the current signature fn extract_hash_hex(att:
&Value, field: &str) -> Option<String>) in a common module (e.g., a new util/mod
or shared/helpers module), export it publicly, and replace the duplicate
implementation in src/widget/message_list.rs and the copy in src/app.rs with
calls to that shared function; update use/import paths where needed and run
tests/compile to ensure no behavioral change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 109d4edb-bc0b-4f04-9827-b161222180fd

📥 Commits

Reviewing files that changed from the base of the PR and between 72f81d2 and 031e8e7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml
  • README.md
  • src/action.rs
  • src/app.rs
  • src/main.rs
  • src/screen/group_detail.rs
  • src/screen/login.rs
  • src/screen/main_screen.rs
  • src/screen/profile.rs
  • src/screen/user_search.rs
  • src/widget/message_list.rs
  • src/widget/popup.rs

Comment thread README.md
Comment thread src/action.rs
Comment thread src/main.rs
Comment thread src/main.rs
Comment thread src/screen/main_screen.rs
Comment thread src/screen/main_screen.rs Outdated
Comment thread src/screen/profile.rs
@jgmontoya
jgmontoya force-pushed the feat/profile-update-and-others branch from 031e8e7 to fd4b90b Compare March 8, 2026 17:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main.rs (1)

953-963: ⚠️ Potential issue | 🟠 Major

Normalize ShowUserProfile data to match LoadProfile shape.

LoadProfile (lines 727-748) flattens metadata fields to the top level, but ShowUserProfile sends the raw response. This causes profile fields (name, about, picture, nip05, lud16) to render as "(not set)" when viewing other users' profiles since src/screen/profile.rs expects top-level keys.

🐛 Proposed fix
 Effect::ShowUserProfile { account, pubkey } => {
     let tx = tx.clone();
     tokio::spawn(async move {
         let action =
             match wn::exec(&["--account", &account, "users", "show", &pubkey]).await {
-                Ok(val) => Action::UserProfileLoaded(val),
+                Ok(val) => {
+                    let mut profile = val.clone();
+                    if let Some(meta) = val.get("metadata").and_then(|m| m.as_object()) {
+                        for (k, v) in meta {
+                            profile[k.clone()] = v.clone();
+                        }
+                    }
+                    if profile.get("npub").is_none() {
+                        if let Some(pk) = val.get("pubkey").and_then(|v| v.as_str()) {
+                            profile["npub"] = serde_json::Value::String(pk.to_string());
+                        }
+                    }
+                    Action::UserProfileLoaded(profile)
+                }
                 Err(e) => Action::UserProfileError(e.to_string()),
             };
         let _ = tx.send(Event::Action(action));
     });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 953 - 963, The ShowUserProfile branch returns the
raw response shape, but UI expects the same flattened shape produced by
LoadProfile; update the async block in Effect::ShowUserProfile so that after
wn::exec(...) succeeds you transform the returned value to match LoadProfile's
shape by extracting metadata fields (name, about, picture, nip05, lud16) from
the response.metadata and promoting them to top-level keys (providing None/empty
when absent) before constructing Action::UserProfileLoaded; keep the error
branch as-is (Action::UserProfileError) and send Event::Action(action) as
before.
🧹 Nitpick comments (4)
src/main.rs (2)

536-589: N+1 query pattern for DM invite resolution may cause latency.

Each DM invite triggers a separate wn exec users show call. While pending invites are typically few, this could cause noticeable delay with many invites.

For now, this is acceptable given the typical invite count. If performance becomes an issue, consider batching the queries or using a single bulk user lookup command if the CLI supports it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 536 - 589, The code performs an N+1 query by
calling wn::exec(&["--account", &account, "users", "show", pubkey]) for each
invite in arr; instead, collect all welcomer_pubkey values first, issue a single
batched user lookup (or multiple chunked lookups) via the CLI if it supports a
bulk "users show" or "users list --ids" style command, cache the returned user
objects into a HashMap<pubkey, user_val>, and then iterate inv in arr to resolve
display_name from the cached user_val before writing into group["name"]; update
the logic around collecting pubkeys, calling wn::exec once (or few times), and
replacing per-invite wn::exec calls with HashMap lookups while reusing the
existing display_name extraction and group["name"] assignment.

1037-1061: Consider adding a size limit for media popup loading.

LoadMediaPopup reads the entire file and decodes it in memory. Very large images could cause memory pressure. Consider adding a file size check before loading:

💡 Suggestion
 Effect::LoadMediaPopup { file_path } => {
     let tx = tx.clone();
     tokio::spawn(async move {
+        const MAX_POPUP_SIZE: u64 = 50 * 1024 * 1024; // 50MB
+        if let Ok(meta) = tokio::fs::metadata(&file_path).await {
+            if meta.len() > MAX_POPUP_SIZE {
+                send_log(&tx, format!("Image too large for popup: {} bytes", meta.len()));
+                return;
+            }
+        }
         match tokio::fs::read(&file_path).await {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1037 - 1061, Effect::LoadMediaPopup currently reads
and decodes the entire file into memory (tokio::fs::read +
image::load_from_memory), which can OOM on large files; before reading, call
tokio::fs::metadata(&file_path).await (or File::metadata) and compare against a
defined MAX_POPUP_IMAGE_BYTES constant, and if the file is larger send_log(&tx,
format!("Popup image too large: {} bytes", size)) and skip loading; only proceed
to tokio::fs::read and image::load_from_memory when the size check passes,
keeping existing error handling paths (send_log and
Event::Action(Action::MediaPopupReady)).
src/widget/message_list.rs (1)

91-114: Consider consolidating extract_hash_hex with the version in src/app.rs.

This function duplicates the logic from src/app.rs (lines 894-917). While module isolation can justify duplication, having two implementations risks divergence.

Consider either:

  1. Moving this to a shared utility module
  2. Re-exporting from app.rs

However, if the message list widget intentionally avoids depending on app.rs for decoupling, this duplication is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widget/message_list.rs` around lines 91 - 114, The extract_hash_hex
implementation in message_list.rs duplicates the logic in app.rs; refactor by
moving the shared logic into a single utility (e.g., a new function
extract_hash_hex in a shared module like utils or helpers) and update both
message_list.rs and app.rs to call that shared function (or alternatively
re-export the function from app.rs if you prefer not to add a new module);
ensure the signature (fn extract_hash_hex(att: &Value, field: &str) ->
Option<String>) and behavior are preserved and update the use/import paths in
both files so there is one canonical implementation.
src/screen/main_screen.rs (1)

150-173: Consider avoiding duplicate MessageListWidget construction.

The widget is constructed twice: once to compute image_positions() and once for rendering. While the widget itself is lightweight (holding references), the block.clone() and repeated setup could be avoided.

One option is to split image_positions into a standalone function that takes the same parameters but doesn't require constructing the widget:

// Could compute positions directly without building the widget twice
let image_rects = compute_image_positions(&app.messages, app.message_scroll, ...);

However, given the current architecture where image_positions encapsulates the layout logic within MessageListWidget, this duplication is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screen/main_screen.rs` around lines 150 - 173, Construct the
MessageListWidget once and reuse it: create a single let widget =
MessageListWidget::new(&app.messages,
app.message_scroll).block(block.clone()).my_pubkey(app.account.as_deref()).selected(selected).media_downloads(&app.media_downloads).inline_images(&app.inline_images);
then call let image_rects = widget.image_positions(area); and immediately pass
the same widget to frame.render_widget(widget, area) (remove the second
MessageListWidget::new). This removes the duplicate construction and avoids the
extra block.clone(); keep the subsequent loop that uses image_rects and
frame.render_stateful_widget(StatefulImage::default(), rect, proto) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/widget/message_list.rs`:
- Around line 217-224: The truncation check uses preview.len() (bytes) but
preview was built with content(parent).chars().take(30) (characters), causing
inconsistent "..." for multi-byte chars; update the condition in the label
construction to use a character count instead (e.g., check
content(parent).chars().count() > 30 or preview.chars().count() == 30) so the
logic around preview and the appended "..." is based on characters; adjust the
if that references preview.len() to use one of those char-based checks and keep
the existing author_name(parent), content(parent), and preview variables.

---

Duplicate comments:
In `@src/main.rs`:
- Around line 953-963: The ShowUserProfile branch returns the raw response
shape, but UI expects the same flattened shape produced by LoadProfile; update
the async block in Effect::ShowUserProfile so that after wn::exec(...) succeeds
you transform the returned value to match LoadProfile's shape by extracting
metadata fields (name, about, picture, nip05, lud16) from the response.metadata
and promoting them to top-level keys (providing None/empty when absent) before
constructing Action::UserProfileLoaded; keep the error branch as-is
(Action::UserProfileError) and send Event::Action(action) as before.

---

Nitpick comments:
In `@src/main.rs`:
- Around line 536-589: The code performs an N+1 query by calling
wn::exec(&["--account", &account, "users", "show", pubkey]) for each invite in
arr; instead, collect all welcomer_pubkey values first, issue a single batched
user lookup (or multiple chunked lookups) via the CLI if it supports a bulk
"users show" or "users list --ids" style command, cache the returned user
objects into a HashMap<pubkey, user_val>, and then iterate inv in arr to resolve
display_name from the cached user_val before writing into group["name"]; update
the logic around collecting pubkeys, calling wn::exec once (or few times), and
replacing per-invite wn::exec calls with HashMap lookups while reusing the
existing display_name extraction and group["name"] assignment.
- Around line 1037-1061: Effect::LoadMediaPopup currently reads and decodes the
entire file into memory (tokio::fs::read + image::load_from_memory), which can
OOM on large files; before reading, call tokio::fs::metadata(&file_path).await
(or File::metadata) and compare against a defined MAX_POPUP_IMAGE_BYTES
constant, and if the file is larger send_log(&tx, format!("Popup image too
large: {} bytes", size)) and skip loading; only proceed to tokio::fs::read and
image::load_from_memory when the size check passes, keeping existing error
handling paths (send_log and Event::Action(Action::MediaPopupReady)).

In `@src/screen/main_screen.rs`:
- Around line 150-173: Construct the MessageListWidget once and reuse it: create
a single let widget = MessageListWidget::new(&app.messages,
app.message_scroll).block(block.clone()).my_pubkey(app.account.as_deref()).selected(selected).media_downloads(&app.media_downloads).inline_images(&app.inline_images);
then call let image_rects = widget.image_positions(area); and immediately pass
the same widget to frame.render_widget(widget, area) (remove the second
MessageListWidget::new). This removes the duplicate construction and avoids the
extra block.clone(); keep the subsequent loop that uses image_rects and
frame.render_stateful_widget(StatefulImage::default(), rect, proto) unchanged.

In `@src/widget/message_list.rs`:
- Around line 91-114: The extract_hash_hex implementation in message_list.rs
duplicates the logic in app.rs; refactor by moving the shared logic into a
single utility (e.g., a new function extract_hash_hex in a shared module like
utils or helpers) and update both message_list.rs and app.rs to call that shared
function (or alternatively re-export the function from app.rs if you prefer not
to add a new module); ensure the signature (fn extract_hash_hex(att: &Value,
field: &str) -> Option<String>) and behavior are preserved and update the
use/import paths in both files so there is one canonical implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c924402-21af-49ae-96aa-75ba9f328680

📥 Commits

Reviewing files that changed from the base of the PR and between 031e8e7 and fd4b90b.

📒 Files selected for processing (7)
  • README.md
  • src/action.rs
  • src/app.rs
  • src/main.rs
  • src/screen/main_screen.rs
  • src/screen/profile.rs
  • src/widget/message_list.rs

Comment thread src/widget/message_list.rs
@jgmontoya
jgmontoya force-pushed the feat/profile-update-and-others branch from fd4b90b to 59b9b92 Compare March 8, 2026 19:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/screen/main_screen.rs (1)

24-29: ⚠️ Potential issue | 🟡 Minor

Reserve more than one row for the expanded main-screen hints.

The footer still allocates a single Length(1) row, but the new ChatList and Messages shortcut sets are much longer than one line. This paragraph is rendered unwrapped, so the trailing hints disappear on ordinary terminal widths.

Also applies to: 215-293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screen/main_screen.rs` around lines 24 - 29, The layout currently
reserves only a single row for the footer/hints (see the Layout::vertical(...)
assigned to variable vertical and the trailing Constraint::Length(1) entries),
which truncates the expanded ChatList/Messages shortcut hints; update the footer
allocation to reserve multiple rows (e.g., replace the single Length(1)
Constraint with a larger fixed Length like Length(3) or a dynamic
Constraint::Min(n)) so the hints can render without truncation; apply the same
change wherever the layout builds vertical with two trailing
Constraint::Length(1) rows (including the other occurrences noted) so all hint
areas have sufficient height.
src/widget/message_list.rs (1)

247-249: ⚠️ Potential issue | 🟡 Minor

Use one display-width indent for follow-up lines and inline images.

Attachment/reply lines are padded with prefix.len() + author_prefix.len(), while image_positions starts thumbnails at timestamp width only. That shifts inline images left of the attachment column even for ASCII names, and non-ASCII authors make it worse because len() counts bytes instead of terminal cells.

Suggested fix
-    let indent = prefix.len() + author_prefix.len();
+    let indent = format!("{prefix}{author_prefix}").width();
...
-            let ts_prefix = format!("[{ts}] ");
-            let ts_cols = ts_prefix.width() as u16;
-            let prefix_width = ts_prefix.width() + format!("{author}: ").width();
+            let prefix_width = format!("[{ts}] {author}: ").width();
...
-                    let img_x = inner.x + ts_cols;
-                    let img_w = inner.width.saturating_sub(ts_cols);
+                    let indent = prefix_width as u16;
+                    let img_x = inner.x + indent;
+                    let img_w = inner.width.saturating_sub(indent);

Also applies to: 482-516

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widget/message_list.rs` around lines 247 - 249, The code uses byte
lengths (prefix.len(), author_prefix.len()) to compute indent and image
positions which misaligns multi-byte/non-ASCII author names and inline images;
replace these .len() usages with display cell widths using a Unicode width
helper (e.g., unicode_width::UnicodeWidthStr::width) to compute prefix_width and
author_prefix_width and then set indent = prefix_width + author_prefix_width,
and likewise compute the timestamp/display widths used by image_positions so
thumbnails align with the attachment/reply column; update all occurrences
(including the other block at lines ~482-516) to use the same Unicode-aware
width calculations for consistent alignment.
♻️ Duplicate comments (3)
src/action.rs (1)

36-36: ⚠️ Potential issue | 🟠 Major

Carry the request key through these async success actions.

MessagesLoaded, ProfileImageFetched, and UserProfileLoaded still drop the originating group_id / url / pubkey. Once two loads overlap, the reducer cannot distinguish current data from stale responses, so older payloads can overwrite the active chat or profile.

Also applies to: 83-83, 106-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action.rs` at line 36, The async success enum variants MessagesLoaded,
ProfileImageFetched, and UserProfileLoaded are losing the originating request
key (group_id / url / pubkey); modify those Action enum variants to carry the
original request key alongside the payload (e.g., MessagesLoaded(group_id,
Vec<Value>), ProfileImageFetched(url, Vec<u8>), UserProfileLoaded(pubkey,
Profile)), update every place that constructs these variants in the async
handling paths to include the key, and update the reducer/handlers that match on
these variants to check the carried key against the currently active key before
applying state changes to avoid stale responses overwriting current data.
src/main.rs (2)

953-960: ⚠️ Potential issue | 🟠 Major

Normalize users show output before dispatching UserProfileLoaded.

src/screen/profile.rs reads top-level name, display_name, about, picture, nip05, lud16, and npub, but this branch forwards the raw users show payload. Metadata-only fields will render as (not set), and a missing top-level npub falls back to the logged-in account's key. Mirror the same flattening/fallback logic used in the self-profile loader.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 953 - 960, The branch handling
Effect::ShowUserProfile currently dispatches the raw wn::exec payload into
Action::UserProfileLoaded; instead, normalize/flatten that payload the same way
the self-profile loader does so src/screen/profile.rs can read top-level name,
display_name, about, picture, nip05, lud16, and npub with fallbacks. After
wn::exec returns Ok(val), extract/compute top-level fields (prefer direct
top-level values, then metadata subfields, and ensure npub falls back to the
requested account key) and build a normalized struct/object matching what the
self-profile loader produces, then dispatch
Action::UserProfileLoaded(normalized) (preserve the Err path to
Action::UserProfileError).

810-816: ⚠️ Potential issue | 🟠 Major

Still bound profile-image downloads before buffering them.

--fail fixes HTTP error handling, but this branch still accepts arbitrary body sizes and forwards them straight into ProfileImageFetched. A malicious picture URL can force a large allocation before any decode or validation runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 810 - 816, The code currently pipes the entire curl
output straight into Event::Action(Action::ProfileImageFetched(output.stdout)),
allowing arbitrarily large bodies; fix by enforcing a hard size limit before
sending: after the tokio::process::Command::new("curl") call and when matching
Ok(output) (the branch that currently checks output.status.success() &&
!output.stdout.is_empty()), check output.stdout.len() against a defined
MAX_PROFILE_IMAGE_BYTES constant (e.g., 1_000_000 bytes) and only send
ProfileImageFetched if length <= MAX_PROFILE_IMAGE_BYTES; otherwise log/drop the
image and return an error branch. Also consider adding curl-level protection
(e.g., --max-filesize) to the args list as an extra safeguard. Ensure references
to tokio::process::Command::new("curl"), the result match block, and
Event::Action(Action::ProfileImageFetched(...)) are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.rs`:
- Around line 1040-1048: The LoadMediaPopup handler (and similarly
LoadMediaImage) currently reads and decodes the entire file via
tokio::fs::read() and image::load_from_memory() without limits; change it to
first stat the file (tokio::fs::metadata or File::metadata) and reject files
whose size exceeds a defined MAX_MEDIA_BYTES before calling read, then after
decoding (image::load_from_memory or ImageReader) validate img.width() and
img.height() (or total pixels) against a defined MAX_DIMENSION / MAX_PIXELS and
reject oversized images (send an error Event or skip sending MediaPopupReady) to
avoid OOM or decompression bombs; add constants like MAX_MEDIA_BYTES and
MAX_PIXELS and reuse the same checks in LoadMediaImage.

In `@src/util.rs`:
- Around line 17-30: The code currently uses filter_map and casts to u8 which
silently drops non-numeric entries and wraps values >255; change the logic so
the byte array is validated and rejected if any element is missing, non-integer,
or out of 0..=255. Specifically, replace the filter_map pipeline over arr with
an iterator that attempts to convert every element (use as_u64 or equivalent),
returns None if any element is not present or >255, collects a Vec<u8> (e.g.,
via map(...).and_then/collect::<Option<Vec<u8>>>()), and only then constructs
the hex string from that Vec<u8> (preserving the existing hex formatting and the
None return for empty/malformed cases). Ensure you update references to arr, val
and hex accordingly so malformed JSON yields None rather than a coerced/wrapped
value.

---

Outside diff comments:
In `@src/screen/main_screen.rs`:
- Around line 24-29: The layout currently reserves only a single row for the
footer/hints (see the Layout::vertical(...) assigned to variable vertical and
the trailing Constraint::Length(1) entries), which truncates the expanded
ChatList/Messages shortcut hints; update the footer allocation to reserve
multiple rows (e.g., replace the single Length(1) Constraint with a larger fixed
Length like Length(3) or a dynamic Constraint::Min(n)) so the hints can render
without truncation; apply the same change wherever the layout builds vertical
with two trailing Constraint::Length(1) rows (including the other occurrences
noted) so all hint areas have sufficient height.

In `@src/widget/message_list.rs`:
- Around line 247-249: The code uses byte lengths (prefix.len(),
author_prefix.len()) to compute indent and image positions which misaligns
multi-byte/non-ASCII author names and inline images; replace these .len() usages
with display cell widths using a Unicode width helper (e.g.,
unicode_width::UnicodeWidthStr::width) to compute prefix_width and
author_prefix_width and then set indent = prefix_width + author_prefix_width,
and likewise compute the timestamp/display widths used by image_positions so
thumbnails align with the attachment/reply column; update all occurrences
(including the other block at lines ~482-516) to use the same Unicode-aware
width calculations for consistent alignment.

---

Duplicate comments:
In `@src/action.rs`:
- Line 36: The async success enum variants MessagesLoaded, ProfileImageFetched,
and UserProfileLoaded are losing the originating request key (group_id / url /
pubkey); modify those Action enum variants to carry the original request key
alongside the payload (e.g., MessagesLoaded(group_id, Vec<Value>),
ProfileImageFetched(url, Vec<u8>), UserProfileLoaded(pubkey, Profile)), update
every place that constructs these variants in the async handling paths to
include the key, and update the reducer/handlers that match on these variants to
check the carried key against the currently active key before applying state
changes to avoid stale responses overwriting current data.

In `@src/main.rs`:
- Around line 953-960: The branch handling Effect::ShowUserProfile currently
dispatches the raw wn::exec payload into Action::UserProfileLoaded; instead,
normalize/flatten that payload the same way the self-profile loader does so
src/screen/profile.rs can read top-level name, display_name, about, picture,
nip05, lud16, and npub with fallbacks. After wn::exec returns Ok(val),
extract/compute top-level fields (prefer direct top-level values, then metadata
subfields, and ensure npub falls back to the requested account key) and build a
normalized struct/object matching what the self-profile loader produces, then
dispatch Action::UserProfileLoaded(normalized) (preserve the Err path to
Action::UserProfileError).
- Around line 810-816: The code currently pipes the entire curl output straight
into Event::Action(Action::ProfileImageFetched(output.stdout)), allowing
arbitrarily large bodies; fix by enforcing a hard size limit before sending:
after the tokio::process::Command::new("curl") call and when matching Ok(output)
(the branch that currently checks output.status.success() &&
!output.stdout.is_empty()), check output.stdout.len() against a defined
MAX_PROFILE_IMAGE_BYTES constant (e.g., 1_000_000 bytes) and only send
ProfileImageFetched if length <= MAX_PROFILE_IMAGE_BYTES; otherwise log/drop the
image and return an error branch. Also consider adding curl-level protection
(e.g., --max-filesize) to the args list as an extra safeguard. Ensure references
to tokio::process::Command::new("curl"), the result match block, and
Event::Action(Action::ProfileImageFetched(...)) are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 892e3b8f-bc40-4d6f-abd4-b596776c3937

📥 Commits

Reviewing files that changed from the base of the PR and between fd4b90b and 59b9b92.

📒 Files selected for processing (8)
  • README.md
  • src/action.rs
  • src/app.rs
  • src/main.rs
  • src/screen/main_screen.rs
  • src/screen/profile.rs
  • src/util.rs
  • src/widget/message_list.rs

Comment thread src/main.rs
Comment thread src/util.rs Outdated
- Download, decrypt, and display inline images in chat messages
- Open full-size image popup with 'o' key on selected message
- Upload media via file browser popup ('U' key) with directory navigation
- Reply to messages ('R' key) with quoted context in composer
- Delete own messages ('d' key) with confirmation
- Message selection highlights with '>' marker and visual feedback
- Auto-download image attachments when messages load or arrive
- Distinct download state indicators (downloading/loading/failed with reason)
- Fail explicitly on unexpected media download responses instead of
  producing garbage file paths
- Fix README links and build instructions for whitenoise-rs rename
@jgmontoya
jgmontoya force-pushed the feat/profile-update-and-others branch from 59b9b92 to 14ab906 Compare March 8, 2026 20:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/main.rs (1)

953-963: ⚠️ Potential issue | 🟠 Major

Normalize ShowUserProfile data to match LoadProfile shape.

This forwards raw users show output without flattening metadata fields to the top level. The LoadProfile effect at lines 727-748 performs this normalization (merging metadata object fields to top level), but ShowUserProfile doesn't, causing inconsistent rendering in profile.rs where fields like name, picture, etc. will show "(not set)".

🔧 Proposed fix to normalize the profile data
         Effect::ShowUserProfile { account, pubkey } => {
             let tx = tx.clone();
             tokio::spawn(async move {
                 let action =
                     match wn::exec(&["--account", &account, "users", "show", &pubkey]).await {
-                        Ok(val) => Action::UserProfileLoaded(val),
+                        Ok(val) => {
+                            let mut profile = val.clone();
+                            if let Some(meta) = val.get("metadata").and_then(|m| m.as_object()) {
+                                for (k, v) in meta {
+                                    profile[k.clone()] = v.clone();
+                                }
+                            }
+                            if profile.get("npub").is_none() {
+                                if let Some(pk) = val.get("pubkey").and_then(|v| v.as_str()) {
+                                    profile["npub"] = serde_json::Value::String(pk.to_string());
+                                }
+                            }
+                            Action::UserProfileLoaded(profile)
+                        }
                         Err(e) => Action::UserProfileError(e.to_string()),
                     };
                 let _ = tx.send(Event::Action(action));
             });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 953 - 963, Effect::ShowUserProfile currently
forwards the raw wn::exec output into Action::UserProfileLoaded without
flattening the nested metadata object, causing fields like name/picture to be
missing compared to the LoadProfile path; update the ShowUserProfile handler
(the tokio::spawn block that calls wn::exec) to mirror the LoadProfile
normalization: parse the returned profile value, merge/hoist any fields from its
"metadata" object into the top-level profile map (overwriting or setting
top-level keys like "name", "picture", etc.), then send
Action::UserProfileLoaded with the normalized shape (and keep the same error
path to Action::UserProfileError on Err).
🧹 Nitpick comments (3)
src/main.rs (2)

536-591: Consider logging or limiting DM invite name resolution.

The sequential wn::exec calls for each DM invite work correctly but could be slow with many pending DM invites. This is likely acceptable for the invite listing use case, but consider adding a log entry or limiting resolution to the first N invites if users report sluggish invite loading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 536 - 591, The DM invite name resolution loop
performs sequential wn::exec calls for every invite which can be slow; update
the block that handles Ok(serde_json::Value::Array(mut arr)) (the loop over arr
that calls wn::exec and then returns Action::InvitesLoaded) to either (a) log a
debug/info message when many invites are being resolved and include a count, or
(b) cap resolution to the first N invites (e.g., resolve only for the first N
entries of arr) and leave others unchanged; ensure you reference wn::exec in the
log or capping logic and preserve returning Action::InvitesLoaded(arr) after
modification so behavior remains consistent.

1019-1035: Consider adding a file size check before reading media files.

LoadMediaImage reads the entire file into memory with tokio::fs::read() before sending bytes. While LoadMediaPopup benefits from image::load_from_memory() limits (512 MiB allocation), LoadMediaImage sends raw bytes without decoding. A large file could exhaust memory before the image crate's limits apply.

💡 Optional size guard
         Effect::LoadMediaImage {
             file_hash,
             file_path,
         } => {
             let tx = tx.clone();
             tokio::spawn(async move {
+                // Optional: check file size before reading
+                const MAX_MEDIA_BYTES: u64 = 50 * 1024 * 1024; // 50 MiB
+                if let Ok(meta) = tokio::fs::metadata(&file_path).await {
+                    if meta.len() > MAX_MEDIA_BYTES {
+                        send_log(&tx, format!("Media file too large: {} bytes", meta.len()));
+                        return;
+                    }
+                }
                 match tokio::fs::read(&file_path).await {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1019 - 1035, Effect::LoadMediaImage currently calls
tokio::fs::read(&file_path) and can load arbitrarily large files into memory;
before reading, check the file size with tokio::fs::metadata (or
tokio::fs::File::metadata) and enforce a max allowed size (e.g. same cap used
elsewhere) and handle violations by sending a log/error (use send_log(&tx, ...))
or an Event::Action error; only call tokio::fs::read when metadata.len() is
within the limit, and keep the existing success path that sends
Event::Action::MediaImageLoaded { file_hash, bytes } unchanged.
src/action.rs (1)

36-36: Consider carrying request identity in success actions to prevent race conditions.

MessagesLoaded, ProfileImageFetched (line 83), and UserProfileLoaded (line 106) drop the request context (group_id, url, pubkey respectively). If the user navigates while requests are in-flight, stale responses could update the wrong chat or profile. This is a known issue from a previous review.

For a "Chill" review, this can be deferred if current usage patterns make races unlikely (e.g., UI blocks navigation during loads), but worth addressing if you observe incorrect state updates.

💡 Example for MessagesLoaded
-    MessagesLoaded(Vec<Value>),
+    MessagesLoaded {
+        group_id: String,
+        messages: Vec<Value>,
+    },

Then in the reducer, verify group_id matches app.active_group_id before applying.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/action.rs` at line 36, The success actions drop request identity causing
stale responses to update the wrong state; update the enum variants
MessagesLoaded, ProfileImageFetched, and UserProfileLoaded to carry their
request identifiers (e.g., include group_id for MessagesLoaded, url for
ProfileImageFetched, and pubkey for UserProfileLoaded), ensure callers that
dispatch these actions populate those fields, and in the reducer (where these
actions are handled) verify the incoming identifier matches the current expected
identifier (e.g., compare MessagesLoaded.group_id to app.active_group_id or
analogous checks for url/pubkey) before applying the state update so in-flight
responses don’t overwrite the wrong chat/profile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main.rs`:
- Around line 953-963: Effect::ShowUserProfile currently forwards the raw
wn::exec output into Action::UserProfileLoaded without flattening the nested
metadata object, causing fields like name/picture to be missing compared to the
LoadProfile path; update the ShowUserProfile handler (the tokio::spawn block
that calls wn::exec) to mirror the LoadProfile normalization: parse the returned
profile value, merge/hoist any fields from its "metadata" object into the
top-level profile map (overwriting or setting top-level keys like "name",
"picture", etc.), then send Action::UserProfileLoaded with the normalized shape
(and keep the same error path to Action::UserProfileError on Err).

---

Nitpick comments:
In `@src/action.rs`:
- Line 36: The success actions drop request identity causing stale responses to
update the wrong state; update the enum variants MessagesLoaded,
ProfileImageFetched, and UserProfileLoaded to carry their request identifiers
(e.g., include group_id for MessagesLoaded, url for ProfileImageFetched, and
pubkey for UserProfileLoaded), ensure callers that dispatch these actions
populate those fields, and in the reducer (where these actions are handled)
verify the incoming identifier matches the current expected identifier (e.g.,
compare MessagesLoaded.group_id to app.active_group_id or analogous checks for
url/pubkey) before applying the state update so in-flight responses don’t
overwrite the wrong chat/profile.

In `@src/main.rs`:
- Around line 536-591: The DM invite name resolution loop performs sequential
wn::exec calls for every invite which can be slow; update the block that handles
Ok(serde_json::Value::Array(mut arr)) (the loop over arr that calls wn::exec and
then returns Action::InvitesLoaded) to either (a) log a debug/info message when
many invites are being resolved and include a count, or (b) cap resolution to
the first N invites (e.g., resolve only for the first N entries of arr) and
leave others unchanged; ensure you reference wn::exec in the log or capping
logic and preserve returning Action::InvitesLoaded(arr) after modification so
behavior remains consistent.
- Around line 1019-1035: Effect::LoadMediaImage currently calls
tokio::fs::read(&file_path) and can load arbitrarily large files into memory;
before reading, check the file size with tokio::fs::metadata (or
tokio::fs::File::metadata) and enforce a max allowed size (e.g. same cap used
elsewhere) and handle violations by sending a log/error (use send_log(&tx, ...))
or an Event::Action error; only call tokio::fs::read when metadata.len() is
within the limit, and keep the existing success path that sends
Event::Action::MediaImageLoaded { file_hash, bytes } unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 645a7327-f072-4c33-a0b8-3682bafc0ad6

📥 Commits

Reviewing files that changed from the base of the PR and between 59b9b92 and 14ab906.

📒 Files selected for processing (9)
  • README.md
  • src/action.rs
  • src/app.rs
  • src/main.rs
  • src/screen/main_screen.rs
  • src/screen/profile.rs
  • src/util.rs
  • src/widget/chat_list.rs
  • src/widget/message_list.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/util.rs
  • README.md

@jgmontoya
jgmontoya merged commit 284dc9f into master Mar 8, 2026
5 checks passed
@jgmontoya
jgmontoya deleted the feat/profile-update-and-others branch March 8, 2026 21:25
@coderabbitai coderabbitai Bot mentioned this pull request Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant