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.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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue