binding muted state to a icon within the room ist
This commit is contained in:
parent
e986b44959
commit
92639beb73
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue