add room UI to mute room + persistence layer

This commit is contained in:
Adam Brown 2022-11-02 11:15:23 +00:00
parent e753cd30ad
commit 7e184cf200
15 changed files with 172 additions and 21 deletions

View File

@ -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? { override suspend fun readString(key: String): String? {
return coroutineDispatchers.withIoContext { return coroutineDispatchers.withIoContext {
preferences.getString(key, null) 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) { override suspend fun remove(key: String) {
coroutineDispatchers.withIoContext { coroutineDispatchers.withIoContext {
preferences.edit().remove(key).apply() preferences.edit().remove(key).apply()

View File

@ -18,6 +18,7 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule import app.dapk.st.directory.DirectoryModule
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.domain.room.MutedRoomsStorePersistence
import app.dapk.st.engine.MatrixEngine import app.dapk.st.engine.MatrixEngine
import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.firebase.messaging.MessagingModule
import app.dapk.st.home.BetaVersionUpgradeUseCase import app.dapk.st.home.BetaVersionUpgradeUseCase
@ -163,6 +164,7 @@ internal class FeatureModules internal constructor(
context, context,
storeModule.value.messageStore(), storeModule.value.messageStore(),
deviceMeta, deviceMeta,
MutedRoomsStorePersistence(storeModule.value.cachingPreferences)
) )
} }
val homeModule by unsafeLazy { val homeModule by unsafeLazy {

View File

@ -6,7 +6,8 @@ class JobBag {
private val jobs = mutableMapOf<String, Job>() private val jobs = mutableMapOf<String, Job>()
fun add(key: String, job: Job) { fun replace(key: String, job: Job) {
jobs[key]?.cancel()
jobs[key] = job jobs[key] = job
} }

View File

@ -3,7 +3,9 @@ package app.dapk.st.core
interface Preferences { interface Preferences {
suspend fun store(key: String, value: String) suspend fun store(key: String, value: String)
suspend fun store(key: String, value: Set<String>)
suspend fun readString(key: String): String? suspend fun readString(key: String): String?
suspend fun readStrings(key: String): Set<String>?
suspend fun clear() suspend fun clear()
suspend fun remove(key: String) suspend fun remove(key: String)
} }
@ -17,4 +19,14 @@ suspend fun CachedPreferences.readBoolean(key: String, defaultValue: Boolean) =
.toBooleanStrict() .toBooleanStrict()
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) {
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)
}

View File

@ -4,11 +4,23 @@ import app.dapk.st.core.CachedPreferences
import app.dapk.st.core.Preferences import app.dapk.st.core.Preferences
class CachingPreferences(private val cache: PropertyCache, private val preferences: Preferences) : CachedPreferences { class CachingPreferences(private val cache: PropertyCache, private val preferences: Preferences) : CachedPreferences {
override suspend fun store(key: String, value: String) { override suspend fun store(key: String, value: String) {
cache.setValue(key, value) cache.setValue(key, value)
preferences.store(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? { override suspend fun readString(key: String): String? {
return cache.getValue(key) ?: preferences.readString(key)?.also { return cache.getValue(key) ?: preferences.readString(key)?.also {
cache.setValue(key, it) cache.setValue(key, it)

View File

@ -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()
}
}

View File

@ -21,7 +21,7 @@ internal fun directoryReducer(
multi(ComponentLifecycle::class) { action -> multi(ComponentLifecycle::class) { action ->
when (action) { when (action) {
ComponentLifecycle.OnVisible -> async { _ -> 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 }) shortcutHandler.onDirectoryUpdate(it.map { it.overview })
when (it.isEmpty()) { when (it.isEmpty()) {
true -> dispatch(DirectoryStateChange.Empty) true -> dispatch(DirectoryStateChange.Empty)

View File

@ -38,7 +38,7 @@ class DirectoryReducerTest {
@Test @Test
fun `given directory content, when Visible, then updates shortcuts and dispatches room state`() = runReducerTest { fun `given directory content, when Visible, then updates shortcuts and dispatches room state`() = runReducerTest {
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) } 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))) fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
reduce(ComponentLifecycle.OnVisible) reduce(ComponentLifecycle.OnVisible)
@ -49,7 +49,7 @@ class DirectoryReducerTest {
@Test @Test
fun `given no directory content, when Visible, then updates shortcuts and dispatches empty state`() = runReducerTest { fun `given no directory content, when Visible, then updates shortcuts and dispatches empty state`() = runReducerTest {
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(emptyList()) } 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())) fakeChatEngine.givenDirectory().returns(flowOf(emptyList()))
reduce(ComponentLifecycle.OnVisible) reduce(ComponentLifecycle.OnVisible)

View File

@ -10,6 +10,7 @@ import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.state.MessengerState import app.dapk.st.messenger.state.MessengerState
import app.dapk.st.domain.room.MutedRoomsStore
import app.dapk.st.messenger.state.messengerReducer import app.dapk.st.messenger.state.messengerReducer
class MessengerModule( class MessengerModule(
@ -17,6 +18,7 @@ class MessengerModule(
private val context: Context, private val context: Context,
private val messageOptionsStore: MessageOptionsStore, private val messageOptionsStore: MessageOptionsStore,
private val deviceMeta: DeviceMeta, private val deviceMeta: DeviceMeta,
private val mutedRoomsStore: MutedRoomsStore,
) : ProvidableModule { ) : ProvidableModule {
internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState { internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState {
@ -27,6 +29,7 @@ class MessengerModule(
CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager), CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager),
deviceMeta, deviceMeta,
messageOptionsStore, messageOptionsStore,
mutedRoomsStore,
RoomId(launchPayload.roomId), RoomId(launchPayload.roomId),
launchPayload.attachments, launchPayload.attachments,
it it

View File

@ -87,9 +87,17 @@ internal fun MessengerScreen(
Column { Column {
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = { Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
// OverflowMenu { OverflowMenu {
// DropdownMenuItem(text = { Text("Settings", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {}) 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) { when (state.composerState) {

View File

@ -10,6 +10,11 @@ sealed interface ScreenAction : Action {
data class CopyToClipboard(val model: BubbleModel) : ScreenAction data class CopyToClipboard(val model: BubbleModel) : ScreenAction
object SendMessage : ScreenAction object SendMessage : ScreenAction
object OpenGalleryPicker : ScreenAction object OpenGalleryPicker : ScreenAction
sealed interface Notifications : ScreenAction {
object Mute : Notifications
object Unmute : Notifications
}
} }
sealed interface ComponentLifecycle : Action { sealed interface ComponentLifecycle : Action {
@ -18,7 +23,8 @@ sealed interface ComponentLifecycle : Action {
} }
sealed interface MessagesStateChange : 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 { sealed interface ComposerStateChange : Action {

View File

@ -8,6 +8,7 @@ import app.dapk.st.core.asString
import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.BubbleModel import app.dapk.st.design.components.BubbleModel
import app.dapk.st.domain.application.message.MessageOptionsStore 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.ChatEngine
import app.dapk.st.engine.MessengerPageState import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent import app.dapk.st.engine.RoomEvent
@ -26,6 +27,7 @@ internal fun messengerReducer(
copyToClipboard: CopyToClipboard, copyToClipboard: CopyToClipboard,
deviceMeta: DeviceMeta, deviceMeta: DeviceMeta,
messageOptionsStore: MessageOptionsStore, messageOptionsStore: MessageOptionsStore,
mutedRoomsStore: MutedRoomsStore,
roomId: RoomId, roomId: RoomId,
initialAttachments: List<MessageAttachment>?, initialAttachments: List<MessageAttachment>?,
eventEmitter: suspend (MessengerEvent) -> Unit, eventEmitter: suspend (MessengerEvent) -> Unit,
@ -36,16 +38,19 @@ internal fun messengerReducer(
roomState = Lce.Loading(), roomState = Lce.Loading(),
composerState = initialComposerState(initialAttachments), composerState = initialComposerState(initialAttachments),
viewerState = null, viewerState = null,
isMuted = false,
), ),
async(ComponentLifecycle::class) { action -> async(ComponentLifecycle::class) { action ->
val state = getState() val state = getState()
when (action) { when (action) {
is ComponentLifecycle.Visible -> { is ComponentLifecycle.Visible -> {
jobBag.add("messages", chatEngine.messages(state.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled()) jobBag.replace(
.onEach { dispatch(MessagesStateChange.Content(it)) } "messages", chatEngine.messages(state.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
.launchIn(coroutineScope) .onEach { dispatch(MessagesStateChange.Content(it)) }
.launchIn(coroutineScope)
) )
dispatch(MessagesStateChange.MuteContent(mutedRoomsStore.isMuted(roomId)))
} }
ComponentLifecycle.Gone -> jobBag.cancel("messages") 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, timestampUtc = this.utcTimestamp,
) )
private fun initialComposerState(initialAttachments: List<MessageAttachment>?) = initialAttachments private fun initialComposerState(initialAttachments: List<MessageAttachment>?) = initialAttachments
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?.let { ComposerState.Attachments(it, null) } ?.let { ComposerState.Attachments(it, null) }

View File

@ -14,7 +14,8 @@ data class MessengerScreenState(
val roomId: RoomId, val roomId: RoomId,
val roomState: Lce<MessengerPageState>, val roomState: Lce<MessengerPageState>,
val composerState: ComposerState, val composerState: ComposerState,
val viewerState: ViewerState? val viewerState: ViewerState?,
val isMuted: Boolean,
) )
data class ViewerState( data class ViewerState(

View File

@ -3,10 +3,12 @@ package app.dapk.st.messenger
import android.os.Build import android.os.Build
import app.dapk.st.core.* import app.dapk.st.core.*
import app.dapk.st.design.components.BubbleModel 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.RoomEvent
import app.dapk.st.engine.RoomState import app.dapk.st.engine.RoomState
import app.dapk.st.engine.SendMessage import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.EventId 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.UserId
import app.dapk.st.matrix.common.asString import app.dapk.st.matrix.common.asString
import app.dapk.st.messenger.state.* import app.dapk.st.messenger.state.*
@ -14,6 +16,7 @@ import app.dapk.st.navigator.MessageAttachment
import fake.FakeChatEngine import fake.FakeChatEngine
import fake.FakeMessageOptionsStore import fake.FakeMessageOptionsStore
import fixture.* import fixture.*
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf 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 val A_ROOM_ID = aRoomId("messenger state room id")
private const val A_MESSAGE_CONTENT = "message content" private const val A_MESSAGE_CONTENT = "message content"
private val AN_EVENT_ID = anEventId("state event") 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_SELF_ID = aUserId("self")
private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image) private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image)
@ -48,6 +52,7 @@ class MessengerReducerTest {
private val fakeChatEngine = FakeChatEngine() private val fakeChatEngine = FakeChatEngine()
private val fakeCopyToClipboard = FakeCopyToClipboard() private val fakeCopyToClipboard = FakeCopyToClipboard()
private val fakeDeviceMeta = FakeDeviceMeta() private val fakeDeviceMeta = FakeDeviceMeta()
private val fakeMutedRoomsStore = FakeMutedRoomsStore()
private val fakeJobBag = FakeJobBag() private val fakeJobBag = FakeJobBag()
private val runReducerTest = testReducer { fakeEventSource -> private val runReducerTest = testReducer { fakeEventSource ->
@ -57,6 +62,7 @@ class MessengerReducerTest {
fakeCopyToClipboard.instance, fakeCopyToClipboard.instance,
fakeDeviceMeta.instance, fakeDeviceMeta.instance,
fakeMessageOptionsStore.instance, fakeMessageOptionsStore.instance,
fakeMutedRoomsStore,
A_ROOM_ID, A_ROOM_ID,
emptyList(), emptyList(),
fakeEventSource, fakeEventSource,
@ -71,6 +77,7 @@ class MessengerReducerTest {
roomState = Lce.Loading(), roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "", reply = null), composerState = ComposerState.Text(value = "", reply = null),
viewerState = null, viewerState = null,
isMuted = false,
) )
) )
} }
@ -83,6 +90,7 @@ class MessengerReducerTest {
roomState = Lce.Loading(), roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "", reply = null), composerState = ComposerState.Text(value = "", reply = null),
viewerState = null, viewerState = null,
isMuted = false,
) )
) )
} }
@ -95,20 +103,22 @@ class MessengerReducerTest {
roomState = Lce.Loading(), roomState = Lce.Loading(),
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null), composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
viewerState = null, viewerState = null,
isMuted = false,
) )
) )
} }
@Test @Test
fun `given messages emits state, when Visible, then dispatches content`() = runReducerTest { fun `given room is muted and messages emits state, when Visible, then dispatches content and mute changes`() = runReducerTest {
fakeJobBag.instance.expect { it.add("messages", any()) } fakeMutedRoomsStore.givenIsMuted(A_ROOM_ID).returns(ROOM_IS_MUTED)
fakeJobBag.instance.expect { it.replace("messages", any()) }
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED) fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state)) fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
reduce(ComponentLifecycle.Visible) reduce(ComponentLifecycle.Visible)
assertOnlyDispatches(listOf(MessagesStateChange.Content(state))) assertOnlyDispatches(listOf(MessagesStateChange.MuteContent(isMuted = ROOM_IS_MUTED), MessagesStateChange.Content(state)))
} }
@Test @Test
@ -330,6 +340,7 @@ class MessengerReducerTest {
fakeCopyToClipboard.instance, fakeCopyToClipboard.instance,
fakeDeviceMeta.instance, fakeDeviceMeta.instance,
fakeMessageOptionsStore.instance, fakeMessageOptionsStore.instance,
FakeMutedRoomsStore(),
A_ROOM_ID, A_ROOM_ID,
initialAttachments = initialAttachments, initialAttachments = initialAttachments,
fakeEventSource, fakeEventSource,
@ -359,4 +370,8 @@ class FakeDeviceMeta {
val instance = mockk<DeviceMeta>() val instance = mockk<DeviceMeta>()
fun givenApiVersion() = every { instance.apiVersion }.delegateReturn() fun givenApiVersion() = every { instance.apiVersion }.delegateReturn()
} }
class FakeMutedRoomsStore : MutedRoomsStore by mockk() {
fun givenIsMuted(roomId: RoomId) = coEvery { isMuted(roomId) }.delegateReturn()
}

View File

@ -1,18 +1,31 @@
package test.impl package test.impl
import app.dapk.st.core.Preferences import app.dapk.st.core.Preferences
import test.unit
class InMemoryPreferences : Preferences { class InMemoryPreferences : Preferences {
private val prefs = mutableMapOf<String, String>() private val prefs = mutableMapOf<String, String>()
private val setPrefs = mutableMapOf<String, Set<String>>()
override suspend fun store(key: String, value: String) { override suspend fun store(key: String, value: String) {
prefs[key] = value 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 readString(key: String): String? = prefs[key]
override suspend fun remove(key: String) = prefs.remove(key).unit() override suspend fun readStrings(key: String): Set<String>? = setPrefs[key]
override suspend fun clear() = prefs.clear()
override suspend fun remove(key: String) {
prefs.remove(key)
setPrefs.remove(key)
}
override suspend fun clear() {
prefs.clear()
setPrefs.clear()
}
} }