feat: a real-time websocket connection

- more schemas
 - schemas for all sendable and receivable socket frames(packets)
 - multi-threading woohoo!
 - handling for ready event
 - sending out the ping event in 30-second intervals
 - connect-disconnect-reconnect logic (implemented, but to be used)
 - caches for all the things!
This commit is contained in:
Infi 2022-12-23 03:43:20 +01:00
parent 851bbebd43
commit 589c00d3ee
14 changed files with 651 additions and 69 deletions

View File

@ -1,20 +1,27 @@
package chat.revolt.api package chat.revolt.api
import android.os.Handler
import android.os.Looper
import android.util.Log
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.routes.user.fetchSelf import chat.revolt.api.routes.user.fetchSelf
import chat.revolt.api.routes.user.fetchSelfWithNewToken import chat.revolt.api.schemas.*
import chat.revolt.api.schemas.CompleteUser
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import io.ktor.client.plugins.websocket.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
const val REVOLT_BASE = "https://api.revolt.chat" const val REVOLT_BASE = "https://api.revolt.chat"
const val REVOLT_SUPPORT = "https://support.revolt.chat" const val REVOLT_SUPPORT = "https://support.revolt.chat"
const val REVOLT_MARKETING = "https://revolt.chat" const val REVOLT_MARKETING = "https://revolt.chat"
const val REVOLT_FILES = "https://autumn.revolt.chat" const val REVOLT_FILES = "https://autumn.revolt.chat"
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
private const val BACKEND_IS_STABLE = false private const val BACKEND_IS_STABLE = false
@ -26,6 +33,8 @@ val RevoltHttp = HttpClient(OkHttp) {
json(RevoltJson) json(RevoltJson)
} }
install(WebSockets)
if (BACKEND_IS_STABLE) { if (BACKEND_IS_STABLE) {
install(HttpRequestRetry) { install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 5) retryOnServerErrors(maxRetries = 5)
@ -46,23 +55,70 @@ val RevoltHttp = HttpClient(OkHttp) {
} }
} }
val mainHandler = Handler(Looper.getMainLooper())
object RevoltAPI { object RevoltAPI {
const val TOKEN_HEADER_NAME = "x-session-token" const val TOKEN_HEADER_NAME = "x-session-token"
// discount caching solution(/-s)! LRU would be better but this is fine for now, until it's not... // FIXME discount caching solutions! LRU would be better but this is fine for now
val userCache = val userCache = mutableMapOf<String, User>()
mutableMapOf<String, CompleteUser>() val serverCache = mutableMapOf<String, Server>()
val channelCache = mutableMapOf<String, Channel>()
val emojiCache = mutableMapOf<String, Emoji>()
val messageCache = mutableMapOf<String, Message>()
var selfId: String? = null var selfId: String? = null
var sessionToken: String = "" var sessionToken: String = ""
private set private set
private var socketThread: Thread? = null
fun setSessionHeader(token: String) { fun setSessionHeader(token: String) {
sessionToken = token sessionToken = token
} }
suspend fun loginAs(token: String) {
setSessionHeader(token)
fetchSelf()
startSocketOps()
}
suspend fun connectWS() {
socketThread = Thread {
try {
runBlocking {
RealtimeSocket.connect(sessionToken)
}
} catch (e: Exception) {
if (e is InterruptedException) {
Log.d("RevoltAPI", "Socket interrupted")
} else {
Log.e("RevoltAPI", "WebSocket error", e)
}
RealtimeSocket.open = false
}
}
socketThread!!.start()
}
private suspend fun startSocketOps() {
connectWS()
// Send a ping every roughly 30 seconds else the socket dies
// Same interval as the web clients (/revolt.js)
// Note: This will run even if the socket is closed (sendPing will just exit early)
mainHandler.post(object : Runnable {
override fun run() {
runBlocking {
RealtimeSocket.sendPing()
}
mainHandler.postDelayed(this, 30 * 1000)
}
})
}
suspend fun initialize() { suspend fun initialize() {
if (sessionToken != "") { if (sessionToken != "") {
fetchSelf() fetchSelf()
@ -85,6 +141,11 @@ object RevoltAPI {
sessionToken = "" sessionToken = ""
userCache.clear() userCache.clear()
serverCache.clear()
channelCache.clear()
emojiCache.clear()
socketThread?.interrupt()
} }
/** /**
@ -92,7 +153,8 @@ object RevoltAPI {
*/ */
suspend fun checkSessionToken(token: String): Boolean { suspend fun checkSessionToken(token: String): Boolean {
return try { return try {
fetchSelfWithNewToken(token) setSessionHeader(token)
fetchSelf()
true true
} catch (e: Exception) { } catch (e: Exception) {
false false
@ -100,5 +162,5 @@ object RevoltAPI {
} }
} }
@kotlinx.serialization.Serializable @Serializable
data class RevoltError(val type: String) data class RevoltError(val type: String)

View File

@ -0,0 +1,108 @@
package chat.revolt.api.realtime
import android.util.Log
import chat.revolt.api.REVOLT_WEBSOCKET
import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.api.realtime.frames.receivable.*
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
import chat.revolt.api.realtime.frames.sendable.PingFrame
import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import java.util.Calendar
object RealtimeSocket {
var socket: WebSocketSession? = null
var open: Boolean = false
suspend fun connect(token: String) {
RevoltHttp.ws(REVOLT_WEBSOCKET) {
socket = this
Log.d("RealtimeSocket", "Connected to websocket.")
open = true
// Send authorization frame
val authFrame = AuthorizationFrame("Authenticate", token)
val authFrameString =
RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame)
Log.d("RealtimeSocket", "Sending authorization frame: $authFrameString")
send(RevoltJson.encodeToString(AuthorizationFrame.serializer(), authFrame))
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val frameString = frame.readText()
val frameType =
RevoltJson.decodeFromString(AnyFrame.serializer(), frameString).type
handleFrame(frameType, frameString)
}
}
}
}
suspend fun sendPing() {
if (!open) return
val pingPacket = PingFrame("Ping", Calendar.getInstance().timeInMillis.toInt())
socket?.send(RevoltJson.encodeToString(PingFrame.serializer(), pingPacket))
Log.d("RealtimeSocket", "Sent ping frame with ${pingPacket.data}")
}
private fun handleFrame(type: String, rawFrame: String) {
when (type) {
"Pong" -> {
val pongFrame = RevoltJson.decodeFromString(PongFrame.serializer(), rawFrame)
Log.d("RealtimeSocket", "Received pong frame for ${pongFrame.data}")
}
"Bulk" -> {
val bulkFrame = RevoltJson.decodeFromString(BulkFrame.serializer(), rawFrame)
Log.d("RealtimeSocket", "Received bulk frame with ${bulkFrame.v.size} sub-frames.")
bulkFrame.v.forEach { subFrame ->
val subFrameType =
RevoltJson.decodeFromString(AnyFrame.serializer(), subFrame.toString()).type
handleFrame(subFrameType, subFrame.toString())
}
}
"Ready" -> {
val readyFrame = RevoltJson.decodeFromString(ReadyFrame.serializer(), rawFrame)
Log.d(
"RealtimeSocket",
"Received ready frame with ${readyFrame.users.size} users, ${readyFrame.servers.size} servers, ${readyFrame.channels.size} channels, and ${readyFrame.emojis.size} emojis."
)
Log.d("RealtimeSocket", "Adding users to cache.")
readyFrame.users.forEach { user ->
RevoltAPI.userCache[user.id!!] = user
}
Log.d("RealtimeSocket", "Adding servers to cache.")
readyFrame.servers.forEach { server ->
RevoltAPI.serverCache[server.id!!] = server
}
Log.d("RealtimeSocket", "Adding channels to cache.")
readyFrame.channels.forEach { channel ->
RevoltAPI.channelCache[channel.id!!] = channel
}
Log.d("RealtimeSocket", "Adding emojis to cache.")
readyFrame.emojis.forEach { emoji ->
RevoltAPI.emojiCache[emoji.id!!] = emoji
}
}
"UserUpdate" -> {
val userUpdateFrame =
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
}
else -> {
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
}
}
}
}

View File

@ -0,0 +1,239 @@
package chat.revolt.api.realtime.frames.receivable
import chat.revolt.api.schemas.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class AnyFrame(
val type: String,
)
@Serializable
data class ErrorFrame(
val type: String = "Error",
val error: String,
)
@Serializable
data class BulkFrame(
val type: String = "Bulk",
val v: List<JsonObject>
)
@Serializable
data class PongFrame(
val type: String = "Pong",
val data: Int
)
@Serializable
data class ReadyFrame(
val type: String = "Ready",
val users: List<User>,
val servers: List<Server>,
val channels: List<Channel>,
val emojis: List<Emoji>
)
typealias MessageFrame = Message
@Serializable
data class MessageUpdateFrame(
val type: String = "MessageUpdate",
val id: String,
val channel: String,
val data: JsonObject
)
@Serializable
data class Appendable(
val embeds: List<Embed>? = null,
)
@Serializable
data class MessageAppendFrame(
val type: String = "MessageAppend",
val id: String,
val channel: String,
val append: Appendable
)
@Serializable
data class MessageDeleteFrame(
val type: String = "MessageDelete",
val id: String,
val channel: String
)
@Serializable
data class MessageReactFrame(
val type: String = "MessageReact",
val id: String,
val channel_id: String,
val user_id: String,
val emoji_id: String,
)
@Serializable
data class MessageUnreactFrame(
val type: String = "MessageUnreact",
val id: String,
val channel_id: String,
val user_id: String,
val emoji_id: String,
)
@Serializable
data class MessageRemoveReactionFrame(
val type: String = "MessageRemoveReaction",
val id: String,
val channel_id: String,
val emoji_id: String,
)
/* ChannelCreate: we already have a "type" property in channel so we just alias the type */
typealias ChannelCreateFrame = Channel
@Serializable
data class ChannelUpdateFrame(
val type: String = "ChannelUpdate",
val id: String,
val data: Channel,
val clear: List<String>? = null // "Icon" or "Description"
)
@Serializable
data class ChannelDeleteFrame(
val type: String = "ChannelDelete",
val id: String
)
@Serializable
data class ChannelGroupJoinFrame(
val type: String = "ChannelGroupJoin",
val id: String,
val user: String
)
@Serializable
data class ChannelGroupLeaveFrame(
val type: String = "ChannelGroupLeave",
val id: String,
val user: String
)
@Serializable
data class ChannelStartTypingFrame(
val type: String = "ChannelStartTyping",
val id: String,
val user: String
)
@Serializable
data class ChannelStopTypingFrame(
val type: String = "ChannelStopTyping",
val id: String,
val user: String
)
@Serializable
data class ChannelAckFrame(
val type: String = "ChannelAck",
val id: String,
val user: String,
@SerialName("message_id")
val messageId: String
)
@Serializable
data class ServerCreateFrame(
val type: String = "ServerCreate",
val id: String,
val server: Server
)
@Serializable
data class ServerUpdateFrame(
val type: String = "ServerUpdate",
val id: String,
val data: Server,
val clear: List<String>? = null // "Icon", "Banner" or "Description"
)
@Serializable
data class ServerDeleteFrame(
val type: String = "ServerDelete",
val id: String
)
@Serializable
data class ServerUserChoice(
val server: String,
val user: String,
)
@Serializable
data class ServerMemberUpdateFrame(
val type: String = "ServerMemberUpdate",
val id: ServerUserChoice,
val data: Member,
val clear: List<String>? = null // "Nickname" or "Avatar"
)
@Serializable
data class ServerMemberJoinFrame(
val type: String = "ServerMemberJoin",
val id: String,
val user: String
)
@Serializable
data class ServerMemberLeaveFrame(
val type: String = "ServerMemberLeave",
val id: String,
val user: String
)
@Serializable
data class ServerRoleUpdateFrame(
val type: String = "ServerRoleUpdate",
val id: String,
@SerialName("role_id")
val roleId: String,
val data: Role,
val clear: List<String>? = null // "Colour"
)
@Serializable
data class ServerRoleDeleteFrame(
val type: String = "ServerRoleDelete",
val id: String,
@SerialName("role_id")
val roleId: String
)
@Serializable
data class UserUpdateFrame(
val type: String = "UserUpdate",
val id: String,
val data: User,
val clear: List<String>? = null // "ProfileContent", "ProfileBackground", "StatusText" or "Avatar"
)
@Serializable
data class UserRelationshipFrame(
val type: String = "UserRelationship",
val id: String,
val user: User,
val status: String,
)
typealias EmojiCreateFrame = Emoji
@Serializable
data class EmojiDeleteFrame(
val type: String = "EmojiDelete",
val id: String,
)

View File

@ -0,0 +1,27 @@
package chat.revolt.api.realtime.frames.sendable
import kotlinx.serialization.Serializable
@Serializable
data class AuthorizationFrame(
val type: String,
val token: String
)
@Serializable
data class PingFrame(
val type: String,
val data: Int
)
@Serializable
data class BeginTypingFrame(
val type: String,
val channel: String
)
@Serializable
data class EndTypingFrame(
val type: String,
val channel: String
)

View File

@ -4,12 +4,12 @@ import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltError import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson import chat.revolt.api.RevoltJson
import chat.revolt.api.schemas.CompleteUser import chat.revolt.api.schemas.User
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
suspend fun fetchSelf(): CompleteUser { suspend fun fetchSelf(): User {
val response = RevoltHttp.get("/users/@me") { val response = RevoltHttp.get("/users/@me") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
} }
@ -22,15 +22,10 @@ suspend fun fetchSelf(): CompleteUser {
// Not an error // Not an error
} }
val user = RevoltJson.decodeFromString(CompleteUser.serializer(), response) val user = RevoltJson.decodeFromString(User.serializer(), response)
RevoltAPI.userCache[user.id!!] = user RevoltAPI.userCache[user.id!!] = user
RevoltAPI.selfId = user.id RevoltAPI.selfId = user.id
return user return user
}
suspend fun fetchSelfWithNewToken(token: String): CompleteUser {
RevoltAPI.setSessionHeader(token)
return fetchSelf()
} }

View File

@ -8,7 +8,7 @@ import kotlinx.serialization.encoding.*
@Serializable @Serializable
data class MessagesInChannel( data class MessagesInChannel(
val messages: List<Message>? = null, val messages: List<Message>? = null,
val users: List<CompleteUser>? = null, val users: List<User>? = null,
val members: List<Member>? = null val members: List<Member>? = null
) )
@ -20,7 +20,61 @@ data class Member(
@SerialName("joined_at") @SerialName("joined_at")
val joinedAt: String? = null, val joinedAt: String? = null,
val avatar: Avatar? = null, val avatar: AutumnResource? = null,
val roles: List<String>? = null, val roles: List<String>? = null,
val nickname: String? = null val nickname: String? = null
) )
@Serializable
data class Channel(
val channelType: ChannelType? = null,
@SerialName("_id")
val id: String? = null,
val user: String? = null,
val name: String? = null,
val owner: String? = null,
val description: String? = null,
val recipients: List<String>? = null,
val icon: AutumnResource? = null,
val lastMessageID: String? = null,
val active: Boolean? = null,
val permissions: Long? = null,
val server: String? = null,
val rolePermissions: Map<String, DefaultPermissions>? = null,
val defaultPermissions: DefaultPermissions? = null,
val nsfw: Boolean? = null,
val type: String? = null, // this is _only_ used for websocket events!
)
@Serializable
enum class ChannelType(val value: String) {
DirectMessage("DirectMessage"),
Group("Group"),
SavedMessages("SavedMessages"),
TextChannel("TextChannel"),
VoiceChannel("VoiceChannel");
companion object : KSerializer<ChannelType> {
override val descriptor: SerialDescriptor
get() {
return PrimitiveSerialDescriptor(
"chat.revolt.api.schemas.ChannelType",
PrimitiveKind.STRING
)
}
override fun deserialize(decoder: Decoder): ChannelType =
when (val value = decoder.decodeString()) {
"DirectMessage" -> DirectMessage
"Group" -> Group
"SavedMessages" -> SavedMessages
"TextChannel" -> TextChannel
"VoiceChannel" -> VoiceChannel
else -> throw IllegalArgumentException("ChannelType could not parse: $value")
}
override fun serialize(encoder: Encoder, value: ChannelType) {
return encoder.encodeString(value.value)
}
}
}

