Skip to content

[PM-25654] Add image attachment preview#5864

Open
amrg101 wants to merge 3 commits intobitwarden:mainfrom
amrg101:main
Open

[PM-25654] Add image attachment preview#5864
amrg101 wants to merge 3 commits intobitwarden:mainfrom
amrg101:main

Conversation

@amrg101
Copy link

@amrg101 amrg101 commented Sep 10, 2025

🎟️ Tracking

https://community.bitwarden.com/t/view-attachments-without-downloading/52/19

📔 Objective

Viewing image attachments is a hassle since you have to download, save, and open them in another app.
This adds a built-in preview for images, Ideally, we’d have a full file manager that supports all types, but since images are the ones most often needing quick viewing, I think this is okay for now.

The implementation:

  • Downloads the file temporarily.
  • Loads and displays the image with zoom and pan support.
  • Automatically deletes the file once it has been loaded.

📸 Screenshots

Screen_Recording_20250910_170903_Bitwarden.Dev.mp4

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@amrg101 amrg101 changed the title Add image attachment preview Add image attachment preview feature Sep 10, 2025
@bitwarden-bot
Copy link

Thank you for your contribution! We've added this to our internal tracking system for review.
ID: PM-25654
Link: https://bitwarden.atlassian.net/browse/PM-25654

Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process.

@bitwarden-bot bitwarden-bot changed the title Add image attachment preview feature [PM-25654] Add image attachment preview Sep 10, 2025
@SaintPatrck SaintPatrck added app:password-manager Bitwarden Password Manager app context t:new-feature labels Sep 11, 2025
@SaintPatrck
Copy link
Contributor

Hi @amrg101!

Thank you for submitting this awesome new feature. I've passed it along to the appropriate teams for product review. We'll provide status updates as they become available.

@speedy1804
Copy link

wow this looks great, would love to see that on iOS too

@speedy1804
Copy link

@SaintPatrck are there any new information for this PR?

Copy link
Contributor

@SaintPatrck SaintPatrck left a comment

Choose a reason for hiding this comment

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

Hi @amrg101

Apologies for the delay. This feature has been approved. If you are still willing to make the suggested changes, we can move the pull request forward. If you do not have the time, let us know and we can take it from here.

Overall, the changes look good. We will need tests for all of this new functionality. I've also left a few comments that need to be addressed.

}
}
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

All files must end in a newline.

Suggested change
}
}


/**
* Displays a secure, temporary image attachment in a full-screen dialog with zoom and pan
* capabilities. The dialog can be dismissed by clicking the close button or pressing the back button.
Copy link
Contributor

Choose a reason for hiding this comment

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

Max line length is 100.

Suggested change
* capabilities. The dialog can be dismissed by clicking the close button or pressing the back button.
* capabilities. The dialog can be dismissed by clicking the close button or pressing the back
* button.

LaunchedEffect(attachmentFile) {
val loadedPainter = withContext(Dispatchers.IO) {
try {
val bitmap = BitmapFactory.decodeFile(attachmentFile.path)
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 It might be worth downsampling the image to prevent OOM exceptions for large files since we support attachments up to 500MB.

shouldShowPremiumWarningDialog = true
return@BitwardenStandardIconButton
}
onAttachmentPreviewClick(attachmentItem)
Copy link
Contributor

Choose a reason for hiding this comment

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

We also need a large file warning here, like the download button has.

)
onAttachmentPreviewClick = vaultCommonItemTypeHandlers
.onAttachmentPreviewClick,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this indention is correct. Can you run the formatter here?

AttachmentPreviewDialog(
attachmentFile = dialog.file,
onDismissRequest = onDismissRequest,
onLoaded = onPreviewLoaded
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing a trailing comma

Suggested change
onLoaded = onPreviewLoaded
onLoaded = onPreviewLoaded,

VaultItemAction.Internal.AttachmentDecryptReceive(
result = result,
fileName = action.attachment.title,
isPreview = false
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing trailing comma

Suggested change
isPreview = false
isPreview = false,

VaultItemAction.Internal.AttachmentDecryptReceive(
result = result,
fileName = action.attachment.title,
isPreview = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing trailing comma

Suggested change
isPreview = true
isPreview = true,

modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = cardStyle, paddingStart = 16.dp)
.cardStyle(cardStyle = cardStyle, paddingStart = 16.dp, paddingEnd = 8.dp)
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 The seems off. I think we need to take an approach similar to BitwardenTextRow and use an outer container (Box) and SpaceBetween. @david-livefront thoughts on this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@SaintPatrck is correct, we should not be adding the additional paddingEnd as that will break alignment with other rows.

var showContent by remember { mutableStateOf(false) }

LaunchedEffect(attachmentFile) {
val loadedPainter = withContext(Dispatchers.IO) {
Copy link
Contributor

Choose a reason for hiding this comment

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

♻️ Not a huge fan of this explicit Dispatchers.IO use, but considering the alternatives it may be the cleanest way. 😢

Nothing to change here, for now.

.background(Color.Black.copy(alpha = 0.5f))
) {
Icon(
imageVector = Icons.Filled.Close,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use our icons.

This one should be painter = rememberVectorPainter(id = BitwardenDrawable.ic_close),

.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scale = (scale * zoom).coerceIn(1f, 5f)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you make some constants for this. 5f is a magic number.

BitmapPainter(bitmap.asImageBitmap())
} else {
null
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we simplify this too:

BitmapFactory
    .decodeFile(attachmentFile.path)
    ?.let { BitmapPainter(it.asImageBitmap()) }

.align(Alignment.TopEnd)
.padding(16.dp)
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.5f))
Copy link
Collaborator

@david-livefront david-livefront Feb 26, 2026

Choose a reason for hiding this comment

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

We should be using our Bitwarden components to ensure that adhere to our style guide.

Something like this should do the trick:

BitwardenTonalIconButton(
    vectorIconRes = BitwardenDrawable.ic_close,
    contentDescription = stringResource(id = BitwardenString.close),
    onClick = onDismissRequest,
    modifier = Modifier
        .padding(top = 8.dp, end = 24.dp)
        .align(Alignment.TopEnd),
)

}
}

if (showContent && painter != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need this showContent at all.

Can we just base it on whether the image is loaded:

        painter?.let {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center,
            ) {
                Image(
                    painter = it,
                    contentDescription = stringResource(id = BitwardenString.preview),

}
}

private fun isImageFile(fileName: String?): Boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The file name will never be null, right?

@amrg101
Copy link
Author

amrg101 commented Feb 27, 2026

@SaintPatrck @david-livefront Thanks for the review! I've addressed the feedback in the latest commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:password-manager Bitwarden Password Manager app context community-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants