- Moves app startup to an IO thread.

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