parent
b92d5369c1
commit
872889fefd
|
|
@ -99,6 +99,12 @@ object RevoltAPI {
|
||||||
Log.e("RevoltAPI", "WebSocket error", e)
|
Log.e("RevoltAPI", "WebSocket error", e)
|
||||||
}
|
}
|
||||||
RealtimeSocket.open = false
|
RealtimeSocket.open = false
|
||||||
|
|
||||||
|
Log.i("RevoltAPI", "Reconnecting in 2.5 seconds...")
|
||||||
|
Thread.sleep(2500)
|
||||||
|
runBlocking {
|
||||||
|
connectWS()
|
||||||
|
} // FIXME ASAP move this to ChatRouting (whenever that's a thing) and do it properly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
socketThread!!.start()
|
socketThread!!.start()
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,12 @@ object RealtimeSocket {
|
||||||
"UserUpdate" -> {
|
"UserUpdate" -> {
|
||||||
val userUpdateFrame =
|
val userUpdateFrame =
|
||||||
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
||||||
// We will genuinely just ignore this frame for now, but it gets really spammy in the logs
|
|
||||||
// FIXME handle this frame
|
val existing = RevoltAPI.userCache[userUpdateFrame.id]
|
||||||
|
?: return // if we don't have the user no point in updating it
|
||||||
|
|
||||||
|
RevoltAPI.userCache[userUpdateFrame.id] =
|
||||||
|
existing.mergeWithPartial(userUpdateFrame.data)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
|
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
|
||||||
|
|
|
||||||
|
|
@ -28,4 +28,34 @@ suspend fun fetchSelf(): User {
|
||||||
RevoltAPI.selfId = user.id
|
RevoltAPI.selfId = user.id
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchUser(id: String): User {
|
||||||
|
val response = RevoltHttp.get("/users/$id") {
|
||||||
|
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||||
|
}
|
||||||
|
.bodyAsText()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||||
|
throw Error(error.type)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = RevoltJson.decodeFromString(User.serializer(), response)
|
||||||
|
|
||||||
|
RevoltAPI.userCache[user.id!!] = user
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getOrFetchUser(id: String): User {
|
||||||
|
return RevoltAPI.userCache[id] ?: fetchUser(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addUserIfUnknown(id: String) {
|
||||||
|
if (RevoltAPI.userCache[id] == null) {
|
||||||
|
RevoltAPI.userCache[id] = fetchUser(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,9 +27,10 @@ data class Member(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Channel(
|
data class Channel(
|
||||||
val channelType: ChannelType? = null,
|
|
||||||
@SerialName("_id")
|
@SerialName("_id")
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
|
@SerialName("channel_type")
|
||||||
|
val channelType: ChannelType? = null,
|
||||||
val user: String? = null,
|
val user: String? = null,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val owner: String? = null,
|
val owner: String? = null,
|
||||||
|
|
@ -46,7 +47,28 @@ data class Channel(
|
||||||
val defaultPermissions: DefaultPermissions? = null,
|
val defaultPermissions: DefaultPermissions? = null,
|
||||||
val nsfw: Boolean? = null,
|
val nsfw: Boolean? = null,
|
||||||
val type: String? = null, // this is _only_ used for websocket events!
|
val type: String? = null, // this is _only_ used for websocket events!
|
||||||
)
|
) {
|
||||||
|
fun mergeWithPartial(partial: Channel): Channel {
|
||||||
|
return Channel(
|
||||||
|
channelType = partial.channelType ?: channelType,
|
||||||
|
id = partial.id ?: id,
|
||||||
|
user = partial.user ?: user,
|
||||||
|
name = partial.name ?: name,
|
||||||
|
owner = partial.owner ?: owner,
|
||||||
|
description = partial.description ?: description,
|
||||||
|
recipients = partial.recipients ?: recipients,
|
||||||
|
icon = partial.icon ?: icon,
|
||||||
|
lastMessageID = partial.lastMessageID ?: lastMessageID,
|
||||||
|
active = partial.active ?: active,
|
||||||
|
permissions = partial.permissions ?: permissions,
|
||||||
|
server = partial.server ?: server,
|
||||||
|
rolePermissions = partial.rolePermissions ?: rolePermissions,
|
||||||
|
defaultPermissions = partial.defaultPermissions ?: defaultPermissions,
|
||||||
|
nsfw = partial.nsfw ?: nsfw,
|
||||||
|
type = partial.type ?: type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ChannelType(val value: String) {
|
enum class ChannelType(val value: String) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,23 @@ data class Message(
|
||||||
fun getAuthor(): User? {
|
fun getAuthor(): User? {
|
||||||
return author?.let { RevoltAPI.userCache[it] }
|
return author?.let { RevoltAPI.userCache[it] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mergeWithPartial(partial: Message): Message {
|
||||||
|
return Message(
|
||||||
|
id = partial.id ?: id,
|
||||||
|
nonce = partial.nonce ?: nonce,
|
||||||
|
channel = partial.channel ?: channel,
|
||||||
|
author = partial.author ?: author,
|
||||||
|
content = partial.content ?: content,
|
||||||
|
reactions = partial.reactions ?: reactions,
|
||||||
|
replies = partial.replies ?: replies,
|
||||||
|
attachments = partial.attachments ?: attachments,
|
||||||
|
edited = partial.edited ?: edited,
|
||||||
|
embeds = partial.embeds ?: embeds,
|
||||||
|
mentions = partial.mentions ?: mentions,
|
||||||
|
type = partial.type ?: type
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,24 @@ data class User(
|
||||||
val bot: Bot? = null,
|
val bot: Bot? = null,
|
||||||
val relationship: String? = null,
|
val relationship: String? = null,
|
||||||
val online: Boolean? = null
|
val online: Boolean? = null
|
||||||
)
|
) {
|
||||||
|
fun mergeWithPartial(partial: User): User {
|
||||||
|
return User(
|
||||||
|
id = partial.id ?: id,
|
||||||
|
username = partial.username ?: username,
|
||||||
|
avatar = partial.avatar ?: avatar,
|
||||||
|
relations = partial.relations ?: relations,
|
||||||
|
badges = partial.badges ?: badges,
|
||||||
|
status = partial.status ?: status,
|
||||||
|
profile = partial.profile ?: profile,
|
||||||
|
flags = partial.flags ?: flags,
|
||||||
|
privileged = partial.privileged ?: privileged,
|
||||||
|
bot = partial.bot ?: bot,
|
||||||
|
relationship = partial.relationship ?: relationship,
|
||||||
|
online = partial.online ?: online
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Bot(
|
data class Bot(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.revolt.api.REVOLT_BASE
|
||||||
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Message(
|
||||||
|
message: MessageSchema
|
||||||
|
) {
|
||||||
|
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
||||||
|
|
||||||
|
Row() {
|
||||||
|
if (author.avatar != null) {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/avatars/${author.avatar.id!!}/user.png",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(50.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
description = "Avatar for ${author.username}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_BASE/users/${author.id}/default_avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(50.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
description = "Avatar for ${author.username}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
|
author.username?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
message.content?.let {
|
||||||
|
Text(
|
||||||
|
text = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
|
import androidx.compose.material.icons.filled.Send
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.schemas.ChannelType
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MessageField(
|
||||||
|
showButtons: Boolean,
|
||||||
|
onToggleButtons: (Boolean) -> Unit,
|
||||||
|
messageContent: String,
|
||||||
|
onMessageContentChange: (String) -> Unit,
|
||||||
|
onSendMessage: () -> Unit,
|
||||||
|
channelType: ChannelType,
|
||||||
|
channelName: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val placeholderResource = when (channelType) {
|
||||||
|
ChannelType.DirectMessage -> R.string.message_field_placeholder_dm
|
||||||
|
ChannelType.Group -> R.string.message_field_placeholder_group
|
||||||
|
ChannelType.TextChannel -> R.string.message_field_placeholder_text
|
||||||
|
ChannelType.VoiceChannel -> R.string.message_field_placeholder_voice
|
||||||
|
ChannelType.SavedMessages -> R.string.message_field_placeholder_notes
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier) {
|
||||||
|
// Additional buttons (currently adding an attachment)
|
||||||
|
AnimatedVisibility(visible = showButtons) {
|
||||||
|
Row(Modifier.weight(1f)) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Placeholder for adding an attachment",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
shape = CircleShape
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.add_attachment_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The small chevron you see when the buttons are hidden
|
||||||
|
AnimatedVisibility(visible = !showButtons) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.height(56.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.KeyboardArrowLeft,
|
||||||
|
contentDescription = stringResource(id = R.string.show_more_alt),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp + 8.dp)
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clickable(onClick = {
|
||||||
|
onToggleButtons(true)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = messageContent,
|
||||||
|
onValueChange = onMessageContentChange,
|
||||||
|
singleLine = false,
|
||||||
|
shape = RoundedCornerShape(100),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
id = placeholderResource,
|
||||||
|
channelName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
errorIndicatorColor = Color.Transparent,
|
||||||
|
placeholderColor = Color.Gray,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send button (visible when text is entered)
|
||||||
|
AnimatedVisibility(visible = messageContent.isNotBlank()) {
|
||||||
|
Button(
|
||||||
|
onClick = onSendMessage,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
shape = CircleShape
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Send,
|
||||||
|
contentDescription = stringResource(id = R.string.send_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,33 @@
|
||||||
package chat.revolt.screens.chat
|
package chat.revolt.screens.chat
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||||
|
import chat.revolt.api.routes.channel.sendMessage
|
||||||
|
import chat.revolt.api.routes.user.addUserIfUnknown
|
||||||
import chat.revolt.api.schemas.Channel
|
import chat.revolt.api.schemas.Channel
|
||||||
import chat.revolt.api.schemas.Message
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
import chat.revolt.components.chat.Message
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.components.chat.MessageField
|
||||||
|
|
||||||
class ChannelScreenViewModel : ViewModel() {
|
class ChannelScreenViewModel : ViewModel() {
|
||||||
private var _channel by mutableStateOf<Channel?>(null)
|
private var _channel by mutableStateOf<Channel?>(null)
|
||||||
|
|
@ -25,22 +38,59 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
val callbacks: RealtimeSocket.ChannelCallback?
|
val callbacks: RealtimeSocket.ChannelCallback?
|
||||||
get() = _callbacks.value
|
get() = _callbacks.value
|
||||||
|
|
||||||
private var _renderableMessages = mutableStateListOf<Message>()
|
private var _renderableMessages = mutableStateListOf<MessageSchema>()
|
||||||
val renderableMessages: List<Message>
|
val renderableMessages: List<MessageSchema>
|
||||||
get() = _renderableMessages
|
get() = _renderableMessages
|
||||||
|
|
||||||
|
private var _typingUsers = mutableStateListOf<String>()
|
||||||
|
val typingUsers: List<String>
|
||||||
|
get() = _typingUsers
|
||||||
|
|
||||||
|
private var _messageContent by mutableStateOf("")
|
||||||
|
val messageContent: String
|
||||||
|
get() = _messageContent
|
||||||
|
|
||||||
|
fun setMessageContent(content: String) {
|
||||||
|
_messageContent = content
|
||||||
|
|
||||||
|
if (content.isEmpty()) {
|
||||||
|
_showButtons = true
|
||||||
|
} else if (showButtons) {
|
||||||
|
_showButtons = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _showButtons by mutableStateOf(true)
|
||||||
|
val showButtons: Boolean
|
||||||
|
get() = _showButtons
|
||||||
|
|
||||||
|
fun setShowButtons(show: Boolean) {
|
||||||
|
_showButtons = show
|
||||||
|
}
|
||||||
|
|
||||||
inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback {
|
inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback {
|
||||||
override fun onMessage(message: MessageFrame) {
|
override fun onMessage(message: MessageFrame) {
|
||||||
Log.d("ChannelScreen", "onMessage: $message")
|
viewModelScope.launch {
|
||||||
|
addUserIfUnknown(message.author!!)
|
||||||
|
}
|
||||||
|
|
||||||
_renderableMessages.add(message)
|
_renderableMessages.add(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTyping(typing: ChannelStartTypingFrame) {
|
override fun onStartTyping(typing: ChannelStartTypingFrame) {
|
||||||
Log.d("ChannelScreen", "onStartTyping: $typing")
|
viewModelScope.launch {
|
||||||
|
addUserIfUnknown(typing.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_typingUsers.contains(typing.user)) {
|
||||||
|
_typingUsers.add(typing.user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopTyping(typing: ChannelStopTypingFrame) {
|
override fun onStopTyping(typing: ChannelStopTypingFrame) {
|
||||||
Log.d("ChannelScreen", "onStopTyping: $typing")
|
if (_typingUsers.contains(typing.user)) {
|
||||||
|
_typingUsers.remove(typing.user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,15 +108,40 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
|
|
||||||
registerCallback()
|
registerCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendPendingMessage() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
sendMessage(channel!!.id!!, messageContent)
|
||||||
|
}
|
||||||
|
_messageContent = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun typingMessageResource(): Int {
|
||||||
|
return when (typingUsers.size) {
|
||||||
|
0 -> R.string.typing_blank
|
||||||
|
1 -> R.string.typing_one
|
||||||
|
in 2..4 -> R.string.typing_many
|
||||||
|
else -> R.string.typing_several
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTypingUsernames(): String {
|
||||||
|
return typingUsers.joinToString {
|
||||||
|
RevoltAPI.userCache[it]?.let { u ->
|
||||||
|
u.username ?: u.id
|
||||||
|
} ?: it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChannelScreen(
|
fun ChannelScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
channelId: String,
|
channelId: String,
|
||||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
viewModel: ChannelScreenViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val channel = viewModel.channel
|
val channel = viewModel.channel
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
LaunchedEffect(channelId) {
|
LaunchedEffect(channelId) {
|
||||||
viewModel.fetchChannel(channelId)
|
viewModel.fetchChannel(channelId)
|
||||||
|
|
@ -86,10 +161,45 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Text(text = channel.name!!)
|
Text(text = "#" + channel.name!!)
|
||||||
|
|
||||||
viewModel.renderableMessages.forEach {
|
Divider()
|
||||||
Text(text = "[" + it.getAuthor()!!.username + "] " + it.content!!)
|
|
||||||
|
// Column nesting is needed to make the vertical scroll work properly
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Column(Modifier.verticalScroll(scrollState)) {
|
||||||
|
viewModel.renderableMessages.forEach {
|
||||||
|
Message(message = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = viewModel.typingUsers.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = viewModel.typingMessageResource(),
|
||||||
|
viewModel.getTypingUsernames()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
channel.channelType?.let {
|
||||||
|
MessageField(
|
||||||
|
showButtons = viewModel.showButtons,
|
||||||
|
onToggleButtons = viewModel::setShowButtons,
|
||||||
|
messageContent = viewModel.messageContent,
|
||||||
|
onMessageContentChange = viewModel::setMessageContent,
|
||||||
|
onSendMessage = viewModel::sendPendingMessage,
|
||||||
|
channelType = it,
|
||||||
|
channelName = channel.name
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,15 +2,10 @@ package chat.revolt.screens.chat
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Send
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -19,11 +14,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.api.REVOLT_FILES
|
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.components.generic.FormTextField
|
|
||||||
import chat.revolt.components.generic.RemoteImage
|
|
||||||
import chat.revolt.components.screens.home.LinkOnHome
|
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -63,8 +54,6 @@ class HomeScreenViewModel @Inject constructor(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
|
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
|
||||||
val user = RevoltAPI.userCache[RevoltAPI.selfId]
|
|
||||||
|
|
||||||
val channelDrawerState = rememberDrawerState(DrawerValue.Closed)
|
val channelDrawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
@ -111,67 +100,7 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
|
||||||
.padding(horizontal = 15.dp, vertical = 15.dp)
|
.padding(horizontal = 15.dp, vertical = 15.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.fillMaxSize()
|
|
||||||
.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
user?.let {
|
|
||||||
Row {
|
|
||||||
RemoteImage(
|
|
||||||
url = "${REVOLT_FILES}/avatars/${it.avatar?.id}/user.png",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(50.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
description = "Avatar for ${it.username} (placeholder!)"
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
|
||||||
it.username?.let { it1 -> Text(text = it1) }
|
|
||||||
it.id?.let { it1 -> Text(text = it1) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "User cache",
|
|
||||||
style = MaterialTheme.typography.displaySmall.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Left,
|
|
||||||
fontSize = 24.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 15.dp, vertical = 15.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Column(modifier = Modifier.height(200.dp)) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
RevoltAPI.userCache.forEach { (_, user) ->
|
|
||||||
Text(text = user.username ?: user.id ?: "null")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column() {
|
|
||||||
FormTextField(
|
|
||||||
value = viewModel.messageContent,
|
|
||||||
label = "Content",
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onChange = viewModel::setMessageContent
|
|
||||||
)
|
|
||||||
LinkOnHome(
|
|
||||||
heading = "Send",
|
|
||||||
icon = Icons.Filled.Send,
|
|
||||||
onClick = viewModel::sendMessage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.logout()
|
viewModel.logout()
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,11 @@ class LoginViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
"Login",
|
"Login",
|
||||||
"No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}"
|
"No MFA required. Login is complete! We should have a session token"
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
RevoltAPI.loginAs(response.firstUserHints!!.token)
|
||||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||||
|
|
||||||
_navigateTo = "home"
|
_navigateTo = "home"
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,11 @@ class MfaScreenViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
"MFA",
|
"MFA",
|
||||||
"Successfully authorized TOTP. Token: ${response.firstUserHints!!.token}"
|
"Successfully authorized with TOTP."
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
RevoltAPI.loginAs(response.firstUserHints!!.token)
|
||||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||||
|
|
||||||
_navigateToHome = true
|
_navigateToHome = true
|
||||||
|
|
@ -101,11 +101,11 @@ class MfaScreenViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
"MFA",
|
"MFA",
|
||||||
"Successfully authorized recovery code. Token: ${response.firstUserHints!!.token}"
|
"Successfully authorized with a recovery code."
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RevoltAPI.loginAs(response.firstUserHints.token)
|
RevoltAPI.loginAs(response.firstUserHints!!.token)
|
||||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||||
|
|
||||||
_navigateToHome = true
|
_navigateToHome = true
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,26 @@
|
||||||
|
|
||||||
<string name="comingsoon_heading">Gah, you found me!</string>
|
<string name="comingsoon_heading">Gah, you found me!</string>
|
||||||
<string name="comingsoon_body">The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection..</string>
|
<string name="comingsoon_body">The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection..</string>
|
||||||
|
|
||||||
|
<string name="typing_blank"><!-- this is a hack to prevent the typing indicator from showing typing_several when it's animating away --></string>
|
||||||
|
<string name="typing_one">%1$s is typing…</string>
|
||||||
|
<string name="typing_many">%1$s are typing…</string>
|
||||||
|
<string name="typing_several">Several people are typing</string>
|
||||||
|
|
||||||
|
<string name="send_alt">Send</string>
|
||||||
|
<string name="add_attachment_alt">Add attachment</string>
|
||||||
|
<string name="show_more_alt">Show more</string>
|
||||||
|
|
||||||
|
<string name="tutorial">Welcome to Revolt\'s in-progress Android experience!</string>
|
||||||
|
<string name="select_channel">Select a server and channel by swiping from the left.</string>
|
||||||
|
|
||||||
|
<string name="avatar_alt">%1$s\'s avatar</string>
|
||||||
|
|
||||||
|
<string name="message_field_placeholder_dm">Message @%1$s</string>
|
||||||
|
<string name="message_field_placeholder_text">Message #%1$s</string>
|
||||||
|
<string name="message_field_placeholder_voice">Message #%1$s</string>
|
||||||
|
<string name="message_field_placeholder_group">Message %1$s</string>
|
||||||
|
<string name="message_field_placeholder_notes">Add a note</string>
|
||||||
|
|
||||||
|
<string name="reply_message_not_cached">Unknown message, tap to jump</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in New Issue