diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index c535584e..cf950bc2 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -1,20 +1,27 @@ 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.fetchSelfWithNewToken -import chat.revolt.api.schemas.CompleteUser +import chat.revolt.api.schemas.* import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* +import io.ktor.client.plugins.websocket.* import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import kotlinx.serialization.Serializable const val REVOLT_BASE = "https://api.revolt.chat" const val REVOLT_SUPPORT = "https://support.revolt.chat" const val REVOLT_MARKETING = "https://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 @@ -26,6 +33,8 @@ val RevoltHttp = HttpClient(OkHttp) { json(RevoltJson) } + install(WebSockets) + if (BACKEND_IS_STABLE) { install(HttpRequestRetry) { retryOnServerErrors(maxRetries = 5) @@ -46,23 +55,70 @@ val RevoltHttp = HttpClient(OkHttp) { } } +val mainHandler = Handler(Looper.getMainLooper()) object RevoltAPI { 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... - val userCache = - mutableMapOf() + // FIXME discount caching solutions! LRU would be better but this is fine for now + val userCache = mutableMapOf() + val serverCache = mutableMapOf() + val channelCache = mutableMapOf() + val emojiCache = mutableMapOf() + val messageCache = mutableMapOf() var selfId: String? = null var sessionToken: String = "" private set + private var socketThread: Thread? = null + fun setSessionHeader(token: String) { 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() { if (sessionToken != "") { fetchSelf() @@ -85,6 +141,11 @@ object RevoltAPI { sessionToken = "" userCache.clear() + serverCache.clear() + channelCache.clear() + emojiCache.clear() + + socketThread?.interrupt() } /** @@ -92,7 +153,8 @@ object RevoltAPI { */ suspend fun checkSessionToken(token: String): Boolean { return try { - fetchSelfWithNewToken(token) + setSessionHeader(token) + fetchSelf() true } catch (e: Exception) { false @@ -100,5 +162,5 @@ object RevoltAPI { } } -@kotlinx.serialization.Serializable +@Serializable data class RevoltError(val type: String) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt new file mode 100644 index 00000000..d5a5369b --- /dev/null +++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt @@ -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") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt b/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt new file mode 100644 index 00000000..2e017403 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt @@ -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 +) + +@Serializable +data class PongFrame( + val type: String = "Pong", + val data: Int +) + +@Serializable +data class ReadyFrame( + val type: String = "Ready", + val users: List, + val servers: List, + val channels: List, + val emojis: List +) + +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? = 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? = 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? = 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? = 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? = 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? = 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, +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/frames/sendable/SendableFrames.kt b/app/src/main/java/chat/revolt/api/realtime/frames/sendable/SendableFrames.kt new file mode 100644 index 00000000..5b5309b9 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/realtime/frames/sendable/SendableFrames.kt @@ -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 +) diff --git a/app/src/main/java/chat/revolt/api/routes/user/User.kt b/app/src/main/java/chat/revolt/api/routes/user/User.kt index 42d12ac2..48965904 100644 --- a/app/src/main/java/chat/revolt/api/routes/user/User.kt +++ b/app/src/main/java/chat/revolt/api/routes/user/User.kt @@ -4,12 +4,12 @@ import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltError import chat.revolt.api.RevoltHttp 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.statement.* import kotlinx.serialization.SerializationException -suspend fun fetchSelf(): CompleteUser { +suspend fun fetchSelf(): User { val response = RevoltHttp.get("/users/@me") { headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) } @@ -22,15 +22,10 @@ suspend fun fetchSelf(): CompleteUser { // Not an error } - val user = RevoltJson.decodeFromString(CompleteUser.serializer(), response) + val user = RevoltJson.decodeFromString(User.serializer(), response) RevoltAPI.userCache[user.id!!] = user RevoltAPI.selfId = user.id return user -} - -suspend fun fetchSelfWithNewToken(token: String): CompleteUser { - RevoltAPI.setSessionHeader(token) - return fetchSelf() } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Channel.kt b/app/src/main/java/chat/revolt/api/schemas/Channel.kt index 8ff04a90..b301480e 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Channel.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Channel.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.encoding.* @Serializable data class MessagesInChannel( val messages: List? = null, - val users: List? = null, + val users: List? = null, val members: List? = null ) @@ -20,7 +20,61 @@ data class Member( @SerialName("joined_at") val joinedAt: String? = null, - val avatar: Avatar? = null, + val avatar: AutumnResource? = null, val roles: List? = null, val nickname: String? = null -) \ No newline at end of file +) + +@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? = null, + val icon: AutumnResource? = null, + val lastMessageID: String? = null, + val active: Boolean? = null, + val permissions: Long? = null, + val server: String? = null, + val rolePermissions: Map? = 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 { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Generic.kt b/app/src/main/java/chat/revolt/api/schemas/Generic.kt new file mode 100644 index 00000000..47d7b269 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Generic.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt index 7b2ac396..80743867 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -7,17 +7,17 @@ import kotlinx.serialization.Serializable data class Message( @SerialName("_id") val id: String? = null, - val nonce: String? = null, val channel: String? = null, val author: String? = null, val content: String? = null, val reactions: Map>? = null, val replies: List? = null, - val attachments: List? = null, + val attachments: List? = null, val edited: String? = null, val embeds: List? = null, - val mentions: List? = null + val mentions: List? = null, + val type: String? = null, // this is _only_ used for websocket events! ) @Serializable diff --git a/app/src/main/java/chat/revolt/api/schemas/Server.kt b/app/src/main/java/chat/revolt/api/schemas/Server.kt new file mode 100644 index 00000000..1207b522 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Server.kt @@ -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? = null, + val categories: List? = null, + val systemMessages: SystemMessages? = null, + val roles: Map? = 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? = 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 +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/User.kt b/app/src/main/java/chat/revolt/api/schemas/User.kt index 4b2a492a..4229a108 100644 --- a/app/src/main/java/chat/revolt/api/schemas/User.kt +++ b/app/src/main/java/chat/revolt/api/schemas/User.kt @@ -6,12 +6,11 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @Serializable -data class CompleteUser( +data class User( @SerialName("_id") val id: String? = null, - val username: String? = null, - val avatar: Avatar? = null, + val avatar: AutumnResource? = null, val relations: List? = null, val badges: Long? = null, val status: Status? = null, @@ -23,42 +22,6 @@ data class CompleteUser( 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 data class Bot( val owner: String? = null @@ -67,7 +30,7 @@ data class Bot( @Serializable data class Profile( val content: String? = null, - val background: Avatar? = null + val background: AutumnResource? = null ) @Serializable diff --git a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt index 84861a96..9cbeacb0 100644 --- a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt @@ -1,7 +1,9 @@ package chat.revolt.screens.chat import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState 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 @@ -55,7 +57,7 @@ class HomeScreenViewModel @Inject constructor( fun sendMessage() { viewModelScope.launch { chat.revolt.api.routes.channel.sendMessage( - "01F7ZSBSFHCAAJQ92ZGTY67HMN", + "01F7ZSBSFHCAAJQ92ZGTY67HMN", // revolt lounge #general (temporarily hardcoded) FIXME 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() { FormTextField( value = viewModel.messageContent, diff --git a/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt b/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt index 5e388e1c..892d05d9 100644 --- a/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt @@ -62,9 +62,8 @@ class GreeterViewModel @Inject constructor( } } - RevoltAPI.initialize() - if (RevoltAPI.isLoggedIn()) { + RevoltAPI.loginAs(token ?: "") _skipLogin = true } @@ -101,6 +100,7 @@ fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hi .height(60.dp) ) } + return } Column( diff --git a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt index 6971b4a0..08981fb7 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt @@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.REVOLT_SUPPORT +import chat.revolt.api.RevoltAPI import chat.revolt.api.routes.account.EmailPasswordAssessment 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.FormTextField import chat.revolt.components.generic.Weblink @@ -76,7 +76,7 @@ class LoginViewModel @Inject constructor( ) try { - fetchSelfWithNewToken(response.firstUserHints.token) + RevoltAPI.loginAs(response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token) _navigateTo = "home" diff --git a/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt b/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt index 668434bd..88d94bbf 100644 --- a/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt @@ -23,11 +23,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import chat.revolt.R +import chat.revolt.api.RevoltAPI import chat.revolt.api.routes.account.MfaResponseRecoveryCode import chat.revolt.api.routes.account.MfaResponseTotpCode import chat.revolt.api.routes.account.authenticateWithMfaRecoveryCode 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.FormTextField import chat.revolt.persistence.KVStorage @@ -80,7 +80,7 @@ class MfaScreenViewModel @Inject constructor( ) try { - fetchSelfWithNewToken(response.firstUserHints.token) + RevoltAPI.loginAs(response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token) _navigateToHome = true @@ -105,7 +105,7 @@ class MfaScreenViewModel @Inject constructor( ) try { - fetchSelfWithNewToken(response.firstUserHints.token) + RevoltAPI.loginAs(response.firstUserHints.token) kvStorage.set("sessionToken", response.firstUserHints.token) _navigateToHome = true