From 5d266cca20412d3c904d6a1bc018f8854606eb84 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 18 Oct 2023 18:04:07 -0400 Subject: [PATCH] - Moves app startup to an IO thread. - Upgrades Shared Preferences serialization to a Single JSON object - Simplifies Shared Preferences state changes - LazyInitializes Video cache --- .../com/vitorpamplona/amethyst/Amethyst.kt | 32 +- .../amethyst/EncryptedStorage.kt | 1 + .../amethyst/LocalPreferences.kt | 286 ++++++++++-------- .../vitorpamplona/amethyst/ServiceManager.kt | 18 +- .../vitorpamplona/amethyst/model/Account.kt | 44 +-- .../amethyst/model/LocalCache.kt | 11 - .../vitorpamplona/amethyst/model/Settings.kt | 34 ++- .../connectivitystatus/ConnectivityStatus.kt | 15 - .../EventNotificationConsumer.kt | 10 +- .../service/notifications/RegisterAccounts.kt | 4 +- .../service/playback/PlaybackService.kt | 5 +- .../amethyst/service/playback/VideoCache.kt | 8 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 73 +++-- .../ui/actions/JoinUserOrChannelView.kt | 18 +- .../amethyst/ui/actions/NewRelayListView.kt | 13 +- .../ui/actions/RelayInformationDialog.kt | 8 +- .../amethyst/ui/components/UrlPreview.kt | 8 +- .../amethyst/ui/components/UrlPreviewCard.kt | 8 +- .../amethyst/ui/components/VideoView.kt | 17 +- .../ui/components/ZoomableContentView.kt | 26 +- .../ui/navigation/AccountSwitchBottomSheet.kt | 18 +- .../amethyst/ui/navigation/AppNavigation.kt | 7 +- .../amethyst/ui/navigation/AppTopBar.kt | 8 +- .../amethyst/ui/navigation/DrawerContent.kt | 51 ++-- .../amethyst/ui/note/ChatroomHeaderCompose.kt | 8 +- .../ui/note/ChatroomMessageCompose.kt | 8 +- .../amethyst/ui/note/MultiSetCompose.kt | 8 +- .../amethyst/ui/note/NoteCompose.kt | 20 +- .../amethyst/ui/note/RelayListRow.kt | 8 +- .../amethyst/ui/note/UserProfilePicture.kt | 15 +- .../amethyst/ui/screen/AccountScreen.kt | 40 ++- .../amethyst/ui/screen/AccountState.kt | 1 + .../ui/screen/AccountStateViewModel.kt | 119 +++++--- .../ui/screen/SharedPreferencesViewModel.kt | 169 +++++++++++ .../amethyst/ui/screen/ThemeViewModel.kt | 16 - .../ui/screen/loggedIn/AccountViewModel.kt | 39 +-- .../ui/screen/loggedIn/ChannelScreen.kt | 8 +- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 3 - .../amethyst/ui/screen/loggedIn/MainScreen.kt | 12 +- .../ui/screen/loggedIn/ProfileScreen.kt | 14 +- .../ui/screen/loggedIn/SearchScreen.kt | 8 +- .../ui/screen/loggedIn/SettingsScreen.kt | 119 +++----- .../ui/screen/loggedOff/LoginScreen.kt | 44 +-- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 13 +- app/src/main/res/values/strings.xml | 1 + .../PushNotificationReceiverService.kt | 9 +- 46 files changed, 692 insertions(+), 713 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index d2a447fdf..5ee6a9490 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -4,16 +4,26 @@ import android.app.Application import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy +import android.util.Log +import androidx.media3.common.util.UnstableApi +import coil.ImageLoader import com.vitorpamplona.amethyst.service.playback.VideoCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlin.time.measureTimedValue + +@UnstableApi class Amethyst : Application() { + val videoCache: VideoCache by lazy { + val newCache = VideoCache() + newCache.initFileCache(instance) + newCache + } -class Amethyst : Application() { - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) override fun onCreate() { super.onCreate() instance = this - VideoCache.initFileCache(instance) - if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( ThreadPolicy.Builder() @@ -34,6 +44,20 @@ class Amethyst : Application() { .build() ) } + + GlobalScope.launch(Dispatchers.IO) { + val (value, elapsed) = measureTimedValue { + // initializes the video cache in a thread + videoCache + } + Log.d("Rendering Metrics", "VideoCache initialized in $elapsed") + } + } + + fun imageLoaderBuilder(): ImageLoader.Builder { + return ImageLoader.Builder(applicationContext).diskCache { + SingletonDiskCache.get(applicationContext) + } } companion object { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt index bfcea8192..0ab47841c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt @@ -6,6 +6,7 @@ import androidx.security.crypto.MasterKey object EncryptedStorage { private const val PREFERENCES_NAME = "secret_keeper" + // returns the preferences for each account or a global file if null. fun prefsFileName(npub: String? = null): String { return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c17c11fb4..4a139e79f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -17,15 +17,20 @@ import com.vitorpamplona.amethyst.model.Nip47URI import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.ServersAvailable import com.vitorpamplona.amethyst.model.Settings +import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.model.parseConnectivityType +import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.LnZapEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.util.Locale @@ -81,22 +86,25 @@ private object PrefKeys { const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars" const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" + + const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" + const val SHARED_SETTINGS = "shared_settings" } object LocalPreferences { private const val comma = "," private var _currentAccount: String? = null - private var _savedAccounts: List? = null + private var _savedAccounts: List? = null - fun currentAccount(): String? { + suspend fun currentAccount(): String? { if (_currentAccount == null) { _currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) } return _currentAccount } - private fun updateCurrentAccount(npub: String) { + private suspend fun updateCurrentAccount(npub: String) { if (_currentAccount != npub) { _currentAccount = npub @@ -106,20 +114,41 @@ object LocalPreferences { } } - private fun savedAccounts(): List { + private fun savedAccounts(): List { if (_savedAccounts == null) { - _savedAccounts = encryptedPreferences() - .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + with(encryptedPreferences()) { + val newSystemOfAccounts = getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let { + Event.mapper.readValue>(it) + } + + if (newSystemOfAccounts != null && newSystemOfAccounts.isNotEmpty()) { + _savedAccounts = newSystemOfAccounts + } else { + val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + + val migrated = oldAccounts.map { npub -> + AccountInfo( + npub, + encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false), + (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank() + ) + } + + println("AAA migrated: $migrated") + + _savedAccounts = migrated + } + } } return _savedAccounts!! } - private fun updateSavedAccounts(accounts: List) { + private suspend fun updateSavedAccounts(accounts: List) = withContext(Dispatchers.IO) { if (_savedAccounts != accounts) { _savedAccounts = accounts encryptedPreferences().edit().apply { - putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) + putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(accounts)) }.apply() } } @@ -127,7 +156,7 @@ object LocalPreferences { private val prefsDirPath: String get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" - private fun addAccount(npub: String) { + private suspend fun addAccount(npub: AccountInfo) { val accounts = savedAccounts().toMutableList() if (npub !in accounts) { accounts.add(npub) @@ -135,22 +164,27 @@ object LocalPreferences { } } - private fun setCurrentAccount(account: Account) { + private suspend fun setCurrentAccount(account: Account) = withContext(Dispatchers.IO) { val npub = account.userProfile().pubkeyNpub() + val accInfo = AccountInfo( + npub, + account.isWriteable(), + account.loginWithExternalSigner + ) updateCurrentAccount(npub) - addAccount(npub) + addAccount(accInfo) } - fun switchToAccount(npub: String) { - updateCurrentAccount(npub) + suspend fun switchToAccount(accountInfo: AccountInfo) = withContext(Dispatchers.IO) { + updateCurrentAccount(accountInfo.npub) } /** * Removes the account from the app level shared preferences */ - private fun removeAccount(npub: String) { + private suspend fun removeAccount(accountInfo: AccountInfo) { val accounts = savedAccounts().toMutableList() - if (accounts.remove(npub)) { + if (accounts.remove(accountInfo)) { updateSavedAccounts(accounts) } } @@ -159,6 +193,8 @@ object LocalPreferences { * Deletes the npub-specific shared preference file */ private fun deleteUserPreferenceFile(npub: String) { + checkNotInMainThread() + val prefsDir = File(prefsDirPath) prefsDir.list()?.forEach { if (it.contains(npub)) { @@ -168,6 +204,8 @@ object LocalPreferences { } private fun encryptedPreferences(npub: String? = null): SharedPreferences { + checkNotInMainThread() + return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) @@ -185,40 +223,31 @@ object LocalPreferences { * condition and the file will probably not be deleted */ @SuppressLint("ApplySharedPref") - fun updatePrefsForLogout(npub: String) { - val userPrefs = encryptedPreferences(npub) + suspend fun updatePrefsForLogout(accountInfo: AccountInfo) = withContext(Dispatchers.IO) { + val userPrefs = encryptedPreferences(accountInfo.npub) userPrefs.edit().clear().commit() - removeAccount(npub) - deleteUserPreferenceFile(npub) + removeAccount(accountInfo) + deleteUserPreferenceFile(accountInfo.npub) if (savedAccounts().isEmpty()) { - val appPrefs = encryptedPreferences() - appPrefs.edit().clear().apply() - } else if (currentAccount() == npub) { - updateCurrentAccount(savedAccounts().elementAt(0)) + encryptedPreferences().edit().clear().apply() + } else if (currentAccount() == accountInfo.npub) { + updateCurrentAccount(savedAccounts().elementAt(0).npub) } } - fun updatePrefsForLogin(account: Account) { + suspend fun updatePrefsForLogin(account: Account) { setCurrentAccount(account) saveToEncryptedStorage(account) } fun allSavedAccounts(): List { - return savedAccounts().map { npub -> - AccountInfo( - npub, - hasPrivKey(npub), - getLoggedInWithExternalSigner(npub) - ) - } + return savedAccounts() } - fun allLocalAccountNPubs(): Set { - return savedAccounts().toSet() - } + suspend fun saveToEncryptedStorage(account: Account) = withContext(Dispatchers.IO) { + checkNotInMainThread() - fun saveToEncryptedStorage(account: Account) { val prefs = encryptedPreferences(account.userProfile().pubkeyNpub()) prefs.edit().apply { account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) } @@ -256,71 +285,102 @@ object LocalPreferences { } putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner) }.apply() - - val globalPrefs = encryptedPreferences() - globalPrefs.edit().apply { - if (account.settings.automaticallyShowImages.prefCode == null) { - remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES) - } else { - putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages.prefCode!!) - } - - if (account.settings.automaticallyStartPlayback.prefCode == null) { - remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK) - } else { - putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback.prefCode!!) - } - if (account.settings.automaticallyShowUrlPreview.prefCode == null) { - remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW) - } else { - putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview.prefCode!!) - } - if (account.settings.automaticallyHideNavigationBars.prefCode == null) { - remove(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS) - } else { - putBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, account.settings.automaticallyHideNavigationBars.prefCode!!) - } - - if (account.settings.automaticallyShowProfilePictures.prefCode == null) { - remove(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE) - } else { - putBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, account.settings.automaticallyShowProfilePictures.prefCode!!) - } - putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "") - }.apply() } - fun updateTheme(theme: Int) { - encryptedPreferences().edit().apply { - putInt(PrefKeys.THEME, theme) - }.apply() - } - - fun getTheme(): Int { - return encryptedPreferences().getInt(PrefKeys.THEME, 0) - } - - fun getPreferredLanguage(): String { - return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: "" - } - - private fun getLoggedInWithExternalSigner(npub: String): Boolean { - return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false) - } - - private fun hasPrivKey(npub: String): Boolean { - return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank() - } - - fun loadFromEncryptedStorage(): Account? { - val acc = loadFromEncryptedStorage(currentAccount()) + suspend fun loadCurrentAccountFromEncryptedStorage(): Account? { + val acc = loadCurrentAccountFromEncryptedStorage(currentAccount()) acc?.registerObservers() return acc } - fun loadFromEncryptedStorage(npub: String?): Account? { - encryptedPreferences(npub).apply { - val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null + suspend fun migrateOldSharedSettings(): Settings? { + val prefs = encryptedPreferences() + loadOldSharedSettings(prefs)?.let { + saveSharedSettings(it, prefs) + return it + } + return null + } + + suspend fun saveSharedSettings(sharedSettings: Settings, prefs: SharedPreferences = encryptedPreferences()) { + with(prefs.edit()) { + putString(PrefKeys.SHARED_SETTINGS, Event.mapper.writeValueAsString(sharedSettings)) + apply() + } + } + + suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? { + with(prefs) { + return try { + getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { + Event.mapper.readValue(it) + } + } catch (e: Throwable) { + Log.w("LocalPreferences", "Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}", e) + e.printStackTrace() + null + } + } + } + + @Deprecated("Turned into a single JSON object") + suspend fun loadOldSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? { + with(prefs) { + if (!contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) { + return null + } + + val automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) { + parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false)) + } else { + ConnectivityType.ALWAYS + } + + val automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) { + parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false)) + } else { + ConnectivityType.ALWAYS + } + val automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) { + parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false)) + } else { + ConnectivityType.ALWAYS + } + val automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) { + parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false)) + } else { + BooleanType.ALWAYS + } + + val automaticallyShowProfilePictures = if (contains(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)) { + parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, false)) + } else { + ConnectivityType.ALWAYS + } + + val themeType = if (contains(PrefKeys.THEME)) { + parseThemeType(getInt(PrefKeys.THEME, ThemeType.SYSTEM.screenCode)) + } else { + ThemeType.SYSTEM + } + + return Settings( + themeType, + getString(PrefKeys.PREFERRED_LANGUAGE, null)?.ifBlank { null }, + automaticallyShowImages, + automaticallyStartPlayback, + automaticallyShowUrlPreview, + automaticallyHideNavigationBars, + automaticallyShowProfilePictures + ) + } + } + + suspend fun loadCurrentAccountFromEncryptedStorage(npub: String?): Account? = withContext(Dispatchers.IO) { + checkNotInMainThread() + + return@withContext with(encryptedPreferences(npub)) { + val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf() @@ -413,40 +473,7 @@ object LocalPreferences { mapOf() } - val settings = Settings() - encryptedPreferences().apply { - settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) { - parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false)) - } else { - ConnectivityType.ALWAYS - } - - settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) { - parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false)) - } else { - ConnectivityType.ALWAYS - } - settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) { - parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false)) - } else { - ConnectivityType.ALWAYS - } - settings.automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) { - parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false)) - } else { - BooleanType.ALWAYS - } - - settings.automaticallyShowProfilePictures = if (contains(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)) { - parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, false)) - } else { - ConnectivityType.ALWAYS - } - - settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "") - } - - val a = Account( + return@with Account( keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), followingChannels = followingChannels, followingCommunities = followingCommunities, @@ -474,11 +501,8 @@ object LocalPreferences { warnAboutPostsWithReports = warnAboutReports, filterSpamFromStrangers = filterSpam, lastReadPerRoute = lastReadPerRoute, - settings = settings, loginWithExternalSigner = loginWithExternalSigner ) - - return a } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index dadff2a6e..02050f908 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -4,7 +4,6 @@ import android.content.Context import android.os.Build import android.util.Log import coil.Coil -import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder @@ -44,14 +43,14 @@ object ServiceManager { private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications private var account: Account? = null - fun start(account: Account, context: Context) { + fun start(account: Account) { this.account = account ExternalSignerUtils.account = account - start(context) + start() } @Synchronized - fun start(context: Context) { + fun start() { if (isStarted && account != null) { return } @@ -63,7 +62,7 @@ object ServiceManager { HttpClient.start(account) OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true) Coil.setImageLoader { - ImageLoader.Builder(context).components { + Amethyst.instance.imageLoaderBuilder().components { if (Build.VERSION.SDK_INT >= 28) { add(ImageDecoderDecoder.Factory()) } else { @@ -71,7 +70,6 @@ object ServiceManager { } add(SvgDecoder.Factory()) }.logger(DebugLogger()) - .diskCache { SingletonDiskCache.get(context.applicationContext) } .okHttpClient { HttpClient.getHttpClient() } .respectCacheHeaders(false) .build() @@ -138,7 +136,9 @@ object ServiceManager { fun trimMemory() { LocalCache.cleanObservers() - val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet() + val accounts = LocalPreferences.allSavedAccounts().mapNotNull { + decodePublicKeyAsHexOrNull(it.npub) + }.toSet() account?.let { LocalCache.pruneOldAndHiddenMessages(it) @@ -152,10 +152,10 @@ object ServiceManager { } } - fun restartIfDifferentAccount(account: Account, context: Context) { + fun restartIfDifferentAccount(account: Account) { if (this.account != account) { pause() - start(account, context) + start(account) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7770df44e..f590d3e3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -73,6 +73,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.math.BigDecimal import java.net.Proxy import java.util.Locale @@ -130,7 +131,6 @@ class Account( var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, var lastReadPerRoute: Map = mapOf(), - var settings: Settings = Settings(), var loginWithExternalSigner: Boolean = false ) { var transientHiddenUsers: ImmutableSet = persistentSetOf() @@ -208,46 +208,6 @@ class Account( var userProfileCache: User? = null - fun updateAutomaticallyStartPlayback( - automaticallyStartPlayback: ConnectivityType - ) { - settings.automaticallyStartPlayback = automaticallyStartPlayback - live.invalidateData() - saveable.invalidateData() - } - - fun updateAutomaticallyShowUrlPreview( - automaticallyShowUrlPreview: ConnectivityType - ) { - settings.automaticallyShowUrlPreview = automaticallyShowUrlPreview - live.invalidateData() - saveable.invalidateData() - } - - fun updateAutomaticallyShowProfilePicture( - automaticallyShowProfilePicture: ConnectivityType - ) { - settings.automaticallyShowProfilePictures = automaticallyShowProfilePicture - live.invalidateData() - saveable.invalidateData() - } - - fun updateAutomaticallyHideHavBars( - automaticallyHideHavBars: BooleanType - ) { - settings.automaticallyHideNavigationBars = automaticallyHideHavBars - live.invalidateData() - saveable.invalidateData() - } - - fun updateAutomaticallyShowImages( - automaticallyShowImages: ConnectivityType - ) { - settings.automaticallyShowImages = automaticallyShowImages - live.invalidateData() - saveable.invalidateData() - } - fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) { warnAboutPostsWithReports = warnReports filterSpamFromStrangers = filterSpam @@ -3178,7 +3138,7 @@ class Account( return lastReadPerRoute[route] ?: 0 } - fun registerObservers() { + suspend fun registerObservers() = withContext(Dispatchers.Main) { // Observes relays to restart connections userProfile().live().relays.observeForever { GlobalScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 8a94f114f..b4ad6aa0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -3,8 +3,6 @@ package com.vitorpamplona.amethyst.model import android.util.Log import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.service.ExternalSignerUtils import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.components.BundledInsert @@ -13,7 +11,6 @@ import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.Nip19 -import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AddressableEvent @@ -286,14 +283,6 @@ object LocalCache { val user = getOrCreateUser(event.pubKey) if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) { if (event.dTag() == "bookmark") { - val loggedInUser = LocalPreferences.currentAccount() - val hexKey = loggedInUser?.bechToBytes() - if (hexKey != null) { - val pubKey = Hex.encode(hexKey) - if (pubKey == event.pubKey) { - ExternalSignerUtils.content.remove(event.id) - } - } user.updateBookmark(event) } // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt index a602bd06f..2a928632e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -4,16 +4,34 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R @Stable -class Settings( - var preferredLanguage: String? = null, - var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS, - var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS, - var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS, - var automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS, - var automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS +data class Settings( + val theme: ThemeType = ThemeType.SYSTEM, + val preferredLanguage: String? = null, + val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS, + val automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS, + val automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS, + val automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS, + val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS ) -enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) { +enum class ThemeType(val screenCode: Int, val resourceId: Int) { + SYSTEM(0, R.string.system), + LIGHT(1, R.string.light), + DARK(2, R.string.dark) +} + +fun parseThemeType(code: Int?): ThemeType { + return when (code) { + ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM + ThemeType.LIGHT.screenCode -> ThemeType.LIGHT + ThemeType.DARK.screenCode -> ThemeType.DARK + else -> { + ThemeType.SYSTEM + } + } +} + +enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val resourceId: Int) { ALWAYS(null, 0, R.string.connectivity_type_always), WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only), NEVER(false, 2, R.string.connectivity_type_never) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt deleted file mode 100644 index ee7151749..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.vitorpamplona.amethyst.service.connectivitystatus - -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf - -object ConnectivityStatus { - private val onMobileData = mutableStateOf(false) - val isOnMobileData: MutableState = onMobileData - - fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) { - if (onMobileData.value != isOnMobileData) { - onMobileData.value = isOnMobileData - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 275b834c8..bdc63eacc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -33,9 +33,10 @@ class EventNotificationConsumer(private val applicationContext: Context) { // PushNotification Wraps don't include a receiver. // Test with all logged in accounts LocalPreferences.allSavedAccounts().forEach { - val acc = LocalPreferences.loadFromEncryptedStorage(it.npub) - if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) { - consumeIfMatchesAccount(event, acc) + if (it.hasPrivKey || it.loggedInWithExternalSigner) { + LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc -> + consumeIfMatchesAccount(event, acc) + } } } } @@ -72,9 +73,6 @@ class EventNotificationConsumer(private val applicationContext: Context) { } } else if (key != null) { pushWrappedEvent.unwrap(key)?.let { notificationEvent -> - if (!LocalCache.justVerify(notificationEvent)) return // invalid event - if (LocalCache.notes[notificationEvent.id] != null) return // already processed - LocalCache.justConsume(notificationEvent, null) unwrapAndConsume(notificationEvent, account)?.let { innerEvent -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 3e6ecdcd4..9d023231b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -18,12 +18,12 @@ class RegisterAccounts( ) { // creates proof that it controls all accounts - private fun signEventsToProveControlOfAccounts( + private suspend fun signEventsToProveControlOfAccounts( accounts: List, notificationToken: String ): List { return accounts.mapNotNull { - val acc = LocalPreferences.loadFromEncryptedStorage(it.npub) + val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) if (acc != null && (acc.isWriteable() || acc.loginWithExternalSigner)) { if (acc.loginWithExternalSigner) { ExternalSignerUtils.account = acc diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt index c76cbe84b..f1e80b39f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt @@ -25,10 +25,7 @@ class PlaybackService : MediaSessionService() { fun newProgressiveDataSource(): MediaSource.Factory { return ProgressiveMediaSource.Factory( - VideoCache.get( - Amethyst.instance, - HttpClient.getHttpClient() - ) + (applicationContext as Amethyst).videoCache.get(HttpClient.getHttpClient()) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt index ea3cd2312..0ffbb0691 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt @@ -10,7 +10,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import okhttp3.OkHttpClient import java.io.File -@UnstableApi object VideoCache { +@UnstableApi class VideoCache { var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 90MB @@ -42,11 +42,7 @@ import java.io.File .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) } - fun get(context: Context, client: OkHttpClient): CacheDataSource.Factory { - if (!this::simpleCache.isInitialized) { - initFileCache(context) - } - + fun get(client: OkHttpClient): CacheDataSource.Factory { // Renews the factory because OkHttpMight have changed. renewCacheFactory(client) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index a5b67c81b..51c3421ba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -14,17 +14,16 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import androidx.core.os.LocaleListCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.service.ExternalSignerUtils -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex @@ -33,7 +32,7 @@ import com.vitorpamplona.amethyst.ui.navigation.debugState import com.vitorpamplona.amethyst.ui.note.Nip47 import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.events.ChannelCreateEvent @@ -50,46 +49,38 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets class MainActivity : AppCompatActivity() { + private val isOnMobileDataState = mutableStateOf(false) + @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { ExternalSignerUtils.start(this) super.onCreate(savedInstanceState) - val language = LocalPreferences.getPreferredLanguage() - if (language.isNotBlank()) { - val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language) - AppCompatDelegate.setApplicationLocales(appLocale) - } - setContent { - val themeViewModel: ThemeViewModel = viewModel() + val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() - themeViewModel.onChange(LocalPreferences.getTheme()) - AmethystTheme(themeViewModel) { + LaunchedEffect(key1 = sharedPreferencesViewModel, isOnMobileDataState) { + sharedPreferencesViewModel.init() + sharedPreferencesViewModel.updateConnectivityStatusState(isOnMobileDataState) + } + + AmethystTheme(sharedPreferencesViewModel) { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - val accountStateViewModel: AccountStateViewModel = viewModel { - AccountStateViewModel(this@MainActivity) + val accountStateViewModel: AccountStateViewModel = viewModel() + + LaunchedEffect(key1 = Unit) { + accountStateViewModel.tryLoginExistingAccountAsync() } - AccountScreen(accountStateViewModel, themeViewModel) + AccountScreen(accountStateViewModel, sharedPreferencesViewModel) } } } - - val networkRequest = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build() - - val connectivityManager = - getSystemService(ConnectivityManager::class.java) as ConnectivityManager - connectivityManager.requestNetwork(networkRequest, networkCallback) } @OptIn(DelicateCoroutinesApi::class) @@ -102,13 +93,22 @@ class MainActivity : AppCompatActivity() { // Only starts after login GlobalScope.launch(Dispatchers.IO) { if (ServiceManager.shouldPauseService) { - ServiceManager.start(this@MainActivity) + ServiceManager.start() } } GlobalScope.launch(Dispatchers.IO) { PushNotificationUtils.init(LocalPreferences.allSavedAccounts()) } + + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() + + (getSystemService(ConnectivityManager::class.java) as ConnectivityManager) + .registerNetworkCallback(networkRequest, networkCallback) } override fun onPause() { @@ -120,6 +120,10 @@ class MainActivity : AppCompatActivity() { if (ServiceManager.shouldPauseService) { ServiceManager.pause() } + + (getSystemService(ConnectivityManager::class.java) as ConnectivityManager) + .unregisterNetworkCallback(networkCallback) + super.onPause() } @@ -153,7 +157,7 @@ class MainActivity : AppCompatActivity() { GlobalScope.launch(Dispatchers.IO) { if (ServiceManager.shouldPauseService) { ServiceManager.pause() - ServiceManager.start(this@MainActivity) + ServiceManager.start() } } } @@ -166,15 +170,10 @@ class MainActivity : AppCompatActivity() { super.onCapabilitiesChanged(network, networkCapabilities) GlobalScope.launch(Dispatchers.IO) { - val hasMobileData = - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) - val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData") - Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi") - ConnectivityStatus.updateConnectivityStatus( - hasMobileData, - hasWifi - ) + val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $isOnMobileData") + + isOnMobileDataState.value = isOnMobileData } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index 351637a8c..ad05f63ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -54,12 +54,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.note.ChannelName import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.SearchIcon @@ -69,8 +67,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.events.ChatroomKey -import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -304,13 +300,9 @@ private fun RenderSearchResults( if (searchBarViewModel.isSearching) { val users by searchBarViewModel.searchResultsUsers.collectAsState() val channels by searchBarViewModel.searchResultsChannels.collectAsState() - val scope = rememberCoroutineScope() + val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } Row( @@ -332,10 +324,8 @@ private fun RenderSearchResults( key = { _, item -> "u" + item.pubkeyHex } ) { _, item -> UserComposeForChat(item, accountViewModel) { - scope.launch(Dispatchers.IO) { - val withKey = ChatroomKey(persistentSetOf(item.pubkeyHex)) - accountViewModel.userProfile().createChatroom(withKey) - nav("Room/${withKey.hashCode()}") + accountViewModel.createChatRoomFor(item) { + nav("Room/$it") } searchBarViewModel.clear() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index b3033ec3e..5b4317c78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -58,11 +58,9 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays import com.vitorpamplona.amethyst.service.relays.FeedType @@ -304,7 +302,6 @@ fun ServerConfig( nav: (String) -> Unit ) { var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() val context = LocalContext.current relayInfo?.let { @@ -318,16 +315,12 @@ fun ServerConfig( } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } ServerConfigClickableLine( - loadProfilePicture = automaticallyShowProfilePicture, item = item, + loadProfilePicture = automaticallyShowProfilePicture, onToggleDownload = onToggleDownload, onToggleUpload = onToggleUpload, onToggleFollows = onToggleFollows, @@ -362,8 +355,8 @@ fun ServerConfig( @Composable fun ServerConfigClickableLine( - loadProfilePicture: Boolean, item: RelaySetupInfo, + loadProfilePicture: Boolean, onToggleDownload: (RelaySetupInfo) -> Unit, onToggleUpload: (RelaySetupInfo) -> Unit, onToggleFollows: (RelaySetupInfo) -> Unit, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt index 84f7ec3fb..b5519ce5f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -26,10 +26,8 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.ClickableEmail import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.note.LoadUser @@ -51,11 +49,7 @@ fun RelayInformationDialog( nav: (String) -> Unit ) { val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } Dialog( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index b17da032e..4bdfb395f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -8,19 +8,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) { val automaticallyShowUrlPreview = remember { - when (accountViewModel.account.settings.automaticallyShowUrlPreview) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showUrlPreview.value } if (!automaticallyShowUrlPreview) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index ec2ae6a17..91549dc44 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -17,8 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer @@ -33,11 +31,7 @@ fun UrlPreviewCard( accountViewModel: AccountViewModel ) { val automaticallyShowUrlPreview = remember { - when (accountViewModel.account.settings.automaticallyShowUrlPreview) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showUrlPreview.value } if (!automaticallyShowUrlPreview) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 03d2b720e..21e90066b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -63,8 +64,6 @@ import androidx.media3.session.MediaController import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import com.linc.audiowaveform.infiniteLinearGradient -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.playback.PlaybackClientController import com.vitorpamplona.amethyst.ui.note.LyricsIcon import com.vitorpamplona.amethyst.ui.note.LyricsOffIcon @@ -202,14 +201,8 @@ fun VideoViewInner( onDialog: ((Boolean) -> Unit)? = null ) { val automaticallyStartPlayback = remember { - mutableStateOf( - if (alwaysShowVideo) { true } else { - when (accountViewModel.account.settings.automaticallyStartPlayback) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } + mutableStateOf( + if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value ) } @@ -525,7 +518,7 @@ private fun RenderVideoPlayer( topPaddingForControllers: Dp = Dp.Unspecified, waveform: ImmutableList? = null, keepPlaying: MutableState, - automaticallyStartPlayback: MutableState, + automaticallyStartPlayback: State, activeOnScreen: MutableState, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? @@ -735,7 +728,7 @@ fun DrawWaveform(waveform: ImmutableList, waveformProgress: MutableState, - automaticallyStartPlayback: MutableState, + automaticallyStartPlayback: State, activeOnScreen: MutableState ) { LaunchedEffect(key1 = activeOnScreen.value) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index bad300526..72ffdf684 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -78,9 +78,7 @@ import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import coil.imageLoader import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.service.BlurHashRequester -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation @@ -276,21 +274,14 @@ private fun LocalImageView( content: ZoomableLocalImage, mainImageModifier: Modifier, topPaddingForControllers: Dp = Dp.Unspecified, - accountViewModel: AccountViewModel?, + accountViewModel: AccountViewModel, alwayShowImage: Boolean = false ) { if (content.localFile != null && content.localFile.exists()) { BoxWithConstraints(contentAlignment = Alignment.Center) { val showImage = remember { mutableStateOf( - if (alwayShowImage) { true } else { - when (accountViewModel?.account?.settings?.automaticallyShowImages) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - else -> true - } - } + if (alwayShowImage) true else accountViewModel.settings.showImages.value ) } @@ -350,20 +341,13 @@ private fun UrlImageView( content: ZoomableUrlImage, mainImageModifier: Modifier, topPaddingForControllers: Dp = Dp.Unspecified, - accountViewModel: AccountViewModel?, + accountViewModel: AccountViewModel, alwayShowImage: Boolean = false ) { BoxWithConstraints(contentAlignment = Alignment.Center) { val showImage = remember { - mutableStateOf( - if (alwayShowImage) { true } else { - when (accountViewModel?.account?.settings?.automaticallyShowImages) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - else -> true - } - } + mutableStateOf( + if (alwayShowImage) true else accountViewModel.settings.showImages.value ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index 426894a75..a07f12389 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -45,10 +45,8 @@ import androidx.lifecycle.map import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon @@ -141,14 +139,6 @@ fun DisplayAccount( ) } - val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } - if (baseUser == null) { LaunchedEffect(key1 = acc.npub) { launch(Dispatchers.IO) { @@ -168,7 +158,7 @@ fun DisplayAccount( modifier = Modifier .fillMaxWidth() .clickable { - accountStateViewModel.switchUser(acc.npub) + accountStateViewModel.switchUser(acc) } .padding(16.dp, 16.dp), verticalAlignment = Alignment.CenterVertically @@ -186,6 +176,10 @@ fun DisplayAccount( .width(55.dp) .padding(0.dp) ) { + val automaticallyShowProfilePicture = remember { + accountViewModel.settings.showProfilePictures.value + } + AccountPicture(it, automaticallyShowProfilePicture) } Spacer(modifier = Modifier.width(16.dp)) @@ -281,7 +275,7 @@ private fun LogoutButton( TextButton( onClick = { logoutDialog = false - accountStateViewModel.logOff(acc.npub) + accountStateViewModel.logOff(acc) } ) { Text(text = stringResource(R.string.log_out)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 56feca6ae..3dd0c08ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel -import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen @@ -69,7 +69,7 @@ fun AppNavigation( navController: NavHostController, accountViewModel: AccountViewModel, - themeViewModel: ThemeViewModel + sharedPreferencesViewModel: SharedPreferencesViewModel ) { val scope = rememberCoroutineScope() val nav = remember { @@ -263,8 +263,7 @@ fun AppNavigation( Route.Settings.let { route -> composable(route.route, route.arguments, content = { SettingsScreen( - accountViewModel = accountViewModel, - themeViewModel + sharedPreferencesViewModel ) }) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 7ed0a80bf..2a957c646 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -59,7 +59,6 @@ import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache @@ -80,7 +79,6 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy @@ -508,11 +506,7 @@ private fun LoggedInUserPictureDrawer( val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } IconButton( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index db6956c9d..e6f7d559a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.navigation -import android.content.Context import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -28,7 +27,6 @@ import androidx.compose.material.icons.filled.Send import androidx.compose.material3.AlertDialog import androidx.compose.material3.Divider import androidx.compose.material3.DrawerState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -69,10 +67,8 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.HttpClient -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView @@ -89,11 +85,9 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DrawerContent( nav: (String) -> Unit, @@ -102,11 +96,7 @@ fun DrawerContent( accountViewModel: AccountViewModel ) { val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } ModalDrawerSheet( @@ -138,7 +128,12 @@ fun DrawerContent( accountViewModel ) - BottomContent(accountViewModel.account.userProfile(), drawerState, automaticallyShowProfilePicture, nav) + BottomContent( + accountViewModel.account.userProfile(), + drawerState, + automaticallyShowProfilePicture, + nav + ) } } } @@ -165,11 +160,7 @@ fun ProfileContent( val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } Box { @@ -574,7 +565,9 @@ fun ListContent( conectOrbotDialogOpen = false disconnectTorDialog = false checked = true - enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope) + coroutineScope.launch(Dispatchers.IO) { + enableTor(accountViewModel.account, true, proxyPort) + } }, onError = { accountViewModel.toast( @@ -602,7 +595,13 @@ fun ListContent( onClick = { disconnectTorDialog = false checked = false - enableTor(accountViewModel.account, false, proxyPort, context, coroutineScope) + coroutineScope.launch(Dispatchers.IO) { + enableTor( + accountViewModel.account, + false, + proxyPort + ) + } } ) { Text(text = stringResource(R.string.yes)) @@ -621,20 +620,16 @@ fun ListContent( } } -private fun enableTor( +private suspend fun enableTor( account: Account, checked: Boolean, - portNumber: MutableState, - context: Context, - scope: CoroutineScope + portNumber: MutableState ) { account.proxyPort = portNumber.value.toInt() account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) - scope.launch(Dispatchers.IO) { - LocalPreferences.saveToEncryptedStorage(account) - ServiceManager.pause() - ServiceManager.start(context) - } + LocalPreferences.saveToEncryptedStorage(account) + ServiceManager.pause() + ServiceManager.start() } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt index d8ad31b7b..022378dd4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt @@ -47,10 +47,8 @@ import androidx.lifecycle.map import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -188,11 +186,7 @@ private fun ChannelRoomCompose( val hasNewMessages = remember { mutableStateOf(false) } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 712d0536d..6448c8ac6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -43,10 +43,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.map import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy @@ -396,11 +394,7 @@ private fun MessageBubbleLines( availableBubbleSize: MutableState ) { val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } if (drawAuthorInfo) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 53f653af0..4070f729c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -43,10 +43,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.ImageUrlType import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy @@ -524,11 +522,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( accountViewModel: AccountViewModel ) { val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } WatchUserMetadata(author) { baseUserPicture -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 857e7ea73..889bc6f87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -84,12 +84,10 @@ import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji @@ -730,11 +728,7 @@ fun ShortCommunityHeader(baseNote: AddressableNote, fontWeight: FontWeight = Fon val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } Row(verticalAlignment = Alignment.CenterVertically) { @@ -2791,11 +2785,7 @@ private fun RenderAuthorImages( val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } if (isChannel) { @@ -3793,11 +3783,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView ) { Column { val automaticallyShowUrlPreview = remember { - when (accountViewModel.account.settings.automaticallyShowUrlPreview) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showUrlPreview.value } if (automaticallyShowUrlPreview) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index d224e01c3..4930a552a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -32,12 +32,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.map import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -128,11 +126,7 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: val ripple = rememberRipple(bounded = false, radius = Size15dp) val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } val clickableModifier = remember(relay) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 5e0286d24..72280d34c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -44,11 +44,9 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ExternalSignerUtils -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -81,13 +79,6 @@ fun NoteAuthorPicture( onClick: ((User) -> Unit)? = null ) { val author by baseNote.live().authorChanges.observeAsState(baseNote.author) - val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } Crossfade(targetState = author) { if (it == null) { @@ -336,11 +327,7 @@ fun PictureAndFollowingMark( } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } RobohashAsyncImageProxy( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 428bdcdd7..be28fd7aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -2,11 +2,19 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage @@ -14,33 +22,53 @@ import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage @Composable fun AccountScreen( accountStateViewModel: AccountStateViewModel, - themeViewModel: ThemeViewModel + sharedPreferencesViewModel: SharedPreferencesViewModel ) { val accountState by accountStateViewModel.accountContent.collectAsState() Column() { - Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state -> + Crossfade( + targetState = accountState, + animationSpec = tween(durationMillis = 100), + label = "AccountState" + ) { state -> when (state) { + is AccountState.Loading -> { + LoadingAccounts() + } is AccountState.LoggedOff -> { LoginPage(accountStateViewModel, isFirstLogin = true) } is AccountState.LoggedIn -> { val accountViewModel: AccountViewModel = viewModel( key = state.account.userProfile().pubkeyHex, - factory = AccountViewModel.Factory(state.account) + factory = AccountViewModel.Factory(state.account, sharedPreferencesViewModel.sharedPrefs) ) - MainScreen(accountViewModel, accountStateViewModel, themeViewModel) + MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel) } is AccountState.LoggedInViewOnly -> { val accountViewModel: AccountViewModel = viewModel( key = state.account.userProfile().pubkeyHex, - factory = AccountViewModel.Factory(state.account) + factory = AccountViewModel.Factory(state.account, sharedPreferencesViewModel.sharedPrefs) ) - MainScreen(accountViewModel, accountStateViewModel, themeViewModel) + MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel) } } } } } + +@Composable +fun LoadingAccounts() { + Column( + Modifier + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(stringResource(R.string.loading_account)) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt index 25f3ece42..2a687608f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen import com.vitorpamplona.amethyst.model.Account sealed class AccountState { + object Loading : AccountState() object LoggedOff : AccountState() class LoggedInViewOnly(val account: Account) : AccountState() class LoggedIn(val account: Account) : AccountState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 090892a66..e0cbc2cca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -1,9 +1,10 @@ package com.vitorpamplona.amethyst.ui.screen -import android.content.Context import android.util.Log import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account @@ -20,32 +21,36 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.regex.Pattern val EMAIL_PATTERN = Pattern.compile(".+@.+\\.[a-z]+") @Stable -class AccountStateViewModel(val context: Context) : ViewModel() { - private val _accountContent = MutableStateFlow(AccountState.LoggedOff) +class AccountStateViewModel() : ViewModel() { + private val _accountContent = MutableStateFlow(AccountState.Loading) val accountContent = _accountContent.asStateFlow() - init { - Log.d("Init", "AccountStateViewModel") + fun tryLoginExistingAccountAsync() { // pulls account from storage. - - // Keeps it in the the UI thread to void blinking the login page. - // viewModelScope.launch(Dispatchers.IO) { - tryLoginExistingAccount() - // } - } - - private fun tryLoginExistingAccount() { - LocalPreferences.loadFromEncryptedStorage()?.let { - startUI(it) + viewModelScope.launch { + tryLoginExistingAccount() } } - fun startUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) { + private suspend fun tryLoginExistingAccount() = withContext(Dispatchers.IO) { + LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { + startUI(it) + } ?: run { + requestLoginUI() + } + } + + private suspend fun requestLoginUI() { + _accountContent.update { AccountState.LoggedOff } + } + + suspend fun loginAndStartUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) = withContext(Dispatchers.IO) { val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.hexToByteArray() val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) @@ -63,36 +68,22 @@ class AccountStateViewModel(val context: Context) : ViewModel() { } LocalPreferences.updatePrefsForLogin(account) - startUI(account) - } - fun switchUser(npub: String) { - prepareLogoutOrSwitch() - LocalPreferences.switchToAccount(npub) - tryLoginExistingAccount() - } - - fun newKey(useProxy: Boolean, proxyPort: Int) { - val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) - val account = Account(KeyPair(), proxy = proxy, proxyPort = proxyPort) - // saves to local preferences - LocalPreferences.updatePrefsForLogin(account) startUI(account) } @OptIn(DelicateCoroutinesApi::class) - fun startUI(account: Account) { + suspend fun startUI(account: Account) = withContext(Dispatchers.Main) { if (account.keyPair.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } } else { _accountContent.update { AccountState.LoggedInViewOnly(account) } } GlobalScope.launch(Dispatchers.IO) { - ServiceManager.restartIfDifferentAccount(account, context) - } - GlobalScope.launch(Dispatchers.Main) { - account.saveable.observeForever(saveListener) + ServiceManager.restartIfDifferentAccount(account) } + + account.saveable.observeForever(saveListener) } @OptIn(DelicateCoroutinesApi::class) @@ -102,28 +93,58 @@ class AccountStateViewModel(val context: Context) : ViewModel() { } } - @OptIn(DelicateCoroutinesApi::class) - private fun prepareLogoutOrSwitch() { - when (val state = accountContent.value) { + private suspend fun prepareLogoutOrSwitch() = withContext(Dispatchers.Main) { + when (val state = _accountContent.value) { is AccountState.LoggedIn -> { - GlobalScope.launch(Dispatchers.Main) { - state.account.saveable.removeObserver(saveListener) - } + state.account.saveable.removeObserver(saveListener) } is AccountState.LoggedInViewOnly -> { - GlobalScope.launch(Dispatchers.Main) { - state.account.saveable.removeObserver(saveListener) - } + state.account.saveable.removeObserver(saveListener) } else -> {} } - - _accountContent.update { AccountState.LoggedOff } } - fun logOff(npub: String) { - prepareLogoutOrSwitch() - LocalPreferences.updatePrefsForLogout(npub) - tryLoginExistingAccount() + fun login( + key: String, + useProxy: Boolean, + proxyPort: Int, + loginWithExternalSigner: Boolean = false, + onError: () -> Unit + ) { + viewModelScope.launch { + try { + loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner) + } catch (e: Exception) { + Log.e("Login", "Could not sign in", e) + onError() + } + } + } + + fun newKey(useProxy: Boolean, proxyPort: Int) { + viewModelScope.launch { + val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val account = Account(KeyPair(), proxy = proxy, proxyPort = proxyPort) + // saves to local preferences + LocalPreferences.updatePrefsForLogin(account) + startUI(account) + } + } + + fun switchUser(accountInfo: AccountInfo) { + viewModelScope.launch { + prepareLogoutOrSwitch() + LocalPreferences.switchToAccount(accountInfo) + tryLoginExistingAccount() + } + } + + fun logOff(accountInfo: AccountInfo) { + viewModelScope.launch { + prepareLogoutOrSwitch() + LocalPreferences.updatePrefsForLogout(accountInfo) + tryLoginExistingAccount() + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt new file mode 100644 index 000000000..6e794896b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt @@ -0,0 +1,169 @@ +package com.vitorpamplona.amethyst.ui.screen + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.model.BooleanType +import com.vitorpamplona.amethyst.model.ConnectivityType +import com.vitorpamplona.amethyst.model.Settings +import com.vitorpamplona.amethyst.model.ThemeType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Stable +class SettingsState() { + var theme by mutableStateOf(ThemeType.SYSTEM) + var language by mutableStateOf(null) + + var automaticallyShowImages by mutableStateOf(ConnectivityType.ALWAYS) + var automaticallyStartPlayback by mutableStateOf(ConnectivityType.ALWAYS) + var automaticallyShowUrlPreview by mutableStateOf(ConnectivityType.ALWAYS) + var automaticallyHideNavigationBars by mutableStateOf(BooleanType.ALWAYS) + var automaticallyShowProfilePictures by mutableStateOf(ConnectivityType.ALWAYS) + + var isOnMobileData: State = mutableStateOf(false) + + val showProfilePictures = derivedStateOf { + when (automaticallyShowProfilePictures) { + ConnectivityType.WIFI_ONLY -> !isOnMobileData.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + } + + val showUrlPreview = derivedStateOf { + when (automaticallyShowUrlPreview) { + ConnectivityType.WIFI_ONLY -> !isOnMobileData.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + } + + val startVideoPlayback = derivedStateOf { + when (automaticallyStartPlayback) { + ConnectivityType.WIFI_ONLY -> !isOnMobileData.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + } + + val showImages = derivedStateOf { + when (automaticallyShowImages) { + ConnectivityType.WIFI_ONLY -> !isOnMobileData.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + } +} + +@Stable +class SharedPreferencesViewModel : ViewModel() { + val sharedPrefs: SettingsState = SettingsState() + + fun init() { + viewModelScope.launch(Dispatchers.IO) { + val savedSettings = LocalPreferences.loadSharedSettings() + ?: LocalPreferences.migrateOldSharedSettings() + ?: Settings() + + sharedPrefs.theme = savedSettings.theme + sharedPrefs.language = savedSettings.preferredLanguage + sharedPrefs.automaticallyShowImages = savedSettings.automaticallyShowImages + sharedPrefs.automaticallyStartPlayback = savedSettings.automaticallyStartPlayback + sharedPrefs.automaticallyShowUrlPreview = savedSettings.automaticallyShowUrlPreview + sharedPrefs.automaticallyHideNavigationBars = savedSettings.automaticallyHideNavigationBars + sharedPrefs.automaticallyShowProfilePictures = savedSettings.automaticallyShowProfilePictures + + updateLanguageInTheUI() + } + } + + fun updateTheme(newTheme: ThemeType) { + if (sharedPrefs.theme != newTheme) { + sharedPrefs.theme = newTheme + + saveSharedSettings() + } + } + + fun updateLanguage(newLanguage: String?) { + if (sharedPrefs.language != newLanguage) { + sharedPrefs.language = newLanguage + updateLanguageInTheUI() + saveSharedSettings() + } + } + + fun updateLanguageInTheUI() { + if (sharedPrefs.language != null) { + AppCompatDelegate.setApplicationLocales( + LocaleListCompat.forLanguageTags(sharedPrefs.language) + ) + } + } + + fun updateAutomaticallyStartPlayback(newAutomaticallyStartPlayback: ConnectivityType) { + if (sharedPrefs.automaticallyStartPlayback != newAutomaticallyStartPlayback) { + sharedPrefs.automaticallyStartPlayback = newAutomaticallyStartPlayback + saveSharedSettings() + } + } + + fun updateAutomaticallyShowUrlPreview(newAutomaticallyShowUrlPreview: ConnectivityType) { + if (sharedPrefs.automaticallyShowUrlPreview != newAutomaticallyShowUrlPreview) { + sharedPrefs.automaticallyShowUrlPreview = newAutomaticallyShowUrlPreview + saveSharedSettings() + } + } + + fun updateAutomaticallyShowProfilePicture(newAutomaticallyShowProfilePictures: ConnectivityType) { + if (sharedPrefs.automaticallyShowProfilePictures != newAutomaticallyShowProfilePictures) { + sharedPrefs.automaticallyShowProfilePictures = newAutomaticallyShowProfilePictures + saveSharedSettings() + } + } + + fun updateAutomaticallyHideNavBars(newAutomaticallyHideHavBars: BooleanType) { + if (sharedPrefs.automaticallyHideNavigationBars != newAutomaticallyHideHavBars) { + sharedPrefs.automaticallyHideNavigationBars = newAutomaticallyHideHavBars + saveSharedSettings() + } + } + + fun updateAutomaticallyShowImages(newAutomaticallyShowImages: ConnectivityType) { + if (sharedPrefs.automaticallyShowImages != newAutomaticallyShowImages) { + sharedPrefs.automaticallyShowImages = newAutomaticallyShowImages + saveSharedSettings() + } + } + + fun updateConnectivityStatusState(isOnMobileDataState: State) { + if (sharedPrefs.isOnMobileData != isOnMobileDataState) { + sharedPrefs.isOnMobileData = isOnMobileDataState + } + } + + fun saveSharedSettings() { + viewModelScope.launch(Dispatchers.IO) { + LocalPreferences.saveSharedSettings( + Settings( + sharedPrefs.theme, + sharedPrefs.language, + sharedPrefs.automaticallyShowImages, + sharedPrefs.automaticallyStartPlayback, + sharedPrefs.automaticallyShowUrlPreview, + sharedPrefs.automaticallyHideNavigationBars, + sharedPrefs.automaticallyShowProfilePictures + ) + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt deleted file mode 100644 index c64fe3deb..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vitorpamplona.amethyst.ui.screen - -import androidx.compose.runtime.Immutable -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -@Immutable -class ThemeViewModel : ViewModel() { - private val _theme = MutableLiveData(0) - val theme: LiveData = _theme - - fun onChange(newValue: Int) { - _theme.value = newValue - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 03d2af166..1bf26fc70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -17,9 +17,7 @@ import coil.request.ImageRequest import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayInformation @@ -41,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.CombinedZap +import com.vitorpamplona.amethyst.ui.screen.SettingsState import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19 @@ -81,7 +80,7 @@ class StringToastMsg(val title: String, val msg: String) : ToastMsg() class ResourceToastMsg(val titleResId: Int, val resourceId: Int) : ToastMsg() @Stable -class AccountViewModel(val account: Account) : ViewModel(), Dao { +class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao { val accountLiveData: LiveData = account.live.map { it } val accountLanguagesLiveData: LiveData = account.liveLanguages.map { it } val accountMarkAsReadUpdates = mutableStateOf(0) @@ -129,36 +128,6 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } } - fun updateAutomaticallyStartPlayback( - automaticallyStartPlayback: ConnectivityType - ) { - account.updateAutomaticallyStartPlayback(automaticallyStartPlayback) - } - - fun updateAutomaticallyShowUrlPreview( - automaticallyShowUrlPreview: ConnectivityType - ) { - account.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) - } - - fun updateAutomaticallyShowProfilePicture( - automaticallyShowProfilePicture: ConnectivityType - ) { - account.updateAutomaticallyShowProfilePicture(automaticallyShowProfilePicture) - } - - fun updateAutomaticallyHideNavBars( - automaticallyHideHavBars: BooleanType - ) { - account.updateAutomaticallyHideHavBars(automaticallyHideHavBars) - } - - fun updateAutomaticallyShowImages( - automaticallyShowImages: ConnectivityType - ) { - account.updateAutomaticallyShowImages(automaticallyShowImages) - } - fun isWriteable(): Boolean { return account.isWriteable() } @@ -884,9 +853,9 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } } - class Factory(val account: Account) : ViewModelProvider.Factory { + class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { - return AccountViewModel(account) as AccountViewModel + return AccountViewModel(account, settings) as AccountViewModel } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index e169e1e39..638f7791b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -82,7 +82,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -90,7 +89,6 @@ import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.ServersAvailable import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel @@ -696,11 +694,7 @@ fun ShortChannelHeader( } ?: return val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 6d207e204..eda26f757 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -46,7 +46,6 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.AddButton import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel @@ -122,7 +121,6 @@ fun HiddenUsersScreen( onCheckedChange = { warnAboutReports = it accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) } ) @@ -135,7 +133,6 @@ fun HiddenUsersScreen( onCheckedChange = { filterSpam = it accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 3a89257ce..1a10712df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -74,7 +74,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel -import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import kotlinx.coroutines.launch import kotlin.math.abs @@ -83,7 +83,7 @@ import kotlin.math.abs fun MainScreen( accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, - themeViewModel: ThemeViewModel + sharedPreferencesViewModel: SharedPreferencesViewModel ) { val scope = rememberCoroutineScope() var openBottomSheet by rememberSaveable { mutableStateOf(false) } @@ -243,7 +243,7 @@ fun MainScreen( override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val newOffset = bottomBarOffsetHeightPx.value + available.y - if (accountViewModel.account.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) { + if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) { val newBottomBarOffset = if (navState.value?.destination?.route !in InvertedLayouts) { newOffset.coerceIn(-bottomBarHeightPx, 0f) } else { @@ -352,7 +352,7 @@ fun MainScreen( userReactionsStatsModel = userReactionsStatsModel, navController = navController, accountViewModel = accountViewModel, - themeViewModel = themeViewModel + sharedPreferencesViewModel = sharedPreferencesViewModel ) } } @@ -432,6 +432,10 @@ fun FloatingButtons( val accountState by accountStateViewModel.accountContent.collectAsState() when (accountState) { + is AccountState.Loading -> { + // Does nothing. + } + is AccountState.LoggedInViewOnly -> { if (accountViewModel.loggedInWithExternalSigner()) { WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index e11b35c16..c3fd58f4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -95,12 +95,10 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji @@ -884,11 +882,7 @@ private fun DrawAdditionalInfo( val clipboardManager = LocalClipboardManager.current val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } user.toBestDisplayName().let { @@ -1208,11 +1202,7 @@ private fun DisplayBadges( nav: (String) -> Unit ) { val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } LoadAddressableNote( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 63b877ab2..561d04c65 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -50,13 +50,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ChannelName @@ -348,11 +346,7 @@ private fun DisplaySearchResults( } val automaticallyShowProfilePicture = remember { - when (accountViewModel.account.settings.automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } + accountViewModel.settings.showProfilePictures.value } LazyColumn( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt index d73d9d66b..4eb472654 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt @@ -1,7 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,7 +23,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -37,23 +35,22 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.os.LocaleListCompat -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ConnectivityType +import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.model.parseConnectivityType -import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.model.parseThemeType +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.toImmutableMap import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.IOException @@ -79,7 +76,7 @@ fun Context.getLocaleListFromXml(): LocaleListCompat { return LocaleListCompat.forLanguageTags(tagsList.joinToString(",")) } -fun Context.getLangPreferenceDropdownEntries(): Map { +fun Context.getLangPreferenceDropdownEntries(): ImmutableMap { val localeList = getLocaleListFromXml() val map = mutableMapOf() @@ -88,13 +85,13 @@ fun Context.getLangPreferenceDropdownEntries(): Map { map.put(it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() }, it.toLanguageTag()) } } - return map + return map.toImmutableMap() } -fun getLanguageIndex(languageEntries: Map): Int { - val language = LocalPreferences.getPreferredLanguage() +fun getLanguageIndex(languageEntries: ImmutableMap, sharedPreferencesViewModel: SharedPreferencesViewModel): Int { + val language = sharedPreferencesViewModel.sharedPrefs.language var languageIndex = -1 - if (language.isNotBlank()) { + if (language != null) { languageIndex = languageEntries.values.toTypedArray().indexOf(language) } else { languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag()) @@ -104,44 +101,43 @@ fun getLanguageIndex(languageEntries: Map): Int { return languageIndex } -@OptIn(DelicateCoroutinesApi::class) @Composable fun SettingsScreen( - accountViewModel: AccountViewModel, - themeViewModel: ThemeViewModel + sharedPreferencesViewModel: SharedPreferencesViewModel ) { - val scope = rememberCoroutineScope() val selectedItens = persistentListOf( - TitleExplainer(stringResource(ConnectivityType.ALWAYS.reourceId)), - TitleExplainer(stringResource(ConnectivityType.WIFI_ONLY.reourceId)), - TitleExplainer(stringResource(ConnectivityType.NEVER.reourceId)) + TitleExplainer(stringResource(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringResource(ConnectivityType.WIFI_ONLY.resourceId)), + TitleExplainer(stringResource(ConnectivityType.NEVER.resourceId)) ) val themeItens = persistentListOf( - TitleExplainer(stringResource(R.string.system)), - TitleExplainer(stringResource(R.string.light)), - TitleExplainer(stringResource(R.string.dark)) + TitleExplainer(stringResource(ThemeType.SYSTEM.resourceId)), + TitleExplainer(stringResource(ThemeType.LIGHT.resourceId)), + TitleExplainer(stringResource(ThemeType.DARK.resourceId)) ) val booleanItems = persistentListOf( - TitleExplainer(stringResource(ConnectivityType.ALWAYS.reourceId)), - TitleExplainer(stringResource(ConnectivityType.NEVER.reourceId)) + TitleExplainer(stringResource(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringResource(ConnectivityType.NEVER.resourceId)) ) - val settings = accountViewModel.account.settings - val showImagesIndex = settings.automaticallyShowImages.screenCode - val videoIndex = settings.automaticallyStartPlayback.screenCode - val linkIndex = settings.automaticallyShowUrlPreview.screenCode - val hideNavBarsIndex = settings.automaticallyHideNavigationBars.screenCode - val profilePictureIndex = settings.automaticallyShowProfilePictures.screenCode - - val themeIndex = themeViewModel.theme.value ?: 0 + val showImagesIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowImages.screenCode + val videoIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyStartPlayback.screenCode + val linkIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowUrlPreview.screenCode + val hideNavBarsIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyHideNavigationBars.screenCode + val profilePictureIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowProfilePictures.screenCode + val themeIndex = sharedPreferencesViewModel.sharedPrefs.theme.screenCode val context = LocalContext.current - val languageEntries = context.getLangPreferenceDropdownEntries() - val languageList = languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() - val languageIndex = getLanguageIndex(languageEntries) + val languageEntries = remember { + context.getLangPreferenceDropdownEntries() + } + val languageList = remember { + languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() + } + val languageIndex = getLanguageIndex(languageEntries, sharedPreferencesViewModel) Column( Modifier @@ -155,16 +151,7 @@ fun SettingsScreen( languageList, languageIndex ) { - GlobalScope.launch(Dispatchers.Main) { - val job = scope.launch(Dispatchers.IO) { - val locale = languageEntries[languageList[it].title] - accountViewModel.account.settings.preferredLanguage = locale - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } - job.join() - val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it].title]) - AppCompatDelegate.setApplicationLocales(appLocale) - } + sharedPreferencesViewModel.updateLanguage(languageEntries[languageList[it].title]) } Spacer(modifier = HalfVertSpacer) @@ -175,10 +162,7 @@ fun SettingsScreen( themeItens, themeIndex ) { - themeViewModel.onChange(it) - scope.launch(Dispatchers.IO) { - LocalPreferences.updateTheme(it) - } + sharedPreferencesViewModel.updateTheme(parseThemeType(it)) } Spacer(modifier = HalfVertSpacer) @@ -189,12 +173,7 @@ fun SettingsScreen( selectedItens, showImagesIndex ) { - val automaticallyShowImages = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } + sharedPreferencesViewModel.updateAutomaticallyShowImages(parseConnectivityType(it)) } Spacer(modifier = HalfVertSpacer) @@ -205,12 +184,7 @@ fun SettingsScreen( selectedItens, videoIndex ) { - val automaticallyStartPlayback = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } + sharedPreferencesViewModel.updateAutomaticallyStartPlayback(parseConnectivityType(it)) } Spacer(modifier = HalfVertSpacer) @@ -221,12 +195,7 @@ fun SettingsScreen( selectedItens, linkIndex ) { - val automaticallyShowUrlPreview = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } + sharedPreferencesViewModel.updateAutomaticallyShowUrlPreview(parseConnectivityType(it)) } SettingsRow( @@ -235,12 +204,7 @@ fun SettingsScreen( selectedItens, profilePictureIndex ) { - val automaticallyShowProfilePicture = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyShowProfilePicture(automaticallyShowProfilePicture) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } + sharedPreferencesViewModel.updateAutomaticallyShowProfilePicture(parseConnectivityType(it)) } Spacer(modifier = HalfVertSpacer) @@ -251,12 +215,7 @@ fun SettingsScreen( booleanItems, hideNavBarsIndex ) { - val automaticallyHideNavBars = parseBooleanType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyHideNavBars(automaticallyHideNavBars) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } + sharedPreferencesViewModel.updateAutomaticallyHideNavBars(parseBooleanType(it)) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 79d7a8b0b..0f243ff39 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,7 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff import android.app.Activity -import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -92,6 +91,7 @@ fun LoginPage( var errorMessage by remember { mutableStateOf("") } val acceptedTerms = remember { mutableStateOf(!isFirstLogin) } var termsAcceptanceIsRequired by remember { mutableStateOf("") } + val uri = LocalUriHandler.current val context = LocalContext.current var dialogOpen by remember { @@ -102,6 +102,7 @@ fun LoginPage( var connectOrbotDialogOpen by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() var loginWithExternalSigner by remember { mutableStateOf(false) } + val activity = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = { @@ -130,10 +131,7 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - try { - accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt(), true) - } catch (e: Exception) { - Log.e("Login", "Could not sign in", e) + accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt(), true) { errorMessage = context.getString(R.string.invalid_key) } } @@ -254,10 +252,18 @@ fun LoginPage( visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), keyboardActions = KeyboardActions( onGo = { - try { - accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt()) - } catch (e: Exception) { - errorMessage = context.getString(R.string.invalid_key) + if (!acceptedTerms.value) { + termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required) + } + + if (key.value.text.isBlank()) { + errorMessage = context.getString(R.string.key_is_required) + } + + if (acceptedTerms.value && key.value.text.isNotBlank()) { + accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { + errorMessage = context.getString(R.string.invalid_key) + } } } ) @@ -368,10 +374,7 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - try { - accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt()) - } catch (e: Exception) { - Log.e("Login", "Could not sign in", e) + accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { errorMessage = context.getString(R.string.invalid_key) } } @@ -409,15 +412,12 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - try { - accountViewModel.startUI( - key.value.text, - useProxy.value, - proxyPort.value.toInt(), - true - ) - } catch (e: Exception) { - Log.e("Login", "Could not sign in", e) + accountViewModel.login( + key.value.text, + useProxy.value, + proxyPort.value.toInt(), + true + ) { errorMessage = context.getString(R.string.invalid_key) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 44d24b53f..c160b74ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -31,7 +30,8 @@ import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.resolveDefaults import com.patrykandpatrick.vico.compose.style.ChartStyle import com.patrykandpatrick.vico.core.DefaultColors -import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel private val DarkColorPalette = darkColorScheme( primary = Purple200, @@ -328,11 +328,10 @@ val ColorScheme.chartStyle: ChartStyle } @Composable -fun AmethystTheme(themeViewModel: ThemeViewModel, content: @Composable () -> Unit) { - val theme = themeViewModel.theme.observeAsState() - val darkTheme = when (theme.value) { - 2 -> true - 1 -> false +fun AmethystTheme(sharedPrefsViewModel: SharedPreferencesViewModel, content: @Composable () -> Unit) { + val darkTheme = when (sharedPrefsViewModel.sharedPrefs.theme) { + ThemeType.DARK -> true + ThemeType.LIGHT -> false else -> isSystemInDarkTheme() } val colors = if (darkTheme) DarkColorPalette else LightColorPalette diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7a4b2d4c..b03b64456 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -149,6 +149,7 @@ Login Generate a new key Loading feed + Loading account "Error loading replies: " Try again Feed is empty. diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index ea77ee39a..1c7f47662 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.notifications import android.app.NotificationManager +import android.util.Log import android.util.LruCache import androidx.core.content.ContextCompat import com.google.firebase.messaging.FirebaseMessagingService @@ -15,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlin.time.measureTimedValue class PushNotificationReceiverService : FirebaseMessagingService() { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -23,9 +25,12 @@ class PushNotificationReceiverService : FirebaseMessagingService() { // this is called when a message is received override fun onMessageReceived(remoteMessage: RemoteMessage) { scope.launch(Dispatchers.IO) { - parseMessage(remoteMessage.data)?.let { - receiveIfNew(it) + val (value, elapsed) = measureTimedValue { + parseMessage(remoteMessage.data)?.let { + receiveIfNew(it) + } } + Log.d("Time", "Notification processed in $elapsed") } }