Private
Public Access
1
0

More pleasant OOTB experience

This commit is contained in:
2024-04-10 23:35:54 -07:00
parent d474ce1c10
commit 6ed8e88bf0
7 changed files with 159 additions and 67 deletions

View File

@@ -2,12 +2,29 @@ package net.buzzert.kordophonedroid.ui.conversationlist
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.pullrefresh.PullRefreshIndicator
@@ -27,10 +44,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophonedroid.R
import net.buzzert.kordophonedroid.ui.Destination
import net.buzzert.kordophonedroid.ui.LocalNavController
import java.time.LocalDate
@@ -54,8 +72,12 @@ fun ConversationListScreen(
viewModel: ConversationListViewModel = hiltViewModel(),
) {
val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList())
val encounteredError by viewModel.encounteredConnectionError
ConversationListView(
conversations = conversations,
isConfigured = viewModel.isServerConfigured,
encounteredError = encounteredError,
onRefresh = suspend { viewModel.refresh() }
)
}
@@ -64,6 +86,8 @@ fun ConversationListScreen(
@OptIn(ExperimentalMaterialApi::class)
fun ConversationListView(
conversations: List<Conversation>,
isConfigured: Boolean = true,
encounteredError: Boolean = false,
onRefresh: suspend () -> Unit,
) {
val listState = rememberLazyListState()
@@ -77,6 +101,8 @@ fun ConversationListView(
refreshing = false
}
val showErrorScreen = conversations.isEmpty() && encounteredError
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = ::refresh)
val navController = LocalNavController.current
@@ -90,35 +116,49 @@ fun ConversationListView(
})
}
) {
Box(Modifier.pullRefresh(refreshState)) {
LazyColumn(
state = listState,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
items(conversations) { conversation ->
val clickHandler = {
val route = Destination.MessageList.createRoute(conversation.guid)
navController.navigate(route)
}
ConversationListItem(
name = conversation.formattedDisplayName(),
id = conversation.guid,
isUnread = conversation.unreadCount > 0,
lastMessagePreview = conversation.lastMessagePreview ?: "",
date = conversation.date,
onClick = clickHandler
)
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter),
if (showErrorScreen) {
NoContentView(
icon = R.drawable.error,
text = "Connection error",
onSettings = onSettingsInvoked
)
} else if (!isConfigured) {
NoContentView(
icon = R.drawable.storage,
text = "Server not configured",
onSettings = onSettingsInvoked
)
} else {
Box(Modifier.pullRefresh(refreshState)) {
LazyColumn(
state = listState,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
items(conversations) { conversation ->
val clickHandler = {
val route = Destination.MessageList.createRoute(conversation.guid)
navController.navigate(route)
}
ConversationListItem(
name = conversation.formattedDisplayName(),
id = conversation.guid,
isUnread = conversation.unreadCount > 0,
lastMessagePreview = conversation.lastMessagePreview ?: "",
date = conversation.date,
onClick = clickHandler
)
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}
}

View File

@@ -1,26 +1,22 @@
package net.buzzert.kordophonedroid.ui.conversationlist
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.cache
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.Authentication
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URL
import javax.inject.Inject
const val CL_VM_LOG: String = "ConversationListViewModel"
@@ -38,6 +34,14 @@ class ConversationListViewModel @Inject constructor(
.reversed()
}
val isServerConfigured: Boolean
get() = chatRepository.isConfigured
val encounteredConnectionError: State<Boolean>
get() = _encounteredConnectionError
private val _encounteredConnectionError = mutableStateOf(false)
init {
// Watch for config changes
viewModelScope.launch {
@@ -56,6 +60,12 @@ class ConversationListViewModel @Inject constructor(
}
}
}
viewModelScope.launch {
chatRepository.errorEncounteredChannel.collect {
_encounteredConnectionError.value = true
}
}
}
suspend fun refresh() {

View File

@@ -24,7 +24,7 @@ fun NoContentView(
@DrawableRes icon: Int,
text: String,
onSettings: () -> Unit,
modifier: Modifier,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>