feat: upload attachments
This commit is contained in:
parent
56d1c4ff57
commit
1ef7d6610b
|
|
@ -87,9 +87,6 @@ dependencies {
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_libraries_version"
|
debugImplementation "androidx.compose.ui:ui-tooling:$compose_libraries_version"
|
||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_libraries_version"
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_libraries_version"
|
||||||
|
|
||||||
// Browser opening utility (used for legal links)
|
|
||||||
implementation "androidx.browser:browser:1.4.0"
|
|
||||||
|
|
||||||
// Hilt - Dependency Injection
|
// Hilt - Dependency Injection
|
||||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||||
implementation "androidx.hilt:hilt-navigation-compose:1.1.0-alpha01"
|
implementation "androidx.hilt:hilt-navigation-compose:1.1.0-alpha01"
|
||||||
|
|
@ -104,7 +101,9 @@ dependencies {
|
||||||
// AboutLibraries - automated OSS library attribution
|
// AboutLibraries - automated OSS library attribution
|
||||||
implementation "com.mikepenz:aboutlibraries-compose:$aboutlibraries_version"
|
implementation "com.mikepenz:aboutlibraries-compose:$aboutlibraries_version"
|
||||||
|
|
||||||
// Jetpack DataStore - persistent storage
|
// Other AndroidX libraries - used for various things and never seem to have a consistent version
|
||||||
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation "androidx.browser:browser:1.4.0"
|
||||||
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
|
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
|
||||||
implementation "androidx.datastore:datastore:1.1.0-alpha01"
|
implementation "androidx.datastore:datastore:1.1.0-alpha01"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
import chat.revolt.api.RevoltJson
|
import chat.revolt.api.RevoltJson
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.schemas.Embed
|
|
||||||
import chat.revolt.api.schemas.Message
|
import chat.revolt.api.schemas.Message
|
||||||
import chat.revolt.api.schemas.MessagesInChannel
|
import chat.revolt.api.schemas.MessagesInChannel
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
|
@ -59,23 +58,31 @@ data class SendMessageReply(
|
||||||
val mention: Boolean
|
val mention: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class SendMessageBody(
|
||||||
|
val content: String,
|
||||||
|
val nonce: String = ULID.makeNext(),
|
||||||
|
val replies: List<SendMessageReply> = emptyList(),
|
||||||
|
val attachments: List<String>?,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun sendMessage(
|
suspend fun sendMessage(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
content: String,
|
content: String,
|
||||||
nonce: String? = ULID.makeNext(),
|
nonce: String? = ULID.makeNext(),
|
||||||
replies: List<SendMessageReply>? = null,
|
replies: List<SendMessageReply>? = null,
|
||||||
embed: Embed? = null
|
attachments: List<String>? = null,
|
||||||
): String {
|
): String {
|
||||||
val response = RevoltHttp.post("/channels/$channelId/messages") {
|
val response = RevoltHttp.post("/channels/$channelId/messages") {
|
||||||
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||||
|
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(
|
setBody(
|
||||||
mapOf(
|
SendMessageBody(
|
||||||
"content" to content,
|
content = content,
|
||||||
"nonce" to nonce,
|
nonce = nonce ?: ULID.makeNext(),
|
||||||
"replies" to replies,
|
replies = replies ?: emptyList(),
|
||||||
"embed" to embed
|
attachments = attachments
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package chat.revolt.api.routes.microservices.autumn
|
||||||
|
|
||||||
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import chat.revolt.api.RevoltJson
|
||||||
|
import chat.revolt.api.schemas.AutumnError
|
||||||
|
import chat.revolt.api.schemas.AutumnId
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
const val MAX_ATTACHMENTS_PER_MESSAGE = 5
|
||||||
|
|
||||||
|
data class FileArgs(
|
||||||
|
val file: File,
|
||||||
|
val filename: String,
|
||||||
|
val contentType: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun uploadToAutumn(
|
||||||
|
file: File,
|
||||||
|
name: String,
|
||||||
|
tag: String,
|
||||||
|
contentType: ContentType,
|
||||||
|
onProgress: (Long, Long) -> Unit = { _, _ -> }
|
||||||
|
): String {
|
||||||
|
val uploadUrl = "$REVOLT_FILES/$tag"
|
||||||
|
|
||||||
|
val response = RevoltHttp.post(uploadUrl) {
|
||||||
|
setBody(MultiPartFormDataContent(
|
||||||
|
formData {
|
||||||
|
append("file", file.readBytes(), Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, contentType.toString())
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=\"$name\"")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
))
|
||||||
|
onUpload { bytesSentTotal, contentLength ->
|
||||||
|
onProgress(bytesSentTotal, contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val autumnId = RevoltJson.decodeFromString(AutumnId.serializer(), response.bodyAsText())
|
||||||
|
return autumnId.id
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(AutumnError.serializer(), response.bodyAsText())
|
||||||
|
throw Exception(error.type)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -37,4 +37,14 @@ data class Metadata(
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val width: Long? = null,
|
val width: Long? = null,
|
||||||
val height: Long? = null
|
val height: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AutumnId(
|
||||||
|
val id: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AutumnError(
|
||||||
|
val type: String,
|
||||||
)
|
)
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
package chat.revolt.components.chat
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -29,12 +29,15 @@ fun MessageField(
|
||||||
onToggleButtons: (Boolean) -> Unit,
|
onToggleButtons: (Boolean) -> Unit,
|
||||||
messageContent: String,
|
messageContent: String,
|
||||||
onMessageContentChange: (String) -> Unit,
|
onMessageContentChange: (String) -> Unit,
|
||||||
|
onAddAttachment: () -> Unit,
|
||||||
onSendMessage: () -> Unit,
|
onSendMessage: () -> Unit,
|
||||||
channelType: ChannelType,
|
channelType: ChannelType,
|
||||||
channelName: String,
|
channelName: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
forceSendButton: Boolean = false,
|
||||||
|
disabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val focusRequester = remember { FocusRequester() }
|
||||||
val placeholderResource = when (channelType) {
|
val placeholderResource = when (channelType) {
|
||||||
ChannelType.DirectMessage -> R.string.message_field_placeholder_dm
|
ChannelType.DirectMessage -> R.string.message_field_placeholder_dm
|
||||||
ChannelType.Group -> R.string.message_field_placeholder_group
|
ChannelType.Group -> R.string.message_field_placeholder_group
|
||||||
|
|
@ -49,11 +52,8 @@ fun MessageField(
|
||||||
Row(Modifier.weight(1f)) {
|
Row(Modifier.weight(1f)) {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
Toast.makeText(
|
focusRequester.freeFocus() // hide keyboard because it's annoying
|
||||||
context,
|
onAddAttachment()
|
||||||
"Placeholder for adding an attachment",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
|
@ -78,11 +78,11 @@ fun MessageField(
|
||||||
Icons.Default.KeyboardArrowRight,
|
Icons.Default.KeyboardArrowRight,
|
||||||
contentDescription = stringResource(id = R.string.show_more_alt),
|
contentDescription = stringResource(id = R.string.show_more_alt),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp + 8.dp)
|
|
||||||
.padding(vertical = 4.dp)
|
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
onToggleButtons(true)
|
onToggleButtons(true)
|
||||||
})
|
})
|
||||||
|
.size(24.dp + 8.dp)
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +91,8 @@ fun MessageField(
|
||||||
value = messageContent,
|
value = messageContent,
|
||||||
onValueChange = onMessageContentChange,
|
onValueChange = onMessageContentChange,
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
shape = RoundedCornerShape(100),
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
enabled = !disabled,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
|
|
@ -109,14 +110,16 @@ fun MessageField(
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 8.dp)
|
.padding(start = 8.dp)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send button (visible when text is entered)
|
// Send button (visible when text is entered or when forceSendButton is true)
|
||||||
AnimatedVisibility(visible = messageContent.isNotBlank()) {
|
AnimatedVisibility(visible = (messageContent.isNotBlank() || forceSendButton) && !disabled) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onSendMessage,
|
onClick = onSendMessage,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
.size(56.dp),
|
.size(56.dp),
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
|
|
@ -136,9 +139,10 @@ fun MessageFieldPreview() {
|
||||||
MessageField(
|
MessageField(
|
||||||
showButtons = true,
|
showButtons = true,
|
||||||
onToggleButtons = {},
|
onToggleButtons = {},
|
||||||
messageContent = "",
|
messageContent = "Hello world!",
|
||||||
onMessageContentChange = {},
|
onMessageContentChange = {},
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
|
onAddAttachment = {},
|
||||||
channelType = ChannelType.TextChannel,
|
channelType = ChannelType.TextChannel,
|
||||||
channelName = "general"
|
channelName = "general"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.unit.sp
|
||||||
@Composable
|
@Composable
|
||||||
fun PageHeader(
|
fun PageHeader(
|
||||||
text: String,
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
|
|
@ -23,7 +24,7 @@ fun PageHeader(
|
||||||
textAlign = TextAlign.Left,
|
textAlign = TextAlign.Left,
|
||||||
fontSize = 24.sp
|
fontSize = 24.sp
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.padding(horizontal = 15.dp, vertical = 15.dp)
|
.padding(horizontal = 15.dp, vertical = 15.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package chat.revolt.components.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||||
|
import chat.revolt.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttachmentManager(
|
||||||
|
attachments: List<FileArgs>,
|
||||||
|
uploading: Boolean,
|
||||||
|
onRemove: (FileArgs) -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(uploading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
attachments.forEach { attachment ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
|
||||||
|
MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
onRemove(attachment)
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(attachment.filename, maxLines = 1)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.remove_attachment_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package chat.revolt.screens.chat.views
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -24,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
|
@ -43,10 +46,17 @@ import chat.revolt.R
|
||||||
import chat.revolt.RevoltTweenFloat
|
import chat.revolt.RevoltTweenFloat
|
||||||
import chat.revolt.RevoltTweenInt
|
import chat.revolt.RevoltTweenInt
|
||||||
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||||
import chat.revolt.components.chat.MessageField
|
import chat.revolt.components.chat.MessageField
|
||||||
import chat.revolt.components.generic.CollapsibleCard
|
import chat.revolt.components.generic.CollapsibleCard
|
||||||
import chat.revolt.components.generic.PageHeader
|
import chat.revolt.components.generic.PageHeader
|
||||||
import chat.revolt.components.screens.chat.ChannelIcon
|
import chat.revolt.components.screens.chat.ChannelIcon
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.uploadToAutumn
|
||||||
|
import chat.revolt.components.screens.chat.AttachmentManager
|
||||||
|
import io.ktor.http.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class ChannelScreenViewModel : ViewModel() {
|
class ChannelScreenViewModel : ViewModel() {
|
||||||
private var _channel by mutableStateOf<Channel?>(null)
|
private var _channel by mutableStateOf<Channel?>(null)
|
||||||
|
|
@ -87,6 +97,35 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
_showButtons = show
|
_showButtons = show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _attachments = mutableStateListOf<FileArgs>()
|
||||||
|
val attachments: List<FileArgs>
|
||||||
|
get() = _attachments
|
||||||
|
|
||||||
|
fun setAttachments(attachments: List<FileArgs>) {
|
||||||
|
_attachments.clear()
|
||||||
|
_attachments.addAll(attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAttachment(fileArgs: FileArgs) {
|
||||||
|
_attachments.add(fileArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(fileArgs: FileArgs) {
|
||||||
|
_attachments.remove(fileArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popAttachmentBatch() {
|
||||||
|
setAttachments(_attachments.drop(MAX_ATTACHMENTS_PER_MESSAGE))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _sendingMessage by mutableStateOf(false)
|
||||||
|
val sendingMessage: Boolean
|
||||||
|
get() = _sendingMessage
|
||||||
|
|
||||||
|
private fun setSendingMessage(sending: Boolean) {
|
||||||
|
_sendingMessage = sending
|
||||||
|
}
|
||||||
|
|
||||||
inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback {
|
inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback {
|
||||||
override fun onMessage(message: MessageFrame) {
|
override fun onMessage(message: MessageFrame) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -173,10 +212,37 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPendingMessage() {
|
fun sendPendingMessage() {
|
||||||
|
|
||||||
|
setSendingMessage(true)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
sendMessage(channel!!.id!!, messageContent)
|
val attachmentIds = arrayListOf<String>()
|
||||||
|
|
||||||
|
attachments.take(MAX_ATTACHMENTS_PER_MESSAGE).forEach {
|
||||||
|
try {
|
||||||
|
val id = uploadToAutumn(
|
||||||
|
it.file,
|
||||||
|
it.filename,
|
||||||
|
"attachments",
|
||||||
|
ContentType.parse(it.contentType)
|
||||||
|
)
|
||||||
|
Log.d("ChannelScreen", "Uploaded attachment with id $id")
|
||||||
|
attachmentIds.add(id)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ChannelScreen", "Failed to upload attachment", e)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(
|
||||||
|
channel!!.id!!,
|
||||||
|
messageContent,
|
||||||
|
attachments = if (attachmentIds.isEmpty()) null else attachmentIds
|
||||||
|
)
|
||||||
|
|
||||||
|
_messageContent = ""
|
||||||
|
popAttachmentBatch()
|
||||||
|
setSendingMessage(false)
|
||||||
}
|
}
|
||||||
_messageContent = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun typingMessageResource(): Int {
|
fun typingMessageResource(): Int {
|
||||||
|
|
@ -221,7 +287,10 @@ fun ChannelInfoScreen(
|
||||||
channelType = channel.channelType!!,
|
channelType = channel.channelType!!,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
)
|
||||||
PageHeader(text = channel.name ?: channel.id!!)
|
PageHeader(
|
||||||
|
text = channel.name ?: channel.id!!,
|
||||||
|
modifier = Modifier.offset((-8).dp, 0.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
|
@ -277,10 +346,39 @@ fun ChannelScreen(
|
||||||
viewModel: ChannelScreenViewModel = viewModel()
|
viewModel: ChannelScreenViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val channel = viewModel.channel
|
val channel = viewModel.channel
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val channelInfoOpen = remember { mutableStateOf(false) }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val channelInfoOpen = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val pickFileLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenMultipleDocuments()
|
||||||
|
) { uriList ->
|
||||||
|
uriList.let { uris ->
|
||||||
|
uris.forEach {
|
||||||
|
DocumentFile.fromSingleUri(context, it)?.let docfile@{ file ->
|
||||||
|
val mFile = File(context.cacheDir, file.name ?: "attachment")
|
||||||
|
|
||||||
|
mFile.outputStream().use { output ->
|
||||||
|
@Suppress("Recycle")
|
||||||
|
context.contentResolver.openInputStream(it)?.copyTo(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.addAttachment(
|
||||||
|
FileArgs(
|
||||||
|
file = mFile,
|
||||||
|
contentType = file.type ?: "application/octet-stream",
|
||||||
|
filename = file.name ?: "attachment"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(channelId) {
|
LaunchedEffect(channelId) {
|
||||||
viewModel.fetchChannel(channelId)
|
viewModel.fetchChannel(channelId)
|
||||||
}
|
}
|
||||||
|
|
@ -386,17 +484,27 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = viewModel.attachments.isNotEmpty()) {
|
||||||
channel.channelType?.let {
|
AttachmentManager(
|
||||||
MessageField(
|
attachments = viewModel.attachments,
|
||||||
showButtons = viewModel.showButtons,
|
uploading = viewModel.sendingMessage,
|
||||||
onToggleButtons = viewModel::setShowButtons,
|
onRemove = viewModel::removeAttachment
|
||||||
messageContent = viewModel.messageContent,
|
|
||||||
onMessageContentChange = viewModel::setMessageContent,
|
|
||||||
onSendMessage = viewModel::sendPendingMessage,
|
|
||||||
channelType = it,
|
|
||||||
channelName = channel.name ?: channel.id!!
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageField(
|
||||||
|
showButtons = viewModel.showButtons,
|
||||||
|
onToggleButtons = viewModel::setShowButtons,
|
||||||
|
messageContent = viewModel.messageContent,
|
||||||
|
onMessageContentChange = viewModel::setMessageContent,
|
||||||
|
onSendMessage = viewModel::sendPendingMessage,
|
||||||
|
onAddAttachment = {
|
||||||
|
pickFileLauncher.launch(arrayOf("*/*"))
|
||||||
|
},
|
||||||
|
channelType = channel.channelType!!,
|
||||||
|
channelName = channel.name ?: channel.id!!,
|
||||||
|
forceSendButton = viewModel.attachments.isNotEmpty(),
|
||||||
|
disabled = viewModel.sendingMessage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
|
|
||||||
<string name="send_alt">Send</string>
|
<string name="send_alt">Send</string>
|
||||||
<string name="add_attachment_alt">Add attachment</string>
|
<string name="add_attachment_alt">Add attachment</string>
|
||||||
|
<string name="remove_attachment_alt">Remove attachment</string>
|
||||||
<string name="show_more_alt">Show more</string>
|
<string name="show_more_alt">Show more</string>
|
||||||
|
|
||||||
<string name="tutorial">Welcome to Revolt\'s in-progress Android experience!</string>
|
<string name="tutorial">Welcome to Revolt\'s in-progress Android experience!</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue