Skip to content

Chat UX polish: italic, clickable links, thumbnails, streaming autoscroll - #8

Merged
aciderix merged 1 commit into
mainfrom
claude/chat-ux-polish
Apr 21, 2026
Merged

aciderix merged 1 commit into
mainfrom
claude/chat-ux-polish

Conversation

@aciderix

@aciderix aciderix commented Apr 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Small UX corrections in the conversation view:

  • Italic — _foo_ / *foo* now actually render italic (the prior SpanStyle was a no-op that only reset weight/decoration).
  • Clickable links — both markdown links [label](https://…) and bare http(s)://… URLs in model output are now tappable and open in the system browser via ACTION_VIEW. Implemented with ClickableText + pushStringAnnotation(URL_TAG, …); falls back to plain Text when a paragraph has no URLs so selection/long-press still works.
  • Image thumbnails — when the user attaches an image, a 120dp thumbnail now renders above the text inside their message bubble. Attachments are copied into <filesDir>/attachments/att-<nano>.<ext> on pick so (a) the thumbnail survives ChatStore reload, and (b) we don't depend on the original content:// URI permission. GeminiMessage gained attachmentPaths: List<String>, persisted by ChatStore. The 📎 image (png, 128KB)\n prefix RestGeminiCore adds to the bubble is hidden when a thumbnail is present (the thumbnail already conveys it).
  • Streaming autoscroll — LaunchedEffect now also keys on the tail message's text, not only messages.size, so the view stays pinned to the bottom while a model response streams in. The "only scroll if the user is near the bottom" rule is preserved.

Files touched

  • app/.../ui/chat/MarkdownText.kt — italic fix, URL annotations (markdown + bare), ProseText wrapper, openUrl intent helper.
  • app/.../ui/chat/ChatScreen.kt — tailText key on autoscroll effect, AttachmentThumbnails composable + decodeThumbnail (sampled BitmapFactory, ~320 px target).
  • app/.../ui/chat/ChatViewModel.kt — persistAttachment copies picked bytes into cache; PendingAttachment / Attachment carry localPath.
  • core-bridge/.../RestGeminiCore.kt — propagate localPath onto the user's GeminiMessage.attachmentPaths.
  • core-bridge/.../storage/ChatStore.kt — persist & restore attachmentPaths.
  • domain/.../GeminiCore.kt — new attachmentPaths field on GeminiMessage.

Test plan

  • Send a message containing _italique_, *italique*, [Google](https://google.com), and a bare https://example.com/foo — verify italic renders, both links open the browser.
  • Attach a PNG/JPEG, send — thumbnail shows in the user bubble, no redundant 📎 image … prefix line.
  • Scroll the conversation up while a long model response is streaming — view should not yank to bottom. Scroll back down, the streaming should keep pinning.
  • Save a chat with attachments, reload it from "Open…" — thumbnails still appear.
  • CI: debug + release APKs build.

https://claude.ai/code/session_015dVKN2jG34HKP9SeBpSvS5

Summary by Sourcery

Polish the chat conversation UI with richer text rendering, clickable links, image thumbnails for user messages, and smoother autoscroll during streaming responses.

New Features:

  • Render italic markdown in chat messages using proper text styling.
  • Make markdown and bare HTTP(S) links in model messages tappable and openable via the system browser.
  • Display thumbnails for image attachments in user message bubbles based on locally persisted files.

Enhancements:

  • Improve streaming autoscroll so the list stays pinned to the latest message content while respecting the user's scroll position.
  • Hide redundant attachment descriptor text in bubbles when an image thumbnail is present.
  • Persist and restore attachment file paths through the core bridge and chat store so thumbnails survive reloads.

Summary by CodeRabbit

  • New Features

    • Image attachment thumbnails now display above messages in the chat.
    • URLs in messages are now clickable and automatically launch in your browser.
  • Improvements

    • Enhanced auto-scroll behavior for streamed messages, keeping the chat pinned to the bottom as new text arrives.

…roll

- MarkdownText: italic now actually renders italic (was a no-op style);
  markdown links [label](url) and bare http(s)://… URLs become clickable
  via ClickableText + ACTION_VIEW intent.
- ChatScreen: sent image attachments render a 120dp thumbnail above the
  text in the user bubble; autoscroll now also keys on the tail message
  text so the view stays pinned to the bottom during streaming.
- Attachments are copied into <filesDir>/attachments and their paths are
  stored on GeminiMessage.attachmentPaths (and persisted via ChatStore)
  so reloads still show thumbnails.
@sourcery-ai

sourcery-ai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Polishes the chat UX by fixing italic markdown rendering, adding clickable links in model output, showing persistent image thumbnails for user messages, and improving streaming autoscroll while threading attachment file paths through view model, domain, and storage layers.

Sequence diagram for image attachment lifecycle and thumbnails

sequenceDiagram
  actor User
  participant ChatScreen
  participant ChatViewModel
  participant RestGeminiCore
  participant ChatStore
  participant FileSystem

  User->>ChatScreen: Selects image attachment
  ChatScreen->>ChatViewModel: onAttachmentPicked(uri)
  ChatViewModel->>ChatViewModel: loadPendingAttachment(uri)
  ChatViewModel->>ChatViewModel: persistAttachment(context, id, bytes, mime)
  ChatViewModel->>FileSystem: Write bytes to filesDir/attachments/id.ext
  FileSystem-->>ChatViewModel: absolutePath
  ChatViewModel-->>ChatScreen: PendingAttachment(localPath)

  User->>ChatScreen: Presses send
  ChatScreen->>ChatViewModel: sendMessage(text, pendingAttachments)
  ChatViewModel->>ChatViewModel: Map PendingAttachment to Attachment(bytes, mimeType, localPath)
  ChatViewModel->>RestGeminiCore: startChatTurn(text, attachments)

  RestGeminiCore->>RestGeminiCore: Build GeminiMessage
  RestGeminiCore->>RestGeminiCore: attachmentPaths = attachments.mapNotNull(localPath)
  RestGeminiCore->>ChatStore: saveMessage(GeminiMessage(attachmentPaths))
  ChatStore->>FileSystem: Write JSON with attachmentPaths

  ChatStore-->>RestGeminiCore: Persisted
  RestGeminiCore-->>ChatViewModel: Streamed response
  ChatViewModel-->>ChatScreen: Updated message list

  ChatStore->>ChatStore: loadMessage(json)
  ChatStore->>FileSystem: Read JSON with attachmentPaths
  ChatStore-->>RestGeminiCore: GeminiMessage(attachmentPaths)
  RestGeminiCore-->>ChatViewModel: Restored conversation
  ChatViewModel-->>ChatScreen: Messages with attachmentPaths

  ChatScreen->>ChatScreen: MessageBubble(message)
  ChatScreen->>ChatScreen: AttachmentThumbnails(message.attachmentPaths)
  ChatScreen->>FileSystem: decodeThumbnail(path) via BitmapFactory
  FileSystem-->>ChatScreen: ImageBitmap
  ChatScreen-->>User: Bubble shows 120dp thumbnails above text
Loading

Sequence diagram for clickable links in model output

sequenceDiagram
  actor User
  participant ChatScreen
  participant MarkdownText
  participant ProseText
  participant AndroidContext
  participant SystemBrowser

  ChatScreen->>MarkdownText: Render model message text
  MarkdownText->>MarkdownText: annotateInline(line, baseColor)
  MarkdownText->>MarkdownText: Detect [label](url) and bare http(s) URLs
  MarkdownText->>MarkdownText: pushStringAnnotation(URL_TAG, url)
  MarkdownText->>ProseText: ProseText(AnnotatedString)
  ProseText->>ProseText: hasLinks = getStringAnnotations(URL_TAG).isNotEmpty()
  ProseText->>User: Display ClickableText with underlined links

  User->>ProseText: Taps on link
  ProseText->>ProseText: getStringAnnotations(URL_TAG, offset)
  ProseText->>AndroidContext: openUrl(context, url)
  AndroidContext->>AndroidContext: Normalise URL and build ACTION_VIEW Intent
  AndroidContext->>SystemBrowser: startActivity(intent)
  SystemBrowser-->>User: Open link in system browser
Loading

Updated class diagram for GeminiMessage attachments and thumbnails

classDiagram

  class GeminiMessage {
    String id
    String text
    Boolean isUser
    Long timestamp
    MessageRole role
    ToolCall toolCall
    ToolCallResult toolResult
    List~String~ attachmentPaths
  }

  class Attachment {
    ByteArray bytes
    String mimeType
    String localPath
    equals(other)
    hashCode()
  }

  class PendingAttachment {
    String id
    ByteArray bytes
    String mimeType
    String displayName
    Int sizeBytes
    String localPath
    equals(other)
    hashCode()
  }

  class ChatViewModel {
    RestGeminiCore core
    sendMessage(text, attachments)
    loadPendingAttachment(context, uri)
    persistAttachment(context, id, bytes, mime)
  }

  class RestGeminiCore {
    startChatTurn(text, attachments)
    buildUserGeminiMessage(text, attachments)
  }

  class ChatStore {
    saveMessage(GeminiMessage)
    loadMessage(jsonObject)
  }

  ChatViewModel --> PendingAttachment : creates
  ChatViewModel --> Attachment : maps_to_payload
  Attachment --> GeminiMessage : localPath_copied_to_attachmentPaths
  RestGeminiCore --> GeminiMessage : constructs
  ChatStore --> GeminiMessage : persists_and_restores
Loading

File-Level Changes

Change Details Files
Make markdown prose support italic text and tappable links for both markdown and bare URLs.
  • Replace direct Text usages in prose rendering with a ProseText wrapper that conditionally uses ClickableText when URL annotations are present so long-press/selection still work for non-link text.
  • Implement URL tagging in annotateInline for markdown links and bare http/https URLs, including a helper to detect URL boundaries and strip trailing punctuation.
  • Add an openUrl helper that normalizes missing schemes, launches ACTION_VIEW intents for URLs, and shows a Toast on failure.
  • Fix italic handling by applying SpanStyle(fontStyle = FontStyle.Italic) to foo/foo segments instead of the previous no-op style.
app/src/main/kotlin/com/gemini/app/ui/chat/MarkdownText.kt
Improve chat screen behavior with streaming autoscroll and inline image thumbnails in user bubbles.
  • Key the LaunchedEffect that drives autoscroll on the tail message text in addition to messages.size, thinking, and error so streaming responses keep the list pinned when the user is near the bottom.
  • Render user message image attachments as a row of 120dp thumbnails above the text using a new AttachmentThumbnails composable and a sampled decodeThumbnail helper to limit bitmap size.
  • Strip the RestGeminiCore "📎 image …" prefix line from the visible text when thumbnails are present, only rendering remaining text if non-blank.
app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt
Persist picked attachments to app-owned storage and propagate their local paths through the attachment pipeline.
  • Extend PendingAttachment and Attachment to carry an optional localPath field and include it when constructing the send payload.
  • Introduce persistAttachment in ChatViewModel to write picked image bytes into /attachments with a stable att-. naming scheme based on MIME type and return the absolute path.
  • Populate PendingAttachment.localPath at pick time so downstream layers can surface thumbnails even after ChatStore reloads or URI permission loss.
app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
Store and restore attachment file paths on Gemini messages so thumbnails survive app restarts.
  • Add an attachmentPaths: List field to GeminiMessage with a default empty list and documentation clarifying it is for local thumbnail rendering.
  • Update ChatStore serialization to optionally write a JSON array of attachmentPaths when non-empty, and deserialization to read it back into the GeminiMessage.
  • Include attachmentPaths derived from Attachment.localPath when creating user GeminiMessage instances in RestGeminiCore.
core-bridge/src/main/kotlin/com/gemini/bridge/storage/ChatStore.kt
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
domain/src/main/kotlin/com/gemini/domain/GeminiCore.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The pull request adds image attachment thumbnail rendering in the chat UI with local file persistence, and introduces interactive URL link support in markdown messages. Changes span the UI layer (ChatScreen), view model (ChatViewModel), message serialization (ChatStore, RestGeminiCore), domain model (GeminiMessage), and markdown rendering (MarkdownText).

Changes

Cohort / File(s) Summary
Attachment Thumbnail Rendering
app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt
Added attachment thumbnail display above user messages with image bitmap decoding and caching; updated auto-scroll effect to include streamed text; conditional text rendering for attachment-prefixed messages.
Attachment Persistence
app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt, core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
Extended PendingAttachment with localPath field; added persistAttachment helper to save images to app storage with MIME-type-based extensions; updated Attachment data class to include optional localPath and modified equality logic; set message attachmentPaths from stored file paths.
Message Storage Layer
core-bridge/src/main/kotlin/com/gemini/bridge/storage/ChatStore.kt, domain/src/main/kotlin/com/gemini/domain/GeminiCore.kt
Added attachmentPaths: List<String> field to GeminiMessage; implemented conditional JSON serialization/deserialization for attachmentPaths during message persistence.
Markdown Link Support
app/src/main/kotlin/com/gemini/app/ui/chat/MarkdownText.kt
Introduced ProseText composable for clickable text rendering; added bare URL and markdown link detection with ClickableText; normalized URLs with automatic https:// prefix; extended inline annotation parsing for emphasis and link styling; implemented openUrl intent launching with fallback toast.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ChatScreen
    participant ChatViewModel
    participant StorageAPI as App Storage
    participant RestGeminiCore
    participant ChatStore as Message Store

    User->>ChatScreen: Select image attachment
    ChatScreen->>ChatViewModel: readAttachment(uri, type)
    ChatViewModel->>ChatViewModel: Generate attachment ID
    ChatViewModel->>StorageAPI: persistAttachment(bytes, id, mimeType)
    StorageAPI-->>ChatViewModel: localPath (absolute file path)
    ChatViewModel->>ChatViewModel: Create PendingAttachment(id, localPath)
    User->>ChatViewModel: sendMessage(text, attachments)
    ChatViewModel->>ChatViewModel: Create Attachment(bytes, mimeType, localPath)
    ChatViewModel->>RestGeminiCore: sendMessage(message, attachments)
    RestGeminiCore->>RestGeminiCore: Map localPaths to attachmentPaths
    RestGeminiCore->>RestGeminiCore: Create GeminiMessage(attachmentPaths)
    RestGeminiCore->>ChatStore: Store message
    ChatStore->>ChatStore: Serialize attachmentPaths to JSON
    RestGeminiCore-->>ChatScreen: GeminiMessage with attachmentPaths
    ChatScreen->>ChatScreen: decodeThumbnail(path) for each path
    ChatScreen->>ChatScreen: Render AttachmentThumbnails
    ChatScreen-->>User: Display message with thumbnails
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Attachments now dance with their thumbnail pride,
Links click and sparkle in markdown's tide,
Bitmaps sampled, files persist with care,
Images blooming in the chat's bright air! 📎✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes all four main changes: italic text, clickable links, image thumbnails, and streaming autoscroll behavior—all of which are reflected in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/chat-ux-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sourcery-ai sourcery-ai 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.

Hey - I've found 3 issues, and left some high level feedback:

  • Decoding thumbnails synchronously in decodeThumbnail (called from AttachmentThumbnails) does file IO and bitmap work on the main thread; consider moving this to a background/produceState-style loader or using an image-loading library to avoid jank when many attachments are present.
  • The hardcoded MaterialThemeLink color in annotateInline bypasses the current theme’s color scheme; you may want to derive the link color from MaterialTheme.colorScheme (e.g., primary) and thread it through instead of keeping a fixed constant to improve theming and contrast behavior.
  • The comment on persistAttachment says the image is copied into cache, but it currently uses context.filesDir/attachments; either switch to cacheDir or adjust the comment to reflect that these thumbnails are persisted in app files, not cache.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Decoding thumbnails synchronously in `decodeThumbnail` (called from `AttachmentThumbnails`) does file IO and bitmap work on the main thread; consider moving this to a background/`produceState`-style loader or using an image-loading library to avoid jank when many attachments are present.
- The hardcoded `MaterialThemeLink` color in `annotateInline` bypasses the current theme’s color scheme; you may want to derive the link color from `MaterialTheme.colorScheme` (e.g., `primary`) and thread it through instead of keeping a fixed constant to improve theming and contrast behavior.
- The comment on `persistAttachment` says the image is copied into cache, but it currently uses `context.filesDir/attachments`; either switch to `cacheDir` or adjust the comment to reflect that these thumbnails are persisted in app files, not cache.

## Individual Comments

### Comment 1
<location path="app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt" line_range="1090-1099" />
<code_context>
+    Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
</code_context>
<issue_to_address>
**issue (performance):** Bitmap decoding is happening synchronously on the main thread, which can cause jank for large or multiple images.

`decodeThumbnail(path)` is run inside `remember { ... }`, so both the bounds check and `BitmapFactory.decodeFile` execute on the main thread during composition. Decoding several large (20MB+) images this way can stall the UI. Move decoding to a background dispatcher (e.g. `produceState` / `LaunchedEffect` with `withContext(Dispatchers.IO)`) or use an image-loading library that manages threading and caching for you.
</issue_to_address>

### Comment 2
<location path="app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt" line_range="269-278" />
<code_context>
+    // Copy the picked image into app-owned cache so the chat bubble can show a
+    // thumbnail without holding an Android content:// permission that may be
+    // revoked, and so reloads from ChatStore can still find the file.
+    private fun persistAttachment(
+        context: Context,
+        id: String,
+        bytes: ByteArray,
+        mime: String
+    ): String? = runCatching {
+        val ext = when {
+            mime.contains("png") -> "png"
+            mime.contains("webp") -> "webp"
+            mime.contains("gif") -> "gif"
+            mime.contains("heic") -> "heic"
+            mime.contains("heif") -> "heif"
+            else -> "jpg"
+        }
+        val dir = java.io.File(context.filesDir, "attachments").also { it.mkdirs() }
+        val file = java.io.File(dir, "$id.$ext")
+        file.writeBytes(bytes)
+        file.absolutePath
+    }.getOrNull()
</code_context>
<issue_to_address>
**issue (bug_risk):** Persisted attachment files under `filesDir/attachments` are never cleaned up and may accumulate indefinitely.

Because these files are never deleted, internal storage can grow without bound, particularly for users who send many large images. Consider using `cacheDir` so the system can reclaim space, or implement an explicit cleanup strategy (e.g., tie deletion to message removal, cap the directory size, or periodically delete old attachments).
</issue_to_address>

### Comment 3
<location path="app/src/main/kotlin/com/gemini/app/ui/chat/MarkdownText.kt" line_range="328-330" />
<code_context>
+    )
+}
+
+private fun openUrl(context: Context, url: String) {
+    runCatching {
+        val normalised = if (url.startsWith("http://") || url.startsWith("https://")) url
+            else "https://$url"
+        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(normalised))
</code_context>
<issue_to_address>
**issue (bug_risk):** For non-http(s) links (mailto:, tel:, custom app schemes), the normalization prepends `https://`, which breaks those URIs.

The normalization currently assumes anything not starting with `http://` or `https://` is a bare host and prepends `https://`, so `mailto:foo@bar.com` becomes `https://mailto:foo@bar.com`. Instead, distinguish between URLs with and without a scheme (e.g., `Uri.parse(url).scheme == null` or a host-style regex) and only prepend `https://` when there is no scheme, leaving `mailto:`, `tel:`, and custom schemes unchanged.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1090 to +1099
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
paths.forEach { path ->
val bitmap = remember(path) { decodeThumbnail(path) }
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(120.dp)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (performance): Bitmap decoding is happening synchronously on the main thread, which can cause jank for large or multiple images.

decodeThumbnail(path) is run inside remember { ... }, so both the bounds check and BitmapFactory.decodeFile execute on the main thread during composition. Decoding several large (20MB+) images this way can stall the UI. Move decoding to a background dispatcher (e.g. produceState / LaunchedEffect with withContext(Dispatchers.IO)) or use an image-loading library that manages threading and caching for you.

Comment on lines +269 to +278
private fun persistAttachment(
context: Context,
id: String,
bytes: ByteArray,
mime: String
): String? = runCatching {
val ext = when {
mime.contains("png") -> "png"
mime.contains("webp") -> "webp"
mime.contains("gif") -> "gif"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Persisted attachment files under filesDir/attachments are never cleaned up and may accumulate indefinitely.

Because these files are never deleted, internal storage can grow without bound, particularly for users who send many large images. Consider using cacheDir so the system can reclaim space, or implement an explicit cleanup strategy (e.g., tie deletion to message removal, cap the directory size, or periodically delete old attachments).

Comment on lines +328 to +330
private fun openUrl(context: Context, url: String) {
runCatching {
val normalised = if (url.startsWith("http://") || url.startsWith("https://")) url

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): For non-http(s) links (mailto:, tel:, custom app schemes), the normalization prepends https://, which breaks those URIs.

The normalization currently assumes anything not starting with http:// or https:// is a bare host and prepends https://, so mailto:foo@bar.com becomes https://mailto:foo@bar.com. Instead, distinguish between URLs with and without a scheme (e.g., Uri.parse(url).scheme == null or a host-style regex) and only prepend https:// when there is no scheme, leaving mailto:, tel:, and custom schemes unchanged.

@aciderix
aciderix merged commit b555ed4 into main Apr 21, 2026
3 of 4 checks passed
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.

2 participants