diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt
index ae5ae224..ce78e3df 100644
--- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt
+++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt
@@ -4,6 +4,7 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
+import chat.revolt.api.realtime.DisconnectionState
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.routes.user.fetchSelf
import chat.revolt.api.schemas.*
@@ -98,13 +99,7 @@ object RevoltAPI {
} else {
Log.e("RevoltAPI", "WebSocket error", e)
}
- 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
+ RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected)
}
}
socketThread!!.start()
diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt
index 21caf8cb..5e9d4e46 100644
--- a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt
+++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt
@@ -2,6 +2,7 @@ package chat.revolt.api.realtime
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
import chat.revolt.api.REVOLT_WEBSOCKET
import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltHttp
@@ -14,16 +15,29 @@ import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import java.util.Calendar
+enum class DisconnectionState {
+ Disconnected,
+ Reconnecting,
+ Connected
+}
+
object RealtimeSocket {
var socket: WebSocketSession? = null
- var open: Boolean = false
+
+ private var _disconnectionState = mutableStateOf(DisconnectionState.Disconnected)
+ val disconnectionState: DisconnectionState
+ get() = _disconnectionState.value
+
+ fun updateDisconnectionState(state: DisconnectionState) {
+ _disconnectionState.value = state
+ }
suspend fun connect(token: String) {
RevoltHttp.ws(REVOLT_WEBSOCKET) {
socket = this
Log.d("RealtimeSocket", "Connected to websocket.")
- open = true
+ updateDisconnectionState(DisconnectionState.Connected)
// Send authorization frame
val authFrame = AuthorizationFrame("Authenticate", token)
@@ -46,7 +60,7 @@ object RealtimeSocket {
}
suspend fun sendPing() {
- if (!open) return
+ if (disconnectionState != DisconnectionState.Connected) return
val pingPacket = PingFrame("Ping", Calendar.getInstance().timeInMillis.toInt())
socket?.send(RevoltJson.encodeToString(PingFrame.serializer(), pingPacket))
diff --git a/app/src/main/java/chat/revolt/components/chat/DisconnectedNotice.kt b/app/src/main/java/chat/revolt/components/chat/DisconnectedNotice.kt
new file mode 100644
index 00000000..c4af2107
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/chat/DisconnectedNotice.kt
@@ -0,0 +1,82 @@
+package chat.revolt.components.chat
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import chat.revolt.R
+import chat.revolt.api.realtime.DisconnectionState
+
+@Composable
+private fun DisconnectedNoticeBase(
+ background: Color,
+ icon: ImageVector,
+ text: String,
+ canTapToRetry: Boolean = false,
+ onRetry: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(background)
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ .clickable(enabled = canTapToRetry, onClick = onRetry),
+ ) {
+ Icon(
+ modifier = Modifier.padding(end = 8.dp),
+ imageVector = icon,
+ contentDescription = null
+ )
+ Text(
+ text = text,
+ fontWeight = FontWeight.Bold
+ )
+ if (canTapToRetry) {
+ Text(
+ text = stringResource(R.string.tap_to_reconnect),
+ modifier = Modifier.padding(start = 8.dp),
+ fontWeight = FontWeight.Normal
+ )
+ }
+ }
+}
+
+@Composable
+fun DisconnectedNotice(
+ state: DisconnectionState,
+ onReconnect: () -> Unit
+) {
+ when (state) {
+ DisconnectionState.Disconnected -> DisconnectedNoticeBase(
+ background = Color(0xfffe4654),
+ icon = Icons.Default.Warning,
+ text = stringResource(id = R.string.disconnected),
+ canTapToRetry = true,
+ onRetry = onReconnect
+ )
+ DisconnectionState.Reconnecting -> DisconnectedNoticeBase(
+ background = Color(0xfffcb205),
+ icon = Icons.Default.Refresh,
+ text = stringResource(id = R.string.reconnecting),
+ )
+ DisconnectionState.Connected -> DisconnectedNoticeBase(
+ background = Color(0xff4b9f6a),
+ icon = Icons.Default.Done,
+ text = stringResource(id = R.string.reconnected),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/components/screens/splash/DisconnectedScreen.kt b/app/src/main/java/chat/revolt/components/screens/splash/DisconnectedScreen.kt
new file mode 100644
index 00000000..be1d68a1
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/screens/splash/DisconnectedScreen.kt
@@ -0,0 +1,57 @@
+package chat.revolt.components.screens.splash
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import chat.revolt.R
+
+@Composable
+fun DisconnectedScreen(
+ onRetry: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.surface),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(R.string.no_connection),
+ style = MaterialTheme.typography.displaySmall.copy(
+ fontSize = 30.sp,
+ fontWeight = FontWeight.Black,
+ textAlign = TextAlign.Center
+ ),
+ modifier = Modifier
+ .padding(horizontal = 20.dp, vertical = 10.dp)
+ .fillMaxWidth(),
+ )
+
+ Text(
+ text = stringResource(R.string.no_connection_message),
+ color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
+ style = MaterialTheme.typography.titleMedium.copy(
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.Normal,
+ ),
+ modifier = Modifier
+ .padding(horizontal = 20.dp, vertical = 10.dp)
+ .fillMaxWidth()
+ )
+
+ Button(onClick = onRetry) {
+ Text(stringResource(R.string.tap_to_retry))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/SplashScreen.kt b/app/src/main/java/chat/revolt/screens/SplashScreen.kt
index 5332e8cc..0cf20b46 100644
--- a/app/src/main/java/chat/revolt/screens/SplashScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/SplashScreen.kt
@@ -1,5 +1,9 @@
package chat.revolt.screens
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -17,15 +21,20 @@ import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.drawableResource
+import chat.revolt.components.screens.splash.DisconnectedScreen
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
+@SuppressLint("StaticFieldLeak")
class SplashScreenViewModel @Inject constructor(
- private val kvStorage: KVStorage
+ private val kvStorage: KVStorage,
+ @ApplicationContext private val context: Context
) : ViewModel() {
+
private var _navigateTo by mutableStateOf("")
val navigateTo: String
get() = _navigateTo
@@ -34,8 +43,36 @@ class SplashScreenViewModel @Inject constructor(
_navigateTo = value
}
- init {
+ private var _isConnected by mutableStateOf(false)
+ val isConnected: Boolean
+ get() = _isConnected
+
+ fun setIsConnected(value: Boolean) {
+ _isConnected = value
+ }
+
+ private fun hasInternetConnection(): Boolean {
+ val connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ val networkCapabilities = connectivityManager.activeNetwork ?: return false
+ val actNw =
+ connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
+
+ return when {
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
+ else -> false
+ }
+ }
+
+ fun checkLoggedInState() {
viewModelScope.launch {
+ setIsConnected(hasInternetConnection())
+
+ if (!isConnected) return@launch
+
val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login")
val valid = RevoltAPI.checkSessionToken(token)
@@ -49,10 +86,23 @@ class SplashScreenViewModel @Inject constructor(
}
}
}
+
+ init {
+ checkLoggedInState()
+ }
}
@Composable
fun SplashScreen(navController: NavController, viewModel: SplashScreenViewModel = hiltViewModel()) {
+ if (!viewModel.isConnected) {
+ DisconnectedScreen(
+ onRetry = {
+ viewModel.checkLoggedInState()
+ }
+ )
+ return
+ }
+
Column(
modifier = Modifier
.fillMaxWidth()
diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
index 0b503f4f..c1f8173b 100644
--- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
@@ -1,5 +1,6 @@
package chat.revolt.screens.chat
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -33,7 +34,10 @@ import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.RemoteImage
import chat.revolt.screens.chat.views.HomeScreen
import chat.revolt.R
+import chat.revolt.api.realtime.DisconnectionState
+import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.schemas.ChannelType
+import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.screens.chat.DrawerChannel
import chat.revolt.screens.chat.views.ChannelScreen
import kotlinx.coroutines.launch
@@ -60,120 +64,130 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
- DismissibleNavigationDrawer(
- drawerState = channelDrawerState,
- drawerContent = {
- ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant) {
- Column(Modifier.fillMaxWidth()) {
- Row {
- Column(
- modifier = Modifier
- .verticalScroll(rememberScrollState())
- .background(MaterialTheme.colorScheme.surface)
- ) {
- IconButton(
- onClick = { viewModel.goToHome() },
+ Column() {
+ AnimatedVisibility(visible = RealtimeSocket.disconnectionState != DisconnectionState.Connected) {
+ DisconnectedNotice(
+ state = RealtimeSocket.disconnectionState,
+ onReconnect = {
+ RealtimeSocket.updateDisconnectionState(DisconnectionState.Reconnecting)
+ scope.launch { RevoltAPI.connectWS() }
+ })
+ }
+ DismissibleNavigationDrawer(
+ drawerState = channelDrawerState,
+ drawerContent = {
+ ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant) {
+ Column(Modifier.fillMaxWidth()) {
+ Row {
+ Column(
modifier = Modifier
- .padding(8.dp)
- .size(48.dp)
+ .verticalScroll(rememberScrollState())
+ .background(MaterialTheme.colorScheme.surface)
) {
- Icon(
- Icons.Default.Home,
- contentDescription = stringResource(id = R.string.home),
- modifier = Modifier.padding(4.dp)
- )
- }
-
- RevoltAPI.serverCache.values.forEach { server ->
- if (server.name == null) return@forEach
-
- if (server.icon != null) {
- RemoteImage(
- url = "$REVOLT_FILES/icons/${server.icon.id!!}/server.png?max_side=256",
- modifier = Modifier
- .padding(8.dp)
- .size(48.dp)
- .clip(CircleShape)
- .clickable { viewModel.setCurrentServer(server.id!!) },
- description = "${server.name}"
+ IconButton(
+ onClick = { viewModel.goToHome() },
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ ) {
+ Icon(
+ Icons.Default.Home,
+ contentDescription = stringResource(id = R.string.home),
+ modifier = Modifier.padding(4.dp)
)
- } else {
- // return a placeholder icon, currently the first letter of the server name in a circle
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .padding(8.dp)
- .size(48.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.surfaceVariant)
- .clickable { viewModel.setCurrentServer(server.id!!) }
- ) {
- Text(
- text = server.name.first().toString(),
- fontSize = 20.sp,
- fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.onSurface
+ }
+
+ RevoltAPI.serverCache.values.forEach { server ->
+ if (server.name == null) return@forEach
+
+ if (server.icon != null) {
+ RemoteImage(
+ url = "$REVOLT_FILES/icons/${server.icon.id!!}/server.png?max_side=256",
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ .clip(CircleShape)
+ .clickable { viewModel.setCurrentServer(server.id!!) },
+ description = "${server.name}"
)
+ } else {
+ // return a placeholder icon, currently the first letter of the server name in a circle
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .padding(8.dp)
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .clickable { viewModel.setCurrentServer(server.id!!) }
+ ) {
+ Text(
+ text = server.name.first().toString(),
+ fontSize = 20.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
}
}
}
- }
- Crossfade(targetState = viewModel.currentServer) {
- Column(
- Modifier
- .weight(1f)
- ) {
- if (it == "home") {
- Column(
- Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
- ) {
- RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.DirectMessage }
- .forEach { channel ->
- DrawerChannel(
- name = "DM #${channel.id}", // TODO get user or group name
- channelType = ChannelType.DirectMessage,
- selected = channel.id == (navBackStackEntry?.arguments?.getString(
- "channelId"
- ) ?: false),
- onClick = {
- navController.navigate("channel/${channel.id}")
- scope.launch {
- channelDrawerState.close()
+ Crossfade(targetState = viewModel.currentServer) {
+ Column(
+ Modifier
+ .weight(1f)
+ ) {
+ if (it == "home") {
+ Column(
+ Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.DirectMessage }
+ .forEach { channel ->
+ DrawerChannel(
+ name = "DM #${channel.id}", // TODO get user or group name
+ channelType = ChannelType.DirectMessage,
+ selected = channel.id == (navBackStackEntry?.arguments?.getString(
+ "channelId"
+ ) ?: false),
+ onClick = {
+ navController.navigate("channel/${channel.id}")
+ scope.launch {
+ channelDrawerState.close()
+ }
}
- }
- )
- }
- }
- } else {
- val server = RevoltAPI.serverCache[it]
+ )
+ }
+ }
+ } else {
+ val server = RevoltAPI.serverCache[it]
- Text(
- text = server?.name ?: stringResource(R.string.unknown),
- fontWeight = FontWeight.Black,
- fontSize = 24.sp,
- modifier = Modifier.padding(16.dp)
- )
+ Text(
+ text = server?.name ?: stringResource(R.string.unknown),
+ fontWeight = FontWeight.Black,
+ fontSize = 24.sp,
+ modifier = Modifier.padding(16.dp)
+ )
- Column(
- Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
- ) {
- server?.channels?.forEach { channelId ->
- RevoltAPI.channelCache[channelId]?.let { ch ->
- DrawerChannel(
- name = ch.name!!,
- channelType = ch.channelType!!,
- selected = navBackStackEntry?.arguments?.getString(
- "channelId"
- ) == ch.id,
- onClick = {
- scope.launch { channelDrawerState.close() }
- navController.navigate("channel/${ch.id}")
- })
+ Column(
+ Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ server?.channels?.forEach { channelId ->
+ RevoltAPI.channelCache[channelId]?.let { ch ->
+ DrawerChannel(
+ name = ch.name!!,
+ channelType = ch.channelType!!,
+ selected = navBackStackEntry?.arguments?.getString(
+ "channelId"
+ ) == ch.id,
+ onClick = {
+ scope.launch { channelDrawerState.close() }
+ navController.navigate("channel/${ch.id}")
+ })
+ }
}
}
}
@@ -182,18 +196,19 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
}
}
}
- }
- }
- ) {
- Column(Modifier.fillMaxSize()) {
- NavHost(navController = navController, startDestination = "home") {
- composable("home") {
- HomeScreen(navController = navController)
- }
- composable("channel/{channelId}") { backStackEntry ->
- val channelId = backStackEntry.arguments?.getString("channelId")
- if (channelId != null) {
- ChannelScreen(navController, channelId = channelId)
+ },
+ modifier = Modifier.weight(1f),
+ ) {
+ Column(Modifier.fillMaxSize()) {
+ NavHost(navController = navController, startDestination = "home") {
+ composable("home") {
+ HomeScreen(navController = navController)
+ }
+ composable("channel/{channelId}") { backStackEntry ->
+ val channelId = backStackEntry.arguments?.getString("channelId")
+ if (channelId != null) {
+ ChannelScreen(navController, channelId = channelId)
+ }
}
}
}
diff --git a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt
index b9f1efba..0edbfa62 100644
--- a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt
@@ -1,21 +1,24 @@
package chat.revolt.screens.chat.views
import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.api.RevoltAPI
+import chat.revolt.components.screens.home.LinkOnHome
import chat.revolt.persistence.KVStorage
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.launch
+import chat.revolt.R
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@@ -23,37 +26,19 @@ import javax.inject.Inject
class HomeScreenViewModel @Inject constructor(
private val kvStorage: KVStorage
) : ViewModel() {
- private var _messageContent by mutableStateOf("")
- val messageContent: String
- get() = _messageContent
-
- fun setMessageContent(value: String) {
- _messageContent = value
- }
-
fun logout() {
runBlocking {
kvStorage.remove("sessionToken")
RevoltAPI.logout()
}
}
-
- fun sendMessage() {
- viewModelScope.launch {
- chat.revolt.api.routes.channel.sendMessage(
- "01F7ZSBSFHCAAJQ92ZGTY67HMN", // revolt lounge #general (temporarily hardcoded) FIXME
- messageContent
- )
- }
- setMessageContent("")
- }
}
@Composable
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
Column() {
Text(
- text = "Home (placeholder)",
+ text = stringResource(id = R.string.home),
style = MaterialTheme.typography.displaySmall.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Left,
@@ -64,7 +49,9 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
.fillMaxWidth(),
)
- Button(
+ LinkOnHome(
+ heading = stringResource(id = R.string.logout),
+ icon = Icons.Default.Close,
onClick = {
viewModel.logout()
navController.navigate("login/greeting") {
@@ -72,12 +59,6 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
inclusive = true
}
}
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 30.dp, top = 5.dp, start = 20.dp, end = 20.dp)
- ) {
- Text("Logout")
- }
+ })
}
}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c37dc587..cc7be5f8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -75,7 +75,9 @@
Select a server and channel by swiping from the left.
Unknown
+
Home
+ Log out
%1$s\'s avatar
@@ -92,4 +94,13 @@
Add a note
Unknown message, tap to jump
+
+ Disconnected
+ Tap to reconnect
+ Reconnecting…
+ Reconnected
+
+ No connection
+ You are not connected to the internet. Please check your connection and try again.
+ Tap to retry
\ No newline at end of file