View File

@ -0,0 +1,40 @@
package chat.revolt.api.schemas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AutumnResource(
@SerialName("_id")
val id: String? = null,
val tag: String? = null,
val filename: String? = null,
val metadata: Metadata? = null,
@SerialName("content_type")
val contentType: String? = null,
val size: Long? = null,
val deleted: Boolean? = null,
val reported: Boolean? = null,
@SerialName("message_id")
val messageID: String? = null,
@SerialName("user_id")
val userID: String? = null,
@SerialName("server_id")
val serverID: String? = null,
@SerialName("object_id")
val objectID: String? = null
)
@Serializable
data class Metadata(
val type: String? = null,
val width: Long? = null,
val height: Long? = null
)

View File

@ -7,17 +7,17 @@ import kotlinx.serialization.Serializable
data class Message( data class Message(
@SerialName("_id") @SerialName("_id")
val id: String? = null, val id: String? = null,
val nonce: String? = null, val nonce: String? = null,
val channel: String? = null, val channel: String? = null,
val author: String? = null, val author: String? = null,
val content: String? = null, val content: String? = null,
val reactions: Map<String, List<String>>? = null, val reactions: Map<String, List<String>>? = null,
val replies: List<String>? = null, val replies: List<String>? = null,
val attachments: List<Avatar>? = null, val attachments: List<AutumnResource>? = null,
val edited: String? = null, val edited: String? = null,
val embeds: List<Embed>? = null, val embeds: List<Embed>? = null,
val mentions: List<String>? = null val mentions: List<String>? = null,
val type: String? = null, // this is _only_ used for websocket events!
) )
@Serializable @Serializable

