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