mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-17 20:50:48 +01:00
Merge pull request #194 from ouchadam/feature/extra-settings
Logging and read receipt settings
This commit is contained in:
commit
ef25d01c6d
@ -42,10 +42,15 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||||||
val notificationsModule = featureModules.notificationsModule
|
val notificationsModule = featureModules.notificationsModule
|
||||||
val storeModule = appModule.storeModule.value
|
val storeModule = appModule.storeModule.value
|
||||||
val eventLogStore = storeModule.eventLogStore()
|
val eventLogStore = storeModule.eventLogStore()
|
||||||
|
val loggingStore = storeModule.loggingStore()
|
||||||
|
|
||||||
val logger: (String, String) -> Unit = { tag, message ->
|
val logger: (String, String) -> Unit = { tag, message ->
|
||||||
Log.e(tag, message)
|
Log.e(tag, message)
|
||||||
applicationScope.launch { eventLogStore.insert(tag, message) }
|
applicationScope.launch {
|
||||||
|
if (loggingStore.isEnabled()) {
|
||||||
|
eventLogStore.insert(tag, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attachAppLogger(logger)
|
attachAppLogger(logger)
|
||||||
_appLogger = logger
|
_appLogger = logger
|
||||||
|
@ -126,7 +126,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||||||
attachments
|
attachments
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unsafeLazy { storeModule.value.preferences }
|
unsafeLazy { storeModule.value.cachingPreferences },
|
||||||
)
|
)
|
||||||
|
|
||||||
val featureModules = FeatureModules(
|
val featureModules = FeatureModules(
|
||||||
@ -191,6 +191,7 @@ internal class FeatureModules internal constructor(
|
|||||||
context,
|
context,
|
||||||
base64,
|
base64,
|
||||||
imageContentReader,
|
imageContentReader,
|
||||||
|
storeModule.value.messageStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) }
|
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) }
|
||||||
@ -205,6 +206,8 @@ internal class FeatureModules internal constructor(
|
|||||||
deviceMeta,
|
deviceMeta,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
coreAndroidModule.themeStore(),
|
coreAndroidModule.themeStore(),
|
||||||
|
storeModule.value.loggingStore(),
|
||||||
|
storeModule.value.messageStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
|
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
|
||||||
|
@ -8,5 +8,13 @@ interface Preferences {
|
|||||||
suspend fun remove(key: String)
|
suspend fun remove(key: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CachedPreferences : Preferences {
|
||||||
|
suspend fun readString(key: String, defaultValue: String): String
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun CachedPreferences.readBoolean(key: String, defaultValue: Boolean) = this
|
||||||
|
.readString(key, defaultValue.toString())
|
||||||
|
.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())
|
@ -2,37 +2,49 @@ package app.dapk.st.design.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) {
|
fun TextRow(
|
||||||
|
title: String,
|
||||||
|
content: String? = null,
|
||||||
|
includeDivider: Boolean = true,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
body: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val verticalPadding = 24.dp
|
||||||
val modifier = Modifier.padding(horizontal = 24.dp)
|
val modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = onClick != null) { onClick?.invoke() }) {
|
.clickable(enabled = onClick != null) { onClick?.invoke() }) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(verticalPadding))
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
|
val textModifier = when (enabled) {
|
||||||
|
true -> Modifier
|
||||||
|
false -> Modifier.alpha(0.5f)
|
||||||
|
}
|
||||||
when (content) {
|
when (content) {
|
||||||
null -> {
|
null -> {
|
||||||
Text(text = title, fontSize = 18.sp)
|
Text(text = title, fontSize = 18.sp, modifier = textModifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(text = title, fontSize = 12.sp)
|
Text(text = title, fontSize = 12.sp, modifier = textModifier)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(text = content, fontSize = 18.sp)
|
Text(text = content, fontSize = 18.sp, modifier = textModifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body()
|
body()
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(verticalPadding))
|
||||||
}
|
}
|
||||||
if (includeDivider) {
|
if (includeDivider) {
|
||||||
Divider(modifier = Modifier.fillMaxWidth())
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
@ -56,6 +68,39 @@ fun IconRow(icon: ImageVector, title: String, onClick: (() -> Unit)? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?) {
|
fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?, enabled: Boolean) {
|
||||||
TextRow(title = title, subtitle, includeDivider = false, onClick)
|
TextRow(title = title, subtitle, includeDivider = false, onClick, enabled = enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsToggleRow(title: String, subtitle: String?, state: Boolean, onToggle: () -> Unit) {
|
||||||
|
Toggle(title, subtitle, state, onToggle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Toggle(title: String, subtitle: String?, state: Boolean, onToggle: () -> Unit) {
|
||||||
|
val verticalPadding = 16.dp
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = verticalPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
if (subtitle == null) {
|
||||||
|
Text(text = title)
|
||||||
|
} else {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(text = title)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(text = subtitle, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.wrapContentWidth(),
|
||||||
|
checked = state,
|
||||||
|
onCheckedChange = { onToggle() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
package app.dapk.st.core
|
package app.dapk.st.core
|
||||||
|
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
|
|
||||||
class CoreAndroidModule(
|
class CoreAndroidModule(
|
||||||
private val intentFactory: IntentFactory,
|
private val intentFactory: IntentFactory,
|
||||||
private val preferences: Lazy<Preferences>,
|
private val preferences: Lazy<CachedPreferences>,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun intentFactory() = intentFactory
|
fun intentFactory() = intentFactory
|
||||||
|
|
||||||
private val themeStore by unsafeLazy { ThemeStore(preferences.value) }
|
fun themeStore() = ThemeStore(preferences.value)
|
||||||
|
|
||||||
fun themeStore() = themeStore
|
|
||||||
|
|
||||||
}
|
}
|
@ -8,10 +8,13 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.design.components.SmallTalkTheme
|
import app.dapk.st.design.components.SmallTalkTheme
|
||||||
import app.dapk.st.design.components.ThemeConfig
|
import app.dapk.st.design.components.ThemeConfig
|
||||||
import app.dapk.st.navigator.navigator
|
import app.dapk.st.navigator.navigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import androidx.activity.compose.setContent as _setContent
|
import androidx.activity.compose.setContent as _setContent
|
||||||
@ -29,7 +32,7 @@ abstract class DapkActivity : ComponentActivity(), EffectScope {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled())
|
this.themeConfig = runBlocking { ThemeConfig(themeStore.isMaterialYouEnabled()) }
|
||||||
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||||
@ -45,8 +48,10 @@ abstract class DapkActivity : ComponentActivity(), EffectScope {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (themeConfig.useDynamicTheme != themeStore.isMaterialYouEnabled()) {
|
lifecycleScope.launch {
|
||||||
recreate()
|
if (themeConfig.useDynamicTheme != themeStore.isMaterialYouEnabled()) {
|
||||||
|
recreate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,15 @@
|
|||||||
package app.dapk.st.core
|
package app.dapk.st.core
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
private const val KEY_MATERIAL_YOU_ENABLED = "material_you_enabled"
|
private const val KEY_MATERIAL_YOU_ENABLED = "material_you_enabled"
|
||||||
|
|
||||||
class ThemeStore(
|
class ThemeStore(
|
||||||
private val preferences: Preferences
|
private val preferences: CachedPreferences
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var _isMaterialYouEnabled: Boolean? = null
|
suspend fun isMaterialYouEnabled() = preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED, defaultValue = false)
|
||||||
|
|
||||||
fun isMaterialYouEnabled() = _isMaterialYouEnabled ?: blockingInitialRead()
|
|
||||||
|
|
||||||
private fun blockingInitialRead(): Boolean {
|
|
||||||
return runBlocking {
|
|
||||||
(preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED) ?: false).also { _isMaterialYouEnabled = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun storeMaterialYouEnabled(isEnabled: Boolean) {
|
suspend fun storeMaterialYouEnabled(isEnabled: Boolean) {
|
||||||
_isMaterialYouEnabled = isEnabled
|
|
||||||
preferences.store(KEY_MATERIAL_YOU_ENABLED, isEnabled)
|
preferences.store(KEY_MATERIAL_YOU_ENABLED, isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,4 +25,5 @@ dependencies {
|
|||||||
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4"
|
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4"
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
}
|
testImplementation(testFixtures(project(":core")))
|
||||||
|
testFixturesImplementation(testFixtures(project(":core")))}
|
@ -5,8 +5,12 @@ import app.dapk.st.core.CoroutineDispatchers
|
|||||||
import app.dapk.st.core.Preferences
|
import app.dapk.st.core.Preferences
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
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.domain.eventlog.EventLogPersistence
|
import app.dapk.st.domain.application.eventlog.EventLogPersistence
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
||||||
|
import app.dapk.st.domain.preference.CachingPreferences
|
||||||
|
import app.dapk.st.domain.preference.PropertyCache
|
||||||
import app.dapk.st.domain.profile.ProfilePersistence
|
import app.dapk.st.domain.profile.ProfilePersistence
|
||||||
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||||
import app.dapk.st.domain.sync.OverviewPersistence
|
import app.dapk.st.domain.sync.OverviewPersistence
|
||||||
@ -36,6 +40,9 @@ class StoreModule(
|
|||||||
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
||||||
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
||||||
|
|
||||||
|
private val cache = PropertyCache()
|
||||||
|
val cachingPreferences = CachingPreferences(cache, preferences)
|
||||||
|
|
||||||
fun pushStore() = PushTokenRegistrarPreferences(preferences)
|
fun pushStore() = PushTokenRegistrarPreferences(preferences)
|
||||||
|
|
||||||
fun applicationStore() = ApplicationPreferences(preferences)
|
fun applicationStore() = ApplicationPreferences(preferences)
|
||||||
@ -57,7 +64,12 @@ class StoreModule(
|
|||||||
return EventLogPersistence(database, coroutineDispatchers)
|
return EventLogPersistence(database, coroutineDispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loggingStore(): LoggingStore = LoggingStore(cachingPreferences)
|
||||||
|
|
||||||
|
fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences)
|
||||||
|
|
||||||
fun memberStore(): MemberStore {
|
fun memberStore(): MemberStore {
|
||||||
return MemberPersistence(database, coroutineDispatchers)
|
return MemberPersistence(database, coroutineDispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package app.dapk.st.domain.eventlog
|
package app.dapk.st.domain.application.eventlog
|
||||||
|
|
||||||
|
import app.dapk.db.DapkDb
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.core.withIoContext
|
import app.dapk.st.core.withIoContext
|
||||||
import app.dapk.db.DapkDb
|
|
||||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -42,6 +42,7 @@ class EventLogPersistence(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> database.eventLoggerQueries.selectLatestByLogFiltered(logKey, filter)
|
else -> database.eventLoggerQueries.selectLatestByLogFiltered(logKey, filter)
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList(context = coroutineDispatchers.io)
|
.mapToList(context = coroutineDispatchers.io)
|
@ -0,0 +1,17 @@
|
|||||||
|
package app.dapk.st.domain.application.eventlog
|
||||||
|
|
||||||
|
import app.dapk.st.core.CachedPreferences
|
||||||
|
import app.dapk.st.core.readBoolean
|
||||||
|
import app.dapk.st.core.store
|
||||||
|
|
||||||
|
private const val KEY_LOGGING_ENABLED = "key_logging_enabled"
|
||||||
|
|
||||||
|
class LoggingStore(private val cachedPreferences: CachedPreferences) {
|
||||||
|
|
||||||
|
suspend fun isEnabled() = cachedPreferences.readBoolean(KEY_LOGGING_ENABLED, defaultValue = false)
|
||||||
|
|
||||||
|
suspend fun setEnabled(isEnabled: Boolean) {
|
||||||
|
cachedPreferences.store(KEY_LOGGING_ENABLED, isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package app.dapk.st.domain.application.message
|
||||||
|
|
||||||
|
import app.dapk.st.core.CachedPreferences
|
||||||
|
import app.dapk.st.core.readBoolean
|
||||||
|
import app.dapk.st.core.store
|
||||||
|
|
||||||
|
private const val KEY_READ_RECEIPTS_DISABLED = "key_read_receipts_disabled"
|
||||||
|
|
||||||
|
class MessageOptionsStore(private val cachedPreferences: CachedPreferences) {
|
||||||
|
|
||||||
|
suspend fun isReadReceiptsDisabled() = cachedPreferences.readBoolean(KEY_READ_RECEIPTS_DISABLED, defaultValue = true)
|
||||||
|
|
||||||
|
suspend fun setReadReceiptsDisabled(isDisabled: Boolean) {
|
||||||
|
cachedPreferences.store(KEY_READ_RECEIPTS_DISABLED, isDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package app.dapk.st.domain.preference
|
||||||
|
|
||||||
|
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 readString(key: String): String? {
|
||||||
|
return cache.getValue(key) ?: preferences.readString(key)?.also {
|
||||||
|
cache.setValue(key, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun readString(key: String, defaultValue: String): String {
|
||||||
|
return readString(key) ?: (defaultValue.also { cache.setValue(key, it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(key: String) {
|
||||||
|
preferences.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
preferences.clear()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package app.dapk.st.domain.preference
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class PropertyCache {
|
||||||
|
|
||||||
|
private val map = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
fun <T> getValue(key: String): T? {
|
||||||
|
return map[key] as? T?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(key: String, value: Any) {
|
||||||
|
map[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package fake
|
||||||
|
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import test.delegateReturn
|
||||||
|
|
||||||
|
class FakeLoggingStore {
|
||||||
|
val instance = mockk<LoggingStore>()
|
||||||
|
|
||||||
|
fun givenLoggingIsEnabled() = coEvery { instance.isEnabled() }.delegateReturn()
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package fake
|
||||||
|
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import test.delegateReturn
|
||||||
|
|
||||||
|
class FakeMessageOptionsStore {
|
||||||
|
val instance = mockk<MessageOptionsStore>()
|
||||||
|
|
||||||
|
fun givenReadReceiptsDisabled() = coEvery { instance.isReadReceiptsDisabled() }.delegateReturn()
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package fixture
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.domain.StoreCleaner
|
import app.dapk.st.domain.StoreCleaner
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
@ -8,6 +8,7 @@ dependencies {
|
|||||||
implementation project(":matrix:services:room")
|
implementation project(":matrix:services:room")
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
|
implementation project(":domains:store")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
|
@ -3,6 +3,7 @@ package app.dapk.st.messenger
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
@ -22,10 +23,21 @@ class MessengerModule(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val base64: Base64,
|
private val base64: Base64,
|
||||||
private val imageMetaReader: ImageContentReader,
|
private val imageMetaReader: ImageContentReader,
|
||||||
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
internal fun messengerViewModel(): MessengerViewModel {
|
internal fun messengerViewModel(): MessengerViewModel {
|
||||||
return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase(), LocalIdFactory(), imageMetaReader, clock)
|
return MessengerViewModel(
|
||||||
|
messageService,
|
||||||
|
roomService,
|
||||||
|
roomStore,
|
||||||
|
credentialsStore,
|
||||||
|
timelineUseCase(),
|
||||||
|
LocalIdFactory(),
|
||||||
|
imageMetaReader,
|
||||||
|
messageOptionsStore,
|
||||||
|
clock
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun timelineUseCase(): TimelineUseCaseImpl {
|
private fun timelineUseCase(): TimelineUseCaseImpl {
|
||||||
|
@ -3,6 +3,7 @@ package app.dapk.st.messenger
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
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.RoomId
|
||||||
@ -30,6 +31,7 @@ internal class MessengerViewModel(
|
|||||||
private val observeTimeline: ObserveTimelineUseCase,
|
private val observeTimeline: ObserveTimelineUseCase,
|
||||||
private val localIdFactory: LocalIdFactory,
|
private val localIdFactory: LocalIdFactory,
|
||||||
private val imageContentReader: ImageContentReader,
|
private val imageContentReader: ImageContentReader,
|
||||||
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
||||||
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
||||||
@ -101,7 +103,7 @@ internal class MessengerViewModel(
|
|||||||
private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred<Unit> {
|
private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred<Unit> {
|
||||||
return async {
|
return async {
|
||||||
runCatching {
|
runCatching {
|
||||||
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent)
|
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled())
|
||||||
roomStore.markRead(state.roomState.roomOverview.roomId)
|
roomStore.markRead(state.roomState.roomOverview.roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import app.dapk.st.matrix.room.RoomService
|
|||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import fake.FakeCredentialsStore
|
import fake.FakeCredentialsStore
|
||||||
|
import fake.FakeMessageOptionsStore
|
||||||
import fake.FakeRoomStore
|
import fake.FakeRoomStore
|
||||||
import fixture.*
|
import fixture.*
|
||||||
import internalfake.FakeLocalIdFactory
|
import internalfake.FakeLocalIdFactory
|
||||||
@ -25,6 +26,7 @@ import java.time.Instant
|
|||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
private const val A_CURRENT_TIMESTAMP = 10000L
|
private const val A_CURRENT_TIMESTAMP = 10000L
|
||||||
|
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 const val A_LOCAL_ID = "local.1111-2222-3333"
|
private const val A_LOCAL_ID = "local.1111-2222-3333"
|
||||||
@ -40,6 +42,7 @@ class MessengerViewModelTest {
|
|||||||
private val fakeRoomStore = FakeRoomStore()
|
private val fakeRoomStore = FakeRoomStore()
|
||||||
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
|
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
|
||||||
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
|
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
|
||||||
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
|
||||||
private val viewModel = MessengerViewModel(
|
private val viewModel = MessengerViewModel(
|
||||||
fakeMessageService,
|
fakeMessageService,
|
||||||
@ -49,6 +52,7 @@ class MessengerViewModelTest {
|
|||||||
fakeObserveTimelineUseCase,
|
fakeObserveTimelineUseCase,
|
||||||
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
|
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
|
||||||
imageContentReader = FakeImageContentReader(),
|
imageContentReader = FakeImageContentReader(),
|
||||||
|
messageOptionsStore = fakeMessageOptionsStore.instance,
|
||||||
clock = fixedClock(A_CURRENT_TIMESTAMP),
|
clock = fixedClock(A_CURRENT_TIMESTAMP),
|
||||||
factory = runViewModelTest.testMutableStateFactory(),
|
factory = runViewModelTest.testMutableStateFactory(),
|
||||||
)
|
)
|
||||||
@ -68,8 +72,9 @@ class MessengerViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
||||||
|
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
||||||
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
|
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
|
||||||
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID) }
|
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = READ_RECEIPTS_ARE_DISABLED) }
|
||||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||||
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
||||||
|
|
||||||
@ -153,4 +158,4 @@ class FakeRoomService : RoomService by mockk() {
|
|||||||
|
|
||||||
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
|
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
|
||||||
|
|
||||||
class FakeImageContentReader: ImageContentReader by mockk()
|
class FakeImageContentReader : ImageContentReader by mockk()
|
@ -1,6 +1,11 @@
|
|||||||
package app.dapk.st.settings
|
package app.dapk.st.settings
|
||||||
|
|
||||||
import app.dapk.st.core.*
|
import app.dapk.st.core.BuildMeta
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.core.ThemeStore
|
||||||
|
import app.dapk.st.core.isAtLeastS
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.push.PushTokenRegistrars
|
import app.dapk.st.push.PushTokenRegistrars
|
||||||
|
|
||||||
internal class SettingsItemFactory(
|
internal class SettingsItemFactory(
|
||||||
@ -8,18 +13,19 @@ internal class SettingsItemFactory(
|
|||||||
private val deviceMeta: DeviceMeta,
|
private val deviceMeta: DeviceMeta,
|
||||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||||
private val themeStore: ThemeStore,
|
private val themeStore: ThemeStore,
|
||||||
|
private val loggingStore: LoggingStore,
|
||||||
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun root() = general() + theme() + data() + account() + about()
|
suspend fun root() = general() + theme() + data() + account() + advanced() + about()
|
||||||
|
|
||||||
private suspend fun general() = listOf(
|
private suspend fun general() = listOf(
|
||||||
SettingItem.Header("General"),
|
SettingItem.Header("General"),
|
||||||
SettingItem.Text(SettingItem.Id.Encryption, "Encryption"),
|
SettingItem.Text(SettingItem.Id.Encryption, "Encryption"),
|
||||||
SettingItem.Text(SettingItem.Id.EventLog, "Event log"),
|
|
||||||
SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id)
|
SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun theme() = listOfNotNull(
|
private suspend fun theme() = listOfNotNull(
|
||||||
SettingItem.Header("Theme"),
|
SettingItem.Header("Theme"),
|
||||||
SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()).takeIf {
|
SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()).takeIf {
|
||||||
deviceMeta.isAtLeastS()
|
deviceMeta.isAtLeastS()
|
||||||
@ -36,6 +42,21 @@ internal class SettingsItemFactory(
|
|||||||
SettingItem.Text(SettingItem.Id.SignOut, "Sign out"),
|
SettingItem.Text(SettingItem.Id.SignOut, "Sign out"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private suspend fun advanced(): List<SettingItem> {
|
||||||
|
val loggingIsEnabled = loggingStore.isEnabled()
|
||||||
|
return listOf(
|
||||||
|
SettingItem.Header("Advanced"),
|
||||||
|
SettingItem.Toggle(
|
||||||
|
SettingItem.Id.ToggleSendReadReceipts,
|
||||||
|
"Don't send message read receipts",
|
||||||
|
subtitle = "Requires the Homeserver to be running Synapse 1.65+",
|
||||||
|
state = messageOptionsStore.isReadReceiptsDisabled()
|
||||||
|
),
|
||||||
|
SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = loggingIsEnabled),
|
||||||
|
SettingItem.Text(SettingItem.Id.EventLog, "Event log", enabled = loggingIsEnabled),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun about() = listOf(
|
private fun about() = listOf(
|
||||||
SettingItem.Header("About"),
|
SettingItem.Header("About"),
|
||||||
SettingItem.Text(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
|
SettingItem.Text(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
|
||||||
|
@ -3,6 +3,8 @@ package app.dapk.st.settings
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import app.dapk.st.core.*
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.crypto.CryptoService
|
import app.dapk.st.matrix.crypto.CryptoService
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
@ -18,6 +20,8 @@ class SettingsModule(
|
|||||||
private val deviceMeta: DeviceMeta,
|
private val deviceMeta: DeviceMeta,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val themeStore: ThemeStore,
|
private val themeStore: ThemeStore,
|
||||||
|
private val loggingStore: LoggingStore,
|
||||||
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
internal fun settingsViewModel(): SettingsViewModel {
|
internal fun settingsViewModel(): SettingsViewModel {
|
||||||
@ -27,9 +31,11 @@ class SettingsModule(
|
|||||||
cryptoService,
|
cryptoService,
|
||||||
syncService,
|
syncService,
|
||||||
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
||||||
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore),
|
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
||||||
pushModule.pushTokenRegistrars(),
|
pushModule.pushTokenRegistrars(),
|
||||||
themeStore,
|
themeStore,
|
||||||
|
loggingStore,
|
||||||
|
messageOptionsStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.LocalActivityResultRegistryOwner
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -42,10 +41,7 @@ import app.dapk.st.core.StartObserving
|
|||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.components.Header
|
import app.dapk.st.core.components.Header
|
||||||
import app.dapk.st.core.getActivity
|
import app.dapk.st.core.getActivity
|
||||||
import app.dapk.st.design.components.SettingsTextRow
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.design.components.Spider
|
|
||||||
import app.dapk.st.design.components.SpiderPage
|
|
||||||
import app.dapk.st.design.components.TextRow
|
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.matrix.crypto.ImportResult
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
import app.dapk.st.settings.SettingsEvent.*
|
import app.dapk.st.settings.SettingsEvent.*
|
||||||
@ -197,11 +193,11 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||||||
items(content.value) { item ->
|
items(content.value) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is SettingItem.Text -> {
|
is SettingItem.Text -> {
|
||||||
val itemOnClick = onClick.takeIf { item.id != SettingItem.Id.Ignored }?.let {
|
val itemOnClick = onClick.takeIf {
|
||||||
{ it.invoke(item) }
|
item.id != SettingItem.Id.Ignored && item.enabled
|
||||||
}
|
}?.let { { it.invoke(item) } }
|
||||||
|
|
||||||
SettingsTextRow(item.content, item.subtitle, itemOnClick)
|
SettingsTextRow(item.content, item.subtitle, itemOnClick, enabled = item.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SettingItem.AccessToken -> {
|
is SettingItem.AccessToken -> {
|
||||||
@ -223,7 +219,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is SettingItem.Header -> Header(item.label)
|
is SettingItem.Header -> Header(item.label)
|
||||||
is SettingItem.Toggle -> Toggle(item, onToggle = {
|
is SettingItem.Toggle -> SettingsToggleRow(item.content, item.subtitle, item.state, onToggle = {
|
||||||
onClick(item)
|
onClick(item)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -242,23 +238,6 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Toggle(item: SettingItem.Toggle, onToggle: () -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, end = 24.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(text = item.content)
|
|
||||||
Switch(
|
|
||||||
checked = item.state,
|
|
||||||
onCheckedChange = { onToggle() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
||||||
Column {
|
Column {
|
||||||
@ -313,6 +292,7 @@ private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) {
|
|||||||
is OpenUrl -> {
|
is OpenUrl -> {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW).apply { data = it.url.toUri() })
|
context.startActivity(Intent(Intent.ACTION_VIEW).apply { data = it.url.toUri() })
|
||||||
}
|
}
|
||||||
|
|
||||||
RecreateActivity -> {
|
RecreateActivity -> {
|
||||||
context.getActivity()?.recreate()
|
context.getActivity()?.recreate()
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,8 @@ internal sealed interface SettingItem {
|
|||||||
val id: Id
|
val id: Id
|
||||||
|
|
||||||
data class Header(val label: String, override val id: Id = Id.Ignored) : SettingItem
|
data class Header(val label: String, override val id: Id = Id.Ignored) : SettingItem
|
||||||
data class Text(override val id: Id, val content: String, val subtitle: String? = null) : SettingItem
|
data class Text(override val id: Id, val content: String, val subtitle: String? = null, val enabled: Boolean = true) : SettingItem
|
||||||
data class Toggle(override val id: Id, val content: String, val state: Boolean) : SettingItem
|
data class Toggle(override val id: Id, val content: String, val subtitle: String? = null, val state: Boolean) : SettingItem
|
||||||
data class AccessToken(override val id: Id, val content: String, val accessToken: String) : SettingItem
|
data class AccessToken(override val id: Id, val content: String, val accessToken: String) : SettingItem
|
||||||
|
|
||||||
enum class Id {
|
enum class Id {
|
||||||
@ -57,6 +57,8 @@ internal sealed interface SettingItem {
|
|||||||
PrivacyPolicy,
|
PrivacyPolicy,
|
||||||
Ignored,
|
Ignored,
|
||||||
ToggleDynamicTheme,
|
ToggleDynamicTheme,
|
||||||
|
ToggleEnableLogs,
|
||||||
|
ToggleSendReadReceipts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import app.dapk.st.core.Lce
|
|||||||
import app.dapk.st.core.ThemeStore
|
import app.dapk.st.core.ThemeStore
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.st.domain.StoreCleaner
|
import app.dapk.st.domain.StoreCleaner
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.crypto.CryptoService
|
import app.dapk.st.matrix.crypto.CryptoService
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.matrix.crypto.ImportResult
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
@ -32,6 +34,8 @@ internal class SettingsViewModel(
|
|||||||
private val settingsItemFactory: SettingsItemFactory,
|
private val settingsItemFactory: SettingsItemFactory,
|
||||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||||
private val themeStore: ThemeStore,
|
private val themeStore: ThemeStore,
|
||||||
|
private val loggingStore: LoggingStore,
|
||||||
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
factory: MutableStateFactory<SettingsScreenState> = defaultStateFactory(),
|
factory: MutableStateFactory<SettingsScreenState> = defaultStateFactory(),
|
||||||
) : DapkViewModel<SettingsScreenState, SettingsEvent>(
|
) : DapkViewModel<SettingsScreenState, SettingsEvent>(
|
||||||
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))),
|
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))),
|
||||||
@ -52,31 +56,23 @@ internal class SettingsViewModel(
|
|||||||
|
|
||||||
fun onClick(item: SettingItem) {
|
fun onClick(item: SettingItem) {
|
||||||
when (item.id) {
|
when (item.id) {
|
||||||
SignOut -> {
|
SignOut -> viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
cacheCleaner.cleanCache(removeCredentials = true)
|
||||||
cacheCleaner.cleanCache(removeCredentials = true)
|
_events.emit(SignedOut)
|
||||||
_events.emit(SignedOut)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessToken -> {
|
AccessToken -> viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
require(item is SettingItem.AccessToken)
|
||||||
require(item is SettingItem.AccessToken)
|
_events.emit(CopyToClipboard("Token copied", item.accessToken))
|
||||||
_events.emit(CopyToClipboard("Token copied", item.accessToken))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearCache -> {
|
ClearCache -> viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
cacheCleaner.cleanCache(removeCredentials = false)
|
||||||
cacheCleaner.cleanCache(removeCredentials = false)
|
_events.emit(Toast(message = "Cache deleted"))
|
||||||
_events.emit(Toast(message = "Cache deleted"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EventLog -> {
|
EventLog -> viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
_events.emit(OpenEventLog)
|
||||||
_events.emit(OpenEventLog)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Encryption -> {
|
Encryption -> {
|
||||||
@ -85,10 +81,8 @@ internal class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivacyPolicy -> {
|
PrivacyPolicy -> viewModelScope.launch {
|
||||||
viewModelScope.launch {
|
_events.emit(OpenUrl(PRIVACY_POLICY_URL))
|
||||||
_events.emit(OpenUrl(PRIVACY_POLICY_URL))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PushProvider -> {
|
PushProvider -> {
|
||||||
@ -100,16 +94,29 @@ internal class SettingsViewModel(
|
|||||||
Ignored -> {
|
Ignored -> {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
ToggleDynamicTheme -> {
|
|
||||||
viewModelScope.launch {
|
ToggleDynamicTheme -> viewModelScope.launch {
|
||||||
themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled())
|
themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled())
|
||||||
start()
|
refreshRoot()
|
||||||
_events.emit(RecreateActivity)
|
_events.emit(RecreateActivity)
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleEnableLogs -> viewModelScope.launch {
|
||||||
|
loggingStore.setEnabled(!loggingStore.isEnabled())
|
||||||
|
refreshRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSendReadReceipts -> viewModelScope.launch {
|
||||||
|
messageOptionsStore.setReadReceiptsDisabled(!messageOptionsStore.isReadReceiptsDisabled())
|
||||||
|
refreshRoot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshRoot() {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
fun fetchPushProviders() {
|
fun fetchPushProviders() {
|
||||||
updatePageState<Page.PushProviders> { copy(options = Lce.Loading()) }
|
updatePageState<Page.PushProviders> { copy(options = Lce.Loading()) }
|
||||||
@ -146,9 +153,11 @@ internal class SettingsViewModel(
|
|||||||
is ImportResult.Error -> {
|
is ImportResult.Error -> {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
is ImportResult.Update -> {
|
is ImportResult.Update -> {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
is ImportResult.Success -> {
|
is ImportResult.Success -> {
|
||||||
syncService.forceManualRefresh(it.roomIds.toList())
|
syncService.forceManualRefresh(it.roomIds.toList())
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package app.dapk.st.settings.eventlogger
|
package app.dapk.st.settings.eventlogger
|
||||||
|
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.domain.eventlog.LogLine
|
import app.dapk.st.domain.application.eventlog.LogLine
|
||||||
|
|
||||||
data class EventLoggerState(
|
data class EventLoggerState(
|
||||||
val logs: Lce<List<String>>,
|
val logs: Lce<List<String>>,
|
||||||
|
@ -2,7 +2,7 @@ package app.dapk.st.settings.eventlogger
|
|||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.domain.eventlog.EventLogPersistence
|
import app.dapk.st.domain.application.eventlog.EventLogPersistence
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.dapk.st.settings
|
package app.dapk.st.settings
|
||||||
|
|
||||||
import app.dapk.st.core.ThemeStore
|
import app.dapk.st.core.ThemeStore
|
||||||
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateReturn
|
import test.delegateReturn
|
||||||
@ -8,5 +9,5 @@ import test.delegateReturn
|
|||||||
class FakeThemeStore {
|
class FakeThemeStore {
|
||||||
val instance = mockk<ThemeStore>()
|
val instance = mockk<ThemeStore>()
|
||||||
|
|
||||||
fun givenMaterialYouIsEnabled() = every { instance.isMaterialYouEnabled() }.delegateReturn()
|
fun givenMaterialYouIsEnabled() = coEvery { instance.isMaterialYouEnabled() }.delegateReturn()
|
||||||
}
|
}
|
@ -4,6 +4,8 @@ import app.dapk.st.core.BuildMeta
|
|||||||
import app.dapk.st.core.DeviceMeta
|
import app.dapk.st.core.DeviceMeta
|
||||||
import app.dapk.st.push.PushTokenRegistrars
|
import app.dapk.st.push.PushTokenRegistrars
|
||||||
import app.dapk.st.push.Registrar
|
import app.dapk.st.push.Registrar
|
||||||
|
import fake.FakeLoggingStore
|
||||||
|
import fake.FakeMessageOptionsStore
|
||||||
import internalfixture.aSettingHeaderItem
|
import internalfixture.aSettingHeaderItem
|
||||||
import internalfixture.aSettingTextItem
|
import internalfixture.aSettingTextItem
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
@ -15,6 +17,8 @@ import test.delegateReturn
|
|||||||
|
|
||||||
private val A_SELECTION = Registrar("A_SELECTION")
|
private val A_SELECTION = Registrar("A_SELECTION")
|
||||||
private const val ENABLED_MATERIAL_YOU = true
|
private const val ENABLED_MATERIAL_YOU = true
|
||||||
|
private const val DISABLED_LOGGING = false
|
||||||
|
private const val DISABLED_READ_RECEIPTS = true
|
||||||
|
|
||||||
class SettingsItemFactoryTest {
|
class SettingsItemFactoryTest {
|
||||||
|
|
||||||
@ -22,20 +26,30 @@ class SettingsItemFactoryTest {
|
|||||||
private val deviceMeta = DeviceMeta(apiVersion = 31)
|
private val deviceMeta = DeviceMeta(apiVersion = 31)
|
||||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||||
private val fakeThemeStore = FakeThemeStore()
|
private val fakeThemeStore = FakeThemeStore()
|
||||||
|
private val fakeLoggingStore = FakeLoggingStore()
|
||||||
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
|
||||||
private val settingsItemFactory = SettingsItemFactory(buildMeta, deviceMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance)
|
private val settingsItemFactory = SettingsItemFactory(
|
||||||
|
buildMeta,
|
||||||
|
deviceMeta,
|
||||||
|
fakePushTokenRegistrars.instance,
|
||||||
|
fakeThemeStore.instance,
|
||||||
|
fakeLoggingStore.instance,
|
||||||
|
fakeMessageOptionsStore.instance,
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when creating root items, then is expected`() = runTest {
|
fun `when creating root items, then is expected`() = runTest {
|
||||||
fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION)
|
fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION)
|
||||||
fakeThemeStore.givenMaterialYouIsEnabled().returns(ENABLED_MATERIAL_YOU)
|
fakeThemeStore.givenMaterialYouIsEnabled().returns(ENABLED_MATERIAL_YOU)
|
||||||
|
fakeLoggingStore.givenLoggingIsEnabled().returns(DISABLED_LOGGING)
|
||||||
|
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(DISABLED_READ_RECEIPTS)
|
||||||
|
|
||||||
val result = settingsItemFactory.root()
|
val result = settingsItemFactory.root()
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
aSettingHeaderItem("General"),
|
aSettingHeaderItem("General"),
|
||||||
aSettingTextItem(SettingItem.Id.Encryption, "Encryption"),
|
aSettingTextItem(SettingItem.Id.Encryption, "Encryption"),
|
||||||
aSettingTextItem(SettingItem.Id.EventLog, "Event log"),
|
|
||||||
aSettingTextItem(SettingItem.Id.PushProvider, "Push provider", A_SELECTION.id),
|
aSettingTextItem(SettingItem.Id.PushProvider, "Push provider", A_SELECTION.id),
|
||||||
SettingItem.Header("Theme"),
|
SettingItem.Header("Theme"),
|
||||||
SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = ENABLED_MATERIAL_YOU),
|
SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = ENABLED_MATERIAL_YOU),
|
||||||
@ -43,6 +57,15 @@ class SettingsItemFactoryTest {
|
|||||||
aSettingTextItem(SettingItem.Id.ClearCache, "Clear cache"),
|
aSettingTextItem(SettingItem.Id.ClearCache, "Clear cache"),
|
||||||
aSettingHeaderItem("Account"),
|
aSettingHeaderItem("Account"),
|
||||||
aSettingTextItem(SettingItem.Id.SignOut, "Sign out"),
|
aSettingTextItem(SettingItem.Id.SignOut, "Sign out"),
|
||||||
|
aSettingHeaderItem("Advanced"),
|
||||||
|
SettingItem.Toggle(
|
||||||
|
SettingItem.Id.ToggleSendReadReceipts,
|
||||||
|
"Don't send message read receipts",
|
||||||
|
subtitle = "Requires the Homeserver to be running Synapse 1.65+",
|
||||||
|
state = DISABLED_READ_RECEIPTS
|
||||||
|
),
|
||||||
|
SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = DISABLED_LOGGING),
|
||||||
|
aSettingTextItem(SettingItem.Id.EventLog, "Event log", enabled = DISABLED_LOGGING),
|
||||||
aSettingHeaderItem("About"),
|
aSettingHeaderItem("About"),
|
||||||
aSettingTextItem(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
|
aSettingTextItem(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
|
||||||
aSettingTextItem(SettingItem.Id.Ignored, "Version", buildMeta.versionName),
|
aSettingTextItem(SettingItem.Id.Ignored, "Version", buildMeta.versionName),
|
||||||
|
@ -5,7 +5,7 @@ import app.dapk.st.core.Lce
|
|||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.matrix.crypto.ImportResult
|
||||||
import fake.*
|
import fake.*
|
||||||
import fixture.FakeStoreCleaner
|
import fake.FakeStoreCleaner
|
||||||
import fixture.aRoomId
|
import fixture.aRoomId
|
||||||
import internalfake.FakeSettingsItemFactory
|
import internalfake.FakeSettingsItemFactory
|
||||||
import internalfake.FakeUriFilenameResolver
|
import internalfake.FakeUriFilenameResolver
|
||||||
@ -41,6 +41,8 @@ internal class SettingsViewModelTest {
|
|||||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||||
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
||||||
private val fakeThemeStore = FakeThemeStore()
|
private val fakeThemeStore = FakeThemeStore()
|
||||||
|
private val fakeLoggingStore = FakeLoggingStore()
|
||||||
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
|
||||||
private val viewModel = SettingsViewModel(
|
private val viewModel = SettingsViewModel(
|
||||||
fakeStoreCleaner,
|
fakeStoreCleaner,
|
||||||
@ -51,6 +53,8 @@ internal class SettingsViewModelTest {
|
|||||||
fakeSettingsItemFactory.instance,
|
fakeSettingsItemFactory.instance,
|
||||||
fakePushTokenRegistrars.instance,
|
fakePushTokenRegistrars.instance,
|
||||||
fakeThemeStore.instance,
|
fakeThemeStore.instance,
|
||||||
|
fakeLoggingStore.instance,
|
||||||
|
fakeMessageOptionsStore.instance,
|
||||||
runViewModelTest.testMutableStateFactory(),
|
runViewModelTest.testMutableStateFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import app.dapk.st.settings.SettingItem
|
|||||||
internal fun aSettingTextItem(
|
internal fun aSettingTextItem(
|
||||||
id: SettingItem.Id = SettingItem.Id.Ignored,
|
id: SettingItem.Id = SettingItem.Id.Ignored,
|
||||||
content: String = "text-content",
|
content: String = "text-content",
|
||||||
subtitle: String? = null
|
subtitle: String? = null,
|
||||||
) = SettingItem.Text(id, content, subtitle)
|
enabled: Boolean = true,
|
||||||
|
) = SettingItem.Text(id, content, subtitle, enabled)
|
||||||
|
|
||||||
internal fun aSettingHeaderItem(
|
internal fun aSettingHeaderItem(
|
||||||
label: String = "header-label",
|
label: String = "header-label",
|
||||||
|
@ -15,7 +15,7 @@ private val SERVICE_KEY = RoomService::class
|
|||||||
interface RoomService : MatrixService {
|
interface RoomService : MatrixService {
|
||||||
|
|
||||||
suspend fun joinedMembers(roomId: RoomId): List<JoinedMember>
|
suspend fun joinedMembers(roomId: RoomId): List<JoinedMember>
|
||||||
suspend fun markFullyRead(roomId: RoomId, eventId: EventId)
|
suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean)
|
||||||
|
|
||||||
suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember?
|
suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember?
|
||||||
suspend fun findMembers(roomId: RoomId, userIds: List<UserId>): List<RoomMember>
|
suspend fun findMembers(roomId: RoomId, userIds: List<UserId>): List<RoomMember>
|
||||||
|
@ -30,9 +30,9 @@ class DefaultRoomService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun markFullyRead(roomId: RoomId, eventId: EventId) {
|
override suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean) {
|
||||||
logger.matrixLog(ROOM, "marking room fully read ${roomId.value}")
|
logger.matrixLog(ROOM, "marking room fully read ${roomId.value}")
|
||||||
httpClient.execute(markFullyReadRequest(roomId, eventId))
|
httpClient.execute(markFullyReadRequest(roomId, eventId, isPrivate))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? {
|
override suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? {
|
||||||
@ -97,10 +97,10 @@ internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersRes
|
|||||||
method = MatrixHttpClient.Method.GET,
|
method = MatrixHttpClient.Method.GET,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun markFullyReadRequest(roomId: RoomId, eventId: EventId) = httpRequest<Unit>(
|
internal fun markFullyReadRequest(roomId: RoomId, eventId: EventId, isPrivate: Boolean) = httpRequest<Unit>(
|
||||||
path = "_matrix/client/r0/rooms/${roomId.value}/read_markers",
|
path = "_matrix/client/r0/rooms/${roomId.value}/read_markers",
|
||||||
method = MatrixHttpClient.Method.POST,
|
method = MatrixHttpClient.Method.POST,
|
||||||
body = jsonBody(MarkFullyReadRequest(eventId, eventId, hidden = true))
|
body = jsonBody(MarkFullyReadRequest(eventId, eventId, hidden = isPrivate))
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun createRoomRequest(invites: List<UserId>, isDM: Boolean, visibility: RoomVisibility, name: String? = null) = httpRequest<ApiCreateRoomResponse>(
|
internal fun createRoomRequest(invites: List<UserId>, isDM: Boolean, visibility: RoomVisibility, name: String? = null) = httpRequest<ApiCreateRoomResponse>(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user