View File

@ -0,0 +1,70 @@
package chat.revolt.api.schemas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Server(
@SerialName("_id")
val id: String? = null,
val owner: String? = null,
val name: String? = null,
val description: String? = null,
val channels: List<String>? = null,
val categories: List<Category>? = null,
val systemMessages: SystemMessages? = null,
val roles: Map<String, Role>? = null,
val defaultPermissions: Long? = null,
val icon: AutumnResource? = null,
val banner: AutumnResource? = null,
val flags: Long? = null,
val analytics: Boolean? = null,
val discoverable: Boolean? = null,
)
@Serializable
data class Category(
val id: String? = null,
val title: String? = null,
val channels: List<String>? = null
)
@Serializable
data class SystemMessages(
val userJoined: String? = null,
val userLeft: String? = null,
val userKicked: String? = null,
val userBanned: String? = null
)
@Serializable
data class Role(
val name: String? = null,
val permissions: DefaultPermissions? = null,
val colour: String? = null,
val hoist: Boolean? = null,
val rank: Double? = null
)
@Serializable
data class DefaultPermissions(
val a: Long? = null,
val d: Long? = null
)
@Serializable
data class Emoji(
@SerialName("_id")
val id: String? = null,
val parent: EmojiParent? = null,
val creatorID: String? = null,
val name: String? = null,
val animated: Boolean? = null,
val type: String? = null, // this is _only_ used for websocket events!
)
@Serializable
data class EmojiParent(
val type: String? = null,
val id: String? = null
)

