Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,22 @@ private fun mediaPresenter(
scope.launch {
context.imageLoader.diskCache?.openSnapshot(uri)?.use {
val byteArray = it.data.toFile().readBytes()
val fileName = uri.substringAfterLast("/")
var fileName = uri.substringBefore("?").substringBefore("#").substringAfterLast("/")
val lastAt = fileName.lastIndexOf('@')
val lastDot = fileName.lastIndexOf('.')
if (lastAt > lastDot && lastAt < fileName.length - 1) {
fileName = fileName.substring(0, lastAt) + "." + fileName.substring(lastAt + 1)
Comment on lines +310 to +314
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The filename normalization logic (strip query/fragment, convert Bluesky @jpeg suffix, add fallback extension) is duplicated in both save() and share(). Please extract this into a small helper (e.g., fun normalizeFileNameFromUri(uri, fallbackBaseName, mimeType?)) so the two paths can’t drift and future fixes only need to be made once.

Copilot uses AI. Check for mistakes.
}
if (fileName.isEmpty()) {
fileName = "image"
}
if (!fileName.contains(".")) {
val extension =
android.webkit.MimeTypeMap
.getSingleton()
.getExtensionFromMimeType(getMimeType(byteArray)) ?: "jpg"
fileName = "$fileName.$extension"
}
saveByteArrayToDownloads(context, byteArray, fileName)
}
withContext(Dispatchers.Main) {
Comment on lines +324 to 328
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

In save(), the success toast is shown unconditionally after the openSnapshot(uri) call. If diskCache is null or openSnapshot(uri) returns null (e.g., media not yet cached), this will still display “save success” even though nothing was written. Consider moving the success toast inside the .use { ... } block and adding an ?: run { ... } branch (similar to StatusMediaScreen.shareMedia) to show a “downloading/failed” message instead.

Copilot uses AI. Check for mistakes.
Expand All @@ -325,10 +340,28 @@ private fun mediaPresenter(
scope.launch {
context.imageLoader.diskCache?.openSnapshot(uri)?.use {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

In share(), openSnapshot(uri) is nullable but there’s no else branch for the null case. Right now sharing silently does nothing if the media isn’t in the disk cache yet (or disk cache is disabled). Add an ?: run { ... } fallback to surface a user-visible message (and avoid proceeding) when the snapshot is unavailable.

Suggested change
context.imageLoader.diskCache?.openSnapshot(uri)?.use {
val snapshot =
context.imageLoader.diskCache?.openSnapshot(uri)
?: run {
Toast.makeText(
context,
"Unable to share image. Please try again after it has finished loading.",
Toast.LENGTH_SHORT,
).show()
return@launch
}
snapshot.use {

Copilot uses AI. Check for mistakes.
val originFile = it.data.toFile()
var fileName = uri.substringBefore("?").substringBefore("#").substringAfterLast("/")
val lastAt = fileName.lastIndexOf('@')
val lastDot = fileName.lastIndexOf('.')
if (lastAt > lastDot && lastAt < fileName.length - 1) {
fileName = fileName.substring(0, lastAt) + "." + fileName.substring(lastAt + 1)
}
if (fileName.isEmpty()) {
fileName = "image"
}
if (!fileName.contains(".")) {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(originFile.absolutePath, options)
val extension =
android.webkit.MimeTypeMap
.getSingleton()
.getExtensionFromMimeType(options.outMimeType) ?: "jpg"
fileName = "$fileName.$extension"
}
val targetFile =
File(
context.cacheDir,
uri.substringAfterLast("/"),
fileName,
)
originFile.copyTo(targetFile, overwrite = true)
val uri =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -927,10 +927,13 @@ private fun statusMediaPresenter(
scope.launch {
context.imageLoader.diskCache?.openSnapshot(data.url)?.use {
val originFile = it.data.toFile()
val status = state.status.takeSuccess() as? UiTimelineV2.Post
val statusKeyString = statusKey.toString()
val userHandle = status?.user?.handle?.canonical ?: "unknown"
val targetFile =
File(
context.cacheDir,
data.url.substringAfterLast("/"),
data.getFileName(statusKeyString, userHandle),
)
originFile.copyTo(targetFile, overwrite = true)
val uri =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,21 @@ public fun UiMedia.getFileName(
): String {
val key = statusKey.sanitizeFileName()
val handle = userHandle.sanitizeFileName()
val originalName = url.substringAfterLast("/")
val path = url.substringBefore("?").substringBefore("#")
val originalName = path.substringAfterLast("/")
val lastDotIndex = originalName.lastIndexOf('.')
val lastAtIndex = originalName.lastIndexOf('@')
val separatorIndex = maxOf(lastDotIndex, lastAtIndex)
val extension =
if (originalName.contains(".")) {
originalName.substringAfterLast(".")
if (separatorIndex >= 0 && separatorIndex < originalName.length - 1) {
originalName.substring(separatorIndex + 1)
} else {
when (this) {
is UiMedia.Audio -> "mp3"
is UiMedia.Gif -> "gif"
is UiMedia.Image -> "jpg"
is UiMedia.Video -> "mp4"
}
}.removeSuffix("?name=orig")
}
return "${key}_$handle.$extension"
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,43 @@ class UiMediaFileNameTest {
fileName,
)
}

@Test
fun handlesUrlWithQueryParams() {
val media = UiMedia.Image(url = "https://example.com/image.jpg?name=orig&size=large")

val fileName =
media.getFileName(
statusKey = "post123",
userHandle = "alice",
)

assertEquals("post123_alice.jpg", fileName)
}

@Test
fun handlesUrlWithoutExtensionAndAppendsFallback() {
val media = UiMedia.Image(url = "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:x/bafkrei")

val fileName =
media.getFileName(
statusKey = "post123",
userHandle = "alice",
)

assertEquals("post123_alice.jpg", fileName)
}

@Test
fun handlesBlueskyFormatUrl() {
val media = UiMedia.Image(url = "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:x/bafkrei...@jpeg")

val fileName =
media.getFileName(
statusKey = "post123",
userHandle = "alice",
)

assertEquals("post123_alice.jpeg", fileName)
}
}
Loading