feat: receive messages
this is honestly half a prototype img: https://autumn.revolt.chat/attachments/GbiNbIf_JysZ10m4ULxGl4SfoOZiK8lj5ZL8fMs39r/image.png
This commit is contained in:
parent
a5288e4ed7
commit
77138cd521
|
|
@ -1 +1,2 @@
|
|||
/build
|
||||
/build
|
||||
/release
|
||||
|
|
@ -16,6 +16,7 @@ import chat.revolt.screens.SplashScreen
|
|||
import chat.revolt.screens.about.AboutScreen
|
||||
import chat.revolt.screens.about.AttributionScreen
|
||||
import chat.revolt.screens.about.PlaceholderScreen
|
||||
import chat.revolt.screens.chat.ChannelScreen
|
||||
import chat.revolt.screens.chat.HomeScreen
|
||||
import chat.revolt.screens.login.GreeterScreen
|
||||
import chat.revolt.screens.login.LoginScreen
|
||||
|
|
@ -92,6 +93,11 @@ fun AppEntrypoint() {
|
|||
}
|
||||
|
||||
composable("chat/home") { HomeScreen(navController) }
|
||||
composable("chat/channel/{channelId}") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
|
||||
ChannelScreen(navController, channelId)
|
||||
}
|
||||
|
||||
composable("about") { AboutScreen(navController) }
|
||||
composable("about/oss") { AttributionScreen(navController) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package chat.revolt.api.realtime
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import chat.revolt.api.REVOLT_WEBSOCKET
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
|
|
@ -94,6 +95,43 @@ object RealtimeSocket {
|
|||
RevoltAPI.emojiCache[emoji.id!!] = emoji
|
||||
}
|
||||
}
|
||||
"Message" -> {
|
||||
val messageFrame = RevoltJson.decodeFromString(MessageFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received message frame for ${messageFrame.id} in channel ${messageFrame.channel}."
|
||||
)
|
||||
|
||||
RevoltAPI.messageCache[messageFrame.id!!] = messageFrame
|
||||
|
||||
channelCallbacks[messageFrame.channel]?.forEach { callback ->
|
||||
callback.onMessage(messageFrame)
|
||||
}
|
||||
}
|
||||
"ChannelStartTyping" -> {
|
||||
val typingFrame =
|
||||
RevoltJson.decodeFromString(ChannelStartTypingFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received channel start typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
||||
)
|
||||
|
||||
channelCallbacks[typingFrame.id]?.forEach { callback ->
|
||||
callback.onStartTyping(typingFrame)
|
||||
}
|
||||
}
|
||||
"ChannelStopTyping" -> {
|
||||
val typingFrame =
|
||||
RevoltJson.decodeFromString(ChannelStopTypingFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received channel stop typing frame for ${typingFrame.id} from ${typingFrame.user}."
|
||||
)
|
||||
|
||||
channelCallbacks[typingFrame.id]?.forEach { callback ->
|
||||
callback.onStopTyping(typingFrame)
|
||||
}
|
||||
}
|
||||
"UserUpdate" -> {
|
||||
val userUpdateFrame =
|
||||
RevoltJson.decodeFromString(UserUpdateFrame.serializer(), rawFrame)
|
||||
|
|
@ -105,4 +143,26 @@ object RealtimeSocket {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelCallback {
|
||||
fun onStartTyping(typing: ChannelStartTypingFrame)
|
||||
fun onStopTyping(typing: ChannelStopTypingFrame)
|
||||
fun onMessage(message: MessageFrame)
|
||||
}
|
||||
|
||||
private val channelCallbacks: MutableMap<String, List<ChannelCallback>> = mutableStateMapOf()
|
||||
|
||||
fun registerChannelCallback(channelId: String, callback: ChannelCallback) {
|
||||
val callbacks = channelCallbacks[channelId] ?: emptyList()
|
||||
channelCallbacks[channelId] = callbacks + callback
|
||||
|
||||
Log.d("RealtimeSocket", "Registered channel callback for $channelId.")
|
||||
}
|
||||
|
||||
fun unregisterChannelCallback(channelId: String, callback: ChannelCallback) {
|
||||
val callbacks = channelCallbacks[channelId] ?: emptyList()
|
||||
channelCallbacks[channelId] = callbacks - callback
|
||||
|
||||
Log.d("RealtimeSocket", "Unregistered channel callback for $channelId")
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,9 @@ data class Channel(
|
|||
val active: Boolean? = null,
|
||||
val permissions: Long? = null,
|
||||
val server: String? = null,
|
||||
@SerialName("role_permissions")
|
||||
val rolePermissions: Map<String, DefaultPermissions>? = null,
|
||||
@SerialName("default_permissions")
|
||||
val defaultPermissions: DefaultPermissions? = null,
|
||||
val nsfw: Boolean? = null,
|
||||
val type: String? = null, // this is _only_ used for websocket events!
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -18,7 +19,11 @@ data class Message(
|
|||
val embeds: List<Embed>? = null,
|
||||
val mentions: List<String>? = null,
|
||||
val type: String? = null, // this is _only_ used for websocket events!
|
||||
)
|
||||
) {
|
||||
fun getAuthor(): User? {
|
||||
return author?.let { RevoltAPI.userCache[it] }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Embed(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package chat.revolt.screens.chat
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.NavController
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.realtime.RealtimeSocket
|
||||
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.MessageFrame
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import chat.revolt.api.schemas.Message
|
||||
|
||||
class ChannelScreenViewModel : ViewModel() {
|
||||
private var _channel by mutableStateOf<Channel?>(null)
|
||||
val channel: Channel?
|
||||
get() = _channel
|
||||
|
||||
private var _callbacks = mutableStateOf<RealtimeSocket.ChannelCallback?>(null)
|
||||
val callbacks: RealtimeSocket.ChannelCallback?
|
||||
get() = _callbacks.value
|
||||
|
||||
private var _renderableMessages = mutableStateListOf<Message>()
|
||||
val renderableMessages: List<Message>
|
||||
get() = _renderableMessages
|
||||
|
||||
inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback {
|
||||
override fun onMessage(message: MessageFrame) {
|
||||
Log.d("ChannelScreen", "onMessage: $message")
|
||||
_renderableMessages.add(message)
|
||||
}
|
||||
|
||||
override fun onStartTyping(typing: ChannelStartTypingFrame) {
|
||||
Log.d("ChannelScreen", "onStartTyping: $typing")
|
||||
}
|
||||
|
||||
override fun onStopTyping(typing: ChannelStopTypingFrame) {
|
||||
Log.d("ChannelScreen", "onStopTyping: $typing")
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerCallback() {
|
||||
_callbacks.value = ChannelScreenCallback()
|
||||
RealtimeSocket.registerChannelCallback(channel!!.id!!, callbacks!!)
|
||||
}
|
||||
|
||||
fun fetchChannel(id: String) {
|
||||
if (id in RevoltAPI.channelCache) {
|
||||
_channel = RevoltAPI.channelCache[id]
|
||||
} else {
|
||||
Log.e("ChannelScreen", "Channel $id not in cache, for now this is fatal!") // FIXME
|
||||
}
|
||||
|
||||
registerCallback()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
navController: NavController,
|
||||
channelId: String,
|
||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
val channel = viewModel.channel
|
||||
|
||||
LaunchedEffect(channelId) {
|
||||
viewModel.fetchChannel(channelId)
|
||||
}
|
||||
|
||||
DisposableEffect(channelId) {
|
||||
onDispose {
|
||||
viewModel.callbacks?.let {
|
||||
RealtimeSocket.unregisterChannelCallback(channelId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == null) {
|
||||
CircularProgressIndicator()
|
||||
return
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(text = channel.name!!)
|
||||
|
||||
viewModel.renderableMessages.forEach {
|
||||
Text(text = "[" + it.getAuthor()!!.username + "] " + it.content!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,8 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -61,52 +56,52 @@ class HomeScreenViewModel @Inject constructor(
|
|||
messageContent
|
||||
)
|
||||
}
|
||||
setMessageContent("")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
|
||||
val user = RevoltAPI.userCache[RevoltAPI.selfId]
|
||||
|
||||
Column() {
|
||||
Text(
|
||||
text = "Home (placeholder)",
|
||||
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
|
||||
.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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
val channelDrawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DismissibleNavigationDrawer(drawerState = channelDrawerState, drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "User cache",
|
||||
text = "Revolt Lounge",
|
||||
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Black)
|
||||
)
|
||||
Divider()
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
RevoltAPI.channelCache.values
|
||||
.filter { channel ->
|
||||
channel.server == "01F7ZSBSFHQ8TA81725KQCSDDP"
|
||||
}
|
||||
.forEach { channel ->
|
||||
NavigationDrawerItem(
|
||||
selected = false,
|
||||
label = { Text(text = "#" + channel.name) },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
channelDrawerState.close()
|
||||
navController.navigate("chat/channel/${channel.id}")
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Column() {
|
||||
Text(
|
||||
text = "Home (placeholder)",
|
||||
style = MaterialTheme.typography.displaySmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Left,
|
||||
|
|
@ -118,42 +113,80 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
|
|||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.height(200.dp)
|
||||
.padding(10.dp)
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
RevoltAPI.userCache.forEach { (_, user) ->
|
||||
Text(text = user.username ?: user.id ?: "null")
|
||||
}
|
||||
}
|
||||
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() {
|
||||
FormTextField(
|
||||
value = viewModel.messageContent,
|
||||
label = "Content",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onChange = viewModel::setMessageContent
|
||||
)
|
||||
LinkOnHome(
|
||||
heading = "Send",
|
||||
icon = Icons.Filled.Send,
|
||||
onClick = viewModel::sendMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.logout()
|
||||
navController.navigate("login/greeting") {
|
||||
popUpTo("chat/home") {
|
||||
inclusive = true
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
it.username?.let { it1 -> Text(text = it1) }
|
||||
it.id?.let { it1 -> Text(text = it1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 30.dp, top = 5.dp, start = 20.dp, end = 20.dp)
|
||||
) {
|
||||
Text("Logout")
|
||||
|
||||
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(
|
||||
onClick = {
|
||||
viewModel.logout()
|
||||
navController.navigate("login/greeting") {
|
||||
popUpTo("chat/home") {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 30.dp, top = 5.dp, start = 20.dp, end = 20.dp)
|
||||
) {
|
||||
Text("Logout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ val DarkColorScheme = darkColorScheme(
|
|||
onBackground = Color(FOREGROUND),
|
||||
surfaceVariant = Color(0xff172333),
|
||||
onSurfaceVariant = Color(FOREGROUND),
|
||||
surface = Color(0xff111a26),
|
||||
onSurface = Color(FOREGROUND),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
Loading…
Reference in New Issue