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