- 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:
@@ -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)
|
||||
|
||||
-15
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -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 ->
|
||||
|
||||
+2
-2
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+6
-12
@@ -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
|
||||
}
|
||||
}
|
||||
+4
-35
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
+5
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user