diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e65a0f92..f898083a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,13 +22,13 @@ plugins {
val composeBomVersion = "2025.03.00"
val accompanistVersion = "0.34.0"
val okhttpVersion = "4.12.0"
-val navVersion = "2.8.7"
+val navVersion = "2.9.0"
val hiltVersion = "2.52"
val glideVersion = "4.16.0"
val ktorVersion = "3.0.0-beta-2"
-val media3Version = "1.5.0"
+val media3Version = "1.7.1"
val livekitVersion = "2.2.0"
-val material3Version = "1.4.0-alpha10"
+val material3Version = "1.4.0-alpha15"
val androidXTestVersion = "1.6.1"
fun property(fileName: String, propertyName: String, fallbackEnv: String? = null): String? {
@@ -184,14 +184,14 @@ sentry {
dependencies {
// Android/Kotlin Core
- implementation("androidx.core:core-ktx:1.15.0")
- implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.10")
+ implementation("androidx.core:core-ktx:1.16.0")
+ implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.20")
// Kotlinx - various first-party extensions for Kotlin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.1")
// Compose BOM
val composeBom = platform("androidx.compose:compose-bom:$composeBomVersion")
@@ -200,16 +200,16 @@ dependencies {
androidTestImplementation(composeBom)
// Jetpack Compose
- implementation("androidx.compose.ui:ui:1.8.0-rc01")
+ implementation("androidx.compose.ui:ui:1.8.2")
implementation("androidx.compose.ui:ui-util")
implementation("androidx.compose.material3:material3:$material3Version")
implementation("androidx.compose.material3:material3-window-size-class:$material3Version")
implementation("androidx.compose.material:material-icons-core:1.7.8")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.runtime:runtime-livedata")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
- implementation("androidx.activity:activity-compose:1.10.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
+ implementation("androidx.activity:activity-compose:1.10.1")
// Accompanist - Jetpack Compose Extensions
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
@@ -243,26 +243,26 @@ dependencies {
implementation("com.mikepenz:aboutlibraries-core:11.3.0-rc02")
// Sentry - crash reporting
- implementation("io.sentry:sentry-android:7.16.0")
- implementation("io.sentry:sentry-compose-android:7.16.0")
+ implementation("io.sentry:sentry-android:8.13.2")
+ implementation("io.sentry:sentry-compose-android:8.13.2")
- // Other AndroidX libraries - used for various things and never seem to have a consistent version
- implementation("androidx.documentfile:documentfile:1.0.1")
+ // Other AndroidX libraries
+ implementation("androidx.documentfile:documentfile:1.1.0")
implementation("androidx.browser:browser:1.8.0")
- implementation("androidx.webkit:webkit:1.12.1")
- implementation("androidx.core:core-splashscreen:1.2.0-beta01")
+ implementation("androidx.webkit:webkit:1.14.0")
+ implementation("androidx.core:core-splashscreen:1.2.0-beta02")
implementation("androidx.palette:palette-ktx:1.0.0")
// Libraries used for legacy View-based UI
- implementation("androidx.constraintlayout:constraintlayout:2.2.0")
- implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.2.1")
+ implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.12.0")
// hCaptcha - captcha provider
implementation("com.github.hcaptcha:hcaptcha-android-sdk:3.8.1")
// JDK Desugaring - polyfill for new Java APIs
- coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
// AndroidX Media3 w/ ExoPlayer
implementation("androidx.media3:media3-exoplayer:$media3Version")
@@ -273,15 +273,17 @@ dependencies {
// Compose libraries
implementation("me.saket.telephoto:zoomable-image:1.0.0-alpha02")
implementation("me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02")
- implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0")
+ implementation("androidx.constraintlayout:constraintlayout-compose:1.1.1")
+ implementation("dev.chrisbanes.haze:haze:1.6.4")
+ implementation("dev.chrisbanes.haze:haze-materials:1.6.4")
// ZXing - QR Code generation
implementation("com.google.zxing:core:3.5.3")
// Persistence
implementation("app.cash.sqldelight:android-driver:2.0.1")
- implementation("androidx.datastore:datastore:1.1.2")
- implementation("androidx.datastore:datastore-preferences:1.1.2")
+ implementation("androidx.datastore:datastore:1.1.7")
+ implementation("androidx.datastore:datastore-preferences:1.1.7")
// Markup
implementation("org.jetbrains:markdown:0.7.3")
@@ -292,7 +294,7 @@ dependencies {
// implementation "io.livekit:livekit-android:$livekit_version"
// Firebase - Cloud Messaging
- implementation(platform("com.google.firebase:firebase-bom:33.9.0"))
+ implementation(platform("com.google.firebase:firebase-bom:33.15.0"))
implementation("com.google.firebase:firebase-messaging")
// Shimmer - loading animations
diff --git a/app/src/main/java/chat/revolt/activities/ShareTargetActivity.kt b/app/src/main/java/chat/revolt/activities/ShareTargetActivity.kt
index 9b673543..e7d6e611 100644
--- a/app/src/main/java/chat/revolt/activities/ShareTargetActivity.kt
+++ b/app/src/main/java/chat/revolt/activities/ShareTargetActivity.kt
@@ -377,6 +377,16 @@ fun ShareTargetScreen(
attachments = viewModel.attachments,
uploading = viewModel.attachmentsUploading,
uploadProgress = viewModel.attachmentProgress,
+ onToggleSpoiler = {
+ val index = viewModel.attachments
+ .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier }
+ if (index != -1) {
+ val attachment = viewModel.attachments[index]
+ viewModel.attachments[index] = attachment.copy(
+ spoiler = !attachment.spoiler
+ )
+ }
+ },
onRemove = {},
canRemove = false
)
diff --git a/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt b/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt
index 65cdd54d..3011ec53 100644
--- a/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt
+++ b/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt
@@ -26,7 +26,8 @@ data class FileArgs(
val file: File,
val filename: String,
val contentType: String,
- val pickerIdentifier: String? = null
+ val spoiler: Boolean = false,
+ val pickerIdentifier: String? = null,
)
suspend fun uploadToAutumn(
diff --git a/app/src/main/java/chat/revolt/composables/chat/MessageAttachment.kt b/app/src/main/java/chat/revolt/composables/chat/MessageAttachment.kt
index 328495b1..6a489bb0 100644
--- a/app/src/main/java/chat/revolt/composables/chat/MessageAttachment.kt
+++ b/app/src/main/java/chat/revolt/composables/chat/MessageAttachment.kt
@@ -1,5 +1,6 @@
package chat.revolt.composables.chat
+import android.annotation.SuppressLint
import android.text.format.Formatter
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -20,6 +21,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -34,6 +39,11 @@ import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.composables.generic.RemoteImage
import chat.revolt.composables.media.AudioPlayer
+import dev.chrisbanes.haze.hazeEffect
+import dev.chrisbanes.haze.hazeSource
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+import dev.chrisbanes.haze.rememberHazeState
@Composable
fun FileAttachment(attachment: AutumnResource) {
@@ -71,9 +81,14 @@ fun FileAttachment(attachment: AutumnResource) {
}
}
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun ImageAttachment(attachment: AutumnResource) {
val url = "$REVOLT_FILES/attachments/${attachment.id}/${attachment.filename}"
+ var spoilerShown by remember { mutableStateOf(false) }
+ val hazeState =
+ if (attachment.filename?.startsWith("SPOILER_") == true) rememberHazeState() else null
BoxWithConstraints {
RemoteImage(
@@ -83,9 +98,34 @@ fun ImageAttachment(attachment: AutumnResource) {
.width(attachment.metadata?.width?.toInt()?.dp ?: maxWidth)
.aspectRatio(
attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()
+ )
+ .then(
+ if (hazeState != null) Modifier.hazeSource(state = hazeState)
+ else Modifier
),
description = attachment.filename ?: "Image"
)
+ if (attachment.filename?.startsWith("SPOILER_") == true && !spoilerShown) {
+ Box(
+ modifier = Modifier
+ .hazeEffect(state = hazeState, style = HazeMaterials.ultraThin())
+ .width(attachment.metadata?.width?.toInt()?.dp ?: maxWidth)
+ .aspectRatio(
+ attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()
+ )
+ .clickable { spoilerShown = true },
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.medium)
+ .hazeEffect(state = hazeState, style = HazeMaterials.regular())
+ .padding(8.dp)
+ ) {
+ Text(stringResource(R.string.attachment_spoiler))
+ }
+ }
+ }
}
}
@@ -108,6 +148,7 @@ fun VideoPlayButton() {
)
}
+@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun VideoAttachment(attachment: AutumnResource) {
val url = "$REVOLT_FILES/attachments/${attachment.id}/${attachment.filename}"
diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/AttachmentManager.kt b/app/src/main/java/chat/revolt/composables/screens/chat/AttachmentManager.kt
index e5017d94..3e02d219 100644
--- a/app/src/main/java/chat/revolt/composables/screens/chat/AttachmentManager.kt
+++ b/app/src/main/java/chat/revolt/composables/screens/chat/AttachmentManager.kt
@@ -7,6 +7,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -21,10 +22,13 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ProgressIndicatorDefaults
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
@@ -37,6 +41,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -53,10 +58,16 @@ import java.io.File
@Composable
fun FilePreviewSheet(
- args: FileArgs, canRemove: Boolean, onRemove: () -> Unit, onDismiss: () -> Unit
+ args: FileArgs,
+ canRemove: Boolean,
+ onRemove: () -> Unit,
+ onToggleSpoiler: () -> Unit,
+ onDismiss: () -> Unit
) {
val context = LocalContext.current
+ var localIsSpoiler by remember { mutableStateOf(args.spoiler) }
+
Column(
Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -80,6 +91,39 @@ fun FilePreviewSheet(
color = LocalContentColor.current.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
+
+ Box(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.medium)
+ .clickable {
+ onToggleSpoiler()
+ localIsSpoiler = !localIsSpoiler
+ }
+ .padding(top = 8.dp)
+ ) {
+ ListItem(
+ headlineContent = {
+ Text(
+ stringResource(R.string.attachment_preview_spoiler)
+ )
+ },
+ supportingContent = {
+ Text(
+ stringResource(R.string.attachment_preview_spoiler_description)
+ )
+ },
+ trailingContent = {
+ Switch(
+ checked = localIsSpoiler,
+ onCheckedChange = null,
+ )
+ },
+ colors = ListItemDefaults.colors().copy(
+ containerColor = Color.Transparent,
+ )
+ )
+ }
+
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
onDismiss()
@@ -111,6 +155,7 @@ fun AttachmentManager(
uploading: Boolean,
uploadProgress: Float = 0f,
onRemove: (FileArgs) -> Unit,
+ onToggleSpoiler: (FileArgs) -> Unit,
canRemove: Boolean = true,
canPreview: Boolean = true
) {
@@ -126,18 +171,26 @@ fun AttachmentManager(
}, sheetState = sheetState
) {
previewingAttachment?.let {
- FilePreviewSheet(args = it, canRemove = canRemove, onRemove = {
- onRemove(it)
- scope.launch {
- sheetState.hide()
- showPreviewSheet = false
+ FilePreviewSheet(
+ args = it,
+ canRemove = canRemove,
+ onRemove = {
+ onRemove(it)
+ scope.launch {
+ sheetState.hide()
+ showPreviewSheet = false
+ }
+ },
+ onToggleSpoiler = {
+ onToggleSpoiler(it)
+ },
+ onDismiss = {
+ scope.launch {
+ sheetState.hide()
+ showPreviewSheet = false
+ }
}
- }, onDismiss = {
- scope.launch {
- sheetState.hide()
- showPreviewSheet = false
- }
- })
+ )
}
}
}
@@ -159,20 +212,21 @@ fun AttachmentManager(
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
attachments.forEach { attachment ->
- Row(modifier = Modifier
- .padding(4.dp)
- .clip(MaterialTheme.shapes.small)
- .clickable {
- if (canPreview) {
- previewingAttachment = attachment
- showPreviewSheet = true
+ Row(
+ modifier = Modifier
+ .padding(4.dp)
+ .clip(MaterialTheme.shapes.small)
+ .clickable {
+ if (canPreview) {
+ previewingAttachment = attachment
+ showPreviewSheet = true
+ }
}
- }
- .background(
- color = MaterialTheme.colorScheme.background,
- shape = MaterialTheme.shapes.small
- )
- .padding(8.dp)) {
+ .background(
+ color = MaterialTheme.colorScheme.background,
+ shape = MaterialTheme.shapes.small
+ )
+ .padding(8.dp)) {
Text(attachment.filename, maxLines = 1)
}
Spacer(modifier = Modifier.width(8.dp))
@@ -190,13 +244,14 @@ fun AttachmentManager(
@Preview
@Composable
fun AttachmentManagerPreview() {
- AttachmentManager(attachments = listOf(
- FileArgs(
- filename = "file1.png", contentType = "image/png", file = File("file1.png")
- ), FileArgs(
- filename = "file2.png", contentType = "image/png", file = File("file2.png")
- ), FileArgs(
- filename = "file3.png", contentType = "image/png", file = File("file3.png")
- )
- ), uploading = false, onRemove = {})
+ AttachmentManager(
+ attachments = listOf(
+ FileArgs(
+ filename = "file1.png", contentType = "image/png", file = File("file1.png")
+ ), FileArgs(
+ filename = "file2.png", contentType = "image/png", file = File("file2.png")
+ ), FileArgs(
+ filename = "file3.png", contentType = "image/png", file = File("file3.png")
+ )
+ ), uploading = false, onToggleSpoiler = {}, onRemove = {})
}
diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
index 1825658e..94099fbc 100644
--- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt
@@ -61,7 +61,6 @@ import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.DrawerState
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -95,7 +94,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
@@ -155,6 +153,7 @@ import com.valentinilk.shimmer.shimmer
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
+import logcat.logcat
import java.io.File
import kotlin.math.max
@@ -957,6 +956,21 @@ fun ChannelScreen(
canPreview = true,
onRemove = {
viewModel.draftAttachments.remove(it)
+ },
+ onToggleSpoiler = {
+ val index = viewModel.draftAttachments
+ .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier }
+ logcat {
+ "Toggling spoiler for attachment at index $index"
+ }
+ if (index != -1) {
+ val attachment =
+ viewModel.draftAttachments[index]
+ viewModel.draftAttachments[index] =
+ attachment.copy(
+ spoiler = !attachment.spoiler
+ )
+ }
}
)
}
diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt
index 1e41e22e..375ecbbb 100644
--- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt
@@ -344,7 +344,7 @@ class ChannelScreenViewModel @Inject constructor(
try {
val id = uploadToAutumn(
it.file,
- it.filename,
+ if (it.spoiler) "SPOILER_${it.filename}" else it.filename,
"attachments",
ContentType.parse(it.contentType),
onProgress = { current, total ->
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 177b4ff7..fbac9dbb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -113,8 +113,12 @@
\@ off
You can only reply to %1$d messages at a time.
+ Spoiler
+
Remove
Close
+ Mark as spoiler
+ This attachment will only be revealed when tapped.
Smileys & Emotions
People & Body