Skip to content

feat(mediaviewer): show the position, add forward and draw actions to the media viewer - #6823

Open
ToteMeiSter wants to merge 104 commits into
nextcloud:masterfrom
ToteMeiSter:feat/media-viewer-actions
Open

ToteMeiSter wants to merge 104 commits into
nextcloud:masterfrom
ToteMeiSter:feat/media-viewer-actions

Conversation

@ToteMeiSter

Copy link
Copy Markdown
Contributor

Closes #6822
Fixes #6806

Depends on #6820: "Draw" opens its attachment preview with the brush. The branch is stacked on #6820, so the diff against master also shows #6818–#6820; the change of this PR is the last commit.

🖼️ Screenshots

🏚️ Before 🏡 After
Media viewer with sender and date, overflow menu with Share and Save "N of M" counter, Forward and Draw buttons, overflow menu with all actions

Screenshots from the Huawei DEL-LX9 will follow in a comment.

Feature

Media viewer:

  • a counter "N of M" in the top bar. The newest item is "M of M". While older media can still be loaded, it shows "N of M+";
  • overflow menu: "Save to gallery", "Show all media", "Show in chat", "Reply", "Share", "Delete";
  • buttons "Forward" and "Draw" ("Draw": photos only, not GIF or SVG);
  • video: all actions except "Draw".

Visibility rules are not copied. The viewer uses the same buildMessageActionsState as the message menu, with the message and the conversation from the local database. When the item is not in the database (loaded from the server only), "Reply", "Delete", "Forward" and "Draw" are hidden. The rules are re-evaluated when the online state changes.

The chat runs the actions with its own handlers. The viewer starts ChatActivity with CLEAR_TOP | SINGLE_TOP and the action plus the message id. The chat checks the permissions again before it runs the action. The request survives a configuration change, but it does not run again after process death.

  • "Reply" and "Delete": the same code as the message menu, with the same delete confirmation.
  • "Forward": the existing "Forward to" picker, a confirmation, then ShareOperationWorker.shareFile — the share step that ends an upload. The file is shared into the target conversation again, without a new upload. The caption is sent as metaData. A server error is now shown to the user; before, the error was silent (this also affects "File from cloud").
  • "Draw": the attachment preview opens with the brush on a copy of the file. The original message and the cached file are not changed. The cache path must be inside shared_attachments.