View File

@ -6,12 +6,11 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.* import kotlinx.serialization.encoding.*
@Serializable @Serializable
data class CompleteUser( data class User(
@SerialName("_id") @SerialName("_id")
val id: String? = null, val id: String? = null,
val username: String? = null, val username: String? = null,
val avatar: Avatar? = null, val avatar: AutumnResource? = null,
val relations: List<Relation>? = null, val relations: List<Relation>? = null,
val badges: Long? = null, val badges: Long? = null,
val status: Status? = null, val status: Status? = null,
@ -23,42 +22,6 @@ data class CompleteUser(
val online: Boolean? = null val online: Boolean? = null
) )
@Serializable
data class Avatar(
@SerialName("_id")
val id: String? = null,
val tag: String? = null,
val filename: String? = null,
val metadata: Metadata? = null,
@SerialName("content_type")
val contentType: String? = null,
val size: Long? = null,
val deleted: Boolean? = null,
val reported: Boolean? = null,
@SerialName("message_id")
val messageID: String? = null,
@SerialName("user_id")
val userID: String? = null,
@SerialName("server_id")
val serverID: String? = null,
@SerialName("object_id")
val objectID: String? = null
)
@Serializable
data class Metadata(
val type: String? = null,
val width: Long? = null,
val height: Long? = null
)
@Serializable @Serializable
data class Bot( data class Bot(
val owner: String? = null val owner: String? = null
@ -67,7 +30,7 @@ data class Bot(
@Serializable @Serializable
data class Profile( data class Profile(
val content: String? = null, val content: String? = null,
val background: Avatar? = null val background: AutumnResource? = null
) )
@Serializable @Serializable

View File

@ -1,7 +1,9 @@
package chat.revolt.screens.chat package chat.revolt.screens.chat
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -55,7 +57,7 @@ class HomeScreenViewModel @Inject constructor(
fun sendMessage() { fun sendMessage() {
viewModelScope.launch { viewModelScope.launch {
chat.revolt.api.routes.channel.sendMessage( chat.revolt.api.routes.channel.sendMessage(
"01F7ZSBSFHCAAJQ92ZGTY67HMN", "01F7ZSBSFHCAAJQ92ZGTY67HMN", // revolt lounge #general (temporarily hardcoded) FIXME
messageContent messageContent
) )
} }
@ -103,6 +105,28 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
} }
} }
// a scrollable list of all users in user cache
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
.verticalScroll(rememberScrollState())
.height(200.dp)
) {
RevoltAPI.userCache.forEach { (_, user) ->
Text(text = user.username ?: user.id ?: "null")
}
}
Column() { Column() {
FormTextField( FormTextField(
value = viewModel.messageContent, value = viewModel.messageContent,

View File

@ -62,9 +62,8 @@ class GreeterViewModel @Inject constructor(
} }
} }
RevoltAPI.initialize()
if (RevoltAPI.isLoggedIn()) { if (RevoltAPI.isLoggedIn()) {
RevoltAPI.loginAs(token ?: "")
_skipLogin = true _skipLogin = true
} }
@ -101,6 +100,7 @@ fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hi
.height(60.dp) .height(60.dp)
) )
} }
return
} }
Column( Column(

View File

@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.REVOLT_SUPPORT import chat.revolt.api.REVOLT_SUPPORT
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.account.EmailPasswordAssessment import chat.revolt.api.routes.account.EmailPasswordAssessment
import chat.revolt.api.routes.account.negotiateAuthentication import chat.revolt.api.routes.account.negotiateAuthentication
import chat.revolt.api.routes.user.fetchSelfWithNewToken
import chat.revolt.components.generic.AnyLink import chat.revolt.components.generic.AnyLink
import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.FormTextField
import chat.revolt.components.generic.Weblink import chat.revolt.components.generic.Weblink
@ -76,7 +76,7 @@ class LoginViewModel @Inject constructor(
) )
try { try {
fetchSelfWithNewToken(response.firstUserHints.token) RevoltAPI.loginAs(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateTo = "home" _navigateTo = "home"

View File

@ -23,11 +23,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.account.MfaResponseRecoveryCode import chat.revolt.api.routes.account.MfaResponseRecoveryCode
import chat.revolt.api.routes.account.MfaResponseTotpCode import chat.revolt.api.routes.account.MfaResponseTotpCode
import chat.revolt.api.routes.account.authenticateWithMfaRecoveryCode import chat.revolt.api.routes.account.authenticateWithMfaRecoveryCode
import chat.revolt.api.routes.account.authenticateWithMfaTotpCode import chat.revolt.api.routes.account.authenticateWithMfaTotpCode
import chat.revolt.api.routes.user.fetchSelfWithNewToken
import chat.revolt.components.generic.CollapsibleCard import chat.revolt.components.generic.CollapsibleCard
import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.FormTextField
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
@ -80,7 +80,7 @@ class MfaScreenViewModel @Inject constructor(
) )
try { try {
fetchSelfWithNewToken(response.firstUserHints.token) RevoltAPI.loginAs(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateToHome = true _navigateToHome = true
@ -105,7 +105,7 @@ class MfaScreenViewModel @Inject constructor(
) )
try { try {
fetchSelfWithNewToken(response.firstUserHints.token) RevoltAPI.loginAs(response.firstUserHints.token)
kvStorage.set("sessionToken", response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token)
_navigateToHome = true _navigateToHome = true