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.readBoolean(key: String) = this.readString(key)?.toBooleanStrict()
suspend fun Preferences.store(key: String, value: Boolean) = this.store(key, value.toString()) 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() 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() 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 coroutineDispatchers: CoroutineDispatchers,
) { ) {
private val muteableStore by unsafeLazy { MutedStorePersistence(preferences) }
fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers) fun overviewStore(): OverviewStore = OverviewPersistence(database, coroutineDispatchers)
fun roomStore(): RoomStore = RoomPersistence( fun roomStore(): RoomStore {
database = database, return RoomPersistence(
overviewPersistence = OverviewPersistence(database, coroutineDispatchers), database = database,
coroutineDispatchers = coroutineDispatchers, overviewPersistence = OverviewPersistence(database, coroutineDispatchers),
muteableStore = MutedStorePersistence(preferences), coroutineDispatchers = coroutineDispatchers,
) muteableStore = muteableStore,
)
}
fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences) fun credentialsStore(): CredentialsStore = CredentialsPreferences(credentialPreferences)
fun syncStore(): SyncStore = SyncTokenPreferences(preferences) 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.core.removeFromSet
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.MuteableStore 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" private const val KEY_MUTE = "mute"
@ -12,20 +16,23 @@ internal class MutedStorePersistence(
private val preferences: Preferences private val preferences: Preferences
) : MuteableStore { ) : MuteableStore {
private val allMutedFlow = MutableSharedFlow<Set<RoomId>>(replay = 1)
override suspend fun mute(roomId: RoomId) { 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) { 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 { private suspend fun Set<String>.notifyChange() = allMutedFlow.emit(this.map { RoomId(it) }.toSet())
val allMuted = allMuted()
return allMuted.contains(roomId)
}
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() 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.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* 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()) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
body(overview, secondaryText, room.typing) body(overview, secondaryText, room.typing)
} }
Spacer(modifier = Modifier.width(6.dp)) if (hasUnread) {
Box(Modifier.align(Alignment.CenterVertically)) { Spacer(modifier = Modifier.width(6.dp))
Box( Box(Modifier.align(Alignment.CenterVertically)) {
Modifier UnreadCircle(room)
.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 (room.isMuted) {
Spacer(modifier = Modifier.width(6.dp))
Icon(
imageVector = Icons.Filled.VolumeOff,
contentDescription = "",
)
}
} }
} else { } else {
body(overview, secondaryText, room.typing) 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 @Composable
private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) { private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) {
val bodySize = 14.sp val bodySize = 14.sp

View File

@ -22,15 +22,15 @@ internal class DirectoryUseCase(
overviewDatasource(), overviewDatasource(),
messageService.localEchos(), messageService.localEchos(),
roomStore.observeUnreadCountById(), roomStore.observeUnreadCountById(),
syncService.events() syncService.events(),
) { overviewState, localEchos, unread, events -> roomStore.observeMuted(),
val allMuted = roomStore.allMuted() ) { overviewState, localEchos, unread, events, muted ->
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview -> overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
DirectoryItem( DirectoryItem(
overview = roomOverview, overview = roomOverview,
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0), unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }?.engine(), 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.DeviceKeyFactory
import app.dapk.st.olm.OlmStore import app.dapk.st.olm.OlmStore
import app.dapk.st.olm.OlmWrapper 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 import java.time.Clock
internal object MatrixFactory { internal object MatrixFactory {
@ -146,7 +149,7 @@ internal object MatrixFactory {
singleRoomStore = object : SingleRoomStore { singleRoomStore = object : SingleRoomStore {
override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId) override suspend fun mute(roomId: RoomId) = roomStore.mute(roomId)
override suspend fun unmute(roomId: RoomId) = roomStore.unmute(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( return combine(
roomDatasource(roomId), roomDatasource(roomId),
messageService.localEchos(roomId), messageService.localEchos(roomId),
syncService.events(roomId) syncService.events(roomId),
) { roomState, localEchos, events -> roomService.observeIssMuted(roomId),
) { roomState, localEchos, events, isMuted ->
MessengerPageState( MessengerPageState(
roomState = when { roomState = when {
localEchos.isEmpty() -> roomState localEchos.isEmpty() -> roomState
@ -38,7 +39,7 @@ internal class TimelineUseCaseImpl(
}, },
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(), typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
self = userId, 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.RoomMember
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.room.internal.* import app.dapk.st.matrix.room.internal.*
import kotlinx.coroutines.flow.Flow
private val SERVICE_KEY = RoomService::class private val SERVICE_KEY = RoomService::class
@ -26,7 +27,7 @@ interface RoomService : MatrixService {
suspend fun muteRoom(roomId: RoomId) suspend fun muteRoom(roomId: RoomId)
suspend fun unmuteRoom(roomId: RoomId) suspend fun unmuteRoom(roomId: RoomId)
suspend fun isMuted(roomId: RoomId): Boolean fun observeIssMuted(roomId: RoomId): Flow<Boolean>
data class JoinedMember( data class JoinedMember(
val userId: UserId, val userId: UserId,

View File

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

View File

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

View File

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