Also fixed (#6806): "Save" from the viewer looked for the file in the cache root, but the viewer stores files in folders per file id, so an empty file was created. The dialog now resolves the source by file id.

Safe passing of the file path when forwarding

This is part of the design of the new "Forward" action, not a fix of an existing vulnerability: there is no file forwarding in master.

ConversationsListActivity is exported, so any app can start it with any extras. The file path is therefore not put into the intent. The chat reads the path from the message in the database, stores it in process memory (PendingFileForward) and passes only a random one-time key. A forged or unknown key sends nothing. This way another app cannot make Talk share a private file.

How to check on a device

  1. Open a chat with several photos and a video. Tap a photo: the counter shows "N of M"; the newest is "M of M".
  2. Swipe to older media: "+" disappears when nothing more can be loaded.
  3. Overflow menu: check each entry. "Show in chat" scrolls to the message; "Delete" asks for confirmation.
  4. "Forward": pick a conversation, confirm. The file appears there without a new upload.
  5. "Draw" on a photo: the preview opens with the brush. Send: the drawing goes to the same chat, the original stays.
  6. Open the viewer from Shared items: "Show all media" closes the viewer.
  7. Offline: "Reply", "Delete", "Forward" are hidden.

Tested by the author on a Huawei DEL-LX9 with a fork build (develop) that has this feature together with other fork changes. Unit tests: MediaViewerActionsTest, MediaViewerChatRequestTest, MessageActionsStateTest.

🚧 TODO

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

The change was written with the help of Claude Code (Anthropic). The author reviewed the code, built it and ran the unit tests, and checked the behavior on a device with the fork build.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eff612d1-8594-490e-b6ad-99856be49ea6
📥 Commits

Reviewing files that changed from the base of the PR and between ce420ca and 8c902c5.

📒 Files selected for processing (1)
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentSheet.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentSheet.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request adds an attachment sheet for recent media, in-app CameraX photo capture, and crop and drawing tools in the attachment preview. It adds voice and video recording controls and CameraX video recording to chat, including recording restoration across activity recreation. The media viewer gains item counters, chat actions, navigation, and file forwarding. Android media-permission handling, image orientation details, and save-file resolution also change.

Fixed issue severity: Medium

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 8c902

The "Show in chat" navigation gap is fixed. No outstanding merge-blocking risk was found in the reviewed change.

Security Architecture Review

Security architecture risk: 🔵 Low · up to ce420

The reviewed flows retain account and conversation binding, permission checks, and forwarding confirmation. No introduced security defect was established. Recovery behavior and coverage of the wider stacked changes remain incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — Each forwarding job carries one account id, one remote file path, and one target conversation token. Its authenticated sharing authority comes from that account's stored credentials; the new caller does not supply separate credentials or a new service identity.

Trust Boundaries and Controls

  • observed — ChatActivity and MediaViewerActivity remain non-exported activities. The receiving chat reloads the requested message through an account- and room-bound lookup, rechecks action eligibility, and rejects drawing paths outside the canonical shared-attachments directory.
  • observed — The exported conversation picker checks target chat permission and read-only state before showing forwarding confirmation. It rejects unknown handles and handles belonging to another account. Confirmation captures the chosen conversation and atomically removes the handle before enqueueing the share.

Resilience and Maintainability Implications

  • observed — Normal media-request execution removes the action from the intent and clears the pending request before asynchronous loading. Restoration uses only the saved pending request. Forwarding metadata remains process-local, so an old forwarding handle cannot recover its path after process death.

Hardening Proposals

  • proposed — Consider explicit cleanup of abandoned edit outputs and pending forwarding entries when their flow is definitively dismissed, while preserving files needed by queued uploads and state needed for configuration changes.
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#6822] The viewer adds the counter, action visibility, chat-action requests, forwarding, and drawing support. The change also includes viewer-action tests. [#6806] The save dialog resolves the cached… Remove the CameraX dependencies from this PR. Keep the viewer actions and tests, the drawing support required by Draw, and the file-ID save fix.
Out of Scope Changes check ⚠️ Warning The whole PR also adds in-app photo capture, video-message recording, and a replacement attachment sheet with a recent-media picker. These features do not implement #6822 or #6806. The latest permissi… Move in-app capture, video-message recording, the replacement attachment sheet, the recent-media picker, and their supporting changes to separate PRs. Retain the media-viewer work, drawing support required by #6822, and the file-ID save fix…
Docstring Coverage ⚠️ Warning Docstring coverage is 36.39% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 382 functions across 54 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the media viewer’s counter and its Forward and Draw actions.
Description check ✅ Passed The description covers the feature, behavior, testing, and required template sections. The milestone is not set, and the TODO notes pending maintainer discussion.
Full details: Linked Issues check

Explanation

[#6822] The viewer adds the counter, action visibility, chat-action requests, forwarding, and drawing support. The change also includes viewer-action tests. [#6806] The save dialog resolves the cached source by file ID. However, the whole PR adds CameraX dependencies in app/build.gradle.kts, contrary to #6822’s requirement for no new dependencies.

Full details: Out of Scope Changes check

Explanation

The whole PR also adds in-app photo capture, video-message recording, and a replacement attachment sheet with a recent-media picker. These features do not implement #6822 or #6806. The latest permission-refresh change in AttachmentSheet.kt supports that attachment-sheet feature and is also outside these issues.

Resolution

Move in-app capture, video-message recording, the replacement attachment sheet, the recent-media picker, and their supporting changes to separate PRs. Retain the media-viewer work, drawing support required by #6822, and the file-ID save fix for #6806.

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@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: 3

🧹 Nitpick comments (1)
app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt (1)

102-112: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Handle the case where the user removes or replaces the file while a drawing save is in progress.

saveDrawing captures description.uri. It then calls finishEdit after the IO work completes. If oldUri is no longer in files, replaceFile returns early. discardIfOwnEdit(oldUri) still runs, and the new edited output is never referenced. The new output file stays in the cache as an orphan. In this case, delete newUri's file instead.

Proposed fix
     private fun finishEdit(oldUri: String, newUri: Uri?) {
         if (newUri == null) {
             editFailed = true
-        } else {
+        } else if (oldUri in files) {
             replaceFile(oldUri, newUri.toString())
             discardIfOwnEdit(oldUri)
+        } else {
+            discardIfOwnEdit(newUri.toString())
         }
         isEditing = false
     }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e176f2d9-c3fa-4ab6-a85a-d9a593922193
📥 Commits

Reviewing files that changed from the base of the PR and between f3c9097 and 16e3a25.

📒 Files selected for processing (108)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/AttachmentEditLogic.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/AttachmentEditing.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/AttachmentToolBar.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/AttachmentToolBarPreviews.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/CameraCaptureActions.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/CaptionInputBar.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/DetailVariants.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/DrawingEditor.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/DrawingSession.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewScreen.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileDescriber.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/FileThumbnailImage.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/ImageDetailLogic.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/LargePreview.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/ThumbnailStrip.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/ThumbnailStripPreviews.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentAction.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentActionResources.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentSheet.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/CameraTile.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/MediaAccess.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/MediaSelection.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/RecentMedia.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/RecentMediaLoader.kt
  • app/src/main/java/com/nextcloud/talk/camera/CaptureActivity.kt
  • app/src/main/java/com/nextcloud/talk/camera/CaptureOrientation.kt
  • app/src/main/java/com/nextcloud/talk/camera/PhotoCamera.kt
  • app/src/main/java/com/nextcloud/talk/camera/PhotoCaptureLogic.kt
  • app/src/main/java/com/nextcloud/talk/camera/PhotoCaptureScreen.kt
  • app/src/main/java/com/nextcloud/talk/camera/TakePhotoInApp.kt
  • app/src/main/java/com/nextcloud/talk/chat/CameraLens.kt
  • app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  • app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
  • app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
  • app/src/main/java/com/nextcloud/talk/chat/RecordButtonGesture.kt
  • app/src/main/java/com/nextcloud/talk/chat/RecordHintPopup.kt
  • app/src/main/java/com/nextcloud/talk/chat/RecordInputMode.kt
  • app/src/main/java/com/nextcloud/talk/chat/RecordingStopCoordinator.kt
  • app/src/main/java/com/nextcloud/talk/chat/VideoMessageRecorder.kt
  • app/src/main/java/com/nextcloud/talk/chat/VideoPreviewPlacement.kt
  • app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt
  • app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
  • app/src/main/java/com/nextcloud/talk/components/StandardAppBar.kt
  • app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
  • app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
  • app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerScreen.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/model/MediaViewerActions.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/model/PendingFileForward.kt
  • app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt
  • app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
  • app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  • app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt
  • app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
  • app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt
  • app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
  • app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt
  • app/src/main/res/drawable/bg_record_hint.xml
  • app/src/main/res/drawable/ic_baseline_flash_auto_24.xml
  • app/src/main/res/drawable/ic_baseline_flash_off_24.xml
  • app/src/main/res/drawable/ic_baseline_flash_on_24.xml
  • app/src/main/res/drawable/ic_baseline_hd_24.xml
  • app/src/main/res/drawable/ic_baseline_speed_24.xml
  • app/src/main/res/drawable/ic_record_hint_arrow.xml
  • app/src/main/res/drawable/video_recording_dot.xml
  • app/src/main/res/layout/activity_chat.xml
  • app/src/main/res/layout/dialog_attachment.xml
  • app/src/main/res/layout/fragment_message_input_voice_recording.xml
  • app/src/main/res/layout/view_message_input.xml
  • app/src/main/res/layout/view_record_hint.xml
  • app/src/main/res/values/colors.xml
  • app/src/main/res/values/dimens.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/AttachmentSelectionAndNamingTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/AttachmentToolBarLogicTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/DrawingGeometryTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/EditedFileOwnershipTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewResultTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/ImageDetailLogicTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/UprightMatrixTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/AttachmentActionTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/MediaAccessTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/MediaSelectionTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/RecentMediaTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/ShareFilePermissionTest.kt
  • app/src/test/java/com/nextcloud/talk/camera/CaptureModeSettingTest.kt
  • app/src/test/java/com/nextcloud/talk/camera/CaptureOrientationTest.kt
  • app/src/test/java/com/nextcloud/talk/camera/PhotoCaptureLogicTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/RecordButtonGestureTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/RecordHintPopupPlacementTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/RecordingStopCoordinatorTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/ShouldVibrateOnRecordingChangeTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoFrameAspectTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoMessageRecordingTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoPreviewPlacementCompactPanelTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoPreviewPlacementTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoRecorderLifecycleTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/VideoRecordingRecreationTest.kt
  • app/src/test/java/com/nextcloud/talk/chat/ui/MessageActionsStateTest.kt
  • app/src/test/java/com/nextcloud/talk/mediaviewer/model/MediaViewerActionsTest.kt
  • app/src/test/java/com/nextcloud/talk/mediaviewer/model/MediaViewerChatRequestTest.kt
💤 Files with no reviewable changes (2)
  • app/src/main/res/layout/dialog_attachment.xml
  • app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

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

Caution

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

⚠️ Outside diff range comments (1)

🟡 Minor · Open the pending message after the conversation loads. · ChatActivity.kt:1440-1445

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt:1440-1445
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Open the pending message after the conversation loads.

If a same-room “Show in chat” intent arrives before currentConversation loads, this method leaves its target pending. The initial-load observer runs runPendingMediaRequest() but never calls openPendingTargetMessage(). The chat therefore does not navigate to the requested message. Call openPendingTargetMessage() from the initial-load branch as well.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 33a490a5-c437-4ef3-b985-c929299efa01
📥 Commits

Reviewing files that changed from the base of the PR and between 16e3a25 and c9ad041.

📒 Files selected for processing (4)
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentAction.kt
  • app/src/main/java/com/nextcloud/talk/camera/PhotoCamera.kt
  • app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/AttachmentActionTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentAction.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/AttachmentActionTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

@coderabbitai Re the outside-diff finding on ChatActivity.kt 1440-1445 of this review (open the pending message after the conversation loads):

Confirmed and fixed in ce420ca: the initial-load branch now calls openPendingTargetMessage() next to runPendingMediaRequest(), and the KDoc of applyIntentTargetsToJoinedRoom() is updated. No double navigation: openPendingTargetMessage() clears the id, and onCreate consumes its own target earlier. The two fixes below this PR (b8eaef7 from #6819, 3d16383 from #6820) came up the stack as 18727d8 and 0eaffee.

This reply and the fix were prepared with the help of an AI assistant (Claude Code) and reviewed before posting.

@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

@coderabbitai Reply to the nitpick on FileAttachmentPreviewViewModel.kt 102-112 of this review (orphan output when the file is removed during a drawing save):

Confirmed, the output file stays in the cache in that case. The race needs the user to remove the page within the short save of a drawing. The file lives in cacheDir/shared_attachments, which the system can reclaim, and nothing references it, so it is not sent or shown. I keep this PR to the reported issues and leave the change out; it can follow if the maintainers prefer it.

This reply was prepared with the help of an AI assistant (Claude Code) and reviewed before posting.

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ac66d67d-ad9d-4afb-870b-283de6a1830c
📥 Commits

Reviewing files that changed from the base of the PR and between c9ad041 and ce420ca.

📒 Files selected for processing (7)
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/AttachmentEditing.kt
  • app/src/main/java/com/nextcloud/talk/attachmentpreview/EditNameClaim.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/AttachmentSheet.kt
  • app/src/main/java/com/nextcloud/talk/attachmentsheet/MediaAccess.kt
  • app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  • app/src/test/java/com/nextcloud/talk/attachmentpreview/EditNameClaimTest.kt
  • app/src/test/java/com/nextcloud/talk/attachmentsheet/CanSelectMoreMediaTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd '^ChatActivity\.kt$' .
file="app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt"
ast-grep outline "$file" --match 'onCreate|openPendingTargetMessage|applyIntentTargetsToJoinedRoom|runPendingMediaRequest' --view expanded
rg -n -C 12 'openPendingTargetMessage\(|applyIntentTargetsToJoinedRoom\(|runPendingMediaRequest\(' "$file"

Length of output: 7238


@ToteMeiSter Thanks for the fix and clarification. In ChatActivity.kt, the initial-load branch now calls openPendingTargetMessage() beside runPendingMediaRequest(). The updated KDoc describes the deferred handling.

openPendingTargetMessage() clears the pending target ID after dispatch. The later call therefore does not repeat navigation for a target already consumed by onCreate. This addresses the finding.


✏️ Learnings added
Learnt from: ToteMeiSter
URL: https://github.com/nextcloud/talk-android/pull/6823

Timestamp: 2026-10-05T13:50:17.581Z
Learning: In app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt, applyIntentTargetsToJoinedRoom() defers message-target navigation when currentConversation is null. The initial-load branch calls openPendingTargetMessage() beside runPendingMediaRequest() to handle deferred targets. openPendingTargetMessage() clears the pending target ID after dispatch, so a target already consumed by onCreate does not trigger navigation again.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd '^FileAttachmentPreviewViewModel\.kt$' .
file="app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt"
ast-grep outline "$file" --match 'saveDrawing|replaceFile|applyCrop' --view expanded
sed -n '1,240p' "$file"

Length of output: 8113


@ToteMeiSter Thanks for the clarification. finishEdit() does not delete the new output if replaceFile() cannot find the source URI. This matches the orphaned-output case you described.

I accept leaving this low-impact cache cleanup out of #6823. The unreferenced output is not sent or shown, and the system can reclaim the cache file.

Would you like me to open a follow-up issue for cleanup if the source disappears during a drawing save?


✏️ Learnings added
Learnt from: ToteMeiSter
URL: https://github.com/nextcloud/talk-android/pull/6823

Timestamp: 2026-10-05T13:54:28.185Z
Learning: In app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt, a drawing output can remain unreferenced in cacheDir/shared_attachments if the source URI disappears before finishEdit() calls replaceFile(). The output is not sent or shown, and Android can reclaim the cache file. The author deliberately deferred this low-impact cleanup from PR #6823 to keep the PR focused on the reported issues; do not insist on including it in this PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

…g start

The start time was taken on ACTION_DOWN, but the recording starts only after
the 400-600 ms hold threshold, so a voice message was judged too short
(or long enough) by a duration which included the hold. Take the time in
beginRecording() after the recording has started and use the monotonic clock.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…alized it

Send or delete of a locked video recording cleared the lock and the
in-progress state at once, while CameraX was still finalizing the file. A new
recording started in that window was silently dropped in beginRecording().
For video the state is now released in onVideoRecordingFinished(), which
CameraX reaches for every outcome (send, cancel, error, too short). Audio, and
a video recorder which is already idle, are still cleared at once.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…rded frame

The locked video recording preview was a fixed 180x240dp box floating over the
message bubbles, squashed to about 2:1 in landscape. Now a scrim dims the chat
pane (it blocks touches and is hidden from TalkBack), and the preview is centered
in it in the aspect of the recorded frame, fitted with margins, at most 75% of
the width and 480dp on the longest side. The placement is a pure function
(videoPreviewPlacement) with unit tests; it is recomputed when the pane changes
size and when the recorder learns the frame aspect from CameraX.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…entation, hide chat from TalkBack

- Center by Gravity.CENTER and change only width and height: the margin
  comparison never settled in RTL and re-laid out the chat every frame.
- The frame aspect is turned into screen coordinates by the difference of the
  recording and the display rotation (screenFrameAspect), so the frame matches
  the picture with auto-rotate off and after a turn during the recording.
- While recording, the siblings of the scrim are hidden from TalkBack and get
  their previous values back afterwards.
- The preview size is recalculated after a camera switch.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
In video mode the locked recording panel is one row: delete, red dot in a
progress ring of the 120 s limit, timer, send. The voice panel is unchanged.
The preview keeps a smaller gap on a short area.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
- Hide the whole recording indicator from TalkBack, so the progress ring
  does not announce its percentage every second.
- Relative padding of the timer, so the gap stays between dot and timer in RTL.
- No inset around the progress ring, so it fills its 24dp.
- The ring scale is set in code only (PROGRESS_MAX).

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
A video recording result that waited for a new activity was delivered from onCreate, before currentConversation
and spreedCapabilities were set: the send path and the preview path could throw. The pending result is now
delivered once the initial capabilities state has initialized both; a running recording still reattaches at once.

The delete and send buttons of the compact video row announced the voice recording labels to TalkBack.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
A short tap on the record button switches between voice and video mode. The
mode is stored in AppPreferences and shown by the button icon and a snackbar
hint. The result is a regular video attachment.

In video mode holding the button records with CameraX (front camera, 720p
with fallback, about 2.5 Mbit/s, at most 120 s, then it stops and is sent).
Sliding left cancels, sliding up locks. The gestures, the lock state, the
timer and the locked recording view are shared with the voice recording. The
preview is shown above the input and the camera can be switched during the
recording. An unfinished recording is cancelled and the camera released when
the chat is paused. Too short recordings show a hint, camera errors a
message. The finished mp4 goes through the regular file upload.

Refs nextcloud#6812

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
- A recording result that arrives before the chat of a recreated activity
  is loaded is held by the recorder and delivered once it is loaded, as the
  results that were already pending.
- No recording of either kind starts while a video is still being finalized.
  Its end would clear the state and the audio focus of the new recording.
- A video that was sent is finalized and uploaded even if the chat is left
  before CameraX has finished; clearing the view model gives the audio focus
  of a video recording back.
- The record button switches the mode on a click as well (TalkBack, Switch
  Access, keyboard); a touch does not switch it twice.
- Recording a video no longer asks for access to the media files, it writes
  to the app cache.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
… popup

The record button used to start a recording on the first touch, so a short
tap that should switch the mode started a recording first. Recording now
starts only when the button is held. A tap shows the record hint as a popup
above the button instead of a snackbar, placed in the window of the button
and kept above it after layout changes.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…ding

After a recreation of the activity the lock observer shows the locked
recording panel at once, but the capabilities arrive later and their handler
replaced the panel with the ordinary message input. The lock stayed set, so
nothing brought the panel back. The handler now keeps the panel of a locked
recording.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The activity is recreated on rotation while the video recorder belonged to
it: onPause cancelled the recording and deleted its file, and binding the
camera to the next activity made the CameraX recorder configure itself in a
state where that is not allowed.

- The video recorder lives in ChatViewModel like the voice recorder. The
  activity attaches its preview and callback, and the recorder lets go of
  them when the activity is destroyed. A result that arrives without an
  activity is delivered to the next one.
- The camera is bound to a lifecycle of the recorder itself, not to the one
  of the activity, so it stays bound while the activity is recreated.
- A new activity picks up the recording and locks it when it was held, so the
  timer, stop, send and cancel are shown.
- The recording is cancelled when the user leaves the chat, not on a
  configuration change.
- A recording cut off by the camera is not lost but shown in the attachment
  preview.
- The video is recorded in the rotation of the sensor.
- A recording is never stopped while a switched camera settles.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
The photo of the system camera is written to the app cache (createAttachmentFile) and
handed over through the own file provider, so it needs no access to the media. Drop the
gate in onPickCameraResult and the permission callback branch that only resumed that
preview; the callback is still used by the voice recording.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The container of the video recording preview did not consume touches, so
taps on it reached the chat below. Make it consume them and hide it from
TalkBack, where it is only decoration.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
View.onTouchEvent posts the click of a released touch, so it ran after the touch router had
reset its flag and switched the recording mode once more, e.g. right after a recording was
sent. The release is now consumed by the touch listener and never reaches the view; the
pressed state is cleared by hand. The click listener stays for TalkBack, Switch Access and
the keyboard. Replaces RecordButtonClickRouter.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
…g start

The start time was taken on ACTION_DOWN, but the recording starts only after
the 400-600 ms hold threshold, so a voice message was judged too short
(or long enough) by a duration which included the hold. Take the time in
beginRecording() after the recording has started and use the monotonic clock.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…alized it

Send or delete of a locked video recording cleared the lock and the
in-progress state at once, while CameraX was still finalizing the file. A new
recording started in that window was silently dropped in beginRecording().
For video the state is now released in onVideoRecordingFinished(), which
CameraX reaches for every outcome (send, cancel, error, too short). Audio, and
a video recorder which is already idle, are still cleared at once.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…rded frame

The locked video recording preview was a fixed 180x240dp box floating over the
message bubbles, squashed to about 2:1 in landscape. Now a scrim dims the chat
pane (it blocks touches and is hidden from TalkBack), and the preview is centered
in it in the aspect of the recorded frame, fitted with margins, at most 75% of
the width and 480dp on the longest side. The placement is a pure function
(videoPreviewPlacement) with unit tests; it is recomputed when the pane changes
size and when the recorder learns the frame aspect from CameraX.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
…entation, hide chat from TalkBack

- Center by Gravity.CENTER and change only width and height: the margin
  comparison never settled in RTL and re-laid out the chat every frame.
- The frame aspect is turned into screen coordinates by the difference of the
  recording and the display rotation (screenFrameAspect), so the frame matches
  the picture with auto-rotate off and after a turn during the recording.
- While recording, the siblings of the scrim are hidden from TalkBack and get
  their previous values back afterwards.
- The preview size is recalculated after a camera switch.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
In video mode the locked recording panel is one row: delete, red dot in a
progress ring of the 120 s limit, timer, send. The voice panel is unchanged.
The preview keeps a smaller gap on a short area.

Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Assisted-by: Claude-Code:claude-opus-5-5
ToteMeiSter and others added 29 commits October 27, 2026 08:54
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO exist from API 33,
the minimum SDK is 26, so lint reported every use as InlinedApi. They are
constants of the same kind as READ_MEDIA_VISUAL_USER_SELECTED already is.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Use mutableIntStateOf for the brush colour, name the modifier parameter of the
lens switch button modifier, use the String.toUri extension, keep /sdcard out
of the preview data, and drop the strings of the HQ toggle and the compression
switch, which nothing reads any more.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Use mutableIntStateOf for the brush colour, name the modifier parameter of the
lens switch button modifier, use the String.toUri extension, keep /sdcard out
of the preview data, and drop the strings of the HQ toggle and the compression
switch, which nothing reads any more.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The view model held an injected Context only to build DateUtils, which lint
reports as StaticFieldLeak. It takes the application context at the place of
use, as ensureCached in the same class already does.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The hint root is a FrameLayout with the arrow aligned to the bottom, and
the video preview container is a sibling of the dimming view instead of
its only child. Geometry, outline clipping and arrow offset stay the same.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Same no-sensor orientation as before, requested in onCreate.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The hint root is a FrameLayout with the arrow aligned to the bottom, and
the video preview container is a sibling of the dimming view instead of
its only child. Geometry, outline clipping and arrow offset stay the same.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The hint root is a FrameLayout with the arrow aligned to the bottom, and
the video preview container is a sibling of the dimming view instead of
its only child. Geometry, outline clipping and arrow offset stay the same.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
The hint root is a FrameLayout with the arrow aligned to the bottom, and
the video preview container is a sibling of the dimming view instead of
its only child. Geometry, outline clipping and arrow offset stay the same.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Same no-sensor orientation as before, requested in onCreate.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
# Conflicts:
#	app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
# Conflicts:
#	app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
#	app/src/main/java/com/nextcloud/talk/chat/VideoMessageRecorder.kt
#	app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
# Conflicts:
#	app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
#	app/src/main/java/com/nextcloud/talk/chat/VideoMessageRecorder.kt
#	app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rge-master

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rge-master

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
…rge-master

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Co-Authored-By: engineering-git-workflow-master (cc3/claude-sonnet-5-5) <engineering-git-workflow-master@ai-office.local>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
@ToteMeiSter
ToteMeiSter force-pushed the feat/media-viewer-actions branch from 1c0d1d8 to eb16fbf Compare October 30, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants