From 480050bed3fdefc8df809e5da3f75140616edf5d Mon Sep 17 00:00:00 2001
From: ialokim <ialokim@mailbox.org>
Date: Wed, 2 Feb 2022 18:39:25 +0100
Subject: [PATCH] make theme setting persistent

---
 .../briar/desktop/DesktopModule.kt            |  6 +++
 .../desktop/conversation/ConversationList.kt  |  2 +-
 .../briar/desktop/settings/Settings.kt        | 30 +++++++++++++
 .../briar/desktop/settings/SettingsImpl.kt    | 43 +++++++++++++++++++
 .../desktop/settings/SettingsViewModel.kt     | 13 ++++--
 .../briarproject/briar/desktop/ui/BriarUi.kt  | 12 +++++-
 .../desktop/viewmodel/SingleStateEvent.kt     | 28 +++++++++---
 .../briar/desktop/DesktopTestModule.kt        |  6 +++
 8 files changed, 127 insertions(+), 13 deletions(-)
 create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt
 create mode 100644 src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt

diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index d232df24b3..b92286f585 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -52,6 +52,8 @@ import org.briarproject.bramble.util.OsUtils.isMac
 import org.briarproject.briar.attachment.AttachmentModule
 import org.briarproject.briar.desktop.attachment.media.ImageCompressor
 import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.SettingsImpl
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
@@ -100,6 +102,10 @@ internal class DesktopModule(
         return DesktopDatabaseConfig(dbDir, keyDir)
     }
 
+    @Provides
+    @Singleton
+    fun provideSettings(settings: SettingsImpl): Settings = settings
+
     @Provides
     @Singleton
     @EventExecutor
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
index 62c9ec49a3..99e4c6ccbd 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
@@ -229,7 +229,7 @@ fun ConversationList(
         }
     }
 
-    onMessageAddedToBottom.react { type ->
+    onMessageAddedToBottom.reactInCoroutine { type ->
         // scroll to bottom for new *outgoing* message or if scroll position was at last message before
         if (type == ConversationViewModel.MessageAddedType.OUTGOING || scrollState.isScrolledToPenultimate()) {
             scope.launch {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt
new file mode 100644
index 0000000000..59fa618dda
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt
@@ -0,0 +1,30 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.settings
+
+import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
+
+interface Settings {
+
+    enum class Theme { AUTO, LIGHT, DARK }
+
+    var theme: Theme
+
+    val invalidateScreen: SingleStateEvent<Unit>
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt
new file mode 100644
index 0000000000..d70e0f4df3
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt
@@ -0,0 +1,43 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.settings
+
+import org.briarproject.briar.desktop.settings.Settings.Theme
+import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO
+import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
+import java.util.prefs.Preferences
+import javax.inject.Inject
+
+const val PREF_THEME = "theme"
+
+class SettingsImpl @Inject internal constructor() : Settings {
+
+    // used for unencrypted settings, namely theme and language
+    private val prefs = Preferences.userNodeForPackage(this::class.java)
+
+    override val invalidateScreen = SingleStateEvent<Unit>()
+
+    override var theme: Theme
+        get() = Theme.valueOf(prefs.get(PREF_THEME, AUTO.name))
+        set(value) {
+            prefs.put(PREF_THEME, value.name)
+            prefs.flush() // write preferences to disk
+            invalidateScreen.emit(Unit)
+        }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
index 7d35ba60cb..3c3aec2b22 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
@@ -19,6 +19,8 @@
 package org.briarproject.briar.desktop.settings
 
 import androidx.compose.runtime.mutableStateOf
+import org.briarproject.briar.desktop.settings.Settings.Theme.DARK
+import org.briarproject.briar.desktop.settings.Settings.Theme.LIGHT
 import org.briarproject.briar.desktop.viewmodel.ViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
 import javax.inject.Inject
@@ -34,18 +36,21 @@ enum class SettingCategory {
 
 class SettingsViewModel
 @Inject
-constructor() : ViewModel {
+constructor(
+    private val settings: Settings,
+) : ViewModel {
     private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY)
     val selectedSetting = _selectedSetting.asState()
 
-    private val _isDarkMode = mutableStateOf(true)
+    private val _isDarkMode = mutableStateOf(settings.theme == DARK)
     val isDarkMode = _isDarkMode.asState()
 
     fun selectSetting(selectedOption: SettingCategory) {
         _selectedSetting.value = selectedOption
     }
 
-    fun toggleTheme() {
-        _isDarkMode.value = !isDarkMode.value
+    fun toggleTheme() { // todo: set theme instead
+        settings.theme = if (settings.theme == DARK) LIGHT else DARK
+        _isDarkMode.value = settings.theme == DARK
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 06cc0ba857..659f8ed263 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -18,6 +18,7 @@
 
 package org.briarproject.briar.desktop.ui
 
+import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
@@ -45,6 +46,9 @@ import org.briarproject.briar.desktop.DesktopFeatureFlags
 import org.briarproject.briar.desktop.expiration.ExpirationBanner
 import org.briarproject.briar.desktop.login.ErrorScreen
 import org.briarproject.briar.desktop.login.StartupScreen
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO
+import org.briarproject.briar.desktop.settings.Settings.Theme.DARK
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import org.briarproject.briar.desktop.theme.BriarTheme
 import org.briarproject.briar.desktop.ui.Screen.EXPIRED
@@ -85,6 +89,7 @@ constructor(
     private val lifecycleManager: LifecycleManager,
     private val eventBus: EventBus,
     private val viewModelProvider: ViewModelProvider,
+    private val settings: Settings,
     private val featureFlags: FeatureFlags,
     private val desktopFeatureFlags: DesktopFeatureFlags,
 ) : BriarUi, EventListener {
@@ -110,6 +115,9 @@ constructor(
     @OptIn(ExperimentalComposeUiApi::class)
     @Composable
     override fun start(onClose: () -> Unit) {
+        // invalidate whole application window in case the theme or language setting is changed
+        settings.invalidateScreen.react {}
+
         val title = i18n("main.title")
         val platformLocalization = object : PlatformLocalization {
             override val copy = i18n("copy")
@@ -133,7 +141,9 @@ constructor(
             ) {
                 var showAbout by remember { mutableStateOf(false) }
                 val settingsViewModel: SettingsViewModel = viewModel()
-                BriarTheme(isDarkTheme = settingsViewModel.isDarkMode.value) {
+                val isDarkTheme = settings.theme == DARK ||
+                    (settings.theme == AUTO && isSystemInDarkTheme())
+                BriarTheme(isDarkTheme) {
                     Column(Modifier.fillMaxSize()) {
                         ExpirationBanner { screenState = EXPIRED; stop() }
                         when (screenState) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
index 273dae37e6..3a1cd50620 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
@@ -44,18 +44,32 @@ class SingleStateEvent<T : Any> {
         state.value = value
     }
 
+    fun reactAndReset(block: (T) -> Unit) {
+        val value = state.value
+        if (value != null) {
+            state.value = null
+            block(value)
+        }
+    }
+
+    /**
+     * React to every new value of type [T] emitted through this event,
+     * by directly reading the state in the calling function.
+     * This can be used to invalidate a composable function.
+     * Make sure to not react to the same event on multiple places.
+     */
+    @Composable
+    inline fun react(noinline block: (T) -> Unit) = reactAndReset(block)
+
     /**
-     * React to every new value of type [T] emitted through this event.
+     * React to every new value of type [T] emitted through this event
+     * inside a LaunchedEffect.
      * Make sure to not react to the same event on multiple places.
      */
     @Composable
-    fun react(block: (T) -> Unit) {
+    fun reactInCoroutine(block: (T) -> Unit) {
         LaunchedEffect(state.value) {
-            val value = state.value
-            if (value != null) {
-                block(value)
-                state.value = null
-            }
+            reactAndReset(block)
         }
     }
 }
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index 99327752b1..4ff59a88cc 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -52,6 +52,8 @@ import org.briarproject.briar.api.test.TestAvatarCreator
 import org.briarproject.briar.attachment.AttachmentModule
 import org.briarproject.briar.desktop.attachment.media.ImageCompressor
 import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.SettingsImpl
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl
 import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl
@@ -105,6 +107,10 @@ internal class DesktopTestModule(
         return DesktopDatabaseConfig(dbDir, keyDir)
     }
 
+    @Provides
+    @Singleton
+    fun provideSettings(settings: SettingsImpl): Settings = settings
+
     @Provides
     @Singleton
     @EventExecutor
-- 
GitLab