binding muted state to a icon within the room ist

This commit is contained in:
Adam Brown 2022-11-02 14:03:16 +00:00
parent e986b44959
commit 92639beb73
11 changed files with 95 additions and 54 deletions

View File

@ -20,13 +20,13 @@ suspend fun CachedPreferences.readBoolean(key: String, defaultValue: Boolean) =
suspend fun Preferences.readBoolean(key: String) = this.readString(key)?.toBooleanStrict()
suspend fun Preferences.store(key: String, value: Boolean) = this.store(key, value.toString())
suspend fun Preferences.append(key: String, value: String) {
suspend fun Preferences.append(key: String, value: String): Set<String> {
val current = this.readStrings(key) ?: emptySet()
this.store(key, current + value)
return (current + value).also { this.store(key, it) }
}
suspend fun Preferences.removeFromSet(key: String, value: String) {
suspend fun Preferences.removeFromSet(key: String, value: String): Set<String> {
val current = this.readStrings(key) ?: emptySet()
this.store(key, current - value)
return (current - value).also { this.store(key, it) }
}

View File

@ -34,13 +34,17 @@ class StoreModule(
private val coroutineDispatchers: CoroutineDispatchers,
) {
private val muteableStore by unsafeLazy { MutedStorePersistence(preferences) }
fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers)
fun roomStore(): RoomStore = RoomPersistence(
database = database,
overviewPersistence = OverviewPersistence(database, coroutineDispatchers),
coroutineDispatchers = coroutineDispatchers,
muteableStore = MutedStorePersistence(preferences),
)
fun roomStore(): RoomStore {
return RoomPersistence(
database = database,
overviewPersistence = OverviewPersistence(database, coroutineDispatchers),
coroutineDispatchers = coroutineDispatchers,
muteableStore = muteableStore,
)
}
fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences)
fun syncStore(): SyncStore = SyncTokenPreferences(preferences)

View File

@ -5,6 +5,10 @@ import app.dapk.st.core.append
import app.dapk.st.core.removeFromSet
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.MuteableStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onStart
private const val KEY_MUTE = "mute"
@ -12,20 +16,23 @@ internal class MutedStorePersistence(
private val preferences: Preferences
) : MuteableStore {
private val allMutedFlow = MutableSharedFlow<Set<RoomId>>(replay = 1)
override suspend fun mute(roomId: RoomId) {
preferences.append(KEY_MUTE, roomId.value)
preferences.append(KEY_MUTE, roomId.value).notifyChange()
}
override suspend fun unmute(roomId: RoomId) {
preferences.removeFromSet(KEY_MUTE, roomId.value)
preferences.removeFromSet(KEY_MUTE, roomId.value).notifyChange()
}
override suspend fun isMuted(roomId: RoomId): Boolean {
val allMuted = allMuted()
return allMuted.contains(roomId)
}
private suspend fun Set<String>.notifyChange() = allMutedFlow.emit(this.map { RoomId(it) }.toSet())
override suspend fun allMuted(): Set<RoomId> {
override suspend fun isMuted(roomId: RoomId) = allMutedFlow.firstOrNull()?.contains(roomId) ?: false
override fun observeMuted(): Flow<Set<RoomId>> = allMutedFlow.onStart { emit(readAll()) }
private suspend fun readAll(): Set<RoomId> {
return preferences.readStrings(KEY_MUTE)?.map { RoomId(it) }?.toSet() ?: emptySet()
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.directory
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -10,6 +11,13 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Speaker
import androidx.compose.material.icons.filled.VolumeMute
import androidx.compose.material.icons.filled.VolumeOff
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.SpeakerNotesOff
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -198,36 +206,24 @@ private fun DirectoryItem(room: DirectoryItem, onClick: (RoomId) -> Unit, clock:
)
}
if (hasUnread) {
if (hasUnread || room.isMuted) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.weight(1f)) {
body(overview, secondaryText, room.typing)
}
Spacer(modifier = Modifier.width(6.dp))
Box(Modifier.align(Alignment.CenterVertically)) {
Box(
Modifier
.align(Alignment.Center)
.background(color = MaterialTheme.colorScheme.primary, shape = CircleShape)
.size(22.dp),
contentAlignment = Alignment.Center
) {
val unreadTextSize = when (room.unreadCount.value > 99) {
true -> 9.sp
false -> 10.sp
}
val unreadLabelContent = when {
room.unreadCount.value > 99 -> "99+"
else -> room.unreadCount.value.toString()
}
Text(
fontSize = unreadTextSize,
fontWeight = FontWeight.Medium,
text = unreadLabelContent,
color = MaterialTheme.colorScheme.onPrimary
)
if (hasUnread) {
Spacer(modifier = Modifier.width(6.dp))
Box(Modifier.align(Alignment.CenterVertically)) {
UnreadCircle(room)
}
}
if (room.isMuted) {
Spacer(modifier = Modifier.width(6.dp))
Icon(
imageVector = Icons.Filled.VolumeOff,
contentDescription = "",
)
}
}
} else {
body(overview, secondaryText, room.typing)
@ -237,6 +233,32 @@ private fun DirectoryItem(room: DirectoryItem, onClick: (RoomId) -> Unit, clock:
}
}
@Composable
private fun BoxScope.UnreadCircle(room: DirectoryItem) {
Box(
Modifier.Companion
.align(Alignment.Center)
.background(color = MaterialTheme.colorScheme.primary, shape = CircleShape)
.size(22.dp),
contentAlignment = Alignment.Center
) {
val unreadTextSize = when (room.unreadCount.value > 99) {
true -> 9.sp
false -> 10.sp
}
val unreadLabelContent = when {
room.unreadCount.value > 99 -> "99+"
else -> room.unreadCount.value.toString()
}
Text(
fontSize = unreadTextSize,
fontWeight = FontWeight.Medium,
text = unreadLabelContent,
color = MaterialTheme.colorScheme.onPrimary
)
}
}
@Composable
private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) {
val bodySize = 14.sp

View File

@ -22,15 +22,15 @@ internal class DirectoryUseCase(
overviewDatasource(),
messageService.localEchos(),
roomStore.observeUnreadCountById(),
syncService.events()
) { overviewState, localEchos, unread, events ->
val allMuted = roomStore.allMuted()
syncService.events(),
roomStore.observeMuted(),
) { overviewState, localEchos, unread, events, muted ->
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
DirectoryItem(
overview = roomOverview,
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }?.engine(),
isMuted = allMuted.contains(roomOverview.roomId),
isMuted = muted.contains(roomOverview.roomId),
)
}
}

View File

@ -28,6 +28,9 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmStore
import app.dapk.st.olm.OlmWrapper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.time.Clock
internal object MatrixFactory {
@ -146,7 +149,7 @@ internal object MatrixFactory {
singleRoomStore = object : SingleRoomStore {
override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId)
override suspend fun unmute(roomId: RoomId) = roomStore.unmute(roomId)
override suspend fun isMuted(roomId: RoomId) = roomStore.isMuted(roomId)
override fun isMuted(roomId: RoomId): Flow<Boolean> = roomStore.observeMuted().map { it.contains(roomId) }.distinctUntilChanged()
}
)

View File

@ -23,8 +23,9 @@ internal class TimelineUseCaseImpl(
return combine(
roomDatasource(roomId),
messageService.localEchos(roomId),
syncService.events(roomId)
) { roomState, localEchos, events ->
syncService.events(roomId),
roomService.observeIssMuted(roomId),
) { roomState, localEchos, events, isMuted ->
MessengerPageState(
roomState = when {
localEchos.isEmpty() -> roomState
@ -38,7 +39,7 @@ internal class TimelineUseCaseImpl(
},
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
self = userId,
isMuted = roomService.isMuted(roomId)
isMuted = isMuted,
)
}
}

View File

@ -6,6 +6,7 @@ import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.room.internal.*
import kotlinx.coroutines.flow.Flow
private val SERVICE_KEY = RoomService::class
@ -26,7 +27,7 @@ interface RoomService : MatrixService {
suspend fun muteRoom(roomId: RoomId)
suspend fun unmuteRoom(roomId: RoomId)
suspend fun isMuted(roomId: RoomId): Boolean
fun observeIssMuted(roomId: RoomId): Flow<Boolean>
data class JoinedMember(
val userId: UserId,

View File

@ -10,6 +10,7 @@ import app.dapk.st.matrix.room.RoomMessenger
import app.dapk.st.matrix.room.RoomService
import io.ktor.client.plugins.*
import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -101,13 +102,13 @@ class DefaultRoomService(
singleRoomStore.unmute(roomId)
}
override suspend fun isMuted(roomId: RoomId) = singleRoomStore.isMuted(roomId)
override fun observeIssMuted(roomId: RoomId): Flow<Boolean> = singleRoomStore.isMuted(roomId)
}
interface SingleRoomStore {
suspend fun mute(roomId: RoomId)
suspend fun unmute(roomId: RoomId)
suspend fun isMuted(roomId: RoomId): Boolean
fun isMuted(roomId: RoomId): Flow<Boolean>
}
internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersResponse>(

View File

@ -25,10 +25,9 @@ interface MuteableStore {
suspend fun mute(roomId: RoomId)
suspend fun unmute(roomId: RoomId)
suspend fun isMuted(roomId: RoomId): Boolean
suspend fun allMuted(): Set<RoomId>
fun observeMuted(): Flow<Set<RoomId>>
}
interface FilterStore {
suspend fun store(key: String, filterId: String)

View File

@ -26,15 +26,18 @@ internal class UnreadEventsProcessor(
isInitialSync -> {
// let's assume everything is read
}
previousState?.readMarker != overview.readMarker -> {
// assume the user has viewed the room
logger.matrixLog(MatrixLogTag.SYNC, "marking room read due to new read marker")
roomStore.markRead(overview.roomId)
}
areWeViewingRoom -> {
logger.matrixLog(MatrixLogTag.SYNC, "marking room read")
roomStore.markRead(overview.roomId)
}
newEvents.isNotEmpty() -> {
logger.matrixLog(MatrixLogTag.SYNC, "insert new unread events")