From 24929c47d4c9bfa5d5e37f6f2f73558219b9e9d7 Mon Sep 17 00:00:00 2001 From: JohnOberhauser <14130581+JohnOberhauser@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:44:25 -0400 Subject: [PATCH] Accounts refactor (#586) - Making it so account data is stored in a database instead of multiple proto files - Removed different thread view options. Tree is not the only option --------- Co-authored-by: John Oberhauser --- app/build.gradle.kts | 1 + .../kotlin/social/firefly/IntentHandler.kt | 33 ++-- .../kotlin/social/firefly/MainApplication.kt | 2 + .../social/firefly/splash/SplashViewModel.kt | 6 +- core/accounts/.gitignore | 1 + core/accounts/build.gradle.kts | 27 +++ .../1.json | 120 ++++++++++++ core/accounts/src/main/AndroidManifest.xml | 4 + .../firefly/core/accounts/AccountsDatabase.kt | 24 +++ .../firefly/core/accounts/AccountsManager.kt | 125 ++++++++++++ .../firefly/core/accounts/AccountsModule.kt | 23 +++ .../core/accounts/dao/ActiveAccountDao.kt | 14 ++ .../firefly/core/accounts/dao/BaseDao.kt | 17 ++ .../core/accounts/dao/MastodonAccountsDao.kt | 66 +++++++ .../core/accounts/model/ActiveAccount.kt | 30 +++ .../core/accounts/model/MastodonAccount.kt | 21 ++ .../datastore/AlreadySignedInException.kt | 3 - .../core/datastore/AppPreferencesDatastore.kt | 13 -- .../firefly/core/datastore/DatastoreModule.kt | 6 - .../firefly/core/datastore/DatastoreUtils.kt | 21 -- .../datastore/UserPreferencesDatastore.kt | 108 ---------- .../UserPreferencesDatastoreManager.kt | 129 ------------ .../datastore/UserPreferencesSerializer.kt | 38 ---- .../core/datastore/app_preferences.proto | 4 +- .../core/datastore/user_preferences.proto | 24 --- core/push/build.gradle.kts | 2 +- .../social/firefly/core/push/PushModule.kt | 2 - .../firefly/core/push/firebase/FcmService.kt | 15 +- core/repository/mastodon/build.gradle.kts | 2 +- .../mastodon/AuthCredentialObserver.kt | 25 +-- .../mastodon/MastodonRepositoryModule.kt | 4 +- core/repository/paging/build.gradle.kts | 2 +- .../core/repository/paging/PagingModule.kt | 4 +- .../HomeTimelineRemoteMediator.kt | 25 +-- core/ui/chooseAccount/build.gradle.kts | 1 + .../ui/chooseAccount/ChooseAccountDialog.kt | 28 ++- .../ChooseAccountDialogViewModel.kt | 18 +- .../ui/chooseAccount/ChooseAccountModule.kt | 5 + .../ui/chooseAccount/ChooseAccountUiState.kt | 10 +- core/usecase/mastodon/build.gradle.kts | 1 + .../usecase/mastodon/MastodonUsecaseModule.kt | 6 +- .../usecase/mastodon/account/GetDomain.kt | 10 +- .../account/GetLoggedInUserAccountId.kt | 12 +- .../core/usecase/mastodon/auth/Login.kt | 13 +- .../core/usecase/mastodon/auth/Logout.kt | 19 +- .../mastodon/auth/LogoutOfAllAccounts.kt | 8 +- .../mastodon/auth/SwitchActiveAccount.kt | 13 +- .../auth/UpdateAllLoggedInAccounts.kt | 43 ++-- feature/feed/build.gradle.kts | 1 + .../social/firefly/feature/feed/FeedModule.kt | 2 + .../firefly/feature/feed/FeedViewModel.kt | 10 +- feature/post/build.gradle.kts | 1 + .../firefly/feature/post/NewPostModule.kt | 4 +- .../firefly/feature/post/NewPostViewModel.kt | 15 +- feature/settings/build.gradle.kts | 1 + .../settings/account/AccountSettingsScreen.kt | 36 ++-- .../account/AccountSettingsViewModel.kt | 35 ++-- .../settings/account/LoggedInAccount.kt | 10 +- feature/thread/build.gradle.kts | 1 + .../feature/thread/ThreadInteractions.kt | 1 - .../firefly/feature/thread/ThreadModule.kt | 3 +- .../firefly/feature/thread/ThreadScreen.kt | 89 +-------- .../firefly/feature/thread/ThreadViewModel.kt | 184 ++++++------------ settings.gradle.kts | 1 + 64 files changed, 735 insertions(+), 787 deletions(-) create mode 100644 core/accounts/.gitignore create mode 100644 core/accounts/build.gradle.kts create mode 100644 core/accounts/schemas/social.firefly.core.accounts.AccountsDatabase/1.json create mode 100644 core/accounts/src/main/AndroidManifest.xml create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsDatabase.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsManager.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsModule.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/ActiveAccountDao.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/BaseDao.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/MastodonAccountsDao.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/model/ActiveAccount.kt create mode 100644 core/accounts/src/main/kotlin/social/firefly/core/accounts/model/MastodonAccount.kt delete mode 100644 core/datastore/src/main/kotlin/social/firefly/core/datastore/AlreadySignedInException.kt delete mode 100644 core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreUtils.kt delete mode 100644 core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastore.kt delete mode 100644 core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastoreManager.kt delete mode 100644 core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesSerializer.kt delete mode 100644 core/datastore/src/main/proto/social/firefly/core/datastore/user_preferences.proto diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26601f988..1e77d6633 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,6 +91,7 @@ dependencies { implementation(project(":feature:followedHashTags")) implementation(project(":feature:bookmarks")) implementation(project(":core:ui:chooseAccount")) + implementation(project(":core:accounts")) implementation(kotlin("reflect")) diff --git a/app/src/main/kotlin/social/firefly/IntentHandler.kt b/app/src/main/kotlin/social/firefly/IntentHandler.kt index a2dd47e2e..89ef079d0 100644 --- a/app/src/main/kotlin/social/firefly/IntentHandler.kt +++ b/app/src/main/kotlin/social/firefly/IntentHandler.kt @@ -3,29 +3,34 @@ package social.firefly import android.content.Intent import android.net.Uri import android.os.Parcelable -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.Event import social.firefly.core.navigation.EventRelay import social.firefly.core.share.ShareInfo class IntentHandler( private val eventRelay: EventRelay, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, + private val accountsManager: AccountsManager, ) { fun handleIntent(intent: Intent) { - if (!userPreferencesDatastoreManager.isLoggedInToAtLeastOneAccount) return - when { - intent.action == Intent.ACTION_SEND -> { - when { - intent.type == "text/plain" -> { - handleSendTextIntentReceived(intent) - } - intent.type?.contains("image") == true -> { - handleSendImageIntentReceived(intent) - } - intent.type?.contains("video") == true -> { - handleSendVideoIntentReceived(intent) + CoroutineScope(Dispatchers.Default).launch { + if (accountsManager.getAllAccounts().isEmpty()) return@launch + when { + intent.action == Intent.ACTION_SEND -> { + when { + intent.type == "text/plain" -> { + handleSendTextIntentReceived(intent) + } + intent.type?.contains("image") == true -> { + handleSendImageIntentReceived(intent) + } + intent.type?.contains("video") == true -> { + handleSendVideoIntentReceived(intent) + } } } } diff --git a/app/src/main/kotlin/social/firefly/MainApplication.kt b/app/src/main/kotlin/social/firefly/MainApplication.kt index 6de458831..5616b4505 100644 --- a/app/src/main/kotlin/social/firefly/MainApplication.kt +++ b/app/src/main/kotlin/social/firefly/MainApplication.kt @@ -21,6 +21,7 @@ import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin import org.koin.dsl.module import social.firefly.common.Version +import social.firefly.core.accounts.accountsModule import social.firefly.core.analytics.AppAnalytics import social.firefly.core.repository.mastodon.AuthCredentialObserver import social.firefly.core.workmanager.workManagerModule @@ -102,6 +103,7 @@ class MainApplication : Application(), ImageLoaderFactory { workManagerModule, pushModule, chooseAccountModule, + accountsModule, ) } } diff --git a/app/src/main/kotlin/social/firefly/splash/SplashViewModel.kt b/app/src/main/kotlin/social/firefly/splash/SplashViewModel.kt index ec4cc0792..6573db97a 100644 --- a/app/src/main/kotlin/social/firefly/splash/SplashViewModel.kt +++ b/app/src/main/kotlin/social/firefly/splash/SplashViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import social.firefly.IntentHandler -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.NavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.repository.mastodon.TimelineRepository @@ -14,10 +14,10 @@ import social.firefly.ui.AppState class SplashViewModel( private val navigateTo: NavigateTo, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, private val timelineRepository: TimelineRepository, private val updateAllLoggedInAccounts: UpdateAllLoggedInAccounts, private val intentHandler: IntentHandler, + private val accountsManager: AccountsManager, ) : ViewModel() { fun initialize(intent: Intent?) { @@ -27,7 +27,7 @@ class SplashViewModel( AppState.navigationCollectionCompletable.await() - if (userPreferencesDatastoreManager.isLoggedInToAtLeastOneAccount) { + if (accountsManager.getAllAccounts().isNotEmpty()) { navigateTo(NavigationDestination.Tabs) intent?.let { intentHandler.handleIntent(intent) } updateAllLoggedInAccounts() diff --git a/core/accounts/.gitignore b/core/accounts/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/accounts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/accounts/build.gradle.kts b/core/accounts/build.gradle.kts new file mode 100644 index 000000000..72747e5b7 --- /dev/null +++ b/core/accounts/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("social.firefly.android.library") + alias(libs.plugins.kotlin.ksp) + alias(libs.plugins.kotlinx.serialization) +} + +android { + namespace = "social.firefly.core.accounts" + + defaultConfig { + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + } +} + +dependencies { + implementation(project(":core:model")) + + implementation(libs.koin.android) + + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room) + + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/accounts/schemas/social.firefly.core.accounts.AccountsDatabase/1.json b/core/accounts/schemas/social.firefly.core.accounts.AccountsDatabase/1.json new file mode 100644 index 000000000..19566d6bf --- /dev/null +++ b/core/accounts/schemas/social.firefly.core.accounts.AccountsDatabase/1.json @@ -0,0 +1,120 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fa0fd12964a2830e2fdd786ebac516aa", + "entities": [ + { + "tableName": "mastodonAccounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accessToken` TEXT NOT NULL, `accountId` TEXT NOT NULL, `domain` TEXT NOT NULL, `avatarUrl` TEXT NOT NULL, `userName` TEXT NOT NULL, `defaultLanguage` TEXT NOT NULL, `serializedPushKeys` TEXT, `lastSeenHomeStatusId` TEXT, PRIMARY KEY(`accountId`))", + "fields": [ + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultLanguage", + "columnName": "defaultLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedPushKeys", + "columnName": "serializedPushKeys", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSeenHomeStatusId", + "columnName": "lastSeenHomeStatusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "activeAccount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `accountType` TEXT NOT NULL, `accountId` TEXT NOT NULL, PRIMARY KEY(`key`), FOREIGN KEY(`accountId`) REFERENCES `mastodonAccounts`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "accountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "mastodonAccounts", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa0fd12964a2830e2fdd786ebac516aa')" + ] + } +} \ No newline at end of file diff --git a/core/accounts/src/main/AndroidManifest.xml b/core/accounts/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/accounts/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsDatabase.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsDatabase.kt new file mode 100644 index 000000000..70a72973b --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsDatabase.kt @@ -0,0 +1,24 @@ +package social.firefly.core.accounts + +import androidx.room.Database +import androidx.room.RoomDatabase +import social.firefly.core.accounts.dao.ActiveAccountDao +import social.firefly.core.accounts.dao.MastodonAccountsDao +import social.firefly.core.accounts.model.ActiveAccount +import social.firefly.core.accounts.model.MastodonAccount + +@Database( + entities = [ + MastodonAccount::class, + ActiveAccount::class, + ], + version = 1, + autoMigrations = [ + + ], + exportSchema = true +) +internal abstract class AccountsDatabase : RoomDatabase() { + abstract fun mastodonAccountsDao(): MastodonAccountsDao + abstract fun activeAccountsDao(): ActiveAccountDao +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsManager.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsManager.kt new file mode 100644 index 000000000..a3ce01dff --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsManager.kt @@ -0,0 +1,125 @@ +package social.firefly.core.accounts + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import social.firefly.core.accounts.dao.ActiveAccountDao +import social.firefly.core.accounts.dao.MastodonAccountsDao +import social.firefly.core.accounts.model.AccountType +import social.firefly.core.accounts.model.ActiveAccount +import social.firefly.core.accounts.model.MastodonAccount + +class AccountsManager( + private val mastodonAccountsDao: MastodonAccountsDao, + private val activeAccountDao: ActiveAccountDao, +) { + + suspend fun getActiveAccount(): MastodonAccount = mastodonAccountsDao.getActiveAccount() + + fun getActiveAccountFlow(): Flow = mastodonAccountsDao.getActiveAccountFlow().filterNotNull() + + suspend fun getAllAccounts(): List = mastodonAccountsDao.getAllAccounts() + + fun getAllAccountsFlow(): Flow> = + mastodonAccountsDao.getAllAccountsFlow() + + suspend fun createNewMastodonUser( + domain: String, + accessToken: String, + accountId: String, + userName: String, + avatarUrl: String, + defaultLanguage: String, + ) { + mastodonAccountsDao.upsert( + MastodonAccount( + accessToken = accessToken, + accountId = accountId, + userName = userName, + avatarUrl = avatarUrl, + domain = domain, + defaultLanguage = defaultLanguage, + ) + ) + activeAccountDao.upsert( + ActiveAccount( + accountType = AccountType.MASTODON, + accountId = accountId, + ) + ) + } + + suspend fun deleteAccount( + accountId: String, + domain: String, + ) { + val activeAccount = mastodonAccountsDao.getActiveAccount() + val accounts = mastodonAccountsDao.getAllAccounts() + if (accountId == activeAccount.accountId && domain == activeAccount.domain) { + val newActiveAccount = (accounts - activeAccount).firstOrNull() + if (newActiveAccount != null) { + activeAccountDao.upsert( + ActiveAccount( + accountType = AccountType.MASTODON, + accountId = newActiveAccount.accountId, + ) + ) + } else { + activeAccountDao.removeActiveAccount() + } + } + mastodonAccountsDao.deleteAccount( + accountId = accountId, + domain = domain, + ) + } + + suspend fun deleteAllAccounts() { + mastodonAccountsDao.deleteAllAccounts() + } + + suspend fun updateAccountInfo( + mastodonAccount: MastodonAccount, + avatarUrl: String, + username: String, + defaultLanguage: String, + ) = mastodonAccountsDao.upsert( + mastodonAccount.copy( + avatarUrl = avatarUrl, + userName = username, + defaultLanguage = defaultLanguage, + ) + ) + + suspend fun updatePushKeys( + mastodonAccount: MastodonAccount, + serializedPushKeys: String, + ) { + mastodonAccountsDao.upsert( + mastodonAccount.copy( + serializedPushKeys = serializedPushKeys + ) + ) + } + + suspend fun setActiveAccount( + mastodonAccount: MastodonAccount, + ) { + activeAccountDao.upsert( + ActiveAccount( + accountType = AccountType.MASTODON, + accountId = mastodonAccount.accountId, + ) + ) + } + + suspend fun updateLastSeenHomeStatusId( + mastodonAccount: MastodonAccount, + lastSeenStatusId: String, + ) { + mastodonAccountsDao.upsert( + mastodonAccount.copy( + lastSeenHomeStatusId = lastSeenStatusId, + ) + ) + } +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsModule.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsModule.kt new file mode 100644 index 000000000..82793bcda --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/AccountsModule.kt @@ -0,0 +1,23 @@ +package social.firefly.core.accounts + +import androidx.room.Room +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val accountsModule = module { + single { + Room.databaseBuilder( + androidContext(), + AccountsDatabase::class.java, + "database-accounts", + ) + .fallbackToDestructiveMigration() + .fallbackToDestructiveMigrationOnDowngrade() + .build() + } + + single { get().mastodonAccountsDao() } + single { get().activeAccountsDao() } + singleOf(::AccountsManager) +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/ActiveAccountDao.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/ActiveAccountDao.kt new file mode 100644 index 000000000..3560b520b --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/ActiveAccountDao.kt @@ -0,0 +1,14 @@ +package social.firefly.core.accounts.dao + +import androidx.room.Dao +import androidx.room.Query +import social.firefly.core.accounts.model.ActiveAccount + +@Dao +interface ActiveAccountDao : BaseDao { + + @Query( + "DELETE FROM activeAccount" + ) + suspend fun removeActiveAccount() +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/BaseDao.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/BaseDao.kt new file mode 100644 index 000000000..dd6d96f7d --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/BaseDao.kt @@ -0,0 +1,17 @@ +package social.firefly.core.accounts.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Upsert + +@Dao +interface BaseDao { + @Upsert + suspend fun upsert(t: T) + + @Upsert + suspend fun upsertAll(t: List) + + @Delete + suspend fun delete(t: T) +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/MastodonAccountsDao.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/MastodonAccountsDao.kt new file mode 100644 index 000000000..d66144486 --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/dao/MastodonAccountsDao.kt @@ -0,0 +1,66 @@ +package social.firefly.core.accounts.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow +import social.firefly.core.accounts.model.MastodonAccount + +@Dao +interface MastodonAccountsDao : BaseDao { + + @Transaction + @Query( + "SELECT * from mastodonAccounts " + + "WHERE accountId IN " + + "(" + + "SELECT accountId FROM activeAccount" + + ") " + + "AND " + + "domain IN " + + "(" + + "SELECT domain FROM activeAccount" + + ") " + ) + suspend fun getActiveAccount(): MastodonAccount + + @Transaction + @Query( + "SELECT * from mastodonAccounts " + + "WHERE accountId IN " + + "(" + + "SELECT accountId FROM activeAccount" + + ") " + + "AND " + + "domain IN " + + "(" + + "SELECT domain FROM activeAccount" + + ") " + ) + fun getActiveAccountFlow(): Flow + + @Query( + "SELECT * from mastodonAccounts" + ) + suspend fun getAllAccounts(): List + + @Query( + "SELECT * from mastodonAccounts" + ) + fun getAllAccountsFlow(): Flow> + + @Query( + "DELETE from mastodonAccounts " + + "WHERE accountId = :accountId " + + "AND domain = :domain" + ) + suspend fun deleteAccount( + accountId: String, + domain: String, + ) + + @Query( + "DELETE FROM mastodonAccounts" + ) + suspend fun deleteAllAccounts() +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/ActiveAccount.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/ActiveAccount.kt new file mode 100644 index 000000000..d411e2f29 --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/ActiveAccount.kt @@ -0,0 +1,30 @@ +package social.firefly.core.accounts.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity( + tableName = "activeAccount", + foreignKeys = [ + ForeignKey( + entity = MastodonAccount::class, + parentColumns = ["accountId"], + childColumns = ["accountId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ] +) +data class ActiveAccount( + @PrimaryKey + val key: Int = 0, + val accountType: AccountType, + val accountId: String, +) + +enum class AccountType { + MASTODON +} \ No newline at end of file diff --git a/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/MastodonAccount.kt b/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/MastodonAccount.kt new file mode 100644 index 000000000..583cee766 --- /dev/null +++ b/core/accounts/src/main/kotlin/social/firefly/core/accounts/model/MastodonAccount.kt @@ -0,0 +1,21 @@ +package social.firefly.core.accounts.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity( + tableName = "mastodonAccounts", +) +data class MastodonAccount( + val accessToken: String, + @PrimaryKey + val accountId: String, + val domain: String, + val avatarUrl: String, + val userName: String, + val defaultLanguage: String, + val serializedPushKeys: String? = null, + val lastSeenHomeStatusId: String? = null, +) \ No newline at end of file diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/AlreadySignedInException.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/AlreadySignedInException.kt deleted file mode 100644 index cefcc20c6..000000000 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/AlreadySignedInException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package social.firefly.core.datastore - -class AlreadySignedInException : IllegalStateException() \ No newline at end of file diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/AppPreferencesDatastore.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/AppPreferencesDatastore.kt index 7214fb0a8..1fc317827 100644 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/AppPreferencesDatastore.kt +++ b/core/datastore/src/main/kotlin/social/firefly/core/datastore/AppPreferencesDatastore.kt @@ -36,17 +36,4 @@ class AppPreferencesDatastore(context: Context) { .build() } } - - val activeUserDatastoreFilename: Flow = - dataStore.data.mapLatest { - it.activeUserDatastoreFilename - }.distinctUntilChanged() - - suspend fun saveActiveUserDatastoreFilename(filename: String) { - dataStore.updateData { - it.toBuilder() - .setActiveUserDatastoreFilename(filename) - .build() - } - } } diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreModule.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreModule.kt index d6b348f7f..da8bafc51 100644 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreModule.kt +++ b/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreModule.kt @@ -6,10 +6,4 @@ import org.koin.dsl.module val dataStoreModule = module { single { AppPreferencesDatastore(androidContext()) } - single { - UserPreferencesDatastoreManager( - androidContext(), - get(), - ) - } } diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreUtils.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreUtils.kt deleted file mode 100644 index db749cb82..000000000 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/DatastoreUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package social.firefly.core.datastore - -import android.content.Context -import java.io.File - -object DatastoreUtils { - fun getAllUserPreferencesDatastoreFilesNames( - context: Context, - ): List { - val datastoreDir = File(context.filesDir, "datastore") - val datastoreFiles = if (datastoreDir.exists() && datastoreDir.isDirectory) { - datastoreDir.listFiles() - } else { - null - } - return datastoreFiles - ?.map { it.name } - ?.filterNot { it == APP_PREFERENCES_DATASTORE_FILENAME } - ?: emptyList() - } -} \ No newline at end of file diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastore.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastore.kt deleted file mode 100644 index 00ea33494..000000000 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastore.kt +++ /dev/null @@ -1,108 +0,0 @@ -package social.firefly.core.datastore - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.Serializer -import androidx.datastore.dataStore -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.launch -import social.firefly.core.datastore.UserPreferences.ThreadType -import timber.log.Timber -import java.io.IOException - -@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) -class UserPreferencesDatastore internal constructor( - val fileName: String, - serializer: Serializer, - context: Context, -) { - private val Context.dataStore: DataStore by dataStore( - fileName = fileName, - serializer = serializer - ) - - private val dataStore = context.dataStore - - private suspend fun preloadData() { - try { - val data = dataStore.data.first() - println(data) - } catch (ioException: IOException) { - Timber.e(t = ioException, message = "Problem preloading data") - } - } - - init { - GlobalScope.launch { - preloadData() - } - } - - val accessToken: Flow = dataStore.data.mapLatest { it.accessToken } - val domain: Flow = dataStore.data.mapLatest { it.domain } - val accountId: Flow = dataStore.data.mapLatest { it.accountId } - val avatarUrl: Flow = dataStore.data.mapLatest { it.avatarUrl } - val userName: Flow = dataStore.data.mapLatest { it.userName } - val serializedPushKeys: Flow = dataStore.data.mapLatest { it.serializedPushKeys } - val lastSeenHomeStatusId: Flow = dataStore.data.mapLatest { it.lastSeenHomeStatusId } - val threadType: Flow = dataStore.data.mapLatest { it.threadType }.distinctUntilChanged() - val defaultLanguage: Flow = dataStore.data.mapLatest { it.defaultLanguage } - - suspend fun saveAvatarUrl(url: String) { - dataStore.updateData { - it.toBuilder() - .setAvatarUrl(url) - .build() - } - } - - suspend fun saveUserName(userName: String) { - dataStore.updateData { - it.toBuilder() - .setUserName(userName) - .build() - } - } - - suspend fun saveSerializedPushKeyPair(serializedPushKeyPair: String) { - dataStore.updateData { - it.toBuilder() - .setSerializedPushKeys(serializedPushKeyPair) - .build() - } - } - - suspend fun saveLastSeenHomeStatusId(statusId: String) { - dataStore.updateData { - it.toBuilder() - .setLastSeenHomeStatusId(statusId) - .build() - } - } - - suspend fun saveThreadType(threadType: ThreadType) { - dataStore.updateData { - it.toBuilder() - .setThreadType(threadType) - .build() - } - } - - suspend fun saveDefaultLanguage(language: String) { - dataStore.updateData { - it.toBuilder() - .setDefaultLanguage(language) - .build() - } - } - - companion object { - const val HOST_NAME_REGEX = "[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)+" - } -} diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastoreManager.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastoreManager.kt deleted file mode 100644 index f85d7469d..000000000 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesDatastoreManager.kt +++ /dev/null @@ -1,129 +0,0 @@ -package social.firefly.core.datastore - -import android.content.Context -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import java.io.File - -/** - * Manages multiple [UserPreferencesDatastore] instances and works in conjunction with - * [AppPreferencesDatastore] to determine which logged in account is active. - */ -class UserPreferencesDatastoreManager( - private val context: Context, - private val appPreferencesDatastore: AppPreferencesDatastore, -) { - private val _dataStores = MutableStateFlow>(emptyList()) - val dataStores = _dataStores.asStateFlow() - val isLoggedInToAtLeastOneAccount: Boolean - get() = dataStores.value.isNotEmpty() - - val isLoggedInToMultipleAccounts: Boolean - get() = dataStores.value.size > 1 - - // counter exists to ensure we don't create a datastore preferences with a name that already exists. - // this could happen if the user logs out and logs back in with the same account. - // That would cause a crash becauses two datastores with the same name can't exist at the same time, - // and even though we delete the datastore, it might be lurking in memory somewhere until - // the user logs in. - private var counter = 0 - - init { - removeLegacyUserPreferences() - val dataStoreFileNames = DatastoreUtils.getAllUserPreferencesDatastoreFilesNames(context) - dataStoreFileNames.forEach { fileName -> - val fileCounter = fileName.split("-").getOrNull(2)?.toIntOrNull() - fileCounter?.let { - if (fileCounter >= counter) { - counter = fileCounter + 1 - } - } - _dataStores.update { - it + UserPreferencesDatastore( - fileName = fileName, - EmptyUserPreferencesSerializer, - context = context, - ) - } - } - } - - val activeUserDatastore: Flow = - appPreferencesDatastore.activeUserDatastoreFilename.map { activeDatastoreId -> - dataStores.value.find { it.fileName == activeDatastoreId } - }.filterNotNull() - - suspend fun createNewUserDatastore( - domain: String, - accessToken: String, - accountId: String, - userName: String, - avatarUrl: String, - defaultLanguage: String, - ) { - require(UserPreferencesDatastore.HOST_NAME_REGEX.toRegex().matches(domain)) - val filePrefix = "$domain-$accountId" - val fileName = "$filePrefix-$counter-prefs.pb" - counter++ - - if (dataStores.value.find { it.fileName.startsWith(filePrefix) } != null) - throw AlreadySignedInException() - - if (dataStores.value.find { it.fileName == fileName } != null) - throw Exception("prefs file already exists") - - val newDataStore = UserPreferencesDatastore( - fileName = fileName, - serializer = UserPreferencesSerializer( - domain = domain, - accessToken = accessToken, - accountId = accountId, - ), - context = context, - ).apply { - saveUserName(userName) - saveAvatarUrl(avatarUrl) - saveDefaultLanguage(defaultLanguage) - } - - _dataStores.update { it + newDataStore } - - appPreferencesDatastore.saveActiveUserDatastoreFilename(fileName) - } - - /** - * @return true if we are deleting the active user's datastore - */ - suspend fun deleteDataStore( - dataStore: UserPreferencesDatastore, - ): Boolean { - // if we are deleting the current user account, update the active user - val isDeletingActiveUser = appPreferencesDatastore.activeUserDatastoreFilename.first() == dataStore.fileName - if (isDeletingActiveUser) { - dataStores.value.filterNot { it == dataStore }.firstOrNull()?.let { - appPreferencesDatastore.saveActiveUserDatastoreFilename(it.fileName) - } ?: appPreferencesDatastore.saveActiveUserDatastoreFilename("") - } - val datastoreDir = File(context.filesDir, "datastore") - val dataStoreFile = File(datastoreDir, dataStore.fileName) - dataStoreFile.delete() - _dataStores.update { - it - dataStore - } - return isDeletingActiveUser - } - - private fun removeLegacyUserPreferences() { - val datastoreDir = File(context.filesDir, "datastore") - val dataStoreFile = File(datastoreDir, "userPreferences.pb") - if (dataStoreFile.exists()) { - dataStoreFile.delete() - } - } -} - diff --git a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesSerializer.kt b/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesSerializer.kt deleted file mode 100644 index c7ba72d16..000000000 --- a/core/datastore/src/main/kotlin/social/firefly/core/datastore/UserPreferencesSerializer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package social.firefly.core.datastore - -import androidx.datastore.core.Serializer -import java.io.InputStream -import java.io.OutputStream - -internal class UserPreferencesSerializer( - domain: String, - accessToken: String, - accountId: String, -) : Serializer { - override val defaultValue: UserPreferences = UserPreferences.newBuilder() - .setDomain(domain) - .setAccessToken(accessToken) - .setAccountId(accountId) - .setThreadType(UserPreferences.ThreadType.TREE) - .build() - - override suspend fun readFrom(input: InputStream): UserPreferences = - UserPreferences.parseFrom(input) - - override suspend fun writeTo( - t: UserPreferences, - output: OutputStream, - ) = t.writeTo(output) -} - -internal object EmptyUserPreferencesSerializer : Serializer { - override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() - - override suspend fun readFrom(input: InputStream): UserPreferences = - UserPreferences.parseFrom(input) - - override suspend fun writeTo( - t: UserPreferences, - output: OutputStream, - ) = t.writeTo(output) -} diff --git a/core/datastore/src/main/proto/social/firefly/core/datastore/app_preferences.proto b/core/datastore/src/main/proto/social/firefly/core/datastore/app_preferences.proto index e7df135f5..09611ff12 100644 --- a/core/datastore/src/main/proto/social/firefly/core/datastore/app_preferences.proto +++ b/core/datastore/src/main/proto/social/firefly/core/datastore/app_preferences.proto @@ -10,7 +10,9 @@ message AppPreferences { DARK = 2; } + reserved 3; + reserved "active_user_datastore_filename"; + bool track_analytics = 1; ThemeType theme_type = 2; - string active_user_datastore_filename = 3; } \ No newline at end of file diff --git a/core/datastore/src/main/proto/social/firefly/core/datastore/user_preferences.proto b/core/datastore/src/main/proto/social/firefly/core/datastore/user_preferences.proto deleted file mode 100644 index e7fa00e4c..000000000 --- a/core/datastore/src/main/proto/social/firefly/core/datastore/user_preferences.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto3"; - -option java_package = "social.firefly.core.datastore"; -option java_multiple_files = true; - -message UserPreferences { - enum ThreadType { - LIST = 0; - DIRECT_REPLIES_LIST = 1; - TREE = 2; - } - - reserved 4, 5, 7, 10; - reserved "client_id", "client_secret"; - string access_token = 1; - string account_id = 2; - string domain = 3; - string serialized_push_keys = 6; - string last_seen_home_status_id = 8; - ThreadType thread_type = 9; - string avatar_url = 11; - string user_name = 12; - string default_language = 13; -} \ No newline at end of file diff --git a/core/push/build.gradle.kts b/core/push/build.gradle.kts index a19376bd6..eceab1f0a 100644 --- a/core/push/build.gradle.kts +++ b/core/push/build.gradle.kts @@ -9,9 +9,9 @@ android { dependencies { implementation(project(":core:repository:mastodon")) - implementation(project(":core:datastore")) implementation(project(":core:common")) implementation(project(":core:model")) + implementation(project(":core:accounts")) implementation(platform(libs.firebase)) implementation(libs.firebase.messaging) diff --git a/core/push/src/main/kotlin/social/firefly/core/push/PushModule.kt b/core/push/src/main/kotlin/social/firefly/core/push/PushModule.kt index 35f0da1f7..f5a23f504 100644 --- a/core/push/src/main/kotlin/social/firefly/core/push/PushModule.kt +++ b/core/push/src/main/kotlin/social/firefly/core/push/PushModule.kt @@ -3,14 +3,12 @@ package social.firefly.core.push import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import social.firefly.common.commonModule -import social.firefly.core.datastore.dataStoreModule import social.firefly.core.repository.mastodon.mastodonRepositoryModule val pushModule = module { includes( commonModule, mastodonRepositoryModule, - dataStoreModule, ) singleOf(::KeyManager) diff --git a/core/push/src/main/kotlin/social/firefly/core/push/firebase/FcmService.kt b/core/push/src/main/kotlin/social/firefly/core/push/firebase/FcmService.kt index ca26c6728..3cedc735a 100644 --- a/core/push/src/main/kotlin/social/firefly/core/push/firebase/FcmService.kt +++ b/core/push/src/main/kotlin/social/firefly/core/push/firebase/FcmService.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.json.Json import org.koin.core.component.KoinComponent import org.koin.core.component.inject import social.firefly.common.appscope.AppScope -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.push.KeyManager import timber.log.Timber @@ -17,7 +17,7 @@ class FcmService : FirebaseMessagingService(), KoinComponent { private val keyManager: KeyManager by inject() // private val pushRepository: PushRepository by inject() private val coroutineScope: AppScope by inject() - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager by inject() + private val accountsManager: AccountsManager by inject() private val tag = FcmService::class.simpleName!! @@ -28,11 +28,14 @@ class FcmService : FirebaseMessagingService(), KoinComponent { override fun onNewToken(token: String) { Timber.tag(tag).d("new token: $token") - - userPreferencesDatastoreManager.dataStores.value.forEach { userPrefsDatastore -> - coroutineScope.launch { + coroutineScope.launch { + accountsManager.getAllAccounts().forEach { val keys = keyManager.generatePushKeys() - userPrefsDatastore.saveSerializedPushKeyPair(Json.encodeToString(keys)) + + accountsManager.updatePushKeys( + mastodonAccount = it, + serializedPushKeys = Json.encodeToString(keys) + ) Timber.tag(tag).d("keys: $keys") diff --git a/core/repository/mastodon/build.gradle.kts b/core/repository/mastodon/build.gradle.kts index 9a506e474..23dc47a48 100644 --- a/core/repository/mastodon/build.gradle.kts +++ b/core/repository/mastodon/build.gradle.kts @@ -10,9 +10,9 @@ android { dependencies { implementation(project(":core:common")) implementation(project(":core:database")) - implementation(project(":core:datastore")) implementation(project(":core:model")) implementation(project(":core:network:mastodon")) + implementation(project(":core:accounts")) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.datetime) diff --git a/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/AuthCredentialObserver.kt b/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/AuthCredentialObserver.kt index b6cbc0d51..f9e62911d 100644 --- a/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/AuthCredentialObserver.kt +++ b/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/AuthCredentialObserver.kt @@ -1,43 +1,36 @@ package social.firefly.core.repository.mastodon import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.network.mastodon.interceptors.AuthCredentialInterceptor /** * Keeps the domain and access token in [AuthCredentialInterceptor] up to date */ -@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) +@OptIn(DelicateCoroutinesApi::class) class AuthCredentialObserver( - userPreferencesDatastoreManager: UserPreferencesDatastoreManager, authCredentialInterceptor: AuthCredentialInterceptor, + accountsManager: AccountsManager, ) { init { GlobalScope.launch { coroutineScope { - userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.accessToken - }.filterNotNull().collectLatest { - if (it.isNotBlank()) { - authCredentialInterceptor.accessToken = it + accountsManager.getActiveAccountFlow().collectLatest { + if (it.accessToken.isNotBlank()) { + authCredentialInterceptor.accessToken = it.accessToken } } } } GlobalScope.launch { - userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.domain - }.filterNotNull().collectLatest { - if (it.isNotBlank()) { - authCredentialInterceptor.domain = it + accountsManager.getActiveAccountFlow().collectLatest { + if (it.domain.isNotBlank()) { + authCredentialInterceptor.domain = it.domain } } } diff --git a/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/MastodonRepositoryModule.kt b/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/MastodonRepositoryModule.kt index 53df21621..8499312c7 100644 --- a/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/MastodonRepositoryModule.kt +++ b/core/repository/mastodon/src/main/kotlin/social/firefly/core/repository/mastodon/MastodonRepositoryModule.kt @@ -2,16 +2,16 @@ package social.firefly.core.repository.mastodon import org.koin.core.module.dsl.singleOf import org.koin.dsl.module +import social.firefly.core.accounts.accountsModule import social.firefly.core.database.databaseModule -import social.firefly.core.datastore.dataStoreModule import social.firefly.core.network.mastodon.mastodonNetworkModule val mastodonRepositoryModule = module { includes( mastodonNetworkModule, - dataStoreModule, databaseModule, + accountsModule, ) single { TimelineRepository(get(), get(), get(), get(), get(), get()) } diff --git a/core/repository/paging/build.gradle.kts b/core/repository/paging/build.gradle.kts index 5cf73dc3a..d397e1d40 100644 --- a/core/repository/paging/build.gradle.kts +++ b/core/repository/paging/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { implementation(project(":core:usecase:mastodon")) implementation(project(":core:common")) implementation(project(":core:model")) - implementation(project(":core:datastore")) + implementation(project(":core:accounts")) implementation(libs.androidx.paging.runtime) implementation(libs.jakewharton.timber) diff --git a/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/PagingModule.kt b/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/PagingModule.kt index d5f641f18..fbf428824 100644 --- a/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/PagingModule.kt +++ b/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/PagingModule.kt @@ -2,7 +2,7 @@ package social.firefly.core.repository.paging import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module -import social.firefly.core.datastore.dataStoreModule +import social.firefly.core.accounts.accountsModule import social.firefly.core.repository.mastodon.mastodonRepositoryModule import social.firefly.core.repository.paging.pagers.status.AccountTimelinePager import social.firefly.core.repository.paging.pagers.accounts.BlocksPager @@ -31,7 +31,7 @@ val pagingModule = module { includes( mastodonRepositoryModule, mastodonUsecaseModule, - dataStoreModule, + accountsModule, ) factoryOf(::HomeTimelineRemoteMediator) diff --git a/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/remotemediators/HomeTimelineRemoteMediator.kt b/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/remotemediators/HomeTimelineRemoteMediator.kt index 7ecb1d94b..7c4d9a4ad 100644 --- a/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/remotemediators/HomeTimelineRemoteMediator.kt +++ b/core/repository/paging/src/main/kotlin/social/firefly/core/repository/paging/remotemediators/HomeTimelineRemoteMediator.kt @@ -4,17 +4,10 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.launch import social.firefly.common.Rel +import social.firefly.core.accounts.AccountsManager import social.firefly.core.database.model.entities.statusCollections.HomeTimelineStatusWrapper -import social.firefly.core.datastore.UserPreferencesDatastoreManager import social.firefly.core.model.Status import social.firefly.core.model.paging.MastodonPagedResponse import social.firefly.core.repository.mastodon.DatabaseDelegate @@ -30,7 +23,7 @@ class HomeTimelineRemoteMediator( private val saveStatusToDatabase: SaveStatusToDatabase, private val databaseDelegate: DatabaseDelegate, private val getInReplyToAccountNames: GetInReplyToAccountNames, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, + private val accountsManager: AccountsManager, ) : RemoteMediator() { private var firstRefreshHasHappened = false @@ -110,7 +103,6 @@ class HomeTimelineRemoteMediator( } } - @OptIn(ExperimentalCoroutinesApi::class) private suspend fun fetchRefresh( state: PagingState, ): MastodonPagedResponse { @@ -120,18 +112,7 @@ class HomeTimelineRemoteMediator( // If this is the first time we are loading the page, we need to start where // the user last left off. Grab the lastSeenHomeStatusId if (!firstRefreshHasHappened) { - val lastSeenId = CompletableDeferred() - with(CoroutineScope(coroutineContext)) { - launch { - userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.lastSeenHomeStatusId - }.collectLatest { - lastSeenId.complete(it) - cancel() - } - } - } - olderThanId = lastSeenId.await() + olderThanId = accountsManager.getActiveAccount().lastSeenHomeStatusId } val mainResponse = timelineRepository.getHomeTimeline( diff --git a/core/ui/chooseAccount/build.gradle.kts b/core/ui/chooseAccount/build.gradle.kts index cfc9fc30f..175aa49f8 100644 --- a/core/ui/chooseAccount/build.gradle.kts +++ b/core/ui/chooseAccount/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(project(":core:datastore")) implementation(project(":core:navigation")) implementation(project(":core:usecase:mastodon")) + implementation(project(":core:accounts")) implementation(libs.coil) implementation(libs.koin.core) diff --git a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialog.kt b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialog.kt index fd6c4b6f3..552cf98b7 100644 --- a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialog.kt +++ b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialog.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage -import kotlinx.coroutines.flow.flowOf import org.koin.androidx.compose.koinViewModel import social.firefly.core.designsystem.theme.FfTheme import social.firefly.core.ui.common.dialog.FfDialog @@ -81,25 +80,20 @@ private fun Account( uiState: ChooseAccountUiState, chooseAccountInteractions: ChooseAccountInteractions, ) { - val avatarUrl by uiState.avatarUrl.collectAsStateWithLifecycle(initialValue = "") - val accountId by uiState.accountId.collectAsStateWithLifecycle(initialValue = "") - val domain by uiState.domain.collectAsStateWithLifecycle(initialValue = "") - val userName by uiState.userName.collectAsStateWithLifecycle(initialValue = "") - Row( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .clickable { - chooseAccountInteractions.onAccountClicked(accountId, domain) + chooseAccountInteractions.onAccountClicked(uiState.accountId, uiState.domain) } .padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - Avatar(avatarUrl) + Avatar(uiState.avatarUrl) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "${userName}@${domain}", + text = "${uiState.userName}@${uiState.domain}", style = FfTheme.typography.labelSmall, ) } @@ -126,16 +120,16 @@ private fun ChooseAccountDialogPreview() { DialogContent( accounts = listOf( ChooseAccountUiState( - accountId = flowOf(), - userName = flowOf("john"), - domain = flowOf("mozilla.social"), - avatarUrl = flowOf() + accountId = "", + userName = "john", + domain = "mozilla.social", + avatarUrl = "" ), ChooseAccountUiState( - accountId = flowOf(), - userName = flowOf("john"), - domain = flowOf("moz.soc"), - avatarUrl = flowOf() + accountId = "", + userName = "john", + domain = "moz.soc", + avatarUrl = "" ) ), chooseAccountInteractions = ChooseAccountInteractionsNoOp diff --git a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialogViewModel.kt b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialogViewModel.kt index e18018f12..58432e997 100644 --- a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialogViewModel.kt +++ b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountDialogViewModel.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.Event import social.firefly.core.navigation.EventRelay import social.firefly.core.navigation.NavigationDestination @@ -16,19 +16,19 @@ import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.usecase.mastodon.auth.SwitchActiveAccount class ChooseAccountDialogViewModel( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, eventRelay: EventRelay, private val navigateTo: NavigateTo, private val switchActiveAccount: SwitchActiveAccount, + private val accountsManager: AccountsManager, ) : ViewModel(), ChooseAccountInteractions { - val accounts = userPreferencesDatastoreManager.dataStores.map { dataStores -> - dataStores.map { dataStore -> + val accounts = accountsManager.getAllAccountsFlow().map { accounts -> + accounts.map { account -> ChooseAccountUiState( - accountId = dataStore.accountId, - userName = dataStore.userName, - domain = dataStore.domain, - avatarUrl = dataStore.avatarUrl, + accountId = account.accountId, + userName = account.userName, + domain = account.domain, + avatarUrl = account.avatarUrl, ) } } @@ -41,7 +41,7 @@ class ChooseAccountDialogViewModel( eventRelay.navigationEvents.collect { event -> when (event) { is Event.ChooseAccountForSharing -> { - if (userPreferencesDatastoreManager.isLoggedInToMultipleAccounts) { + if (accountsManager.getAllAccounts().size > 1) { _isOpen.update { true } } else { navigateTo( diff --git a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountModule.kt b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountModule.kt index 4c734c96a..25a049d38 100644 --- a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountModule.kt +++ b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountModule.kt @@ -2,7 +2,12 @@ package social.firefly.core.ui.chooseAccount import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module +import social.firefly.core.accounts.accountsModule val chooseAccountModule = module { + includes( + accountsModule, + ) + viewModelOf(::ChooseAccountDialogViewModel) } \ No newline at end of file diff --git a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountUiState.kt b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountUiState.kt index bdf3741d3..7e63b24ce 100644 --- a/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountUiState.kt +++ b/core/ui/chooseAccount/src/main/kotlin/social/firefly/core/ui/chooseAccount/ChooseAccountUiState.kt @@ -1,10 +1,8 @@ package social.firefly.core.ui.chooseAccount -import kotlinx.coroutines.flow.Flow - data class ChooseAccountUiState( - val accountId: Flow, - val userName: Flow, - val domain: Flow, - val avatarUrl: Flow, + val accountId: String, + val userName: String, + val domain: String, + val avatarUrl: String, ) \ No newline at end of file diff --git a/core/usecase/mastodon/build.gradle.kts b/core/usecase/mastodon/build.gradle.kts index e05c31cf2..147bbacc1 100644 --- a/core/usecase/mastodon/build.gradle.kts +++ b/core/usecase/mastodon/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(project(":core:analytics")) implementation(project(":core:navigation")) implementation(project(":core:ui:htmlcontent")) + implementation(project(":core:accounts")) implementation(libs.androidx.browser) implementation(libs.androidx.navigation.compose) diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/MastodonUsecaseModule.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/MastodonUsecaseModule.kt index d71240009..7f074b25a 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/MastodonUsecaseModule.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/MastodonUsecaseModule.kt @@ -4,6 +4,7 @@ import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import social.firefly.common.appscope.AppScope +import social.firefly.core.accounts.accountsModule import social.firefly.core.analytics.analyticsModule import social.firefly.core.navigation.navigationModule import social.firefly.core.repository.mastodon.mastodonRepositoryModule @@ -52,6 +53,7 @@ val mastodonUsecaseModule = mastodonRepositoryModule, analyticsModule, navigationModule, + accountsModule, ) // factory because it holds global variables @@ -70,18 +72,18 @@ val mastodonUsecaseModule = single { Logout( - userPreferencesDatastoreManager = get(), appScope = get(), databaseDelegate = get(), navigateTo = get(), + accountsManager = get(), ) } single { LogoutOfAllAccounts( - userPreferencesDatastoreManager = get(), appScope = get(), databaseDelegate = get(), navigateTo = get(), + accountsManager = get(), ) } diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetDomain.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetDomain.kt index ee2a10214..1ffe34396 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetDomain.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetDomain.kt @@ -2,14 +2,12 @@ package social.firefly.core.usecase.mastodon.account import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import kotlinx.coroutines.flow.mapLatest +import social.firefly.core.accounts.AccountsManager class GetDomain( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, + private val accountsManager: AccountsManager, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(): Flow = userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.domain - } + operator fun invoke(): Flow = accountsManager.getActiveAccountFlow().mapLatest { it.domain } } \ No newline at end of file diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetLoggedInUserAccountId.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetLoggedInUserAccountId.kt index 0f4053147..2fbe4cd06 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetLoggedInUserAccountId.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/account/GetLoggedInUserAccountId.kt @@ -1,22 +1,16 @@ package social.firefly.core.usecase.mastodon.account -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.runBlocking -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager /** * Synchronously gets the account ID of the current logged in user */ class GetLoggedInUserAccountId( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, + private val accountsManager: AccountsManager, ) { - @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): String = runBlocking { - userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.accountId - }.first() + accountsManager.getActiveAccount().accountId } } diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Login.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Login.kt index f6f2b2363..2e2e2b8a0 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Login.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Login.kt @@ -8,8 +8,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.parameter.parametersOf import social.firefly.common.annotations.PreferUseCase import social.firefly.common.utils.StringFactory -import social.firefly.core.datastore.AlreadySignedInException -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.model.Account import social.firefly.core.navigation.NavigationDestination import social.firefly.core.navigation.usecases.NavigateTo @@ -24,11 +23,11 @@ import timber.log.Timber * This use case handles all logic related to logging in */ class Login( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, private val openLink: OpenLink, private val navigateTo: NavigateTo, private val databaseDelegate: DatabaseDelegate, private val showSnackbar: ShowSnackbar, + private val accountsManager: AccountsManager, ): KoinComponent { private lateinit var clientId: String private lateinit var clientSecret: String @@ -90,7 +89,7 @@ class Login( baseUrl = host, ) val defaultLanguage = account.source?.defaultLanguage ?: "" - userPreferencesDatastoreManager.createNewUserDatastore( + accountsManager.createNewMastodonUser( domain = host, accessToken = accessToken, accountId = account.accountId, @@ -102,12 +101,6 @@ class Login( databaseDelegate.clearAllTables() } navigateTo(NavigationDestination.Tabs) - } catch (e: AlreadySignedInException) { - showSnackbar( - text = StringFactory.resource(R.string.already_signed_in_to_account_error), - isError = true, - ) - Timber.e(e) } catch (exception: Exception) { showSnackbar( text = StringFactory.resource(R.string.error_signing_in), diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Logout.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Logout.kt index d34d41c13..698620d6a 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Logout.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/Logout.kt @@ -4,10 +4,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import social.firefly.common.appscope.AppScope -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.NavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.repository.mastodon.DatabaseDelegate @@ -16,26 +15,32 @@ import social.firefly.core.repository.mastodon.DatabaseDelegate * Handles data related cleanup. */ class Logout( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, private val databaseDelegate: DatabaseDelegate, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val appScope: AppScope, private val navigateTo: NavigateTo, + private val accountsManager: AccountsManager, ) { @OptIn(DelicateCoroutinesApi::class) operator fun invoke(accountId: String, domain: String) = GlobalScope.launch(ioDispatcher) { appScope.reset() - val accountToDelete = userPreferencesDatastoreManager.dataStores.value.find { - it.accountId.first() == accountId && it.domain.first() == domain + val accounts = accountsManager.getAllAccounts() + val activeAccount = accountsManager.getActiveAccount() + val accountToDelete = accounts.find { + it.accountId == accountId && it.domain == domain } if (accountToDelete == null) return@launch - val isDeletingActiveUserDataStore = userPreferencesDatastoreManager.deleteDataStore(accountToDelete) + val isDeletingActiveUserDataStore = accountToDelete == activeAccount + accountsManager.deleteAccount( + accountId = accountId, + domain = domain, + ) // logging out of active account if (isDeletingActiveUserDataStore) { - if (!userPreferencesDatastoreManager.isLoggedInToAtLeastOneAccount) { + if (accounts.size <= 1) { navigateTo(NavigationDestination.Auth) } else { navigateTo(NavigationDestination.Tabs) diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/LogoutOfAllAccounts.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/LogoutOfAllAccounts.kt index 8e4fc5d4c..17a95c565 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/LogoutOfAllAccounts.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/LogoutOfAllAccounts.kt @@ -6,26 +6,24 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import social.firefly.common.appscope.AppScope -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.NavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.repository.mastodon.DatabaseDelegate class LogoutOfAllAccounts( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, private val databaseDelegate: DatabaseDelegate, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val appScope: AppScope, private val navigateTo: NavigateTo, + private val accountsManager: AccountsManager, ) { @OptIn(DelicateCoroutinesApi::class) operator fun invoke() = GlobalScope.launch(ioDispatcher) { appScope.reset() navigateTo(NavigationDestination.Auth) - userPreferencesDatastoreManager.dataStores.value.forEach { - userPreferencesDatastoreManager.deleteDataStore(it) - } + accountsManager.deleteAllAccounts() databaseDelegate.clearAllTables() } } \ No newline at end of file diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/SwitchActiveAccount.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/SwitchActiveAccount.kt index c94b9cda0..6780a98c7 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/SwitchActiveAccount.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/SwitchActiveAccount.kt @@ -1,29 +1,26 @@ package social.firefly.core.usecase.mastodon.auth import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -import social.firefly.core.datastore.AppPreferencesDatastore -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager import social.firefly.core.navigation.NavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.repository.mastodon.DatabaseDelegate class SwitchActiveAccount( - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, - private val appPreferencesDatastore: AppPreferencesDatastore, private val databaseDelegate: DatabaseDelegate, private val navigateTo: NavigateTo, + private val accountsManager: AccountsManager, ){ suspend operator fun invoke( accountId: String, domain: String, ) { - userPreferencesDatastoreManager.dataStores.value.find { - it.accountId.first() == accountId && it.domain.first() == domain + accountsManager.getAllAccounts().find { + it.accountId == accountId && it.domain == domain }?.let { - appPreferencesDatastore.saveActiveUserDatastoreFilename(it.fileName) + accountsManager.setActiveAccount(it) } withContext(Dispatchers.IO) { diff --git a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/UpdateAllLoggedInAccounts.kt b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/UpdateAllLoggedInAccounts.kt index f6d50d2b1..72509f574 100644 --- a/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/UpdateAllLoggedInAccounts.kt +++ b/core/usecase/mastodon/src/main/kotlin/social/firefly/core/usecase/mastodon/auth/UpdateAllLoggedInAccounts.kt @@ -2,25 +2,29 @@ package social.firefly.core.usecase.mastodon.auth import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.first -import social.firefly.core.datastore.UserPreferencesDatastoreManager +import social.firefly.core.accounts.AccountsManager +import social.firefly.core.accounts.model.MastodonAccount +import social.firefly.core.model.Account import social.firefly.core.repository.mastodon.VerificationRepository import timber.log.Timber class UpdateAllLoggedInAccounts( private val verificationRepository: VerificationRepository, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, + private val accountsManager: AccountsManager, ) { suspend operator fun invoke() = coroutineScope { - userPreferencesDatastoreManager.dataStores.value.map { dataStore -> + accountsManager.getAllAccounts().map { account -> async { - val accessToken = dataStore.accessToken.first() - val domain = dataStore.domain.first() + val accessToken = account.accessToken + val domain = account.domain try { - verificationRepository.verifyUserCredentials( - accessToken, - domain, + AccountWrapper( + mastodonAccount = account, + account = verificationRepository.verifyUserCredentials( + accessToken, + domain, + ) ) } catch (e: Exception) { Timber.e(e) @@ -29,15 +33,18 @@ class UpdateAllLoggedInAccounts( } }.mapNotNull { it.await() - }.forEach { account -> - userPreferencesDatastoreManager.dataStores.value.find { - it.accountId.first() == account.accountId - }?.apply { - saveAvatarUrl(account.avatarUrl) - saveUserName(account.username) - val defaultLanguage = account.source?.defaultLanguage ?: "" - saveDefaultLanguage(defaultLanguage) - } + }.forEach { accountWrapper -> + accountsManager.updateAccountInfo( + mastodonAccount = accountWrapper.mastodonAccount, + avatarUrl = accountWrapper.account.avatarUrl, + username = accountWrapper.account.username, + defaultLanguage = accountWrapper.account.source?.defaultLanguage ?: "" + ) } } + + data class AccountWrapper( + val mastodonAccount: MastodonAccount, + val account: Account, + ) } \ No newline at end of file diff --git a/feature/feed/build.gradle.kts b/feature/feed/build.gradle.kts index 0f10dbf43..51fc1702e 100644 --- a/feature/feed/build.gradle.kts +++ b/feature/feed/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":core:ui:common")) implementation(project(":core:ui:postcard")) implementation(project(":core:usecase:mastodon")) + implementation(project(":core:accounts")) implementation(libs.androidx.paging.runtime) diff --git a/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedModule.kt b/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedModule.kt index 9b2f7ed59..b2e0883cf 100644 --- a/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedModule.kt +++ b/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedModule.kt @@ -3,6 +3,7 @@ package social.firefly.feature.feed import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module import social.firefly.common.commonModule +import social.firefly.core.accounts.accountsModule import social.firefly.core.analytics.analyticsModule import social.firefly.core.datastore.dataStoreModule import social.firefly.core.navigation.navigationModule @@ -19,6 +20,7 @@ val feedModule = module { postCardModule, navigationModule, analyticsModule, + accountsModule, ) viewModelOf(::FeedViewModel) diff --git a/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedViewModel.kt b/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedViewModel.kt index 235d06efc..fc67df306 100644 --- a/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedViewModel.kt +++ b/feature/feed/src/main/kotlin/social/firefly/feature/feed/FeedViewModel.kt @@ -9,15 +9,14 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.core.parameter.parametersOf import org.koin.java.KoinJavaComponent import social.firefly.common.utils.edit +import social.firefly.core.accounts.AccountsManager import social.firefly.core.analytics.FeedAnalytics import social.firefly.core.analytics.FeedLocation -import social.firefly.core.datastore.UserPreferencesDatastoreManager import social.firefly.core.navigation.BottomBarNavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.repository.mastodon.TimelineRepository @@ -31,13 +30,13 @@ import social.firefly.core.usecase.mastodon.account.GetLoggedInUserAccountId @OptIn(ExperimentalPagingApi::class) class FeedViewModel( private val analytics: FeedAnalytics, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, homeTimelineRemoteMediator: HomeTimelineRemoteMediator, localTimelinePager: LocalTimelinePager, federatedTimelinePager: FederatedTimelinePager, private val timelineRepository: TimelineRepository, getLoggedInUserAccountId: GetLoggedInUserAccountId, private val navigateTo: NavigateTo, + private val accountsManager: AccountsManager, ) : ViewModel(), FeedInteractions { private val userAccountId: String = getLoggedInUserAccountId() @@ -114,7 +113,10 @@ class FeedViewModel( // save the last seen status no more than once per x seconds (SAVE_RATE) if (statusViewedJob == null) { statusViewedJob = viewModelScope.launch { - userPreferencesDatastoreManager.activeUserDatastore.first().saveLastSeenHomeStatusId(statusId) + accountsManager.updateLastSeenHomeStatusId( + mastodonAccount = accountsManager.getActiveAccount(), + lastSeenStatusId = statusId, + ) delay(SAVE_RATE) statusViewedJob = null } diff --git a/feature/post/build.gradle.kts b/feature/post/build.gradle.kts index de3bbd39c..2c0ea4bcf 100644 --- a/feature/post/build.gradle.kts +++ b/feature/post/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(project(":core:analytics")) implementation(project(":core:ui:htmlcontent")) implementation(project(":core:share")) + implementation(project(":core:accounts")) implementation(libs.androidx.navigation.compose) implementation(libs.koin.core) diff --git a/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostModule.kt b/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostModule.kt index c612c4129..3b0718d7a 100644 --- a/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostModule.kt +++ b/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostModule.kt @@ -4,6 +4,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import social.firefly.common.commonModule +import social.firefly.core.accounts.accountsModule import social.firefly.core.analytics.analyticsModule import social.firefly.core.datastore.dataStoreModule import social.firefly.core.navigation.navigationModule @@ -21,6 +22,7 @@ val newPostModule = module { mastodonUsecaseModule, navigationModule, analyticsModule, + accountsModule, ) viewModel { parametersHolder -> @@ -34,8 +36,8 @@ val newPostModule = module { showSnackbar = get(), getLoggedInUserAccountId = get(), accountRepository = get(), - userPreferencesDatastoreManager = get(), statusRepository = get(), + accountsManager = get(), ) } diff --git a/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostViewModel.kt b/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostViewModel.kt index 344b4b54a..06aa1846e 100644 --- a/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostViewModel.kt +++ b/feature/post/src/main/kotlin/social/firefly/feature/post/NewPostViewModel.kt @@ -2,12 +2,9 @@ package social.firefly.feature.post import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -18,8 +15,8 @@ import social.firefly.common.loadResource import social.firefly.common.utils.FileType import social.firefly.common.utils.StringFactory import social.firefly.common.utils.edit +import social.firefly.core.accounts.AccountsManager import social.firefly.core.analytics.NewPostAnalytics -import social.firefly.core.datastore.UserPreferencesDatastoreManager import social.firefly.core.model.StatusVisibility import social.firefly.core.model.request.PollCreate import social.firefly.core.navigation.usecases.PopNavBackstack @@ -37,7 +34,6 @@ import social.firefly.feature.post.status.StatusDelegate import timber.log.Timber import java.util.Locale -@OptIn(ExperimentalCoroutinesApi::class) class NewPostViewModel( private val analytics: NewPostAnalytics, private val replyStatusId: String?, @@ -48,8 +44,8 @@ class NewPostViewModel( private val editStatus: EditStatus, private val popNavBackstack: PopNavBackstack, private val showSnackbar: ShowSnackbar, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, private val statusRepository: StatusRepository, + private val accountsManager: AccountsManager, ) : ViewModel(), NewPostInteractions, KoinComponent { val statusDelegate: StatusDelegate by inject { @@ -151,10 +147,9 @@ class NewPostViewModel( val visibility = status?.visibility ?: StatusVisibility.Public - val defaultLanguageCode = status?.language ?: userPreferencesDatastoreManager - .activeUserDatastore - .flatMapLatest { it.defaultLanguage } - .first() + val defaultLanguageCode = status?.language ?: accountsManager + .getActiveAccount() + .defaultLanguage .ifBlank { Locale.getDefault().language } diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 0286f1409..afcdbdb52 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(project(":core:ui:htmlcontent")) implementation(project(":core:usecase:mastodon")) implementation(project(":core:workmanager")) + implementation(project(":core:accounts")) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.datastore) diff --git a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsScreen.kt b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsScreen.kt index b214d30ec..389feac2b 100644 --- a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsScreen.kt @@ -145,19 +145,15 @@ private fun UserHeader( isTheActiveAccount: Boolean, accountSettingsInteractions: AccountSettingsInteractions, ) { - val avatarUrl by account.avatarUrl.collectAsStateWithLifecycle(initialValue = "") - val accountId by account.accountId.collectAsStateWithLifecycle(initialValue = "") - val domain by account.domain.collectAsStateWithLifecycle(initialValue = "") - val userName by account.userName.collectAsStateWithLifecycle(initialValue = "") Row( verticalAlignment = Alignment.CenterVertically, ) { - Avatar(avatarUrl) + Avatar(account.avatarUrl) Spacer(modifier = Modifier.width(8.dp)) Text( modifier = Modifier.weight(1f), - text = "${userName}@${domain}", + text = "${account.userName}@${account.domain}", style = FfTheme.typography.labelSmall, ) if (isTheActiveAccount) { @@ -174,8 +170,8 @@ private fun UserHeader( modifier = Modifier.padding(start = FfSpacing.md), onClick = { accountSettingsInteractions.onSetAccountAsActiveClicked( - accountId = accountId, - domain = domain, + accountId = account.accountId, + domain = account.domain, ) } ) { @@ -186,11 +182,11 @@ private fun UserHeader( val overflowMenuExpanded = remember { mutableStateOf(false) } val logoutDialog = logoutConfirmationDialog( - accountName = "${userName}@${domain}" + accountName = "${account.userName}@${account.domain}" ) { accountSettingsInteractions.onLogoutClicked( - accountId = accountId, - domain = domain, + accountId = account.accountId, + domain = account.domain, ) } @@ -200,7 +196,7 @@ private fun UserHeader( FfDropDownItem( text = stringResource(id = R.string.manage_account_option), expanded = overflowMenuExpanded, - onClick = { accountSettingsInteractions.onManageAccountClicked(domain) } + onClick = { accountSettingsInteractions.onManageAccountClicked(account.domain) } ) FfDropDownItem( text = stringResource(id = R.string.remove_account), @@ -271,17 +267,17 @@ private fun AccountSettingsScreenPreview() { ) { AccountSettingsScreen( activeAccount = LoggedInAccount( - userName = flowOf("John"), - domain = flowOf("mozilla.social"), - avatarUrl = flowOf(), - accountId = flowOf(), + userName = "John", + domain = "mozilla.social", + avatarUrl = "", + accountId = "", ), otherAccounts = listOf( LoggedInAccount( - userName = flowOf("Birdman"), - domain = flowOf("mozilla.social"), - avatarUrl = flowOf(), - accountId = flowOf(), + userName = "Birdman", + domain = "mozilla.social", + avatarUrl = "", + accountId = "", ) ), accountSettingsInteractions = AccountSettingsInteractionsNoOp diff --git a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsViewModel.kt b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsViewModel.kt index f979ec3f8..a902c59a5 100644 --- a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/AccountSettingsViewModel.kt @@ -3,11 +3,10 @@ package social.firefly.feature.settings.account import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import social.firefly.core.accounts.AccountsManager import social.firefly.core.analytics.SettingsAnalytics -import social.firefly.core.datastore.UserPreferencesDatastoreManager import social.firefly.core.navigation.AuthNavigationDestination import social.firefly.core.navigation.usecases.NavigateTo import social.firefly.core.navigation.usecases.OpenLink @@ -24,31 +23,31 @@ class AccountSettingsViewModel( private val navigateTo: NavigateTo, private val switchActiveAccount: SwitchActiveAccount, private val logoutOfAllAccounts: LogoutOfAllAccounts, - userPreferencesDatastoreManager: UserPreferencesDatastoreManager, updateAllLoggedInAccounts: UpdateAllLoggedInAccounts, + private val accountsManager: AccountsManager, ) : ViewModel(), AccountSettingsInteractions { - val otherAccounts = userPreferencesDatastoreManager.dataStores.combine( - userPreferencesDatastoreManager.activeUserDatastore - ) { dataStores, activeDataStore -> - dataStores.filterNot { - it == activeDataStore - }.map { dataStore -> + val otherAccounts = accountsManager.getAllAccountsFlow().combine( + accountsManager.getActiveAccountFlow() + ) { otherAccounts, activeAccount -> + otherAccounts.filterNot { + it == activeAccount + }.map { account -> LoggedInAccount( - accountId = dataStore.accountId, - userName = dataStore.userName, - domain = dataStore.domain, - avatarUrl = dataStore.avatarUrl, + accountId = account.accountId, + userName = account.userName, + domain = account.domain, + avatarUrl = account.avatarUrl, ) } } - val activeAccount = userPreferencesDatastoreManager.activeUserDatastore.map { dataStore -> + val activeAccount = accountsManager.getActiveAccountFlow().map { account -> LoggedInAccount( - accountId = dataStore.accountId, - userName = dataStore.userName, - domain = dataStore.domain, - avatarUrl = dataStore.avatarUrl, + accountId = account.accountId, + userName = account.userName, + domain = account.domain, + avatarUrl = account.avatarUrl, ) } diff --git a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/LoggedInAccount.kt b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/LoggedInAccount.kt index 5840f7ad3..2308d0521 100644 --- a/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/LoggedInAccount.kt +++ b/feature/settings/src/main/kotlin/social/firefly/feature/settings/account/LoggedInAccount.kt @@ -1,10 +1,8 @@ package social.firefly.feature.settings.account -import kotlinx.coroutines.flow.Flow - data class LoggedInAccount( - val accountId: Flow, - val userName: Flow, - val domain: Flow, - val avatarUrl: Flow, + val accountId: String, + val userName: String, + val domain: String, + val avatarUrl: String, ) diff --git a/feature/thread/build.gradle.kts b/feature/thread/build.gradle.kts index 916e3e034..c93945cc7 100644 --- a/feature/thread/build.gradle.kts +++ b/feature/thread/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(project(":core:common")) implementation(project(":core:navigation")) implementation(project(":core:analytics")) + implementation(project(":core:accounts")) implementation(libs.androidx.paging.runtime) diff --git a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadInteractions.kt b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadInteractions.kt index 63ead63f7..ebf36a429 100644 --- a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadInteractions.kt +++ b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadInteractions.kt @@ -2,7 +2,6 @@ package social.firefly.feature.thread interface ThreadInteractions { fun onsScreenViewed() = Unit - fun onThreadTypeSelected(threadType: ThreadType) = Unit fun onRetryClicked() = Unit fun onShowAllRepliesClicked(statusId: String) = Unit fun onPulledToRefresh() = Unit diff --git a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadModule.kt b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadModule.kt index 27a76f483..d2c1ff967 100644 --- a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadModule.kt +++ b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadModule.kt @@ -3,6 +3,7 @@ package social.firefly.feature.thread import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import social.firefly.common.commonModule +import social.firefly.core.accounts.accountsModule import social.firefly.core.analytics.analyticsModule import social.firefly.core.datastore.dataStoreModule import social.firefly.core.navigation.navigationModule @@ -20,6 +21,7 @@ val threadModule = postCardModule, navigationModule, analyticsModule, + accountsModule, ) viewModel { parametersHolder -> @@ -28,7 +30,6 @@ val threadModule = getLoggedInUserAccountId = get(), getThread = get(), mainStatusId = parametersHolder[0], - userPreferencesDatastoreManager = get(), ) } } diff --git a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadScreen.kt b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadScreen.kt index 7d265bd11..00e2989d3 100644 --- a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadScreen.kt +++ b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,7 +29,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.LoadState import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf import social.firefly.common.Resource @@ -40,10 +37,7 @@ import social.firefly.core.designsystem.theme.FfTheme import social.firefly.core.ui.common.FfSurface import social.firefly.core.ui.common.UiConstants import social.firefly.core.ui.common.appbar.FfCloseableTopAppBar -import social.firefly.core.ui.common.dropdown.FfDropDownItem -import social.firefly.core.ui.common.dropdown.FfIconButtonDropDownMenu import social.firefly.core.ui.common.error.GenericError -import social.firefly.core.ui.common.loading.MaxSizeLoading import social.firefly.core.ui.common.pullrefresh.PullRefreshIndicator import social.firefly.core.ui.common.pullrefresh.pullRefresh import social.firefly.core.ui.common.pullrefresh.rememberPullRefreshState @@ -58,12 +52,8 @@ internal fun ThreadScreen( viewModel: ThreadViewModel = koinViewModel(parameters = { parametersOf(threadStatusId) }), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val threadType by viewModel.threadType.collectAsStateWithLifecycle( - initialValue = ThreadType.TREE - ) ThreadScreen( - threadType = threadType, uiState = uiState, postCardDelegate = viewModel.postCardDelegate, threadInteractions = viewModel, @@ -77,7 +67,6 @@ internal fun ThreadScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ThreadScreen( - threadType: ThreadType, uiState: Resource, postCardDelegate: PostCardInteractions, threadInteractions: ThreadInteractions, @@ -86,12 +75,6 @@ private fun ThreadScreen( Column(Modifier.systemBarsPadding()) { FfCloseableTopAppBar( title = stringResource(id = R.string.thread_screen_title), - actions = { - ThreadTypeButton( - threadType = threadType, - threadInteractions = threadInteractions, - ) - } ) val refreshState = rememberPullRefreshState( @@ -115,7 +98,6 @@ private fun ThreadScreen( modifier = Modifier .widthIn(max = UiConstants.MAX_WIDTH) .align(Alignment.TopCenter), - threadType = threadType, statuses = it, postCardDelegate = postCardDelegate, threadInteractions = threadInteractions, @@ -134,7 +116,6 @@ private fun ThreadScreen( @Composable private fun ThreadList( - threadType: ThreadType, statuses: ThreadPostCardCollection, postCardDelegate: PostCardInteractions, threadInteractions: ThreadInteractions, @@ -201,7 +182,6 @@ private fun ThreadList( } descendants( - threadType = threadType, statuses = statuses, postCardDelegate = postCardDelegate, threadInteractions = threadInteractions @@ -210,7 +190,6 @@ private fun ThreadList( } private fun LazyListScope.descendants( - threadType: ThreadType, statuses: ThreadPostCardCollection, postCardDelegate: PostCardInteractions, threadInteractions: ThreadInteractions, @@ -224,7 +203,7 @@ private fun LazyListScope.descendants( PostCardListItem( uiState = item.uiState, postCardInteractions = postCardDelegate, - showDivider = threadType != ThreadType.TREE, + showDivider = false, ) } is ThreadDescendant.ViewMore -> { @@ -267,69 +246,3 @@ private fun LazyListScope.descendants( } } - -@Composable -private fun ThreadTypeButton( - threadType: ThreadType, - threadInteractions: ThreadInteractions, -) { - val overflowMenuExpanded = remember { mutableStateOf(false) } - - FfIconButtonDropDownMenu( - expanded = overflowMenuExpanded, - dropDownMenuContent = { - for (dropDownOption in ThreadType.entries) { - FfDropDownItem( - text = when (dropDownOption) { - ThreadType.LIST -> { - stringResource(id = R.string.list_view) - } - ThreadType.DIRECT_REPLIES -> { - stringResource(id = R.string.direct_replies_list_view) - } - ThreadType.TREE -> { - stringResource(id = R.string.tree_view) - } - }, - icon = { - Icon( - modifier = Modifier - .size(FfIcons.Sizes.small), - painter = when (dropDownOption) { - ThreadType.LIST -> { - FfIcons.listPlus() - } - ThreadType.DIRECT_REPLIES -> { - FfIcons.list() - } - ThreadType.TREE -> { - FfIcons.treeView() - } - }, - contentDescription = null - ) - }, - expanded = overflowMenuExpanded, - onClick = { threadInteractions.onThreadTypeSelected(dropDownOption) }, - ) - } - } - ) { - Icon( - modifier = Modifier - .size(FfIcons.Sizes.normal), - painter = when (threadType) { - ThreadType.LIST -> { - FfIcons.listPlus() - } - ThreadType.DIRECT_REPLIES -> { - FfIcons.list() - } - ThreadType.TREE -> { - FfIcons.treeView() - } - }, - contentDescription = stringResource(id = R.string.view_type) - ) - } -} diff --git a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadViewModel.kt b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadViewModel.kt index 04f1c8f94..3c33b3522 100644 --- a/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadViewModel.kt +++ b/feature/thread/src/main/kotlin/social/firefly/feature/thread/ThreadViewModel.kt @@ -2,16 +2,12 @@ package social.firefly.feature.thread import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.parameter.parametersOf @@ -23,8 +19,6 @@ import social.firefly.common.tree.toTree import social.firefly.common.utils.edit import social.firefly.core.analytics.FeedLocation import social.firefly.core.analytics.ThreadAnalytics -import social.firefly.core.datastore.UserPreferences -import social.firefly.core.datastore.UserPreferencesDatastoreManager import social.firefly.core.model.Status import social.firefly.core.model.Thread import social.firefly.core.ui.postcard.DepthLinesUiState @@ -40,7 +34,6 @@ class ThreadViewModel( private val getThread: GetThread, private val mainStatusId: String, getLoggedInUserAccountId: GetLoggedInUserAccountId, - private val userPreferencesDatastoreManager: UserPreferencesDatastoreManager, ) : ViewModel(), ThreadInteractions { private val loggedInAccountId = getLoggedInUserAccountId() @@ -71,23 +64,6 @@ class ThreadViewModel( private var getThreadJob: Job? = null private var cachedData: ThreadPostCardCollection? = null - @OptIn(ExperimentalCoroutinesApi::class) - val threadType = userPreferencesDatastoreManager.activeUserDatastore.flatMapLatest { - it.threadType - }.mapLatest { - when (it) { - social.firefly.core.datastore.UserPreferences.ThreadType.LIST -> { - ThreadType.LIST - } - social.firefly.core.datastore.UserPreferences.ThreadType.DIRECT_REPLIES_LIST -> { - ThreadType.DIRECT_REPLIES - } - else -> { - ThreadType.TREE - } - } - } - init { loadThread() } @@ -118,11 +94,10 @@ class ThreadViewModel( } } combine( - threadType, threadDataFlow, postIdsWithHiddenReplies, postIdsIgnoringMaxReplyCount, - ) { threadType, threadData, postsIdsWithHiddenReplies, postIdsIgnoringMaxReplyCount -> + ) { threadData, postsIdsWithHiddenReplies, postIdsIgnoringMaxReplyCount -> when (val threadResource = threadData.threadResource) { is Resource.Loading -> { cachedData?.let { Resource.Loading(cachedData) } ?: Resource.Loading() @@ -137,7 +112,6 @@ class ThreadViewModel( } is Resource.Loaded -> { val data = generateThreadPostCardCollection( - threadType = threadType, threadData = threadData, postsIdsWithHiddenReplies = postsIdsWithHiddenReplies, postIdsIgnoringMaxReplyCount = postIdsIgnoringMaxReplyCount, @@ -153,121 +127,75 @@ class ThreadViewModel( } private fun generateThreadPostCardCollection( - threadType: ThreadType, threadData: ThreadData, postsIdsWithHiddenReplies: List, postIdsIgnoringMaxReplyCount: List, ): ThreadPostCardCollection { val thread = threadData.threadResource.data!! - return when (threadType) { - ThreadType.LIST -> { - val mapToPostCardUiState = fun Status.(): PostCardUiState = toPostCardUiState( - currentUserAccountId = loggedInAccountId, - isClickable = statusId != mainStatusId, - ) - ThreadPostCardCollection( - ancestors = thread.context.ancestors.map { it.mapToPostCardUiState() }, - mainPost = thread.status.mapToPostCardUiState(), - descendants = thread.context.descendants.map { - ThreadDescendant.PostCard(it.mapToPostCardUiState()) - }, - ) - } - - ThreadType.DIRECT_REPLIES -> { - val mapToPostCardUiState = fun Status.(): PostCardUiState = toPostCardUiState( - currentUserAccountId = loggedInAccountId, - showTopRowMetaData = false, - isClickable = statusId != mainStatusId, - ) - ThreadPostCardCollection( - ancestors = thread.context.ancestors.map { it.mapToPostCardUiState() }, - mainPost = thread.status.mapToPostCardUiState(), - descendants = thread.context.descendants - .filter { it.inReplyToId == mainStatusId } - .map { ThreadDescendant.PostCard(it.mapToPostCardUiState()) }, - ) - } - - ThreadType.TREE -> { - val descendants = threadData.repliesTree?.toDepthList( - shouldIgnoreChildren = { - postsIdsWithHiddenReplies.contains(it.statusId) - }, - shouldAddAllChildrenBeyondLimit = { - postIdsIgnoringMaxReplyCount.contains(it.statusId) - }, - childLimit = MAX_REPLY_COUNT, - )?.drop(1)?.filter { - it.depth <= MAX_DEPTH - }?.map { depthItem -> - if (depthItem.hiddenRepliesCount == -1) { - val status = depthItem.value - val isAtMaxDepth = depthItem.depth == MAX_DEPTH - val hasReplies = depthItem.hasReplies - ThreadDescendant.PostCard( - status.toPostCardUiState( - currentUserAccountId = loggedInAccountId, - depthLinesUiState = DepthLinesUiState( - postDepth = depthItem.depth, - depthLines = depthItem.depthLines, - showViewMoreRepliesText = isAtMaxDepth && hasReplies, - expandRepliesButtonUiState = when { - !hasReplies || isAtMaxDepth -> ExpandRepliesButtonUiState.HIDDEN - postsIdsWithHiddenReplies.contains( - status.statusId - ) -> ExpandRepliesButtonUiState.PLUS - else -> ExpandRepliesButtonUiState.MINUS - }, - ), - showTopRowMetaData = false, - ) - ) - } else { - ThreadDescendant.ViewMore( - depthLinesUiState = DepthLinesUiState( - postDepth = depthItem.depth, - depthLines = depthItem.depthLines, - showViewMoreRepliesText = false, - expandRepliesButtonUiState = ExpandRepliesButtonUiState.HIDDEN, - showViewMoreRepliesWithPlusButton = true, - ), - count = depthItem.hiddenRepliesCount, - statusId = depthItem.value.statusId, - ) - } - } ?: emptyList() - - val mapToPostCardUiState = fun Status.(): PostCardUiState = toPostCardUiState( - currentUserAccountId = loggedInAccountId, - showTopRowMetaData = false, - isClickable = statusId != mainStatusId, + val descendants = threadData.repliesTree?.toDepthList( + shouldIgnoreChildren = { + postsIdsWithHiddenReplies.contains(it.statusId) + }, + shouldAddAllChildrenBeyondLimit = { + postIdsIgnoringMaxReplyCount.contains(it.statusId) + }, + childLimit = MAX_REPLY_COUNT, + )?.drop(1)?.filter { + it.depth <= MAX_DEPTH + }?.map { depthItem -> + if (depthItem.hiddenRepliesCount == -1) { + val status = depthItem.value + val isAtMaxDepth = depthItem.depth == MAX_DEPTH + val hasReplies = depthItem.hasReplies + ThreadDescendant.PostCard( + status.toPostCardUiState( + currentUserAccountId = loggedInAccountId, + depthLinesUiState = DepthLinesUiState( + postDepth = depthItem.depth, + depthLines = depthItem.depthLines, + showViewMoreRepliesText = isAtMaxDepth && hasReplies, + expandRepliesButtonUiState = when { + !hasReplies || isAtMaxDepth -> ExpandRepliesButtonUiState.HIDDEN + postsIdsWithHiddenReplies.contains( + status.statusId + ) -> ExpandRepliesButtonUiState.PLUS + else -> ExpandRepliesButtonUiState.MINUS + }, + ), + showTopRowMetaData = false, + ) ) - ThreadPostCardCollection( - ancestors = thread.context.ancestors.map { it.mapToPostCardUiState() }, - mainPost = thread.status.mapToPostCardUiState(), - descendants = descendants, + } else { + ThreadDescendant.ViewMore( + depthLinesUiState = DepthLinesUiState( + postDepth = depthItem.depth, + depthLines = depthItem.depthLines, + showViewMoreRepliesText = false, + expandRepliesButtonUiState = ExpandRepliesButtonUiState.HIDDEN, + showViewMoreRepliesWithPlusButton = true, + ), + count = depthItem.hiddenRepliesCount, + statusId = depthItem.value.statusId, ) } - } + } ?: emptyList() + + val mapToPostCardUiState = fun Status.(): PostCardUiState = toPostCardUiState( + currentUserAccountId = loggedInAccountId, + showTopRowMetaData = false, + isClickable = statusId != mainStatusId, + ) + return ThreadPostCardCollection( + ancestors = thread.context.ancestors.map { it.mapToPostCardUiState() }, + mainPost = thread.status.mapToPostCardUiState(), + descendants = descendants, + ) } override fun onRetryClicked() { loadThread() } - override fun onThreadTypeSelected(threadType: ThreadType) { - viewModelScope.launch { - userPreferencesDatastoreManager.activeUserDatastore.first().saveThreadType( - when (threadType) { - ThreadType.LIST -> UserPreferences.ThreadType.LIST - ThreadType.DIRECT_REPLIES -> UserPreferences.ThreadType.DIRECT_REPLIES_LIST - ThreadType.TREE -> UserPreferences.ThreadType.TREE - } - ) - } - } - override fun onShowAllRepliesClicked(statusId: String) { postIdsIgnoringMaxReplyCount.update { postIdsIgnoringMaxReplyCount.value.toMutableList().apply { diff --git a/settings.gradle.kts b/settings.gradle.kts index 50ba32782..76df2c07b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -66,3 +66,4 @@ include(":feature:followedHashTags") include(":feature:bookmarks") include(":core:ui:chooseAccount") include(":baselineprofile") +include(":core:accounts")