add room UI to mute room + persistence layer
This commit is contained in:
parent
e753cd30ad
commit
7e184cf200
|
@ -19,12 +19,24 @@ internal class SharedPreferencesDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: Set<String>) {
|
||||
coroutineDispatchers.withIoContext {
|
||||
preferences.edit().putStringSet(key, value).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readString(key: String): String? {
|
||||
return coroutineDispatchers.withIoContext {
|
||||
preferences.getString(key, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readStrings(key: String): Set<String>? {
|
||||
return coroutineDispatchers.withIoContext {
|
||||
preferences.getStringSet(key, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remove(key: String) {
|
||||
coroutineDispatchers.withIoContext {
|
||||
preferences.edit().remove(key).apply()
|
||||
|
|
|
@ -18,6 +18,7 @@ import app.dapk.st.core.extensions.ErrorTracker
|
|||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.domain.room.MutedRoomsStorePersistence
|
||||
import app.dapk.st.engine.MatrixEngine
|
||||
import app.dapk.st.firebase.messaging.MessagingModule
|
||||
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
||||
|
@ -163,6 +164,7 @@ internal class FeatureModules internal constructor(
|
|||
context,
|
||||
storeModule.value.messageStore(),
|
||||
deviceMeta,
|
||||
MutedRoomsStorePersistence(storeModule.value.cachingPreferences)
|
||||
)
|
||||
}
|
||||
val homeModule by unsafeLazy {
|
||||
|
|
|
@ -6,7 +6,8 @@ class JobBag {
|
|||
|
||||
private val jobs = mutableMapOf<String, Job>()
|
||||
|
||||
fun add(key: String, job: Job) {
|
||||
fun replace(key: String, job: Job) {
|
||||
jobs[key]?.cancel()
|
||||
jobs[key] = job
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ package app.dapk.st.core
|
|||
interface Preferences {
|
||||
|
||||
suspend fun store(key: String, value: String)
|
||||
suspend fun store(key: String, value: Set<String>)
|
||||
suspend fun readString(key: String): String?
|
||||
suspend fun readStrings(key: String): Set<String>?
|
||||
suspend fun clear()
|
||||
suspend fun remove(key: String)
|
||||
}
|
||||
|
@ -18,3 +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) {
|
||||
val current = this.readStrings(key) ?: emptySet()
|
||||
this.store(key, current + value)
|
||||
}
|
||||
|
||||
suspend fun Preferences.removeFromSet(key: String, value: String) {
|
||||
val current = this.readStrings(key) ?: emptySet()
|
||||
this.store(key, current - value)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,23 @@ import app.dapk.st.core.CachedPreferences
|
|||
import app.dapk.st.core.Preferences
|
||||
|
||||
class CachingPreferences(private val cache: PropertyCache, private val preferences: Preferences) : CachedPreferences {
|
||||
|
||||
override suspend fun store(key: String, value: String) {
|
||||
cache.setValue(key, value)
|
||||
preferences.store(key, value)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: Set<String>) {
|
||||
cache.setValue(key, value)
|
||||
preferences.store(key, value)
|
||||
}
|
||||
|
||||
override suspend fun readStrings(key: String): Set<String>? {
|
||||
return cache.getValue(key) ?: preferences.readStrings(key)?.also {
|
||||
cache.setValue(key, it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readString(key: String): String? {
|
||||
return cache.getValue(key) ?: preferences.readString(key)?.also {
|
||||
cache.setValue(key, it)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package app.dapk.st.domain.room
|
||||
|
||||
import app.dapk.st.core.Preferences
|
||||
import app.dapk.st.core.append
|
||||
import app.dapk.st.core.removeFromSet
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
|
||||
private const val KEY_MUTE = "mute"
|
||||
|
||||
interface MutedRoomsStore {
|
||||
suspend fun mute(roomId: RoomId)
|
||||
suspend fun unmute(roomId: RoomId)
|
||||
suspend fun isMuted(roomId: RoomId): Boolean
|
||||
suspend fun allMuted(): Set<RoomId>
|
||||
}
|
||||
|
||||
class MutedRoomsStorePersistence(
|
||||
private val preferences: Preferences
|
||||
) : MutedRoomsStore {
|
||||
|
||||
override suspend fun mute(roomId: RoomId) {
|
||||
preferences.append(KEY_MUTE, roomId.value)
|
||||
}
|
||||
|
||||
override suspend fun unmute(roomId: RoomId) {
|
||||
preferences.removeFromSet(KEY_MUTE, roomId.value)
|
||||
}
|
||||
|
||||
override suspend fun isMuted(roomId: RoomId): Boolean {
|
||||
val allMuted = allMuted()
|
||||
println("??? isMuted - $roomId")
|
||||
println("??? all - $allMuted")
|
||||
return allMuted.contains(roomId)
|
||||
}
|
||||
|
||||
override suspend fun allMuted(): Set<RoomId> {
|
||||
return preferences.readStrings(KEY_MUTE)?.map { RoomId(it) }?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ internal fun directoryReducer(
|
|||
multi(ComponentLifecycle::class) { action ->
|
||||
when (action) {
|
||||
ComponentLifecycle.OnVisible -> async { _ ->
|
||||
jobBag.add(KEY_SYNCING_JOB, chatEngine.directory().onEach {
|
||||
jobBag.replace(KEY_SYNCING_JOB, chatEngine.directory().onEach {
|
||||
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
||||
when (it.isEmpty()) {
|
||||
true -> dispatch(DirectoryStateChange.Empty)
|
||||
|
|
|
@ -38,7 +38,7 @@ class DirectoryReducerTest {
|
|||
@Test
|
||||
fun `given directory content, when Visible, then updates shortcuts and dispatches room state`() = runReducerTest {
|
||||
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) }
|
||||
fakeJobBag.instance.expect { it.add("sync", any()) }
|
||||
fakeJobBag.instance.expect { it.replace("sync", any()) }
|
||||
fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
|
||||
|
||||
reduce(ComponentLifecycle.OnVisible)
|
||||
|
@ -49,7 +49,7 @@ class DirectoryReducerTest {
|
|||
@Test
|
||||
fun `given no directory content, when Visible, then updates shortcuts and dispatches empty state`() = runReducerTest {
|
||||
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(emptyList()) }
|
||||
fakeJobBag.instance.expect { it.add("sync", any()) }
|
||||
fakeJobBag.instance.expect { it.replace("sync", any()) }
|
||||
fakeChatEngine.givenDirectory().returns(flowOf(emptyList()))
|
||||
|
||||
reduce(ComponentLifecycle.OnVisible)
|
||||
|
|
|
@ -10,6 +10,7 @@ import app.dapk.st.domain.application.message.MessageOptionsStore
|
|||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.messenger.state.MessengerState
|
||||
import app.dapk.st.domain.room.MutedRoomsStore
|
||||
import app.dapk.st.messenger.state.messengerReducer
|
||||
|
||||
class MessengerModule(
|
||||
|
@ -17,6 +18,7 @@ class MessengerModule(
|
|||
private val context: Context,
|
||||
private val messageOptionsStore: MessageOptionsStore,
|
||||
private val deviceMeta: DeviceMeta,
|
||||
private val mutedRoomsStore: MutedRoomsStore,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState {
|
||||
|
@ -27,6 +29,7 @@ class MessengerModule(
|
|||
CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager),
|
||||
deviceMeta,
|
||||
messageOptionsStore,
|
||||
mutedRoomsStore,
|
||||
RoomId(launchPayload.roomId),
|
||||
launchPayload.attachments,
|
||||
it
|
||||
|
|
|
@ -87,9 +87,17 @@ internal fun MessengerScreen(
|
|||
|
||||
Column {
|
||||
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
|
||||
// OverflowMenu {
|
||||
// DropdownMenuItem(text = { Text("Settings", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {})
|
||||
// }
|
||||
OverflowMenu {
|
||||
when (state.isMuted) {
|
||||
true -> DropdownMenuItem(text = { Text("Unmute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {
|
||||
viewModel.dispatch(ScreenAction.Notifications.Unmute)
|
||||
})
|
||||
|
||||
false -> DropdownMenuItem(text = { Text("Mute notifications", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {
|
||||
viewModel.dispatch(ScreenAction.Notifications.Mute)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
when (state.composerState) {
|
||||
|
|
|
@ -10,6 +10,11 @@ sealed interface ScreenAction : Action {
|
|||
data class CopyToClipboard(val model: BubbleModel) : ScreenAction
|
||||
object SendMessage : ScreenAction
|
||||
object OpenGalleryPicker : ScreenAction
|
||||
|
||||
sealed interface Notifications : ScreenAction {
|
||||
object Mute : Notifications
|
||||
object Unmute : Notifications
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ComponentLifecycle : Action {
|
||||
|
@ -18,7 +23,8 @@ sealed interface ComponentLifecycle : Action {
|
|||
}
|
||||
|
||||
sealed interface MessagesStateChange : Action {
|
||||
data class Content(val content: MessengerPageState) : ComposerStateChange
|
||||
data class Content(val content: MessengerPageState) : MessagesStateChange
|
||||
data class MuteContent(val isMuted: Boolean) : MessagesStateChange
|
||||
}
|
||||
|
||||
sealed interface ComposerStateChange : Action {
|
||||
|
|
|
@ -8,6 +8,7 @@ import app.dapk.st.core.asString
|
|||
import app.dapk.st.core.extensions.takeIfContent
|
||||
import app.dapk.st.design.components.BubbleModel
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
import app.dapk.st.domain.room.MutedRoomsStore
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.MessengerPageState
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
|
@ -26,6 +27,7 @@ internal fun messengerReducer(
|
|||
copyToClipboard: CopyToClipboard,
|
||||
deviceMeta: DeviceMeta,
|
||||
messageOptionsStore: MessageOptionsStore,
|
||||
mutedRoomsStore: MutedRoomsStore,
|
||||
roomId: RoomId,
|
||||
initialAttachments: List<MessageAttachment>?,
|
||||
eventEmitter: suspend (MessengerEvent) -> Unit,
|
||||
|
@ -36,16 +38,19 @@ internal fun messengerReducer(
|
|||
roomState = Lce.Loading(),
|
||||
composerState = initialComposerState(initialAttachments),
|
||||
viewerState = null,
|
||||
isMuted = false,
|
||||
),
|
||||
|
||||
async(ComponentLifecycle::class) { action ->
|
||||
val state = getState()
|
||||
when (action) {
|
||||
is ComponentLifecycle.Visible -> {
|
||||
jobBag.add("messages", chatEngine.messages(state.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
|
||||
.onEach { dispatch(MessagesStateChange.Content(it)) }
|
||||
.launchIn(coroutineScope)
|
||||
jobBag.replace(
|
||||
"messages", chatEngine.messages(state.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
|
||||
.onEach { dispatch(MessagesStateChange.Content(it)) }
|
||||
.launchIn(coroutineScope)
|
||||
)
|
||||
dispatch(MessagesStateChange.MuteContent(mutedRoomsStore.isMuted(roomId)))
|
||||
}
|
||||
|
||||
ComponentLifecycle.Gone -> jobBag.cancel("messages")
|
||||
|
@ -134,6 +139,28 @@ internal fun messengerReducer(
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
change(MessagesStateChange.MuteContent::class) { action, state ->
|
||||
state.copy(isMuted = action.isMuted).also {
|
||||
println("??? action - $action previous state: ${state.isMuted} next: ${it.isMuted}")
|
||||
}
|
||||
},
|
||||
|
||||
async(ScreenAction.Notifications::class) { action ->
|
||||
when (action) {
|
||||
ScreenAction.Notifications.Mute -> mutedRoomsStore.mute(roomId)
|
||||
ScreenAction.Notifications.Unmute -> mutedRoomsStore.unmute(roomId)
|
||||
}
|
||||
|
||||
dispatch(
|
||||
MessagesStateChange.MuteContent(
|
||||
isMuted = when (action) {
|
||||
ScreenAction.Notifications.Mute -> true
|
||||
ScreenAction.Notifications.Unmute -> false
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -158,7 +185,6 @@ private fun RoomEvent.toSendMessageReply() = SendMessage.TextMessage.Reply(
|
|||
timestampUtc = this.utcTimestamp,
|
||||
)
|
||||
|
||||
|
||||
private fun initialComposerState(initialAttachments: List<MessageAttachment>?) = initialAttachments
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { ComposerState.Attachments(it, null) }
|
||||
|
|
|
@ -14,7 +14,8 @@ data class MessengerScreenState(
|
|||
val roomId: RoomId,
|
||||
val roomState: Lce<MessengerPageState>,
|
||||
val composerState: ComposerState,
|
||||
val viewerState: ViewerState?
|
||||
val viewerState: ViewerState?,
|
||||
val isMuted: Boolean,
|
||||
)
|
||||
|
||||
data class ViewerState(
|
||||
|
|
|
@ -3,10 +3,12 @@ package app.dapk.st.messenger
|
|||
import android.os.Build
|
||||
import app.dapk.st.core.*
|
||||
import app.dapk.st.design.components.BubbleModel
|
||||
import app.dapk.st.domain.room.MutedRoomsStore
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.engine.RoomState
|
||||
import app.dapk.st.engine.SendMessage
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.common.asString
|
||||
import app.dapk.st.messenger.state.*
|
||||
|
@ -14,6 +16,7 @@ import app.dapk.st.navigator.MessageAttachment
|
|||
import fake.FakeChatEngine
|
||||
import fake.FakeMessageOptionsStore
|
||||
import fixture.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -27,6 +30,7 @@ private const val READ_RECEIPTS_ARE_DISABLED = true
|
|||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||
private const val A_MESSAGE_CONTENT = "message content"
|
||||
private val AN_EVENT_ID = anEventId("state event")
|
||||
private const val ROOM_IS_MUTED = true
|
||||
private val A_SELF_ID = aUserId("self")
|
||||
private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||
private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image)
|
||||
|
@ -48,6 +52,7 @@ class MessengerReducerTest {
|
|||
private val fakeChatEngine = FakeChatEngine()
|
||||
private val fakeCopyToClipboard = FakeCopyToClipboard()
|
||||
private val fakeDeviceMeta = FakeDeviceMeta()
|
||||
private val fakeMutedRoomsStore = FakeMutedRoomsStore()
|
||||
private val fakeJobBag = FakeJobBag()
|
||||
|
||||
private val runReducerTest = testReducer { fakeEventSource ->
|
||||
|
@ -57,6 +62,7 @@ class MessengerReducerTest {
|
|||
fakeCopyToClipboard.instance,
|
||||
fakeDeviceMeta.instance,
|
||||
fakeMessageOptionsStore.instance,
|
||||
fakeMutedRoomsStore,
|
||||
A_ROOM_ID,
|
||||
emptyList(),
|
||||
fakeEventSource,
|
||||
|
@ -71,6 +77,7 @@ class MessengerReducerTest {
|
|||
roomState = Lce.Loading(),
|
||||
composerState = ComposerState.Text(value = "", reply = null),
|
||||
viewerState = null,
|
||||
isMuted = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -83,6 +90,7 @@ class MessengerReducerTest {
|
|||
roomState = Lce.Loading(),
|
||||
composerState = ComposerState.Text(value = "", reply = null),
|
||||
viewerState = null,
|
||||
isMuted = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -95,20 +103,22 @@ class MessengerReducerTest {
|
|||
roomState = Lce.Loading(),
|
||||
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
|
||||
viewerState = null,
|
||||
isMuted = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given messages emits state, when Visible, then dispatches content`() = runReducerTest {
|
||||
fakeJobBag.instance.expect { it.add("messages", any()) }
|
||||
fun `given room is muted and messages emits state, when Visible, then dispatches content and mute changes`() = runReducerTest {
|
||||
fakeMutedRoomsStore.givenIsMuted(A_ROOM_ID).returns(ROOM_IS_MUTED)
|
||||
fakeJobBag.instance.expect { it.replace("messages", any()) }
|
||||
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
|
||||
|
||||
reduce(ComponentLifecycle.Visible)
|
||||
|
||||
assertOnlyDispatches(listOf(MessagesStateChange.Content(state)))
|
||||
assertOnlyDispatches(listOf(MessagesStateChange.MuteContent(isMuted = ROOM_IS_MUTED), MessagesStateChange.Content(state)))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -330,6 +340,7 @@ class MessengerReducerTest {
|
|||
fakeCopyToClipboard.instance,
|
||||
fakeDeviceMeta.instance,
|
||||
fakeMessageOptionsStore.instance,
|
||||
FakeMutedRoomsStore(),
|
||||
A_ROOM_ID,
|
||||
initialAttachments = initialAttachments,
|
||||
fakeEventSource,
|
||||
|
@ -360,3 +371,7 @@ class FakeDeviceMeta {
|
|||
|
||||
fun givenApiVersion() = every { instance.apiVersion }.delegateReturn()
|
||||
}
|
||||
|
||||
class FakeMutedRoomsStore : MutedRoomsStore by mockk() {
|
||||
fun givenIsMuted(roomId: RoomId) = coEvery { isMuted(roomId) }.delegateReturn()
|
||||
}
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
package test.impl
|
||||
|
||||
import app.dapk.st.core.Preferences
|
||||
import test.unit
|
||||
|
||||
class InMemoryPreferences : Preferences {
|
||||
|
||||
private val prefs = mutableMapOf<String, String>()
|
||||
private val setPrefs = mutableMapOf<String, Set<String>>()
|
||||
|
||||
override suspend fun store(key: String, value: String) {
|
||||
prefs[key] = value
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: Set<String>) {
|
||||
setPrefs[key] = value
|
||||
}
|
||||
|
||||
override suspend fun readString(key: String): String? = prefs[key]
|
||||
override suspend fun remove(key: String) = prefs.remove(key).unit()
|
||||
override suspend fun clear() = prefs.clear()
|
||||
override suspend fun readStrings(key: String): Set<String>? = setPrefs[key]
|
||||
|
||||
override suspend fun remove(key: String) {
|
||||
prefs.remove(key)
|
||||
setPrefs.remove(key)
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
prefs.clear()
|
||||
setPrefs.clear()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue