Merge pull request #194 from ouchadam/feature/extra-settings

Logging and read receipt settings
This commit is contained in:
Adam Brown 2022-10-08 12:30:06 +01:00 committed by GitHub
commit ef25d01c6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 356 additions and 120 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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")

View File

@ -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 {

View File

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

View File

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

View File

@ -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"),

View File

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

View File

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

View File

@ -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,
} }
} }

View File

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

View File

@ -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>>,

View File

@ -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

View File

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

View File

@ -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),

View File

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

View File

@ -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",

View File

@ -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>

View File

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