diff --git a/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
index 75cb101dff..4503a65f6e 100644
--- a/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
+++ b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
@@ -125,7 +125,7 @@ public void login() throws InterruptedException {
activityScenario.onActivity(activity -> {
assertEquals(loginName,
- Objects.requireNonNull(activity.currentUserProviderOld.getCurrentUser().blockingGet()).getUserId());
+ Objects.requireNonNull(activity.defaultAccountProvider.getDefaultUserBlocking()).getUserId());
});
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ba8c050440..3923ae833d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -233,10 +233,6 @@
android:name=".remotefilebrowser.activities.RemoteFileBrowserActivity"
android:theme="@style/AppTheme" />
-
-
diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
index f0ab828a56..b2cfa342f9 100644
--- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
@@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -78,9 +79,6 @@ class AccountVerificationActivity : BaseActivity() {
@Inject
lateinit var ncApiCoroutines: NcApiCoroutines
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var cookieManager: CookieManager
@@ -440,6 +438,7 @@ class AccountVerificationActivity : BaseActivity() {
ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
}
val intent = Intent(context, ConversationsListActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, userToSetAsActive.id)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
} else {
diff --git a/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt
index 61ee119f85..e5c443eade 100644
--- a/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt
@@ -14,8 +14,10 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
+import androidx.activity.viewModels
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import autodagger.AutoInjector
@@ -39,7 +41,9 @@ class BrowserLoginActivity : BaseActivity() {
private lateinit var binding: ActivityWebViewLoginBinding
@Inject
- lateinit var viewModel: BrowserLoginActivityViewModel
+ lateinit var viewModelFactory: ViewModelProvider.Factory
+
+ private val viewModel: BrowserLoginActivityViewModel by viewModels { viewModelFactory }
private var reauthorizeAccount = false
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
@@ -111,6 +115,13 @@ class BrowserLoginActivity : BaseActivity() {
BrowserLoginActivityViewModel.PostLoginViewState.PostLoginRestartApp -> {
restartApp()
}
+ BrowserLoginActivityViewModel.PostLoginViewState.PostLoginDifferentAccount -> {
+ Snackbar.make(
+ binding.root,
+ R.string.nc_reauthorize_different_account,
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
}
}
}
@@ -124,17 +135,19 @@ class BrowserLoginActivity : BaseActivity() {
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
}
+ // The account to reauthorize, so a login with another account doesn't change it.
+ val accountToReauthorize = extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID, 0L).takeIf { it != 0L }
if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
val uri = extras.getString(BundleKeys.KEY_FROM_QR)!!
if (uri.startsWith(LoginRepository.ONE_TIME_PREFIX)) {
- viewModel.loginWithOTPQR(uri, reauthorizeAccount)
+ viewModel.loginWithOTPQR(uri, reauthorizeAccount, accountToReauthorize)
} else {
- viewModel.loginWithQR(uri, reauthorizeAccount)
+ viewModel.loginWithQR(uri, reauthorizeAccount, accountToReauthorize)
}
} else if (baseUrl != null) {
- viewModel.startWebBrowserLogin(baseUrl, reauthorizeAccount)
+ viewModel.startWebBrowserLogin(baseUrl, reauthorizeAccount, accountToReauthorize)
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
index 6244e8482c..e07e823c15 100644
--- a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
@@ -37,7 +37,6 @@ import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.ActivityServerSelectionBinding
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.StatusDto
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.AccountUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
@@ -63,9 +62,6 @@ class ServerSelectionActivity : BaseActivity() {
@Inject
lateinit var ncApi: NcApi
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var networkMonitor: NetworkMonitor
diff --git a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt
index 3198b3b179..b7242e848e 100644
--- a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt
@@ -24,8 +24,8 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding
import com.nextcloud.talk.models.ImportAccount
import com.nextcloud.talk.models.json.participants.ParticipantDto
+import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.conversationlist.DirectShareHelper
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice
import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
@@ -44,9 +44,6 @@ import javax.inject.Inject
class SwitchAccountActivity : BaseActivity() {
private lateinit var binding: ActivitySwitchAccountBinding
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var cookieManager: CookieManager
@@ -86,6 +83,18 @@ class SwitchAccountActivity : BaseActivity() {
supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account)
}
+ private fun switchToAccount(user: User) {
+ lifecycleScope.launch {
+ if (userManager.setUserAsActive(user)) {
+ DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity)
+ cookieManager.cookieStore.removeAll()
+ val intent = ConversationsListActivity.createAccountSwitchIntent(this@SwitchAccountActivity, user.id!!)
+ startActivity(intent)
+ finish()
+ }
+ }
+ }
+
@Suppress("Detekt.NestedBlockDepth")
override fun onResume() {
super.onResume()
@@ -95,13 +104,7 @@ class SwitchAccountActivity : BaseActivity() {
if (isAccountImport) {
reauthorizeFromImport(item.account)
} else {
- lifecycleScope.launch {
- if (userManager.setUserAsActive(item.user!!)) {
- DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity)
- cookieManager.cookieStore.removeAll()
- finish()
- }
- }
+ switchToAccount(item.user!!)
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt
index 9699778cb1..c9a3bf05d6 100644
--- a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt
@@ -39,7 +39,22 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
private const val MAX_ARGS = 3
}
+ /**
+ * Result of [parseAndLogin].
+ */
+ sealed interface LoginResult {
+ /** A new account, to be verified and stored with the data in [bundle]. */
+ data class NewAccount(val bundle: Bundle) : LoginResult
+
+ /** An existing account, reauthorized or not, or an account that is being removed. */
+ data object ExistingAccount : LoginResult
+
+ /** A reauthorization in which a different account than the one to reauthorize logged in. */
+ data object DifferentAccount : LoginResult
+ }
+
private var shouldReauthorizeUser = false
+ private var accountToReauthorize: Long? = null
private var shouldLoop = true
suspend fun pollLogin(response: LoginResponse): LoginCompletion? =
@@ -63,8 +78,13 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
* Entry point for QR scanner
*
*/
- fun startLoginFlowFromQR(dataString: String, reAuth: Boolean = false): LoginCompletion? {
+ fun startLoginFlowFromQR(
+ dataString: String,
+ reAuth: Boolean = false,
+ accountToReauthorize: Long? = null
+ ): LoginCompletion? {
shouldReauthorizeUser = reAuth
+ this.accountToReauthorize = accountToReauthorize
if (!dataString.startsWith(PREFIX)) {
Log.e(TAG, "Invalid login URL detected")
@@ -108,9 +128,14 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
/**
* Entry point for a one-time QR code
*/
- suspend fun startOTPLoginFlow(dataString: String, reAuth: Boolean = false): LoginCompletion? =
+ suspend fun startOTPLoginFlow(
+ dataString: String,
+ reAuth: Boolean = false,
+ accountToReauthorize: Long? = null
+ ): LoginCompletion? =
withContext(Dispatchers.IO) {
shouldReauthorizeUser = reAuth
+ this@LoginRepository.accountToReauthorize = accountToReauthorize
if (!dataString.startsWith(ONE_TIME_PREFIX)) {
Log.e(TAG, "Invalid login URL detected")
@@ -158,9 +183,14 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
/**
* Entry point to the login process
*/
- suspend fun startLoginFlow(baseUrl: String, reAuth: Boolean = false): LoginResponse? =
+ suspend fun startLoginFlow(
+ baseUrl: String,
+ reAuth: Boolean = false,
+ accountToReauthorize: Long? = null
+ ): LoginResponse? =
withContext(Dispatchers.IO) {
shouldReauthorizeUser = reAuth
+ this@LoginRepository.accountToReauthorize = accountToReauthorize
val response = network.anonymouslyPostLoginRequest(baseUrl)
return@withContext response
}
@@ -173,23 +203,32 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
}
/**
- * Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise
+ * Returns [LoginResult.NewAccount] if the account is not scheduled for deletion and doesn't exist yet. During a
+ * reauthorization, [LoginResult.DifferentAccount] if another account than the one to reauthorize logged in, set
+ * up or not, which is then left unchanged.
*/
- suspend fun parseAndLogin(loginData: LoginCompletion): Bundle? {
+ suspend fun parseAndLogin(loginData: LoginCompletion): LoginResult {
if (local.checkIfUserIsScheduledForDeletion(loginData)) {
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
local.startAccountRemovalWorker()
- return null
+ return LoginResult.ExistingAccount
} else if (local.checkIfUserExists(loginData)) {
if (shouldReauthorizeUser) {
- local.updateUser(loginData)
+ if (!local.updateUser(loginData, accountToReauthorize)) {
+ Log.w(TAG, "Logged in with a different account than the one to reauthorize. Skipped update.")
+ return LoginResult.DifferentAccount
+ }
} else {
Log.w(TAG, "Tried to add an account that account already exists. Skipped user creation.")
}
- return null
+ return LoginResult.ExistingAccount
+ } else if (shouldReauthorizeUser) {
+ // A reauthorization must not add the account that logged in instead.
+ Log.w(TAG, "Logged in with an account that is not set up during a reauthorization. Skipped user creation.")
+ return LoginResult.DifferentAccount
} else {
- return startAccountVerification(loginData)
+ return LoginResult.NewAccount(startAccountVerification(loginData))
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
index bef3bf3f39..395537520f 100644
--- a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt
@@ -17,19 +17,26 @@ import com.nextcloud.talk.account.data.model.LoginCompletion
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.preferences.AppPreferences
-import kotlinx.coroutines.runBlocking
// local datasource for communicating with room through account manager
// crucial for making sure the login process interacts with the db as expected.
class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) {
- fun updateUser(loginData: LoginCompletion) {
- val currentUser = runBlocking { userManager.getCurrentUser() }
- if (currentUser != null) {
- currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
- currentUser.token = loginData.appPassword
- runBlocking { userManager.updateOrCreateUser(currentUser) }
- }
+ /**
+ * Stores the new credentials of a reauthorized account. That is the account that logged in, found by its login
+ * name and server, as the browser login flow grants an app password for whoever is logged in there. Returns false
+ * without any change if it is not the account with the internal id [accountToReauthorize], or if nothing was
+ * stored, e.g. because the account was removed meanwhile.
+ */
+ suspend fun updateUser(loginData: LoginCompletion, accountToReauthorize: Long?): Boolean {
+ val user = userManager.getUserWithUsernameAndServer(loginData.loginName, loginData.server)
+ ?.takeIf { accountToReauthorize == null || it.id == accountToReauthorize }
+ ?: return false
+ return userManager.updateCredentials(
+ user.id!!,
+ loginData.appPassword,
+ appPreferences.temporaryClientCertAlias
+ ) > 0
}
fun startAccountRemovalWorker(): LiveData {
diff --git a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt
index de3ae70b7c..481719fe07 100644
--- a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt
@@ -38,6 +38,7 @@ class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRep
data object PostLoginRestartApp : PostLoginViewState()
data object PostLoginError : PostLoginViewState()
data class PostLoginContinue(val data: Bundle) : PostLoginViewState()
+ data object PostLoginDifferentAccount : PostLoginViewState()
}
private val _postLoginState = MutableStateFlow(PostLoginViewState.None)
@@ -52,9 +53,21 @@ class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRep
private var savedResponse: LoginResponse? = null
- fun startWebBrowserLogin(baseUrl: String, reAuth: Boolean = false) {
+ // The view model survives a recreation of the activity, which starts the login again. A started login is
+ // continued instead of starting a second one, whose session the browser would not authorize. After process
+ // death the view model is new, so the login is started again.
+ private var isLoginStarted = false
+
+ private fun startLoginOnce(): Boolean {
+ if (isLoginStarted) return false
+ isLoginStarted = true
+ return true
+ }
+
+ fun startWebBrowserLogin(baseUrl: String, reAuth: Boolean = false, accountToReauthorize: Long? = null) {
+ if (!startLoginOnce()) return
viewModelScope.launch {
- val response = repository.startLoginFlow(baseUrl, reAuth)
+ val response = repository.startLoginFlow(baseUrl, reAuth, accountToReauthorize)
savedResponse = response
if (response == null) {
@@ -77,52 +90,43 @@ class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRep
return@launch
}
- val bundle = repository.parseAndLogin(loginCompletionResponse)
- if (bundle == null) {
- _postLoginState.value = PostLoginViewState.PostLoginRestartApp
- return@launch
- }
-
- _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
+ _postLoginState.value = postLoginStateFor(repository.parseAndLogin(loginCompletionResponse))
}
}
}
- fun loginWithQR(dataString: String, reAuth: Boolean = false) {
+ fun loginWithQR(dataString: String, reAuth: Boolean = false, accountToReauthorize: Long? = null) {
+ if (!startLoginOnce()) return
viewModelScope.launch {
- val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth)
+ val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth, accountToReauthorize)
if (loginCompletionResponse == null) {
_postLoginState.value = PostLoginViewState.PostLoginError
return@launch
}
- val bundle = repository.parseAndLogin(loginCompletionResponse)
- if (bundle == null) {
- _postLoginState.value = PostLoginViewState.PostLoginRestartApp
- return@launch
- }
-
- _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
+ _postLoginState.value = postLoginStateFor(repository.parseAndLogin(loginCompletionResponse))
}
}
- fun loginWithOTPQR(dataString: String, reAuth: Boolean = false) {
+ fun loginWithOTPQR(dataString: String, reAuth: Boolean = false, accountToReauthorize: Long? = null) {
+ if (!startLoginOnce()) return
viewModelScope.launch {
- val loginCompletionResponse = repository.startOTPLoginFlow(dataString, reAuth)
+ val loginCompletionResponse = repository.startOTPLoginFlow(dataString, reAuth, accountToReauthorize)
if (loginCompletionResponse == null) {
_postLoginState.value = PostLoginViewState.PostLoginError
return@launch
}
- val bundle = repository.parseAndLogin(loginCompletionResponse)
- if (bundle == null) {
- _postLoginState.value = PostLoginViewState.PostLoginRestartApp
- return@launch
- }
-
- _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
+ _postLoginState.value = postLoginStateFor(repository.parseAndLogin(loginCompletionResponse))
}
}
+ private fun postLoginStateFor(result: LoginRepository.LoginResult): PostLoginViewState =
+ when (result) {
+ is LoginRepository.LoginResult.NewAccount -> PostLoginViewState.PostLoginContinue(result.bundle)
+ LoginRepository.LoginResult.ExistingAccount -> PostLoginViewState.PostLoginRestartApp
+ LoginRepository.LoginResult.DifferentAccount -> PostLoginViewState.PostLoginDifferentAccount
+ }
+
fun cancelLogin() = repository.cancelLoginFlow()
}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
index 366c7989b2..593d08b41a 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
@@ -31,6 +31,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.account.AccountVerificationActivity
import com.nextcloud.talk.account.BrowserLoginActivity
@@ -38,22 +39,25 @@ import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.SwitchAccountActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.events.RemoteWipeEvent
import com.nextcloud.talk.lock.LockedActivity
+import com.nextcloud.talk.users.DefaultAccountProvider
+import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.ViewThemeUtilsFactory
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.utils.ssl.TrustManager
+import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -80,22 +84,98 @@ open class BaseActivity : AppCompatActivity() {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
+ @Inject
+ lateinit var viewThemeUtilsFactory: ViewThemeUtilsFactory
+
@Inject
lateinit var messageUtils: MessageUtils
@Inject
lateinit var context: Context
- @Deprecated("Use CurrentUserProvider instead")
@Inject
- lateinit var currentUserProviderOld: CurrentUserProviderOld
+ lateinit var defaultAccountProvider: DefaultAccountProvider
@Inject
- lateinit var currentUserProvider: CurrentUserProvider
+ lateinit var userManager: UserManager
@Inject
lateinit var logger: Logger
+ /**
+ * Whether this activity may be started without [BundleKeys.KEY_INTERNAL_USER_ID] and then uses the default
+ * account. Only for entry points without an account context, like the conversation list on app start. For
+ * all other activities a missing id is a bug.
+ */
+ protected open val allowsDefaultAccount: Boolean = false
+
+ /**
+ * Internal id of the account this activity was started for, taken from [BundleKeys.KEY_INTERNAL_USER_ID].
+ * Without it, the default account is used and its id is written to the intent, so an activity recreated after a
+ * configuration change stays on the same account. After process death the system restores the original intent,
+ * so the activity uses the default account at that time. That is only intended for activities with
+ * [allowsDefaultAccount]: for all others it fails in debug builds and is logged as an error in release builds.
+ * Returns 0 if there is no account at all.
+ */
+ protected fun resolveUserIdFromIntent(): Long {
+ val userId = intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, 0L)
+ if (userId != 0L) {
+ return userId
+ }
+ if (!allowsDefaultAccount) {
+ val message = "${javaClass.simpleName} was started without ${BundleKeys.KEY_INTERNAL_USER_ID}"
+ check(!BuildConfig.DEBUG) { message }
+ Log.e(TAG, "$message, using the default account")
+ }
+ return (defaultAccountProvider.getDefaultUserBlocking()?.id ?: 0L).also {
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it)
+ }
+ }
+
+ /**
+ * The account this activity was started for (see [resolveUserIdFromIntent]), or null if it doesn't exist. For
+ * activities with [allowsDefaultAccount], a requested account that doesn't exist (anymore) falls back to the
+ * default account. Loaded once on first access, which must be after injection.
+ */
+ private val boundUserLazy = lazy {
+ runBlocking { userManager.getUserWithId(resolveUserIdFromIntent()) }
+ ?: if (allowsDefaultAccount) defaultAccountProvider.getDefaultUserBlocking() else null
+ }
+ private val boundUser: User? by boundUserLazy
+
+ /**
+ * Returns [boundUser], or finishes the activity and returns null if the account doesn't exist.
+ */
+ private fun requireBoundUserOrFinish(): User? =
+ boundUser ?: run {
+ Log.e(TAG, "No user found for id ${resolveUserIdFromIntent()}")
+ if (allowsDefaultAccount) {
+ startActivity(Intent(this, MainActivity::class.java))
+ }
+ finish()
+ null
+ }
+
+ private var userViewThemeUtils: ViewThemeUtils? = null
+
+ /**
+ * Themes this activity, and the fragments and dialogs it hosts, with the server colors of [boundUser]. The theme
+ * is created once and assigned again on every call, because the injection of a subclass also injects
+ * [viewThemeUtils] and overwrites it with the theme of the default account. Reusing the instance keeps fragments
+ * that already took it over on the same one.
+ */
+ private fun applyUserTheme() {
+ val themeUtils = userViewThemeUtils
+ ?: boundUser?.let { viewThemeUtilsFactory.forUser(it) }?.also { userViewThemeUtils = it }
+ themeUtils?.let { viewThemeUtils = it }
+ }
+
+ /**
+ * Resolves [boundUser] and applies its theme, or finishes the activity and returns null if the account doesn't
+ * exist. Must be called right after injection, before anything else is set up.
+ */
+ protected fun setUpBoundUserOrFinish(): User? = requireBoundUserOrFinish()?.also { applyUserTheme() }
+
open val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.TOOLBAR
@@ -130,6 +210,11 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
logger.i(this::class.java.simpleName, "onCreate")
+ // Fragments restored in super.onCreate() take over viewThemeUtils there, so apply the theme of the account
+ // before. Without an account in the intent, the injected theme of the default account is the right one.
+ if (intent.hasExtra(BundleKeys.KEY_INTERNAL_USER_ID)) {
+ applyUserTheme()
+ }
adjustUIForAPILevel35()
super.onCreate(savedInstanceState)
@@ -326,7 +411,10 @@ open class BaseActivity : AppCompatActivity() {
}
override fun startActivity(intent: Intent) {
- val user = currentUserProviderOld.currentUser.blockingGet()
+ // Links to the own server are opened for the account of this screen. Screens without an account (boundUser
+ // never loaded) use the default account, without resolving one from their intent.
+ val user = (if (boundUserLazy.isInitialized()) boundUser else null)
+ ?: defaultAccountProvider.getDefaultUserBlocking()
if (intent.data != null && TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
val uri = intent.data.toString()
if (user?.baseUrl != null && uri.startsWith(user.baseUrl!!)) {
@@ -344,10 +432,11 @@ open class BaseActivity : AppCompatActivity() {
fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileIdNew(uri))
} else if (UriUtils.isInstanceInternalTalkUrl(user.baseUrl!!, uri)) {
// https://cloud.nextcloud.com/call/123456789
- val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, UriUtils.extractRoomTokenFromTalkUrl(uri))
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = ChatActivity.createIntent(
+ context,
+ user.id!!,
+ UriUtils.extractRoomTokenFromTalkUrl(uri)
+ )
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
} else {
diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
index cc3b96651c..490eb4a5db 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
@@ -59,6 +59,7 @@ import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -117,7 +118,6 @@ import com.nextcloud.talk.signaling.SignalingMessageReceiver.OfferMessageListene
import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.ui.dialog.AudioOutputDialog
import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
@@ -133,6 +133,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MODIFIED_BASE_URL
@@ -211,10 +212,6 @@ class CallActivity : CallBaseActivity() {
@Inject
var ncApi: NcApi? = null
- @JvmField
- @Inject
- var userManager: UserManager? = null
-
@JvmField
@Inject
var cache: Cache? = null
@@ -327,6 +324,9 @@ class CallActivity : CallBaseActivity() {
private var hasExternalSignalingServer = false
private var conversationPassword: String? = null
private var powerManagerUtils: PowerManagerUtils? = null
+
+ // Whether onCreate() got past its early exits, so onDestroy() has a call to clean up.
+ private var isCallSetUp = false
private var handler: Handler? = null
private val callingTimeoutRunnable = Runnable { setCallState(CallStatus.CALLING_TIMEOUT) }
@@ -446,18 +446,7 @@ class CallActivity : CallBaseActivity() {
Log.d(TAG, "onCreate")
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
-
- // Register broadcast receiver for ending call from notification
- val endCallFilter = IntentFilter(END_CALL_FROM_NOTIFICATION)
-
- // internal receiver for notification actions, so not exported
- registerPermissionHandlerBroadcastReceiver(
- endCallFromNotificationReceiver,
- endCallFilter,
- permissionUtil!!.privateBroadcastPermission,
- null,
- ReceiverFlag.NotExported
- )
+ conversationUser = setUpBoundUserOrFinish() ?: return
callViewModel = ViewModelProvider(this, viewModelFactory)[CallViewModel::class.java]
@@ -535,7 +524,6 @@ class CallActivity : CallBaseActivity() {
return
}
processExtras(intent.extras!!)
- conversationUser = currentUserProviderOld.currentUser.blockingGet()
if (warnAndFinishIfCallEndToEndEncryptionUnsupported()) {
return
@@ -547,6 +535,25 @@ class CallActivity : CallBaseActivity() {
}
powerManagerUtils = PowerManagerUtils()
+ // Register broadcast receiver for ending call from notification
+ val endCallFilter = IntentFilter(END_CALL_FROM_NOTIFICATION)
+
+ // internal receiver for notification actions, so not exported
+ registerPermissionHandlerBroadcastReceiver(
+ endCallFromNotificationReceiver,
+ endCallFilter,
+ permissionUtil!!.privateBroadcastPermission,
+ null,
+ ReceiverFlag.NotExported
+ )
+
+ isCallSetUp = true
+
+ if (!conversationUser.current) {
+ // Taking part in a call uses its account, so it becomes the last used one.
+ lifecycleScope.launch { userManager.setUserAsActive(conversationUser) }
+ }
+
setCallState(CallStatus.CONNECTING)
initRaiseHandViewModel()
@@ -576,7 +583,7 @@ class CallActivity : CallBaseActivity() {
callRecordingViewModel = ViewModelProvider(this, viewModelFactory).get(
CallRecordingViewModel::class.java
)
- callRecordingViewModel!!.setData(roomToken!!)
+ callRecordingViewModel!!.setData(conversationUser, roomToken!!)
callRecordingViewModel!!.setRecordingState(recordingState)
callRecordingViewModel!!.viewState.observe(this) { viewState: CallRecordingViewModel.ViewState? ->
if (viewState is RecordingStartedState) {
@@ -634,7 +641,7 @@ class CallActivity : CallBaseActivity() {
private fun initRaiseHandViewModel() {
raiseHandViewModel = ViewModelProvider(this, viewModelFactory).get(RaiseHandViewModel::class.java)
- raiseHandViewModel!!.setData(roomToken!!, isBreakoutRoom)
+ raiseHandViewModel!!.setData(conversationUser, roomToken!!, isBreakoutRoom)
raiseHandViewModel!!.viewState.observe(this) { viewState: RaiseHandViewModel.ViewState? ->
var raised = false
if (viewState is RaisedHandState) {
@@ -764,16 +771,18 @@ class CallActivity : CallBaseActivity() {
return
}
val newRoomToken = intent.getStringExtra(KEY_ROOM_TOKEN)
- Log.d(TAG, "onNewIntent: newRoomToken=$newRoomToken roomToken=$roomToken")
+ // Accounts on the same server share the tokens of their common conversations, so compare the account too.
+ val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, conversationUser.id!!)
+ Log.d(TAG, "onNewIntent: newRoomToken=$newRoomToken roomToken=$roomToken newUserId=$newUserId")
when {
// notification tap without extras: just bring the current call back to the front
newRoomToken.isNullOrEmpty() -> Unit
// re-entry for the call this instance is already handling (singleTask reuse)
- newRoomToken == roomToken -> setIntent(intent)
+ newRoomToken == roomToken && newUserId == conversationUser.id -> setIntent(intent)
- // a call for another room was requested while this instance lingered in the background:
+ // a call for another room or account was requested while this instance lingered in the background:
// end the current call and restart cleanly in onDestroy, so no stale state is reused
else -> {
Log.d(TAG, "onNewIntent: call requested for another room, ending current call first")
@@ -1557,36 +1566,42 @@ class CallActivity : CallBaseActivity() {
public override fun onDestroy() {
Log.d(TAG, "onDestroy: currentCallStatus=$currentCallStatus")
- // The call cannot survive the activity being destroyed (WebRTC connections, local stream and
- // signaling listeners all live here), so always clean up and hang up. Background survival is
- // achieved via moveTaskToBack/PiP, which do not destroy the activity.
- if (signalingMessageReceiver != null) {
- signalingMessageReceiver!!.removeListener(localParticipantMessageListener)
- signalingMessageReceiver!!.removeListener(offerMessageListener)
- }
- if (localStream != null) {
- localStream!!.dispose()
- localStream = null
- Log.d(TAG, "Disposed localStream")
- } else {
- Log.d(TAG, "localStream is null")
- }
- if (currentCallStatus !== CallStatus.LEAVING) {
- hangup(true, false)
- }
- CallForegroundService.stop(applicationContext)
+ if (isCallSetUp) {
+ // The call cannot survive the activity being destroyed (WebRTC connections, local stream and
+ // signaling listeners all live here), so always clean up and hang up. Background survival is
+ // achieved via moveTaskToBack/PiP, which do not destroy the activity.
+ if (signalingMessageReceiver != null) {
+ signalingMessageReceiver!!.removeListener(localParticipantMessageListener)
+ signalingMessageReceiver!!.removeListener(offerMessageListener)
+ }
+ if (localStream != null) {
+ localStream!!.dispose()
+ localStream = null
+ Log.d(TAG, "Disposed localStream")
+ } else {
+ Log.d(TAG, "localStream is null")
+ }
+ if (currentCallStatus !== CallStatus.LEAVING) {
+ hangup(true, false)
+ }
+ CallForegroundService.stop(applicationContext)
- powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.IDLE)
+ powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.IDLE)
- try {
- unregisterReceiver(endCallFromNotificationReceiver)
- } catch (e: IllegalArgumentException) {
- Log.w(TAG, "Failed to unregister endCallFromNotificationReceiver", e)
- }
+ try {
+ unregisterReceiver(endCallFromNotificationReceiver)
+ } catch (e: IllegalArgumentException) {
+ Log.w(TAG, "Failed to unregister endCallFromNotificationReceiver", e)
+ }
- pendingCallIntent?.let {
- Log.d(TAG, "onDestroy: starting CallActivity for pending call intent")
- startActivity(it)
+ pendingCallIntent?.let {
+ Log.d(TAG, "onDestroy: starting CallActivity for pending call intent")
+ startActivity(it)
+ }
+ } else {
+ // The screen that started the call marked it as dialing, which hangup() would reset. Without resetting
+ // it here, chats would keep treating the app as being in a call.
+ ApplicationWideCurrentRoomHolder.getInstance().isDialing = false
}
super.onDestroy()
@@ -1634,7 +1649,7 @@ class CallActivity : CallBaseActivity() {
val userId = conversationUser!!.id!!
val server = externalSignalingServer!!
CoroutineScope(Dispatchers.IO).launch {
- userManager!!.updateExternalSignalingServer(userId, server)
+ userManager.updateExternalSignalingServer(userId, server)
}
} else {
conversationUser!!.externalSignalingServer = externalSignalingServer
@@ -2324,14 +2339,12 @@ class CallActivity : CallBaseActivity() {
openConversationListInPrimaryTask()
finishAndRemoveTask()
} else if (switchToRoomToken.isNotEmpty()) {
- val intent = Intent(context, ChatActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val bundle = Bundle()
bundle.putBoolean(KEY_SWITCH_TO_ROOM, true)
bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
- bundle.putString(KEY_ROOM_TOKEN, switchToRoomToken)
bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
- intent.putExtras(bundle)
+ val intent = ChatActivity.createIntent(context, conversationUser.id!!, switchToRoomToken, bundle)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
finish()
} else if (shutDownView) {
@@ -2367,6 +2380,7 @@ class CallActivity : CallBaseActivity() {
if (primaryTask != null) {
val intent = Intent(context, ConversationsListActivity::class.java).apply {
+ putExtra(KEY_INTERNAL_USER_ID, conversationUser.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
primaryTask.startActivity(context, intent, null)
diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
index 16606934f9..a6a1b0edc9 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
@@ -33,7 +33,6 @@ import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityMainBinding
import com.nextcloud.talk.invitation.InvitationsActivity
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.DeepLinkHandler
@@ -41,7 +40,6 @@ import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.ShortcutManagerHelper
import com.nextcloud.talk.utils.UnifiedPushUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
@@ -59,9 +57,6 @@ class MainActivity :
@Inject
lateinit var ncApi: NcApi
- @Inject
- lateinit var userManager: UserManager
-
// MainActivity only routes to ConversationsListActivity or ChatActivity and is never
// visible to the user. The lock check must run in the actual destination activity.
override val skipLockCheckOnResume: Boolean = true
@@ -167,7 +162,7 @@ class MainActivity :
val baseUrl = userId.substringAfterLast("@")
val isValidBaseUrl = baseUrl.isNotBlank() && "https://$baseUrl".toHttpUrlOrNull() != null
- val currentUser = currentUserProviderOld.currentUser.blockingGet()
+ val currentUser = defaultAccountProvider.getDefaultUserBlocking()
if (isValidBaseUrl && currentUser?.baseUrl?.endsWith(baseUrl) == true) {
startConversation(user)
} else {
@@ -188,13 +183,13 @@ class MainActivity :
private fun startConversation(userId: String) {
val roomType = "1"
- val currentUser = currentUserProviderOld.currentUser.blockingGet()
+ val currentUser = defaultAccountProvider.getDefaultUserBlocking() ?: return
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1))
- val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
+ val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
version = apiVersion,
- baseUrl = currentUser?.baseUrl!!,
+ baseUrl = currentUser.baseUrl!!,
roomType = roomType,
invite = userId
)
@@ -209,11 +204,11 @@ class MainActivity :
.subscribe(
{ roomOverall ->
if (isFinishing || isDestroyed) return@subscribe
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
-
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = ChatActivity.createIntent(
+ context,
+ currentUser.id!!,
+ roomOverall.ocs!!.data!!.token
+ )
startActivity(chatIntent)
},
{ e ->
@@ -251,6 +246,7 @@ class MainActivity :
if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) {
if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) {
val invitationsIntent = Intent(this@MainActivity, InvitationsActivity::class.java)
+ invitationsIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, user.id)
startActivity(invitationsIntent)
}
} else {
@@ -260,7 +256,7 @@ class MainActivity :
}
} else {
try {
- val users = userManager.getUsers()
+ val users = userManager.getUsersNotScheduledForDeletion()
if (isFinishing || isDestroyed) return@launch
if (users.isNotEmpty()) {
@@ -339,9 +335,7 @@ class MainActivity :
listIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
listIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtra(KEY_ROOM_TOKEN, deepLinkResult.roomToken)
- chatIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id)
+ val chatIntent = ChatActivity.createIntent(context, targetUser.id!!, deepLinkResult.roomToken)
startActivities(arrayOf(listIntent, chatIntent))
} else {
@@ -377,7 +371,7 @@ class MainActivity :
private fun resolveTargetUser(users: List, deepLinkResult: DeepLinkHandler.DeepLinkResult): User? {
val deepLinkHost = deepLinkResult.serverUrl.toUri().host?.lowercase()
if (deepLinkHost.isNullOrBlank()) {
- return currentUserProviderOld.currentUser.blockingGet()
+ return defaultAccountProvider.getDefaultUserBlocking()
}
// Priority: exact match (username + server) > server match > current user fallback
@@ -396,7 +390,7 @@ class MainActivity :
userHost == deepLinkHost
}
- val currentUser = currentUserProviderOld.currentUser.blockingGet()
+ val currentUser = defaultAccountProvider.getDefaultUserBlocking()
val currentUserMatch = currentUser?.takeIf {
it.baseUrl?.let { url -> url.toUri().host?.lowercase() } == deepLinkHost
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
deleted file mode 100644
index f47aec87f8..0000000000
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.adapters.items
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import com.nextcloud.talk.R
-import com.nextcloud.talk.databinding.RvItemLoadMoreBinding
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFilterable
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-
-object LoadMoreResultsItem :
- AbstractFlexibleItem(),
- IFilterable {
-
- // layout is used as view type for uniqueness
- const val VIEW_TYPE = FlexibleItemViewType.LOAD_MORE_RESULTS_ITEM
-
- class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
- var binding: RvItemLoadMoreBinding = RvItemLoadMoreBinding.bind(view)
- }
-
- override fun getLayoutRes(): Int = R.layout.rv_item_load_more
-
- override fun createViewHolder(
- view: View,
- adapter: FlexibleAdapter>
- ): ViewHolder = ViewHolder(view, adapter)
-
- override fun bindViewHolder(
- adapter: FlexibleAdapter>,
- holder: ViewHolder,
- position: Int,
- payloads: MutableList?
- ) {
- // nothing, it's immutable
- }
-
- override fun filter(constraint: String?): Boolean = true
-
- override fun getItemViewType(): Int = VIEW_TYPE
-
- override fun equals(other: Any?): Boolean = other is LoadMoreResultsItem
-
- override fun hashCode(): Int = 0
-}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
deleted file mode 100644
index cbd9112421..0000000000
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Tim Krüger
- * SPDX-FileCopyrightText: 2021 Andy Scherzinger
- * SPDX-FileCopyrightText: 2017-2019 Mario Danic
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.adapters.items
-
-import android.content.Context
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import com.nextcloud.talk.R
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
-import com.nextcloud.talk.extensions.loadThumbnail
-import com.nextcloud.talk.models.domain.SearchMessageEntry
-import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFilterable
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.davidea.viewholders.FlexibleViewHolder
-
-data class MessageResultItem(
- private val context: Context,
- private val currentUser: User,
- val messageEntry: SearchMessageEntry,
- var showHeader: Boolean = false,
- private val viewThemeUtils: ViewThemeUtils
-) : AbstractFlexibleItem(),
- IFilterable,
- ISectionable {
-
- class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
- var binding: RvItemSearchMessageBinding
-
- init {
- binding = RvItemSearchMessageBinding.bind(view)
- }
- }
-
- override fun getLayoutRes(): Int = R.layout.rv_item_search_message
-
- override fun createViewHolder(
- view: View,
- adapter: FlexibleAdapter>
- ): ViewHolder = ViewHolder(view, adapter)
-
- override fun bindViewHolder(
- adapter: FlexibleAdapter>,
- holder: ViewHolder,
- position: Int,
- payloads: MutableList?
- ) {
- holder.binding.conversationTitle.text = messageEntry.title
- bindMessageExcerpt(holder)
- messageEntry.thumbnailURL?.let { holder.binding.thumbnail.loadThumbnail(it, currentUser) }
- }
-
- private fun bindMessageExcerpt(holder: ViewHolder) {
- viewThemeUtils.platform.highlightText(
- holder.binding.messageExcerpt,
- messageEntry.messageExcerpt,
- messageEntry.searchTerm
- )
- }
-
- override fun filter(constraint: String?): Boolean = true
-
- override fun getItemViewType(): Int = VIEW_TYPE
-
- companion object {
- const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM
- }
-
- override fun getHeader(): GenericTextHeaderItem =
- MessagesTextHeaderItem(context, viewThemeUtils)
- .apply {
- isHidden = showHeader // FlexibleAdapter needs this hack for some reason
- }
-
- override fun setHeader(header: GenericTextHeaderItem?) {
- // nothing, header is always the same
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
index b8bc0993fe..cec49766a6 100644
--- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
+++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
@@ -39,6 +39,7 @@ import com.nextcloud.talk.diagnosis.toMarkdown
import com.nextcloud.talk.errorhandling.ExceptionHandler
import com.nextcloud.talk.logger.LoggerImpl
import com.nextcloud.talk.account.BrowserLoginActivity
+import com.nextcloud.talk.dagger.modules.ApplicationScope
import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DaosModule
@@ -65,6 +66,8 @@ import de.cotech.hw.SecurityKeyManager
import de.cotech.hw.SecurityKeyManagerConfig
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt
import org.webrtc.PeerConnectionFactory
@@ -112,6 +115,10 @@ class NextcloudTalkApplication :
@Inject
lateinit var userManager: UserManager
+ @Inject
+ @field:ApplicationScope
+ lateinit var appScope: CoroutineScope
+
@Inject
lateinit var logger: LoggerImpl
//endregion
@@ -240,6 +247,8 @@ class NextcloudTalkApplication :
}
private fun initWorkers() {
+ appScope.launch { userManager.repairMultipleActiveUsers() }
+
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesSyncWorker::class.java).build()
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build()
diff --git a/app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt b/app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt
index dbcfaadb53..42677896a8 100644
--- a/app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewFragment.kt
@@ -25,6 +25,7 @@ import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import javax.inject.Inject
@@ -101,6 +102,7 @@ class FileAttachmentPreviewFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel.setInitialFiles(filesList)
diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt
index 62936ab258..52f8171815 100644
--- a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt
@@ -28,7 +28,6 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.CallNotificationActivityBinding
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.models.json.participants.ParticipantDto
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
@@ -38,7 +37,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
-import kotlinx.coroutines.runBlocking
import okhttp3.Cache
import java.io.IOException
import javax.inject.Inject
@@ -54,18 +52,13 @@ class CallNotificationActivity : CallBaseActivity() {
@Inject
var cache: Cache? = null
- @Inject
- lateinit var userManager: UserManager
-
private var roomToken: String? = null
private var notificationTimestamp: Int? = null
private var displayName: String? = null
private var callFlag: Int = 0
private var isOneToOneCall: Boolean = true
private var conversationName: String? = null
- private var internalUserId: Long = -1
-
- private var userBeingCalled: User? = null
+ private lateinit var userBeingCalled: User
private var leavingScreen = false
private var handler: Handler? = null
private var binding: CallNotificationActivityBinding? = null
@@ -73,12 +66,12 @@ class CallNotificationActivity : CallBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
+ userBeingCalled = setUpBoundUserOrFinish() ?: return
binding = CallNotificationActivityBinding.inflate(layoutInflater)
setContentView(binding!!.root)
hideNavigationIfNoPipAvailable()
handleExtras()
- userBeingCalled = runBlocking { userManager.getUserWithId(internalUserId) }
setupCallTypeDescription()
setupCallAnswerButtons()
@@ -96,13 +89,12 @@ class CallNotificationActivity : CallBaseActivity() {
callFlag = extras.getInt(BundleKeys.KEY_CALL_FLAG)
isOneToOneCall = extras.getBoolean(KEY_ROOM_ONE_TO_ONE)
conversationName = extras.getString(BundleKeys.KEY_CONVERSATION_NAME, "")
- internalUserId = extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
}
private fun setupAvatar(isOneToOneCall: Boolean, conversationName: String?) {
if (isOneToOneCall) {
binding!!.avatarImageView.loadUserAvatar(
- userBeingCalled!!,
+ userBeingCalled,
conversationName!!,
true,
false
@@ -114,7 +106,7 @@ class CallNotificationActivity : CallBaseActivity() {
private fun hasCallFlagsCapability(): Boolean {
val apiVersion = ApiUtils.getConversationApiVersion(
- userBeingCalled!!,
+ userBeingCalled,
intArrayOf(
ApiUtils.API_V4,
ApiUtils.API_V3,
@@ -124,7 +116,7 @@ class CallNotificationActivity : CallBaseActivity() {
return apiVersion >= ApiUtils.API_V3 &&
hasSpreedFeatureCapability(
- userBeingCalled?.capabilities?.spreedCapability,
+ userBeingCalled.capabilities?.spreedCapability,
SpreedFeatures.CONVERSATION_CALL_FLAGS
)
}
@@ -206,7 +198,7 @@ class CallNotificationActivity : CallBaseActivity() {
}
private fun proceedToCall() {
- if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled?.capabilities?.spreedCapability)) {
+ if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled.capabilities?.spreedCapability)) {
Toast.makeText(context, R.string.nc_call_e2ee_not_supported, Toast.LENGTH_LONG).show()
hangup()
return
diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
index 164916cc03..54749edcb4 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
@@ -92,6 +92,7 @@ import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -130,7 +131,6 @@ import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
-import com.nextcloud.talk.dagger.modules.ViewModelFactoryWithParams
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
@@ -293,13 +293,18 @@ class ChatActivity :
@Inject
lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory
- val chatViewModel: ChatViewModel by viewModels {
- ViewModelFactoryWithParams(ChatViewModel::class.java) {
- chatViewModelFactory.build(
- roomToken,
- conversationThreadId
- )
- }
+ /**
+ * The user of this chat as loaded in [onCreate], used to create [chatViewModel]. Afterwards use
+ * [conversationUser], which reflects later changes of the stored user.
+ */
+ private lateinit var initialUser: User
+
+ val chatViewModel: ChatViewModel by assistedViewModels {
+ chatViewModelFactory.build(
+ initialUser,
+ roomToken,
+ conversationThreadId
+ )
}
lateinit var conversationInfoViewModel: ConversationInfoViewModel
@@ -362,6 +367,8 @@ class ChatActivity :
?: error("roomToken missing")
}
+ val conversationUserId: Long by lazy { resolveUserIdFromIntent() }
+
val conversationThreadId: Long? by lazy {
if (intent.hasExtra(KEY_THREAD_ID)) {
intent.getLongExtra(KEY_THREAD_ID, 0L)
@@ -372,7 +379,8 @@ class ChatActivity :
var openedViaNotification: Boolean = false
var conversationThreadInfo: ThreadInfoDto? = null
- lateinit var conversationUser: User
+ val conversationUser: User
+ get() = chatViewModel.currentUser
lateinit var spreedCapabilities: SpreedCapabilityDto
var chatApiVersion: Int = 1
private var roomPassword: String = ""
@@ -504,6 +512,7 @@ class ChatActivity :
onBackPressedDispatcher.onBackPressed()
} else {
val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
@@ -591,6 +600,9 @@ class ChatActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ // Not loaded in a coroutine: initData() must have run before onResume() (activity and ChatViewModel),
+ // and registerForActivityResult() must be called before the activity is started.
+ initialUser = setUpBoundUserOrFinish() ?: return
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)
@@ -635,61 +647,47 @@ class ChatActivity :
overflowContainerHeightPx.intValue = binding.chatOverflowContainer.height
}
- lifecycleScope.launch {
- currentUserProvider.getCurrentUser()
- .onSuccess { user ->
- conversationUser = user
- handleIntent(intent)
- val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
- val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
-
- chatViewModel.initData(
- user,
- credentials!!,
- urlForChatting,
- conversationThreadId
- )
+ handleIntent(intent)
+ chatViewModel.initData()
+ if (sharedText.isNotEmpty()) {
+ chatViewModel.messageDraft.messageText = sharedText
+ chatViewModel.saveMessageDraft()
+ }
- conversationThreadId?.let {
- val threadUrl = ApiUtils.getUrlForThread(
- version = 1,
- baseUrl = conversationUser!!.baseUrl,
- token = roomToken,
- threadId = it.toInt()
- )
- chatViewModel.getThread(credentials, threadUrl)
- }
+ conversationThreadId?.let {
+ val threadUrl = ApiUtils.getUrlForThread(
+ version = 1,
+ baseUrl = conversationUser.baseUrl,
+ token = roomToken,
+ threadId = it.toInt()
+ )
+ chatViewModel.getThread(conversationUser.getCredentials(), threadUrl)
+ }
- messageInputFragment = getMessageInputFragment()
- messageInputViewModel.setData(chatViewModel.getChatRepository())
+ messageInputFragment = getMessageInputFragment()
+ messageInputViewModel.setData(chatViewModel.getChatRepository())
- initObservers()
+ initObservers()
- pendingTargetMessageId?.let { messageId ->
- lifecycleScope.launch {
- chatViewModel.openMessageFromGlobalSearch(
- messageId = messageId,
- threadId = pendingTargetThreadId,
- searchQuery = pendingTargetSearchQuery
- )
- }
- pendingTargetMessageId = null
- pendingTargetThreadId = null
- pendingTargetSearchQuery = null
- }
+ pendingTargetMessageId?.let { messageId ->
+ lifecycleScope.launch {
+ chatViewModel.openMessageFromGlobalSearch(
+ messageId = messageId,
+ threadId = pendingTargetThreadId,
+ searchQuery = pendingTargetSearchQuery
+ )
+ }
+ pendingTargetMessageId = null
+ pendingTargetThreadId = null
+ pendingTargetSearchQuery = null
+ }
- pickMultipleMedia = registerForActivityResult(
- ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER)
- ) { uris ->
- if (uris.isNotEmpty()) {
- onChooseFileResult(uris)
- }
- }
- }
- .onFailure {
- logger.e(TAG, "Failed to register media picker activity result launcher", it)
- Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
- }
+ pickMultipleMedia = registerForActivityResult(
+ ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER)
+ ) { uris ->
+ if (uris.isNotEmpty()) {
+ onChooseFileResult(uris)
+ }
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@@ -982,19 +980,15 @@ class ChatActivity :
value = reactionsSheetMessageId?.let { id -> chatViewModel.getMessageById(id).first() }
}
reactionsSheetMessage?.let { msg ->
- if (::conversationUser.isInitialized) {
- conversationUser.let { user ->
- ShowReactionsModalBottomSheet(
- chatMessage = msg,
- user = user,
- roomToken = roomToken,
- hasReactPermission = participantPermissions?.hasReactPermission() == true,
- ncApiCoroutines = ncApiCoroutines,
- onDeleteReaction = { emoji -> chatViewModel.deleteReaction(roomToken, msg, emoji) },
- onDismiss = { chatViewModel.dismissReactionsSheet() }
- )
- }
- }
+ ShowReactionsModalBottomSheet(
+ chatMessage = msg,
+ user = conversationUser,
+ roomToken = roomToken,
+ hasReactPermission = participantPermissions?.hasReactPermission() == true,
+ ncApiCoroutines = ncApiCoroutines,
+ onDeleteReaction = { emoji -> chatViewModel.deleteReaction(roomToken, msg, emoji) },
+ onDismiss = { chatViewModel.dismissReactionsSheet() }
+ )
}
val profileSheetMessageId by chatViewModel.profileSheetMessageId.collectAsStateWithLifecycle()
@@ -1005,17 +999,13 @@ class ChatActivity :
?.takeIf { it.actorType.equals("users") }
?.let { msg ->
val actorId = msg.actorId ?: return@let
- if (::conversationUser.isInitialized) {
- conversationUser.let { user ->
- ProfileModalBottomSheet(
- actorId = actorId,
- user = user,
- ncApiCoroutines = ncApiCoroutines,
- onTalkTo = { actorId -> startDirectChat(actorId) },
- onDismiss = { chatViewModel.dismissProfileSheet() }
- )
- }
- }
+ ProfileModalBottomSheet(
+ actorId = actorId,
+ user = conversationUser,
+ ncApiCoroutines = ncApiCoroutines,
+ onTalkTo = { actorId -> startDirectChat(actorId) },
+ onDismiss = { chatViewModel.dismissProfileSheet() }
+ )
}
val messageActionsMessageId by chatViewModel.messageActionsMessageId.collectAsStateWithLifecycle()
@@ -1039,62 +1029,58 @@ class ChatActivity :
onDismiss = { chatViewModel.dismissMessageActions() }
)
} else {
- if (::conversationUser.isInitialized) {
- conversationUser.let { user ->
- MessageActionsBottomSheet(
- actionsState = buildMessageActionsState(
- message = msg,
- user = user,
- conversation = currentConversation,
- hasChatPermission = participantPermissions?.hasChatPermission() == true,
- hasReactPermission = participantPermissions?.hasReactPermission() == true,
- spreedCapabilities = spreedCapabilities,
- isOnline = isOnline,
- dateUtils = dateUtils,
- conversationThreadId = conversationThreadId
- ),
- onEmojiClick = { emoji ->
- if (msg.reactionsSelf?.contains(emoji) == true) {
- chatViewModel.deleteReaction(roomToken, msg, emoji)
- } else {
- chatViewModel.addReaction(roomToken, msg, emoji)
- }
- },
- onReply = {
- if (msg.isThread && conversationThreadId == null) {
- openThread(msg)
- } else {
- messageInputViewModel.reply(msg)
- }
- },
- onReplyPrivately = { replyPrivately(msg) },
- onOpenThread = { msg.threadId?.let { openThread(it) } },
- onForward = { forwardMessage(msg) },
- onEdit = { messageInputViewModel.edit(msg) },
- onCopy = { copyMessage(msg) },
- onCopyMessageLink = { copyMessageLink(msg) },
- onMarkAsUnread = { markAsUnread(msg) },
- onRemind = { remindMeLater(msg) },
- onPin = { pinMessage(msg) },
- onUnpin = { unPinMessage(msg) },
- onTranslate = { translateMessage(msg) },
- onShareToNote = { shareToNotes(msg) },
- onShare = {
- if (msg.getCalculateMessageType() ==
- ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE
- ) {
- checkIfSharable(msg)
- } else {
- msg.message?.let { shareMessageText(it) }
- }
- },
- onSave = { checkIfSaveable(msg) },
- onOpenInFiles = { openInFilesApp(msg) },
- onDelete = { deleteMessage(msg) },
- onDismiss = { chatViewModel.dismissMessageActions() }
- )
- }
- }
+ MessageActionsBottomSheet(
+ actionsState = buildMessageActionsState(
+ message = msg,
+ user = conversationUser,
+ conversation = currentConversation,
+ hasChatPermission = participantPermissions?.hasChatPermission() == true,
+ hasReactPermission = participantPermissions?.hasReactPermission() == true,
+ spreedCapabilities = spreedCapabilities,
+ isOnline = isOnline,
+ dateUtils = dateUtils,
+ conversationThreadId = conversationThreadId
+ ),
+ onEmojiClick = { emoji ->
+ if (msg.reactionsSelf?.contains(emoji) == true) {
+ chatViewModel.deleteReaction(roomToken, msg, emoji)
+ } else {
+ chatViewModel.addReaction(roomToken, msg, emoji)
+ }
+ },
+ onReply = {
+ if (msg.isThread && conversationThreadId == null) {
+ openThread(msg)
+ } else {
+ messageInputViewModel.reply(msg)
+ }
+ },
+ onReplyPrivately = { replyPrivately(msg) },
+ onOpenThread = { msg.threadId?.let { openThread(it) } },
+ onForward = { forwardMessage(msg) },
+ onEdit = { messageInputViewModel.edit(msg) },
+ onCopy = { copyMessage(msg) },
+ onCopyMessageLink = { copyMessageLink(msg) },
+ onMarkAsUnread = { markAsUnread(msg) },
+ onRemind = { remindMeLater(msg) },
+ onPin = { pinMessage(msg) },
+ onUnpin = { unPinMessage(msg) },
+ onTranslate = { translateMessage(msg) },
+ onShareToNote = { shareToNotes(msg) },
+ onShare = {
+ if (msg.getCalculateMessageType() ==
+ ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE
+ ) {
+ checkIfSharable(msg)
+ } else {
+ msg.message?.let { shareMessageText(it) }
+ }
+ },
+ onSave = { checkIfSaveable(msg) },
+ onOpenInFiles = { openInFilesApp(msg) },
+ onDelete = { deleteMessage(msg) },
+ onDismiss = { chatViewModel.dismissMessageActions() }
+ )
}
}
}
@@ -1272,7 +1258,6 @@ class ChatActivity :
private fun startDirectChat(actorId: String) {
lifecycleScope.launch {
try {
- if (!::conversationUser.isInitialized) return@launch
val user = conversationUser
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
@@ -1286,10 +1271,7 @@ class ChatActivity :
retrofitBucket.url,
retrofitBucket.queryMap
)
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
- val chatIntent = Intent(this@ChatActivity, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = createIntent(this@ChatActivity, user.id!!, roomOverall.ocs!!.data!!.token)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
} catch (e: Exception) {
@@ -1332,7 +1314,13 @@ class ChatActivity :
if (isViewableMedia && seedItems.any { it.messageId == messageId.toLong() }) {
startActivity(
- MediaViewerActivity.newIntent(this@ChatActivity, roomToken, seedItems, messageId.toLong())
+ MediaViewerActivity.newIntent(
+ this@ChatActivity,
+ conversationUserId,
+ roomToken,
+ seedItems,
+ messageId.toLong()
+ )
)
} else {
fileViewerUtils.openFile(chatMessage, openWhenDownloadState, downloadState)
@@ -1370,6 +1358,14 @@ class ChatActivity :
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
+ val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, conversationUserId)
+ if (newUserId != conversationUserId) {
+ // A chat instance is bound to one account, so open a fresh instance for the other one.
+ intent.removeFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ finish()
+ startActivity(intent)
+ return
+ }
val extras: Bundle? = intent.extras
val requestedRoomSwitch = extras?.getBoolean(KEY_SWITCH_TO_ROOM, false) == true
@@ -1630,25 +1626,23 @@ class ChatActivity :
if (state.conversationModel.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
state.conversationModel.status == "dnd"
) {
- conversationUser.let { user ->
- val credentials = ApiUtils.getCredentials(user.username, user.token)
+ conversationUser.let {
+ val credentials = ApiUtils.getCredentials(it.username, it.token)
chatViewModel.outOfOfficeStatusOfUser(
credentials!!,
- user.baseUrl!!,
+ it.baseUrl!!,
state.conversationModel!!.name
)
}
}
- if (::conversationUser.isInitialized) {
- conversationUser.let { user ->
- val credentials = ApiUtils.getCredentials(user.username, user.token)
- chatViewModel.fetchUpcomingEvent(
- credentials!!,
- user.baseUrl!!,
- roomToken
- )
- }
+ conversationUser.let {
+ val credentials = ApiUtils.getCredentials(it.username, it.token)
+ chatViewModel.fetchUpcomingEvent(
+ credentials!!,
+ it.baseUrl!!,
+ roomToken
+ )
}
if (state.conversationModel.objectType == ConversationEnums.ObjectType.EVENT &&
@@ -1820,12 +1814,10 @@ class ChatActivity :
chatViewModel.createRoomViewState.observe(this) { state ->
when (state) {
is ChatViewModel.CreateRoomSuccessState -> {
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, state.roomOverall.ocs!!.data!!.token)
+ val token = state.roomOverall.ocs!!.data!!.token
leaveRoom {
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = createIntent(context, conversationUserId, token)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
@@ -2159,7 +2151,7 @@ class ChatActivity :
fun updateToolbarState() {
val conversation = currentConversation
- val user = if (::conversationUser.isInitialized) conversationUser else null
+ val user = conversationUser
val isOneToOne = isOneToOneConversation()
val capabilitiesReady = ::spreedCapabilities.isInitialized
@@ -2171,11 +2163,11 @@ class ChatActivity :
subtitle = buildToolbarSubtitle(conversation),
avatarType = buildAvatarType(conversation),
avatarUrl = buildAvatarUrl(user, conversation),
- credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) },
+ credentials = ApiUtils.getCredentials(user.username, user.token),
userStatus = if (isOneToOne) conversation?.status else null,
showVoiceCall = isCallsEnabled(capabilitiesReady, conversation),
showVideoCall = isCallsEnabled(capabilitiesReady, conversation),
- titleClickable = user?.userId != "?" && !chatToolbarState.isSearchMode,
+ titleClickable = user.userId != "?" && !chatToolbarState.isSearchMode,
overflowItems = buildOverflowItems(),
threadNotificationIcon = buildThreadNotificationIcon(capabilitiesReady),
showEventMenu = conversation?.objectType == ConversationEnums.ObjectType.EVENT,
@@ -2346,35 +2338,31 @@ class ChatActivity :
}
private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
- if (::conversationUser.isInitialized) {
- runOnUiThread {
- val toastInfo = if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
- context.resources.getString(R.string.switch_to_main_room)
- } else {
- context.resources.getString(R.string.switch_to_breakout_room)
- }
- // do not replace with snackbar, as it would disappear with the activity switch
- Toast.makeText(
- context,
- toastInfo,
- Toast.LENGTH_LONG
- ).show()
+ runOnUiThread {
+ val toastInfo = if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
+ context.resources.getString(R.string.switch_to_main_room)
+ } else {
+ context.resources.getString(R.string.switch_to_breakout_room)
}
+ // do not replace with snackbar, as it would disappear with the activity switch
+ Toast.makeText(
+ context,
+ toastInfo,
+ Toast.LENGTH_LONG
+ ).show()
+ }
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, token)
+ val bundle = Bundle()
- if (startCallAfterRoomSwitch) {
- bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
- bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
- }
+ if (startCallAfterRoomSwitch) {
+ bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
+ bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
+ }
- leaveRoom {
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
- chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- startActivity(chatIntent)
- }
+ leaveRoom {
+ val chatIntent = createIntent(context, conversationUserId, token, bundle)
+ chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ startActivity(chatIntent)
}
}
@@ -2405,8 +2393,6 @@ class ChatActivity :
chatViewModel.syncVoiceMessageUiState(message)
message.openWhenDownloaded = openWhenDownloaded
- val baseUrl = conversationUser.baseUrl
- val userId = conversationUser.userId
val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(
conversationUser.capabilities!!
.spreedCapability!!
@@ -2435,8 +2421,7 @@ class ChatActivity :
}
val data: Data = Data.Builder()
- .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
- .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
+ .putLong(KEY_INTERNAL_USER_ID, conversationUserId)
.putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
@@ -2897,6 +2882,7 @@ class ChatActivity :
fun showBrowserScreen() {
val sharingFileBrowserIntent = Intent(this, RemoteFileBrowserActivity::class.java)
+ sharingFileBrowserIntent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
startRemoteFileBrowsingForResult.launch(sharingFileBrowserIntent)
}
@@ -2914,6 +2900,7 @@ class ChatActivity :
if (permissionUtil.isLocationPermissionGranted() && isGpsEnabled) {
val intent = Intent(this, LocationPickerActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
startActivity(intent)
@@ -2980,6 +2967,7 @@ class ChatActivity :
val intent = Intent(this, ConversationInfoActivity::class.java)
intent.putExtras(bundle)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
startActivity(intent)
}
@@ -2990,17 +2978,15 @@ class ChatActivity :
@Suppress("Detekt.TooGenericExceptionCaught")
private fun cancelNotificationsForCurrentConversation() {
- if (::conversationUser.isInitialized) {
- if (!TextUtils.isEmpty(roomToken)) {
- try {
- NotificationUtils.cancelExistingNotificationsForRoom(
- applicationContext,
- conversationUser!!,
- roomToken
- )
- } catch (e: RuntimeException) {
- Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
- }
+ if (!TextUtils.isEmpty(roomToken)) {
+ try {
+ NotificationUtils.cancelExistingNotificationsForRoom(
+ applicationContext,
+ conversationUser!!,
+ roomToken
+ )
+ } catch (e: RuntimeException) {
+ Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
}
}
}
@@ -3021,7 +3007,7 @@ class ChatActivity :
getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
}
- if (::conversationUser.isInitialized && isActivityNotChangingConfigurations() && isNotInCall()) {
+ if (isActivityNotChangingConfigurations() && isNotInCall()) {
if (isLeavingRoom) {
Log.d(TAG, "not leaving room (leave already in progress)")
} else if (validSessionId()) {
@@ -3070,7 +3056,9 @@ class ChatActivity :
super.onDestroy()
logConversationInfos("onDestroy")
- chatViewModel.leaveRoomViewState.removeObserver(leaveRoomObserver)
+ if (::initialUser.isInitialized) {
+ chatViewModel.leaveRoomViewState.removeObserver(leaveRoomObserver)
+ }
findViewById(R.id.toolbar)?.setOnClickListener(null)
@@ -3083,8 +3071,10 @@ class ChatActivity :
private fun joinRoomWithPassword() {
// if ApplicationWideCurrentRoomHolder contains a session (because a call is active), then keep the sessionId
- if (ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken ==
- currentConversation!!.token
+ if (ApplicationWideCurrentRoomHolder.getInstance().isCurrentRoom(
+ currentConversation!!.token,
+ conversationUser.id
+ )
) {
sessionIdAfterRoomJoined = ApplicationWideCurrentRoomHolder.getInstance().session
@@ -3117,11 +3107,8 @@ class ChatActivity :
}
sessionIdAfterRoomJoined = "0"
- var apiVersion = 1
// FIXME Fix API checking with guests?
- if (::conversationUser.isInitialized) {
- apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
- }
+ val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
val startNanoTime = System.nanoTime()
Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime")
@@ -3137,8 +3124,8 @@ class ChatActivity :
}
private fun setupWebsocket() {
- if (currentConversation == null || !::conversationUser.isInitialized) {
- Log.e(TAG, "setupWebsocket: currentConversation or conversationUser is null")
+ if (currentConversation == null) {
+ Log.e(TAG, "setupWebsocket: currentConversation is null")
return
}
@@ -3231,6 +3218,7 @@ class ChatActivity :
private fun openScheduledMessages() {
val intent = Intent(this, ScheduledMessagesActivity::class.java).apply {
putExtra(ScheduledMessagesActivity.ROOM_TOKEN, roomToken)
+ putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
putExtra(ScheduledMessagesActivity.CONVERSATION_NAME, currentConversation?.displayName.orEmpty())
if (conversationThreadId != null && conversationThreadId!! > 0) {
putExtra(ScheduledMessagesActivity.THREAD_ID, conversationThreadId)
@@ -3471,6 +3459,7 @@ class ChatActivity :
private fun showSharedItems() {
val intent = Intent(this, SharedItemsActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
intent.putExtra(
@@ -3505,20 +3494,18 @@ class ChatActivity :
private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
currentConversation?.let {
- if (::conversationUser.isInitialized) {
- if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(spreedCapabilities)) {
- Snackbar.make(binding.root, R.string.nc_call_e2ee_not_supported, Snackbar.LENGTH_LONG).show()
- return
- }
- val pp = ParticipantPermissions(spreedCapabilities, it)
- if (!pp.canStartCall() && currentConversation?.hasCall == false) {
- Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show()
- } else {
- ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
- val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
- if (callIntent != null) {
- startActivity(callIntent)
- }
+ if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(spreedCapabilities)) {
+ Snackbar.make(binding.root, R.string.nc_call_e2ee_not_supported, Snackbar.LENGTH_LONG).show()
+ return
+ }
+ val pp = ParticipantPermissions(spreedCapabilities, it)
+ if (!pp.canStartCall() && currentConversation?.hasCall == false) {
+ Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show()
+ } else {
+ ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
+ val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
+ if (callIntent != null) {
+ startActivity(callIntent)
}
}
}
@@ -3556,6 +3543,7 @@ class ChatActivity :
val callIntent = Intent(this, CallActivity::class.java)
callIntent.putExtras(bundle)
+ callIntent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
return callIntent
} ?: run {
return null
@@ -3634,11 +3622,8 @@ class ChatActivity :
)
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
} else {
- var apiVersion = 1
// FIXME Fix API checking with guests?
- if (::conversationUser.isInitialized) {
- apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
- }
+ val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
chatViewModel.deleteChatMessages(
credentials!!,
@@ -3677,6 +3662,7 @@ class ChatActivity :
val intent = Intent(this, ConversationsListActivity::class.java)
intent.putExtras(bundle)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
@@ -3697,7 +3683,8 @@ class ChatActivity :
setContent {
DateTimeCompose(
bundle,
- chatViewModel
+ chatViewModel,
+ conversationUser
).GetDateTimeDialog(shouldDismiss, this@ChatActivity)
}
}
@@ -3802,6 +3789,7 @@ class ChatActivity :
bundle.putString(BundleKeys.KEY_TRANSLATE_MESSAGE, message?.getRichText())
val intent = Intent(this, TranslateActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
intent.putExtras(bundle)
startActivity(intent)
}
@@ -4022,10 +4010,7 @@ class ChatActivity :
}
fun openNoteToSelfConversation(noteToSelfRoomToken: String) {
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, noteToSelfRoomToken)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = createIntent(context, conversationUserId, noteToSelfRoomToken)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
@@ -4084,8 +4069,6 @@ class ChatActivity :
}
fun userAllowedByPrivilages(message: ChatMessage): Boolean {
- if (!::conversationUser.isInitialized) return false
-
val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
true
} else {
@@ -4182,9 +4165,7 @@ class ChatActivity :
}
fun createPoll() {
- val pollVoteDialog = PollCreateDialogFragment.newInstance(
- roomToken
- )
+ val pollVoteDialog = PollCreateDialogFragment.newInstance(conversationUser, roomToken)
pollVoteDialog.show(supportFragmentManager, TAG)
}
@@ -4196,10 +4177,8 @@ class ChatActivity :
fun openThread(messageId: Long) {
val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putLong(KEY_THREAD_ID, messageId)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = createIntent(context, conversationUserId, roomToken, bundle)
startActivity(chatIntent)
}
@@ -4217,6 +4196,7 @@ class ChatActivity :
val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java)
threadsOverviewIntent.putExtras(bundle)
+ threadsOverviewIntent.putExtra(KEY_INTERNAL_USER_ID, conversationUserId)
startActivity(threadsOverviewIntent)
}
@@ -4303,6 +4283,16 @@ class ChatActivity :
companion object {
val TAG = ChatActivity::class.java.simpleName
+
+ /**
+ * Creates an intent that opens the conversation [roomToken] of the account with the internal id [userId].
+ */
+ fun createIntent(context: Context, userId: Long, roomToken: String, extras: Bundle? = null): Intent =
+ Intent(context, ChatActivity::class.java).apply {
+ extras?.let { putExtras(it) }
+ putExtra(KEY_ROOM_TOKEN, roomToken)
+ putExtra(KEY_INTERNAL_USER_ID, userId)
+ }
private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 2
private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 3
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
index 6b6472b576..a5d3fa86c7 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
@@ -76,6 +76,7 @@ import com.nextcloud.talk.ui.CallStartedBanner
import com.nextcloud.talk.ui.MicInputCloud
import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
@@ -137,14 +138,12 @@ class MessageInputFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
conversationInternalId = arguments?.getString(ChatActivity.CONVERSATION_INTERNAL_ID).orEmpty()
chatActivity = requireActivity() as ChatActivity
- val sharedText = arguments?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
- if (sharedText.isNotEmpty()) {
- hasSharedText = true
- chatActivity.chatViewModel.messageDraft.messageText = sharedText
- chatActivity.chatViewModel.saveMessageDraft()
- }
+ // The shared text itself is put into the draft by ChatActivity. This fragment may be restored before
+ // ChatActivity has created its view model, so it must not access it here.
+ hasSharedText = arguments?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty().isNotEmpty()
if (conversationInternalId.isEmpty()) {
Log.e(TAG, "internalId for conversation passed to MessageInputFragment is empty")
}
@@ -780,6 +779,7 @@ class MessageInputFragment : Fragment() {
val backgroundDrawable = it.getColor(R.color.bg_default, null).toDrawable()
val presenter = MentionAutocompletePresenter(
requireContext(),
+ chatActivity.conversationUser,
chatActivity.roomToken,
chatActivity.chatApiVersion
)
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
index a6e1e2f957..6832cfcf70 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
@@ -27,6 +27,7 @@ import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.databinding.FragmentMessageInputVoiceRecordingBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -54,6 +55,7 @@ class MessageInputVoiceRecordingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
diff --git a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt
index b46932a1df..b1c2a71bdb 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt
@@ -9,7 +9,6 @@ package com.nextcloud.talk.chat
import android.content.ClipData
import android.content.ClipboardManager
-import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.annotation.DrawableRes
@@ -82,7 +81,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.widget.addTextChangedListener
import androidx.emoji2.widget.EmojiEditText
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.R
@@ -93,6 +91,7 @@ import com.nextcloud.talk.chat.ui.model.MessageTypeContent
import com.nextcloud.talk.chat.ui.model.toScheduledMessageUiModel
import com.nextcloud.talk.chat.viewmodels.ScheduledMessagesViewModel
import com.nextcloud.talk.components.ColoredStatusBar
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
@@ -110,7 +109,6 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateConstants
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
@@ -127,12 +125,16 @@ private const val STICKY_HEADER_SCROLL_DELAY = 1200L
class ScheduledMessagesActivity : BaseActivity() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: ScheduledMessagesViewModel.Factory
@Inject
lateinit var dateUtils: DateUtils
- private lateinit var scheduledMessagesViewModel: ScheduledMessagesViewModel
+ private val scheduledMessagesViewModel: ScheduledMessagesViewModel by assistedViewModels {
+ viewModelFactory.build(conversationUser)
+ }
+
+ private lateinit var conversationUser: User
@Inject
lateinit var networkMonitor: NetworkMonitor
@@ -164,14 +166,10 @@ class ScheduledMessagesActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- scheduledMessagesViewModel = ViewModelProvider(this, viewModelFactory)[ScheduledMessagesViewModel::class.java]
+ conversationUser = setUpBoundUserOrFinish() ?: return
setContent {
val colorScheme = viewThemeUtils.getColorScheme(this)
- val currentUser by scheduledMessagesViewModel.currentUserState.collectAsStateWithLifecycle()
- LaunchedEffect(Unit) {
- scheduledMessagesViewModel.loadCurrentUser()
- }
MaterialTheme(colorScheme = colorScheme) {
CompositionLocalProvider(
LocalViewThemeUtils provides viewThemeUtils,
@@ -179,46 +177,43 @@ class ScheduledMessagesActivity : BaseActivity() {
LocalShowThreadButton provides false
) {
ColoredStatusBar()
- currentUser?.let { user ->
- ScheduledMessagesScreen(
- user = user,
- conversationName = conversationName,
- scheduledMessagesViewModel = scheduledMessagesViewModel,
- dateUtils = dateUtils,
- viewThemeUtils = viewThemeUtils,
- onBack = { finish() },
- onLoadScheduledMessages = { loadScheduledMessages(user) },
- onSendNow = { message ->
- sendNow(message, user)
- },
- onReschedule = { message, sendAt, sendWithoutNotification ->
- reschedule(message, sendAt, sendWithoutNotification, user)
- },
- onEdit = { message, sendAt ->
- edit(message, sendAt, user)
- },
- onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, user) },
- onOpenParentMessage = { messageId ->
- openParentMessage(messageId)
- },
- onOpenThread = { threadId ->
- openThread(threadId)
- },
- threadTitle = threadTitle,
- isThreadView = isThreadView,
- onCopyScheduledMessage = { message ->
- copyScheduledMessage(message)
- }
- )
- }
+ ScheduledMessagesScreen(
+ user = conversationUser,
+ conversationName = conversationName,
+ scheduledMessagesViewModel = scheduledMessagesViewModel,
+ dateUtils = dateUtils,
+ viewThemeUtils = viewThemeUtils,
+ onBack = { finish() },
+ onLoadScheduledMessages = { loadScheduledMessages(conversationUser) },
+ onSendNow = { message ->
+ sendNow(message, conversationUser)
+ },
+ onReschedule = { message, sendAt, sendWithoutNotification ->
+ reschedule(message, sendAt, sendWithoutNotification, conversationUser)
+ },
+ onEdit = { message, sendAt ->
+ edit(message, sendAt, conversationUser)
+ },
+ onDeleteScheduledMessage = { message -> deleteScheduledMessage(message, conversationUser) },
+ onOpenParentMessage = { messageId ->
+ openParentMessage(messageId)
+ },
+ onOpenThread = { threadId ->
+ openThread(threadId)
+ },
+ threadTitle = threadTitle,
+ isThreadView = isThreadView,
+ onCopyScheduledMessage = { message ->
+ copyScheduledMessage(message)
+ }
+ )
} // CompositionLocalProvider
}
}
}
private fun openThread(threadId: Long) {
- val intent = Intent(this, ChatActivity::class.java).apply {
- putExtra(KEY_ROOM_TOKEN, roomToken)
+ val intent = ChatActivity.createIntent(this, conversationUser.id!!, roomToken).apply {
putExtra(KEY_THREAD_ID, threadId)
}
startActivity(intent)
@@ -981,17 +976,14 @@ class ScheduledMessagesActivity : BaseActivity() {
}
private fun openParentMessage(messageId: Long?) {
- val intent = Intent(this, ChatActivity::class.java).apply {
- putExtra(KEY_ROOM_TOKEN, roomToken)
-
+ val intent = ChatActivity.createIntent(this, conversationUser.id!!, roomToken).apply {
messageId?.let { putExtra(BundleKeys.KEY_MESSAGE_ID, it.toString()) }
}
startActivity(intent)
}
private fun openThreadParentMessage(messageId: Long?, threadId: Long?) {
- val intent = Intent(this, ChatActivity::class.java).apply {
- putExtra(KEY_ROOM_TOKEN, roomToken)
+ val intent = ChatActivity.createIntent(this, conversationUser.id!!, roomToken).apply {
threadId?.let { putExtra(BundleKeys.KEY_THREAD_ID, it) }
messageId?.let { putExtra(BundleKeys.KEY_MESSAGE_ID, it.toString()) }
}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt
index f475d2c762..7a059e30b1 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt
@@ -69,7 +69,11 @@ sealed interface MessageTypeContent {
object RegularText : MessageTypeContent
object SystemMessage : MessageTypeContent
- data class LinkPreview(val url: String) : MessageTypeContent
+ /**
+ * [authHeader] authenticates images of the account's own server at [serverBaseUrl], e.g. reference previews.
+ */
+ data class LinkPreview(val url: String, val serverBaseUrl: String? = null, val authHeader: String? = null) :
+ MessageTypeContent
data class Media(
val previewUrl: String?,
@@ -79,7 +83,9 @@ sealed interface MessageTypeContent {
val blurhash: String? = null,
val width: Int? = null,
val height: Int? = null,
- val isClassified: Boolean = false
+ val isClassified: Boolean = false,
+ // Credentials of the account, as the preview is always loaded from its server.
+ val authHeader: String? = null
) : MessageTypeContent
data class UploadingMedia(
@@ -291,7 +297,13 @@ fun getMessageTypeContent(user: User, message: ChatMessage, isClassified: Boolea
getDeckContent(message)
} else {
message.extractLinkPreviewUrl(user)
- ?.let { MessageTypeContent.LinkPreview(url = it) }
+ ?.let {
+ MessageTypeContent.LinkPreview(
+ url = it,
+ serverBaseUrl = user.baseUrl,
+ authHeader = ApiUtils.getCredentials(user.username, user.token)
+ )
+ }
?: MessageTypeContent.RegularText
}
@@ -332,7 +344,8 @@ fun getMediaContent(user: User, message: ChatMessage, isClassified: Boolean = fa
blurhash = message.fileParameters.blurhash,
width = message.fileParameters.width,
height = message.fileParameters.height,
- isClassified = isClassified
+ isClassified = isClassified,
+ authHeader = ApiUtils.getCredentials(user.username, user.token)
)
}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
index 5a3e643797..35d03c8dd4 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
@@ -70,6 +70,7 @@ import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
import com.nextcloud.talk.threadsoverview.data.ThreadsRepository
import com.nextcloud.talk.ui.PlaybackSpeed
+import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.CharacterAvatarUtils
@@ -81,7 +82,6 @@ import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.UserIdUtils
import com.nextcloud.talk.utils.throttleLatest
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import com.nextcloud.talk.utils.message.SendMessageUtils
import com.nextcloud.talk.utils.message.groupHashOf
import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -272,8 +272,9 @@ class ChatViewModel @AssistedInject constructor(
private val unifiedSearchRepository: UnifiedSearchRepository,
private val mediaRecorderManager: MediaRecorderManager,
private val audioFocusRequestManager: AudioFocusRequestManager,
- private val currentUserProvider: CurrentUserProvider,
+ private val userManager: UserManager,
@ApplicationScope private val appScope: CoroutineScope,
+ @Assisted private val initialUser: User,
@Assisted private val chatRoomToken: String,
@Assisted private val conversationThreadId: Long?
) : ViewModel(),
@@ -317,8 +318,11 @@ class ChatViewModel @AssistedInject constructor(
get() = results.getOrNull(selectedIndex)
}
- @Deprecated("use currentUserFlow")
- lateinit var currentUser: User
+ /**
+ * The user of this chat with its latest stored values.
+ */
+ val currentUser: User
+ get() = currentUserFlow.value ?: initialUser
private var messageSearchHelper: MessageSearchHelper? = null
private var searchRequestJob: Job? = null
@@ -680,18 +684,20 @@ class ChatViewModel @AssistedInject constructor(
// Current user flows
// ------------------------------
private val currentUserFlow: StateFlow =
- currentUserProvider.currentUserFlow
- .stateIn(viewModelScope, SharingStarted.Eagerly, null)
+ userManager.userFlow(initialUser.id!!)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, initialUser)
private val nonNullUserFlow = currentUserFlow.filterNotNull()
+ // Emits once per bound user, so updates of the user row (e.g. capabilities) don't restart dependent flows.
+ private val boundUserFlow = nonNullUserFlow.distinctUntilChangedBy { it.id }
+
// Unlike conversationFlow below, this is not deduped by lastReadMessage/lastCommonReadMessage,
// so it also reacts to fields those two ignore, e.g. hasCall (see [hasCall]).
private val rawConversationFlow: Flow =
- nonNullUserFlow
+ boundUserFlow
.flatMapLatest { user ->
- val userId = requireNotNull(user.id)
- conversationRepository.observeConversation(userId, chatRoomToken)
+ conversationRepository.observeConversation(requireNotNull(user.id), chatRoomToken)
}
.mapNotNull { result ->
when (result) {
@@ -1149,7 +1155,7 @@ class ChatViewModel @AssistedInject constructor(
}
private fun observeLobbyState() {
- val conversationFromDb = nonNullUserFlow
+ val conversationFromDb = boundUserFlow
.flatMapLatest { user ->
conversationRepository.observeConversation(requireNotNull(user.id), chatRoomToken)
}
@@ -1189,7 +1195,7 @@ class ChatViewModel @AssistedInject constructor(
}
private fun observePinnedMessage() {
- nonNullUserFlow
+ boundUserFlow
.flatMapLatest { user ->
conversationRepository.observeConversation(requireNotNull(user.id), chatRoomToken)
.mapNotNull { result ->
@@ -1721,7 +1727,7 @@ class ChatViewModel @AssistedInject constructor(
* - those get their avatar drawn on the client instead, see [CharacterAvatarUtils].
*/
fun getAvatarUrl(message: ChatMessage): String =
- if (this::currentUser.isInitialized && !message.hasClientSideAvatar()) {
+ if (!message.hasClientSideAvatar()) {
ApiUtils.getUrlForAvatar(
currentUser.baseUrl,
message.actorId,
@@ -1734,15 +1740,13 @@ class ChatViewModel @AssistedInject constructor(
private fun ChatMessage.hasClientSideAvatar(): Boolean =
CharacterAvatarUtils.avatarFor(actorType, actorId, actorDisplayName, guestLabel = null) != null
- fun initData(user: User, credentials: String, urlForChatting: String, threadId: Long?) {
- currentUser = user
-
+ fun initData() {
chatRepository.initData(
- user,
- credentials,
- urlForChatting,
+ currentUser,
+ ApiUtils.getCredentials(currentUser.username, currentUser.token)!!,
+ ApiUtils.getUrlForChat(ApiUtils.API_V1, currentUser.baseUrl, chatRoomToken),
chatRoomToken,
- threadId
+ conversationThreadId
)
observeConversationAndUserFirstTime()
@@ -2099,10 +2103,6 @@ class ChatViewModel @AssistedInject constructor(
* simply leaving the chat, would immediately mark everything read again.
*/
fun markChatAsUnread(lastReadMessage: Int) {
- if (!this::currentUser.isInitialized) {
- return
- }
-
keepMarkedAsUnread = true
localLastReadMessage = lastReadMessage
resetUnreadMarkerCache()
@@ -2120,9 +2120,6 @@ class ChatViewModel @AssistedInject constructor(
}
fun setChatReadMessage(lastReadMessage: Int) {
- if (!this::currentUser.isInitialized) {
- return
- }
// marking as read is the explicit counterpart of marking as unread and hands the read marker
// back to the automatic handling
keepMarkedAsUnread = false
@@ -2373,6 +2370,7 @@ class ChatViewModel @AssistedInject constructor(
val internalConversationId = "${currentUser.id}@$chatRoomToken"
val workerId = UploadAndShareFilesWorker.upload(
+ userId = currentUser.id!!,
fileUri = fileUri,
roomToken = room,
conversationName = displayName,
@@ -2457,7 +2455,7 @@ class ChatViewModel @AssistedInject constructor(
fun getMessageById(messageId: Long): Flow {
val urlForChatting = ApiUtils.getUrlForChat(
1, // Keep API v1 for local message lookup until version wiring is centralized.
- currentUser?.baseUrl,
+ currentUser.baseUrl,
chatRoomToken
)
@@ -2595,9 +2593,8 @@ class ChatViewModel @AssistedInject constructor(
}
}
- suspend fun fetchOpenGraph(url: String): OpenGraphObjectDto? {
- if (!this::currentUser.isInitialized) return null
- return withContext(Dispatchers.IO) {
+ suspend fun fetchOpenGraph(url: String): OpenGraphObjectDto? =
+ withContext(Dispatchers.IO) {
runCatching {
chatNetworkDataSource.getOpenGraph(
currentUser.getCredentials(),
@@ -2606,7 +2603,6 @@ class ChatViewModel @AssistedInject constructor(
)?.openGraphObject
}.getOrNull()
}
- }
suspend fun updateMessageDraft() {
val model = conversationRepository.getLocallyStoredConversation(
@@ -2829,6 +2825,6 @@ class ChatViewModel @AssistedInject constructor(
@AssistedFactory
interface ChatViewModelFactory {
- fun build(roomToken: String, conversationThreadId: Long?): ChatViewModel
+ fun build(user: User, roomToken: String, conversationThreadId: Long?): ChatViewModel
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.kt
index 95e5fa8b29..bd4b18f653 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ScheduledMessagesViewModel.kt
@@ -16,8 +16,10 @@ import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.opengraph.ReferenceDto
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import com.nextcloud.talk.utils.message.SendMessageUtils
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -26,12 +28,11 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import javax.inject.Inject
-class ScheduledMessagesViewModel @Inject constructor(
+class ScheduledMessagesViewModel @AssistedInject constructor(
private val chatRepository: ChatMessageRepository,
- private val currentUserProvider: CurrentUserProvider,
- private val chatNetworkDataSource: ChatNetworkDataSource
+ private val chatNetworkDataSource: ChatNetworkDataSource,
+ @Assisted private val user: User
) : ViewModel() {
sealed interface GetScheduledMessagesState
@@ -53,9 +54,6 @@ class ScheduledMessagesViewModel @Inject constructor(
data class ScheduledMessageErrorState(val error: Throwable? = null) : ScheduledMessageActionState
- private val _currentUserState = MutableStateFlow(null)
- val currentUserState: StateFlow = _currentUserState
-
sealed interface SendNowMessageState
object SendNowMessageIdleState : SendNowMessageState
object SendNowMessageLoadingState : SendNowMessageState
@@ -117,7 +115,6 @@ class ScheduledMessagesViewModel @Inject constructor(
viewModelScope.launch {
try {
- val user = _currentUserState.value ?: currentUserProvider.getCurrentUser().getOrNull() ?: return@launch
if (user.baseUrl.isNullOrBlank()) {
return@launch
}
@@ -143,13 +140,6 @@ class ScheduledMessagesViewModel @Inject constructor(
}
}
- fun loadCurrentUser() {
- viewModelScope.launch {
- val user = currentUserProvider.getCurrentUser().getOrNull()
- _currentUserState.value = user
- }
- }
-
@Suppress("LongParameterList")
fun sendNow(
credentials: String,
@@ -219,9 +209,6 @@ class ScheduledMessagesViewModel @Inject constructor(
private suspend fun getParentMessageById(token: String, parentMessageId: Long, threadId: Long?): ChatMessage? =
withContext(Dispatchers.IO) {
- val userResult = currentUserProvider.getCurrentUser()
- val user = userResult.getOrElse { return@withContext null }
-
val credentials = user.getCredentials()
val apiVersion = ApiUtils.getChatApiVersion(
@@ -278,6 +265,11 @@ class ScheduledMessagesViewModel @Inject constructor(
}
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ScheduledMessagesViewModel
+ }
+
companion object {
private val TAG = ScheduledMessagesViewModel::class.java.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt
index a162e45b87..e19a9d23bf 100644
--- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt
+++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt
@@ -65,7 +65,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.core.net.toUri
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
import autodagger.AutoInjector
import coil.compose.AsyncImage
import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp
@@ -82,6 +84,7 @@ import com.nextcloud.talk.chooseaccount.viewmodel.StatusUiState
import com.nextcloud.talk.chooseaccount.viewmodel.StatusViewModel
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.conversationlist.ConversationsListActivity
+import com.nextcloud.talk.dagger.modules.ViewModelFactoryWithParams
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel
@@ -90,12 +93,12 @@ import com.nextcloud.talk.models.json.status.StatusType
import com.nextcloud.talk.settings.SettingsActivity
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.users.DefaultAccountProvider
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import kotlinx.coroutines.launch
import java.net.CookieManager
import javax.inject.Inject
@@ -110,7 +113,7 @@ class ChooseAccountDialogCompose {
lateinit var userManager: UserManager
@Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
+ lateinit var defaultAccountProvider: DefaultAccountProvider
@Inject
lateinit var cookieManager: CookieManager
@@ -119,13 +122,13 @@ class ChooseAccountDialogCompose {
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
- lateinit var invitationsViewModel: InvitationsViewModel
+ lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
- lateinit var statusViewModel: StatusViewModel
+ lateinit var statusViewModelFactory: StatusViewModel.Factory
@Inject
- lateinit var statusMessageViewModel: StatusMessageViewModel
+ lateinit var statusMessageViewModelFactory: StatusMessageViewModel.Factory
@Inject
lateinit var networkMonitor: NetworkMonitor
@@ -138,6 +141,23 @@ class ChooseAccountDialogCompose {
@Suppress("LongMethod")
fun GetChooseAccountDialog(shouldDismiss: MutableState, activity: Activity, showEcosystem: Boolean) {
if (shouldDismiss.value) return
+ val currentUser = defaultAccountProvider.getDefaultUserBlocking()!!
+ // Scoped to the hosting activity, so they survive configuration changes and are cleared with it. The status
+ // view models are bound to the default account, so they are keyed by it and replaced when the default account
+ // changes while the activity stays open.
+ val invitationsViewModel: InvitationsViewModel = viewModel(factory = viewModelFactory)
+ val statusViewModel: StatusViewModel = viewModel(
+ key = "status-${currentUser.id}",
+ factory = ViewModelFactoryWithParams(StatusViewModel::class.java) {
+ statusViewModelFactory.build(currentUser)
+ }
+ )
+ val statusMessageViewModel: StatusMessageViewModel = viewModel(
+ key = "statusMessage-${currentUser.id}",
+ factory = ViewModelFactoryWithParams(StatusMessageViewModel::class.java) {
+ statusMessageViewModelFactory.build(currentUser)
+ }
+ )
val colorScheme = viewThemeUtils.getColorScheme(activity)
val status = remember { mutableStateOf(null) }
val showOnlineStatusSheet = rememberSaveable { mutableStateOf(false) }
@@ -146,7 +166,6 @@ class ChooseAccountDialogCompose {
val statusViewState by statusViewModel.statusViewState.collectAsStateWithLifecycle()
val invitationsStateByUser by invitationsViewModel.invitationsStateByUser.collectAsStateWithLifecycle()
val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle()
- val currentUser = currentUserProvider.currentUser.blockingGet()!!
val isStatusAvailable = CapabilitiesUtil.isUserStatusAvailable(currentUser)
ecosystemManager = EcosystemManager(activity)
@@ -190,7 +209,7 @@ class ChooseAccountDialogCompose {
},
onOpenSettingsClick = {
shouldDismiss.value = true
- openSettings(activity)
+ openSettings(activity, currentUser)
},
onEcosystemFilesClick = {
shouldDismiss.value = true
@@ -269,8 +288,9 @@ class ChooseAccountDialogCompose {
activity.startActivity(intent)
}
- private fun openSettings(activity: Activity) {
+ private fun openSettings(activity: Activity, user: User) {
val intent = Intent(activity, SettingsActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, user.id)
activity.startActivity(intent)
}
@@ -299,9 +319,9 @@ class ChooseAccountDialogCompose {
scope.launch {
if (userManager.setUserAsActive(userItem.user)) {
cookieManager.cookieStore.removeAll()
- val intent = Intent(activity, ConversationsListActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- activity.startActivity(intent)
+ activity.startActivity(
+ ConversationsListActivity.createAccountSwitchIntent(activity, userItem.user.id!!)
+ )
onSelected()
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt
index 1a26070a89..cf1ff7eb17 100644
--- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt
+++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt
@@ -60,10 +60,16 @@ fun StatusMessageModalBottomSheet(currentStatus: StatusDto, viewModel: StatusMes
var showEmojiPicker by rememberSaveable { mutableStateOf(false) }
val backToStatus: () -> Unit = { showEmojiPicker = false }
- LaunchedEffect(currentStatus) {
- viewModel.init(currentStatus)
- viewModel.checkBackupStatus()
- viewModel.fetchPredefinedStatuses()
+ // The view model keeps unsaved edits across configuration changes, so it is only initialized when the sheet is
+ // opened, not again when it is recreated.
+ var isInitialized by rememberSaveable { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ if (!isInitialized) {
+ viewModel.init(currentStatus)
+ viewModel.checkBackupStatus()
+ viewModel.fetchPredefinedStatuses()
+ isInitialized = true
+ }
}
LaunchedEffect(isDismissed) {
diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.kt
index 4a172e73f0..5a4af66299 100644
--- a/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusMessageViewModel.kt
@@ -11,17 +11,19 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.chooseaccount.data.StatusRepository
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.status.ClearAtDto
import com.nextcloud.talk.models.json.status.StatusDto
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusDto
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.Calendar
-import javax.inject.Inject
private const val ONE_SECOND_IN_MILLIS = 1000L
private const val ONE_MINUTE_IN_SECONDS = 60L
@@ -34,12 +36,11 @@ private const val LAST_SECOND_OF_MINUTE = 59
private const val HTTP_STATUS_CODE_OK = 200
private const val HTTP_STATUS_CODE_NOT_FOUND = 404
-class StatusMessageViewModel @Inject constructor(
+class StatusMessageViewModel @AssistedInject constructor(
private val repository: StatusRepository,
- private val currentUserProvider: CurrentUserProviderOld
+ @Assisted private val currentUser: User
) : ViewModel() {
- private val currentUser = currentUserProvider.currentUser.blockingGet()
private val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
private val _emoji = MutableStateFlow("")
@@ -219,6 +220,11 @@ class StatusMessageViewModel @Inject constructor(
clearAt = statusMessageClearAtFromPosition(position)
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): StatusMessageViewModel
+ }
+
companion object {
private val TAG = StatusMessageViewModel::class.simpleName
const val CLEAR_AT_POS_DONT_CLEAR = 0
diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.kt
index f1f0bbdb5b..6c5cf80955 100644
--- a/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/viewmodel/StatusViewModel.kt
@@ -10,23 +10,24 @@ package com.nextcloud.talk.chooseaccount.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.chooseaccount.data.StatusRepository
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.models.json.status.StatusOverall
import com.nextcloud.talk.models.json.status.StatusType
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class StatusViewModel @Inject constructor(
+class StatusViewModel @AssistedInject constructor(
private val repository: StatusRepository,
- private val currentUserProvider: CurrentUserProviderOld,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted private val currentUser: User
) : ViewModel() {
- private val currentUser = currentUserProvider.currentUser.blockingGet()
private val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
private val _statusViewState = MutableStateFlow(StatusUiState.None)
@@ -62,6 +63,11 @@ class StatusViewModel @Inject constructor(
}
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): StatusViewModel
+ }
+
companion object {
private val TAG = StatusViewModel::class.java.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt
index eacfc72f4e..30100da77a 100644
--- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt
@@ -9,34 +9,45 @@
package com.nextcloud.talk.contacts
import android.annotation.SuppressLint
+import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
+import com.nextcloud.talk.dagger.modules.assistedViewModels
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUserDto
import com.nextcloud.talk.utils.bundle.BundleKeys
+import kotlinx.coroutines.launch
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactsActivity : BaseActivity() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
- private lateinit var contactsViewModel: ContactsViewModel
+ lateinit var viewModelFactory: ContactsViewModel.Factory
+
+ private lateinit var user: User
+
+ private val contactsViewModel: ContactsViewModel by assistedViewModels { viewModelFactory.build(user) }
@SuppressLint("UnrememberedMutableState")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
+ user = setUpBoundUserOrFinish() ?: return
+ observeCreatedRoom()
setContent {
val isAddParticipants = intent.getBooleanExtra(BundleKeys.KEY_ADD_PARTICIPANTS, false)
val hideAlreadyAddedParticipants = intent.getBooleanExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, false)
@@ -72,6 +83,24 @@ class ContactsActivity : BaseActivity() {
}
}
}
+
+ private fun observeCreatedRoom() {
+ lifecycleScope.launch {
+ // Only while visible: a conversation created meanwhile is still opened when the screen is started again.
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ contactsViewModel.roomViewState.collect { state ->
+ if (state is ContactsViewModel.RoomUiState.Success) {
+ state.conversation?.token?.let { token ->
+ val chatIntent = ChatActivity.createIntent(this@ContactsActivity, state.userId, token)
+ chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ startActivity(chatIntent)
+ }
+ contactsViewModel.clearRoomState()
+ }
+ }
+ }
+ }
+ }
}
class CompanionClass {
diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt
index e77ec253d4..6d403ea441 100644
--- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt
+++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt
@@ -70,7 +70,7 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsViewMo
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, paddingValues.calculateBottomPadding())
) {
if (!isAddParticipants) {
- ConversationCreationOptions()
+ ConversationCreationOptions(contactsViewModel.currentUser.id!!)
}
ContactsList(
diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt
index 184a65550e..2dfc071829 100644
--- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt
@@ -13,22 +13,22 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUserDto
import com.nextcloud.talk.models.json.conversations.ConversationDto
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import javax.inject.Inject
-class ContactsViewModel @Inject constructor(
+@Suppress("TooManyFunctions")
+class ContactsViewModel @AssistedInject constructor(
private val repository: ContactsRepository,
- private val currentUserProvider: CurrentUserProvider,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted val currentUser: User
) : ViewModel() {
private val _contactsViewState = MutableStateFlow(ContactsUiState.None)
@@ -57,12 +57,10 @@ class ContactsViewModel @Inject constructor(
private var hideAlreadyAddedParticipants: Boolean = false
- private val currentUserFlow: StateFlow =
- currentUserProvider.currentUserFlow
- .stateIn(viewModelScope, SharingStarted.Eagerly, null)
-
- private val currentUser: User?
- get() = currentUserFlow.value
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ContactsViewModel
+ }
companion object {
private val TAG = ContactsViewModel::class.java.simpleName
@@ -114,10 +112,6 @@ class ContactsViewModel @Inject constructor(
@Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams(query: String = "") {
val user = currentUser
- if (user == null) {
- _contactsViewState.value = ContactsUiState.Error("No current user")
- return
- }
_contactsViewState.value = ContactsUiState.Loading
viewModelScope.launch {
try {
@@ -150,10 +144,6 @@ class ContactsViewModel @Inject constructor(
suspend fun getBlockingContactsFromSearchParams(query: String = "") =
withContext(Dispatchers.IO) {
val user = currentUser
- if (user == null) {
- _contactsViewState.value = ContactsUiState.Error("No current user")
- return@withContext
- }
_contactsViewState.value = ContactsUiState.Loading
try {
val contacts = repository.getContacts(
@@ -180,13 +170,16 @@ class ContactsViewModel @Inject constructor(
}
}
+ /**
+ * Resets [roomViewState] once its result was handled, so a recreated screen doesn't handle it again.
+ */
+ fun clearRoomState() {
+ _roomViewState.value = RoomUiState.None
+ }
+
@Suppress("Detekt.TooGenericExceptionCaught")
fun createRoom(roomType: String, sourceType: String?, userId: String, conversationName: String?) {
val user = currentUser
- if (user == null) {
- _roomViewState.value = RoomUiState.Error("No current user")
- return
- }
viewModelScope.launch {
try {
val room = repository.createRoom(
@@ -198,7 +191,7 @@ class ContactsViewModel @Inject constructor(
)
val conversation: ConversationDto? = room.ocs?.data
- _roomViewState.value = RoomUiState.Success(conversation)
+ _roomViewState.value = RoomUiState.Success(user.id!!, conversation)
} catch (exception: Exception) {
logger.e(TAG, "Failed to create room", exception)
_roomViewState.value = RoomUiState.Error(exception.message ?: "")
@@ -207,14 +200,12 @@ class ContactsViewModel @Inject constructor(
}
fun getImageUri(avatarId: String, requestBigSize: Boolean, isDarkMode: Boolean): String =
- currentUser?.let {
- repository.getImageUri(
- it,
- avatarId,
- requestBigSize,
- isDarkMode
- )
- } ?: ""
+ repository.getImageUri(
+ currentUser,
+ avatarId,
+ requestBigSize,
+ isDarkMode
+ )
sealed class ContactsUiState {
data object None : ContactsUiState()
@@ -225,7 +216,7 @@ class ContactsViewModel @Inject constructor(
sealed class RoomUiState {
data object None : RoomUiState()
- data class Success(val conversation: ConversationDto?) : RoomUiState()
+ data class Success(val userId: Long, val conversation: ConversationDto?) : RoomUiState()
data class Error(val message: String) : RoomUiState()
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt
index a37193f260..db41618eae 100644
--- a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt
+++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt
@@ -9,8 +9,6 @@
package com.nextcloud.talk.contacts.components
import android.content.Context
-import android.content.Intent
-import android.os.Bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -36,13 +34,11 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.nextcloud.talk.R
-import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.contacts.CompanionClass
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUserDto
import com.nextcloud.talk.utils.DisplayUtils
-import com.nextcloud.talk.utils.bundle.BundleKeys
@Suppress("LongMethod")
@Composable
@@ -98,15 +94,8 @@ fun ContactItemRow(contact: AutocompleteUserDto, contactsViewModel: ContactsView
}
}
when (roomUiState) {
- is ContactsViewModel.RoomUiState.Success -> {
- val conversation = (roomUiState as ContactsViewModel.RoomUiState.Success).conversation
- val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
- chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- context.startActivity(chatIntent)
- }
+ // Opening the created conversation is handled once by ContactsActivity, not by every row.
+ is ContactsViewModel.RoomUiState.Success -> {}
is ContactsViewModel.RoomUiState.Error -> {
val errorMessage = (roomUiState as ContactsViewModel.RoomUiState.Error).message
Box(
diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt
index a5527eb5f3..f035cf4104 100644
--- a/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt
+++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt
@@ -34,9 +34,10 @@ import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R
import com.nextcloud.talk.conversationcreation.ConversationCreationActivity
import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
+import com.nextcloud.talk.utils.bundle.BundleKeys
@Composable
-fun ConversationCreationOptions() {
+fun ConversationCreationOptions(userId: Long) {
val context = LocalContext.current
Column {
Row(
@@ -44,6 +45,7 @@ fun ConversationCreationOptions() {
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
.clickable {
val intent = Intent(context, ConversationCreationActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, userId)
context.startActivity(intent)
},
verticalAlignment = Alignment.CenterVertically
@@ -70,6 +72,7 @@ fun ConversationCreationOptions() {
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
.clickable {
val intent = Intent(context, ListOpenConversationsActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, userId)
context.startActivity(intent)
},
verticalAlignment = Alignment.CenterVertically
@@ -98,7 +101,7 @@ fun ConversationCreationOptions() {
fun ConversationCreationOptionsPreview() {
MaterialTheme {
Surface {
- ConversationCreationOptions()
+ ConversationCreationOptions(userId = 1L)
}
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt
index e859da295c..89d965aed4 100644
--- a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt
@@ -13,8 +13,8 @@ import androidx.lifecycle.viewModelScope
import autodagger.AutoInjector
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatMessageDto
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -24,9 +24,6 @@ import javax.inject.Inject
class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource: ChatNetworkDataSource) :
ViewModel() {
- @Inject
- lateinit var currentUserProvider: CurrentUserProvider
-
var threadId: String? = null
private val _getContextChatMessagesState =
@@ -34,17 +31,8 @@ class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource
val getContextChatMessagesState: StateFlow = _getContextChatMessagesState
@Suppress("LongParameterList")
- fun getContextForChatMessages(
- credentials: String,
- baseUrl: String,
- token: String,
- threadId: String?,
- messageId: String,
- title: String
- ) {
+ fun getContextForChatMessages(user: User, token: String, threadId: String?, messageId: String, title: String) {
viewModelScope.launch {
- val user = currentUserProvider.getCurrentUser().getOrThrow()
-
if (!user.hasSpreedFeatureCapability("chat-get-context") ||
!user.hasSpreedFeatureCapability("federation-v1")
) {
@@ -53,8 +41,8 @@ class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource
try {
var messages = chatNetworkDataSource.getContextForChatMessage(
- credentials = credentials,
- baseUrl = baseUrl,
+ credentials = user.getCredentials(),
+ baseUrl = user.baseUrl!!,
token = token,
messageId = messageId,
limit = LIMIT,
diff --git a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt
index b2c872b3f8..95b6faa857 100644
--- a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt
@@ -16,6 +16,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
+import androidx.core.os.BundleCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
@@ -28,10 +29,12 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogRenameConversationBinding
import com.nextcloud.talk.events.ConversationsListFetchDataEvent
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
@@ -54,6 +57,7 @@ class RenameConversationDialogFragment : DialogFragment() {
private lateinit var binding: DialogRenameConversationBinding
private lateinit var viewModel: ConversationInfoEditViewModel
+ private lateinit var user: User
private var roomToken = ""
private var initialName = ""
@@ -61,8 +65,10 @@ class RenameConversationDialogFragment : DialogFragment() {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoEditViewModel::class.java]
+ user = BundleCompat.getParcelable(requireArguments(), KEY_USER, User::class.java)!!
roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
initialName = arguments?.getString(INITIAL_NAME)!!
}
@@ -100,7 +106,7 @@ class RenameConversationDialogFragment : DialogFragment() {
val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.isEnabled = false
positiveButton.setOnClickListener {
- viewModel.renameRoom(roomToken, binding.textEdit.text.toString())
+ viewModel.renameRoom(user, roomToken, binding.textEdit.text.toString())
}
themeDialog()
@@ -185,12 +191,14 @@ class RenameConversationDialogFragment : DialogFragment() {
*/
companion object {
private val TAG = RenameConversationDialogFragment::class.java.simpleName
+ private const val KEY_USER = "keyUser"
private const val KEY_ROOM_TOKEN = "keyRoomToken"
private const val INITIAL_NAME = "initialName"
@JvmStatic
- fun newInstance(roomTokenParam: String, initialName: String): RenameConversationDialogFragment {
+ fun newInstance(user: User, roomTokenParam: String, initialName: String): RenameConversationDialogFragment {
val args = Bundle()
+ args.putParcelable(KEY_USER, user)
args.putString(KEY_ROOM_TOKEN, roomTokenParam)
args.putString(INITIAL_NAME, initialName)
val fragment = RenameConversationDialogFragment()
diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt
index e42ac91317..ab6dbd4998 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt
@@ -83,7 +83,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import coil.compose.AsyncImage
@@ -101,6 +100,7 @@ import com.nextcloud.talk.conversationcreation.ui.CreationResultEffect
import com.nextcloud.talk.conversationcreation.ui.ShareCreatedConversation
import com.nextcloud.talk.conversationcreation.ui.openConversation
import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUserDto
@@ -117,42 +117,30 @@ import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ConversationCreationActivity : BaseActivity() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: ConversationCreationViewModel.Factory
+
+ private lateinit var user: User
+
+ private val conversationCreationViewModel by assistedViewModels { viewModelFactory.build(user) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- val conversationCreationViewModel = ViewModelProvider(
- this,
- viewModelFactory
- )[ConversationCreationViewModel::class.java]
+ user = setUpBoundUserOrFinish() ?: return
setContent {
val colorScheme = viewThemeUtils.getColorScheme(this)
val context = LocalContext.current
- val currentUser by conversationCreationViewModel.currentUser.collectAsState()
MaterialTheme(
colorScheme = colorScheme
) {
- val user = currentUser
- if (user == null) {
- LoadingScreen()
- } else {
- val pickImage = remember(user) { PickImage(this@ConversationCreationActivity, user) }
- ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
- }
+ val pickImage = remember(user) { PickImage(this@ConversationCreationActivity, user) }
+ ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
}
}
}
}
-@Composable
-private fun LoadingScreen() {
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
-}
-
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -414,6 +402,10 @@ fun AddParticipants(
.padding(start = 16.dp, bottom = 16.dp)
.clickable {
val intent = Intent(context, ContactsActivity::class.java)
+ intent.putExtra(
+ BundleKeys.KEY_INTERNAL_USER_ID,
+ conversationCreationViewModel.currentUser.id
+ )
intent.putParcelableArrayListExtra(
"selectedParticipants",
participants as ArrayList
@@ -432,12 +424,8 @@ fun AddParticipants(
}
participants.toSet().forEach { participant ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- val avatarUser = conversationCreationViewModel.currentUser.value
- val imageUri = if (avatarUser == null) {
- null
- } else {
- participant.id?.let { avatarUri(avatarUser, it, DisplayUtils.isDarkModeOn(context)) }
- }
+ val avatarUser = conversationCreationViewModel.currentUser
+ val imageUri = participant.id?.let { avatarUri(avatarUser, it, DisplayUtils.isDarkModeOn(context)) }
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, context, errorPlaceholderImage)
AsyncImage(
@@ -461,6 +449,10 @@ fun AddParticipants(
.fillMaxWidth()
.clickable {
val intent = Intent(context, ContactsActivity::class.java)
+ intent.putExtra(
+ BundleKeys.KEY_INTERNAL_USER_ID,
+ conversationCreationViewModel.currentUser.id
+ )
intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
intent.putExtra(
BundleKeys.KEY_ONLY_LOCAL_PARTICIPANTS,
@@ -797,7 +789,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo
onHandled = { conversationCreationViewModel.clearCreationState() }
)
- val currentUser by conversationCreationViewModel.currentUser.collectAsState()
+ val currentUser = conversationCreationViewModel.currentUser
ShareCreatedConversation(
roomToken = createdPublicConversation,
password = password.takeIf { createdWithPassword },
@@ -806,7 +798,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo
onDismiss = { roomToken ->
createdPublicConversation = null
conversationCreationViewModel.clearCreationState()
- openConversation(context, roomToken)
+ currentUser.id?.let { openConversation(context, it, roomToken) }
}
)
diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.kt
index fcc82073ad..e47b3812c5 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ui/CreatedConversation.kt
@@ -10,7 +10,6 @@ package com.nextcloud.talk.conversationcreation.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.os.Bundle
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
@@ -28,7 +27,6 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.ShareUtils
-import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.copyPasswordToClipboard
private val TAG = ConversationCreationActivity::class.java.simpleName
@@ -69,7 +67,7 @@ fun CreationResultEffect(
onHandled()
} else {
onHandled()
- openConversation(context, roomToken)
+ openConversation(context, state.userId, roomToken)
}
}
}
@@ -128,11 +126,8 @@ fun ShareCreatedConversation(
)
}
-fun openConversation(context: Context, roomToken: String) {
- val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+fun openConversation(context: Context, userId: Long, roomToken: String) {
+ val chatIntent = ChatActivity.createIntent(context, userId, roomToken)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(chatIntent)
}
diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt
index f7b9e47bfc..b562a5afd8 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt
@@ -33,30 +33,27 @@ import com.nextcloud.talk.repositories.passwordpolicy.PasswordPolicyRepository
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.SpreedFeatures
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class ConversationCreationViewModel @Inject constructor(
+class ConversationCreationViewModel @AssistedInject constructor(
private val repository: ConversationCreationRepository,
private val conversationCreator: ConversationCreator,
private val passwordPolicyRepository: PasswordPolicyRepository,
- currentUserProvider: CurrentUserProvider
+ @Assisted val currentUser: User
) : ViewModel() {
private val _selectedParticipants = MutableStateFlow>(emptyList())
val selectedParticipants: StateFlow> = _selectedParticipants
private val roomViewState = MutableStateFlow(RoomUIState.None)
val creationState: StateFlow = roomViewState
- val passwordValidation = PasswordPolicyValidator(passwordPolicyRepository, viewModelScope) { currentUser.value }
+ val passwordValidation = PasswordPolicyValidator(passwordPolicyRepository, viewModelScope) { currentUser }
private val passwordGenerator = PasswordGenerator(passwordPolicyRepository)
@@ -72,11 +69,8 @@ class ConversationCreationViewModel @Inject constructor(
private val _isCreatingRoom = MutableStateFlow(false)
val isCreatingRoom: StateFlow = _isCreatingRoom
- val currentUser: StateFlow = currentUserProvider.currentUserFlow
- .stateIn(viewModelScope, SharingStarted.Eagerly, null)
-
private val spreedCapabilities
- get() = currentUser.value?.capabilities?.spreedCapability
+ get() = currentUser.capabilities?.spreedCapability
val showPresetSelection: Boolean
get() = CapabilitiesUtil.hasSpreedFeatureCapability(
@@ -125,7 +119,7 @@ class ConversationCreationViewModel @Inject constructor(
private var presetsJob: Job? = null
val isLoadingPresets: Boolean
- get() = currentUser.value == null || (showPresetSelection && _presets.value is PresetsUiState.Loading)
+ get() = showPresetSelection && _presets.value is PresetsUiState.Loading
val pinnedParameters: Set
get() = (_presets.value as? PresetsUiState.Success)
@@ -135,11 +129,8 @@ class ConversationCreationViewModel @Inject constructor(
.orEmpty()
init {
- viewModelScope.launch {
- currentUser.filterNotNull().first()
- if (showPresetSelection) {
- loadPresets()
- }
+ if (showPresetSelection) {
+ loadPresets()
}
}
@@ -149,7 +140,7 @@ class ConversationCreationViewModel @Inject constructor(
_presets.value = PresetsUiState.Loading
presetsJob = viewModelScope.launch {
try {
- val user = currentUser.filterNotNull().first()
+ val user = currentUser
val presets = repository.getConversationPresets(
ApiUtils.getCredentials(user.username, user.token),
ApiUtils.getUrlForConversationPresets(user.baseUrl)
@@ -218,9 +209,8 @@ class ConversationCreationViewModel @Inject constructor(
parametersChosenByUser.value += ConversationParameter.ROOM_TYPE to roomType
recomputeParams()
- val user = currentUser.value
- if (allow && user != null && isPasswordEnforced && _password.value.isEmpty()) {
- viewModelScope.launch { _password.value = passwordGenerator.generate(user) }
+ if (allow && isPasswordEnforced && _password.value.isEmpty()) {
+ viewModelScope.launch { _password.value = passwordGenerator.generate(currentUser) }
}
}
@@ -252,8 +242,8 @@ class ConversationCreationViewModel @Inject constructor(
@Suppress("Detekt.TooGenericExceptionCaught")
fun createRoomAndAddParticipants() {
- val user = currentUser.value
- if (_isCreatingRoom.value || user == null) {
+ val user = currentUser
+ if (_isCreatingRoom.value) {
return
}
_isCreatingRoom.value = true
@@ -277,7 +267,7 @@ class ConversationCreationViewModel @Inject constructor(
)
val token = conversation?.token
if (!token.isNullOrEmpty()) {
- roomViewState.value = RoomUIState.Success(conversation)
+ roomViewState.value = RoomUIState.Success(user.id!!, conversation)
} else {
roomViewState.value = RoomUIState.Error()
Log.e(TAG, "The created conversation came back without a token")
@@ -298,6 +288,11 @@ class ConversationCreationViewModel @Inject constructor(
roomViewState.value = RoomUIState.None
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ConversationCreationViewModel
+ }
+
companion object {
private val TAG = ConversationCreationViewModel::class.simpleName
}
@@ -311,7 +306,7 @@ sealed interface PresetsUiState {
sealed class RoomUIState {
data object None : RoomUIState()
- data class Success(val conversation: ConversationDto?) : RoomUIState()
+ data class Success(val userId: Long, val conversation: ConversationDto?) : RoomUIState()
data class Error(val serverMessage: String? = null) : RoomUIState()
}
diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
index 255f89e1de..12b8ea0a0e 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
@@ -128,18 +128,17 @@ class ConversationInfoActivity : BaseActivity() {
lateinit var viewModel: ConversationInfoViewModel
private lateinit var conversationToken: String
- private var conversationUser: User? = null
+ private lateinit var conversationUser: User
private lateinit var credentials: String
private var startGroupChat: Boolean = false
private val workerData: Data?
get() {
- val user = conversationUser ?: return null
return if (conversationToken.isNotEmpty()) {
Data.Builder()
.putString(KEY_ROOM_TOKEN, conversationToken)
- .putLong(BundleKeys.KEY_INTERNAL_USER_ID, user.id!!)
+ .putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
.build()
} else {
null
@@ -153,10 +152,9 @@ class ConversationInfoActivity : BaseActivity() {
val selectedAutocompleteUsers =
intent?.getParcelableArrayListExtraProvider("selectedParticipants")
?: emptyList()
- val user = conversationUser ?: return@executeIfResultOk
if (startGroupChat) {
viewModel.createRoomFromOneToOne(
- user,
+ conversationUser,
viewModel.uiState.value.participants.map { it.participant },
selectedAutocompleteUsers,
conversationToken
@@ -171,13 +169,14 @@ class ConversationInfoActivity : BaseActivity() {
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
- conversationUser?.let { viewModel.getRoom(it, conversationToken) }
+ viewModel.getRoom(conversationUser, conversationToken)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ conversationUser = setUpBoundUserOrFinish() ?: return
conversationToken = requireNotNull(
intent.getStringExtra(KEY_ROOM_TOKEN)
@@ -193,20 +192,10 @@ class ConversationInfoActivity : BaseActivity() {
viewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
- lifecycleScope.launch {
- currentUserProvider.getCurrentUser()
- .onSuccess { user ->
- conversationUser = user
- credentials = ApiUtils.getCredentials(user.username, user.token)!!
- viewModel.getRoom(user, conversationToken)
- if (upcomingEventSummary != null || upcomingEventTime != null) {
- viewModel.setUpcomingEvent(upcomingEventSummary, upcomingEventTime)
- }
- }
- .onFailure {
- Log.e(TAG, "Failed to get current user")
- finish()
- }
+ credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!!
+ viewModel.getRoom(conversationUser, conversationToken)
+ if (upcomingEventSummary != null || upcomingEventTime != null) {
+ viewModel.setUpcomingEvent(upcomingEventSummary, upcomingEventTime)
}
setupCompose()
@@ -224,7 +213,7 @@ class ConversationInfoActivity : BaseActivity() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(eventStatus: EventStatus) {
- conversationUser?.let { viewModel.loadParticipants(it, conversationToken) }
+ viewModel.loadParticipants(conversationUser, conversationToken)
}
private fun setupCompose() {
@@ -235,7 +224,6 @@ class ConversationInfoActivity : BaseActivity() {
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
- val user = conversationUser ?: return@collect
when (event) {
is ConversationInfoUiEvent.ShowSnackbar ->
snackbarHostState.showSnackbar(getString(event.resId))
@@ -243,13 +231,15 @@ class ConversationInfoActivity : BaseActivity() {
snackbarHostState.showSnackbar(event.text)
is ConversationInfoUiEvent.NavigateToChat -> {
startActivity(
- Intent(this@ConversationInfoActivity, ChatActivity::class.java).apply {
- putExtra(KEY_ROOM_TOKEN, event.token)
- }
+ ChatActivity.createIntent(
+ this@ConversationInfoActivity,
+ conversationUser.id!!,
+ event.token
+ )
)
}
ConversationInfoUiEvent.RefreshParticipants ->
- viewModel.loadParticipants(user, conversationToken)
+ viewModel.loadParticipants(conversationUser, conversationToken)
}
}
}
@@ -283,20 +273,22 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun onGuestPasswordSave(request: PasswordRequest, password: String, copyAfterSave: Boolean) {
- val user = conversationUser ?: return
if (copyAfterSave) {
copyPassword(password)
}
when (request) {
PasswordRequest.SET -> {
- val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ conversationUser,
+ intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)
+ )
viewModel.setPassword(
- user = user,
- url = ApiUtils.getUrlForRoomPassword(apiVersion, user.baseUrl!!, conversationToken),
+ user = conversationUser,
+ url = ApiUtils.getUrlForRoomPassword(apiVersion, conversationUser.baseUrl!!, conversationToken),
password = password
)
}
- PasswordRequest.ALLOW_GUESTS -> viewModel.allowGuests(user, conversationToken, true, password)
+ PasswordRequest.ALLOW_GUESTS -> viewModel.allowGuests(conversationUser, conversationToken, true, password)
}
}
@@ -322,6 +314,7 @@ class ConversationInfoActivity : BaseActivity() {
onEditConversation = {
editConversationResult.launch(
Intent(this, ConversationInfoEditActivity::class.java).apply {
+ putExtra(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id)
putExtra(KEY_ROOM_TOKEN, conversationToken)
}
)
@@ -329,60 +322,58 @@ class ConversationInfoActivity : BaseActivity() {
onMessageNotificationLevelClick = { showNotificationLevelDialog() },
onCallNotificationsClick = { viewModel.toggleCallNotifications() },
onImportantConversationClick = {
- val user = conversationUser ?: return@ConversationInfoScreenCallbacks
- viewModel.toggleImportantConversation(credentials, user.baseUrl!!, conversationToken)
+ viewModel.toggleImportantConversation(credentials, conversationUser.baseUrl!!, conversationToken)
},
onSensitiveConversationClick = {
- val user = conversationUser ?: return@ConversationInfoScreenCallbacks
- viewModel.toggleSensitiveConversation(credentials, user.baseUrl!!, conversationToken)
+ viewModel.toggleSensitiveConversation(credentials, conversationUser.baseUrl!!, conversationToken)
},
- onLobbyClick = { conversationUser?.let { viewModel.toggleLobby(it, conversationToken) } },
+ onLobbyClick = { viewModel.toggleLobby(conversationUser, conversationToken) },
onLobbyTimerClick = { showLobbyTimerDialog() },
onAllowGuestsClick = {
- val user = conversationUser ?: return@ConversationInfoScreenCallbacks
val state = viewModel.uiState.value
val allow = !state.guestsAllowed
if (allow && isPasswordEnforced() && !state.hasPassword) {
onPasswordRequest(PasswordRequest.ALLOW_GUESTS)
} else {
- viewModel.allowGuests(user, conversationToken, allow)
+ viewModel.allowGuests(conversationUser, conversationToken, allow)
}
},
onPasswordProtectionClick = {
- val user = conversationUser ?: return@ConversationInfoScreenCallbacks
if (!viewModel.uiState.value.hasPassword) {
onPasswordRequest(PasswordRequest.SET)
} else {
val apiVersion =
- ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
+ ApiUtils.getConversationApiVersion(
+ conversationUser,
+ intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)
+ )
viewModel.setPassword(
- user = user,
- url = ApiUtils.getUrlForRoomPassword(apiVersion, user.baseUrl!!, conversationToken),
+ user = conversationUser,
+ url = ApiUtils.getUrlForRoomPassword(apiVersion, conversationUser.baseUrl!!, conversationToken),
password = ""
)
}
},
onResendInvitationsClick = {
- conversationUser?.let { viewModel.resendInvitations(it, conversationToken) }
+ viewModel.resendInvitations(conversationUser, conversationToken)
},
onSharedItemsClick = { showSharedItems() },
onThreadsClick = { openThreadsOverview() },
onRecordingConsentClick = {
- conversationUser?.let { viewModel.toggleRecordingConsent(it, conversationToken) }
+ viewModel.toggleRecordingConsent(conversationUser, conversationToken)
},
onMessageExpirationClick = { showMessageExpirationDialog() },
onShareConversationClick = {
- val user = conversationUser ?: return@ConversationInfoScreenCallbacks
val state = viewModel.uiState.value
ShareUtils.shareConversationLink(
this,
- user.baseUrl,
+ conversationUser.baseUrl,
state.conversationToken,
- CapabilitiesUtil.canGeneratePrettyURL(user)
+ CapabilitiesUtil.canGeneratePrettyURL(conversationUser)
)
},
onLockConversationClick = {
- conversationUser?.let { viewModel.toggleLock(it, conversationToken) }
+ viewModel.toggleLock(conversationUser, conversationToken)
},
onParticipantClick = { model -> handleParticipantClick(model) },
onParticipantOpsDismiss = { viewModel.setParticipantForOps(null) },
@@ -396,7 +387,7 @@ class ConversationInfoActivity : BaseActivity() {
selectParticipantsToAdd()
},
onListBansClick = { listBans() },
- onArchiveClick = { conversationUser?.let { viewModel.toggleArchive(it, conversationToken) } },
+ onArchiveClick = { viewModel.toggleArchive(conversationUser, conversationToken) },
onLeaveConversationClick = { leaveConversation() },
onClearHistoryClick = { showClearHistoryDialog() },
onDeleteConversationClick = { showDeleteConversationDialog() }
@@ -406,6 +397,7 @@ class ConversationInfoActivity : BaseActivity() {
val conv = viewModel.uiState.value.conversation ?: return
startActivity(
Intent(this, SharedItemsActivity::class.java).apply {
+ putExtra(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id)
putExtra(BundleKeys.KEY_CONVERSATION_NAME, conv.displayName)
putExtra(KEY_ROOM_TOKEN, conversationToken)
putExtra(
@@ -421,14 +413,14 @@ class ConversationInfoActivity : BaseActivity() {
}
fun openThreadsOverview() {
- val user = conversationUser ?: return
val threadsUrl = ApiUtils.getUrlForRecentThreads(
version = 1,
- baseUrl = user.baseUrl,
+ baseUrl = conversationUser.baseUrl,
token = conversationToken
)
startActivity(
Intent(context, ThreadsOverviewActivity::class.java).apply {
+ putExtra(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id)
putExtra(KEY_ROOM_TOKEN, conversationToken)
putExtra(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.recent_threads))
putExtra(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl)
@@ -437,7 +429,6 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun showLobbyTimerDialog() {
- val user = conversationUser ?: return
val currentLobbyTimer = viewModel.uiState.value.conversation?.lobbyTimer ?: 0L
MaterialDialog(this, BottomSheet(WRAP_CONTENT)).show {
val cal = Calendar.getInstance()
@@ -445,7 +436,7 @@ class ConversationInfoActivity : BaseActivity() {
datePicker { _, date ->
showTimePicker(date) { selectedDate ->
viewModel.setLobbyTimerAndSubmit(
- user,
+ conversationUser,
conversationToken,
selectedDate.timeInMillis / DateConstants.SECOND_DIVIDER
)
@@ -514,7 +505,7 @@ class ConversationInfoActivity : BaseActivity() {
private fun listBans() {
val transaction = supportFragmentManager.beginTransaction()
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
- transaction.add(android.R.id.content, DialogBanListFragment(conversationToken))
+ transaction.add(android.R.id.content, DialogBanListFragment.newInstance(conversationToken, conversationUser))
.addToBackStack(null)
.commit()
}
@@ -540,6 +531,7 @@ class ConversationInfoActivity : BaseActivity() {
}
addParticipantsResult.launch(
Intent(this, ContactsActivity::class.java).apply {
+ putExtra(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id)
putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
putParcelableArrayListExtra("selectedParticipants", existingParticipants)
putExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, true)
@@ -550,7 +542,6 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun addParticipantsToConversation(autocompleteUsers: List) {
- val user = conversationUser ?: return
val groupIds = mutableSetOf()
val emailIds = mutableSetOf()
val circleIds = mutableSetOf()
@@ -566,7 +557,7 @@ class ConversationInfoActivity : BaseActivity() {
}
val data = Data.Builder()
- .putLong(BundleKeys.KEY_INTERNAL_USER_ID, user.id!!)
+ .putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
.putString(BundleKeys.KEY_TOKEN, conversationToken)
.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIds.toTypedArray())
.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIds.toTypedArray())
@@ -585,7 +576,7 @@ class ConversationInfoActivity : BaseActivity() {
.observe(this) { workInfo: WorkInfo? ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> {
- viewModel.loadParticipants(user, conversationToken)
+ viewModel.loadParticipants(conversationUser, conversationToken)
reportParticipantsNotAdded(workInfo.outputData, names)
}
WorkInfo.State.FAILED -> reportParticipantsNotAdded(workInfo.outputData, names)
@@ -625,7 +616,7 @@ class ConversationInfoActivity : BaseActivity() {
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
- conversationUser?.id?.let { userId ->
+ conversationUser.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
context,
conversationToken,
@@ -678,10 +669,12 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun clearHistory() {
- val user = conversationUser ?: return
val caps = viewModel.uiState.value.spreedCapabilities ?: return
val apiVersion = ApiUtils.getChatApiVersion(caps, intArrayOf(1))
- viewModel.clearChatHistory(user, ApiUtils.getUrlForChat(apiVersion, user.baseUrl!!, conversationToken))
+ viewModel.clearChatHistory(
+ conversationUser,
+ ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl!!, conversationToken)
+ )
}
private fun deleteConversation() {
@@ -693,7 +686,7 @@ class ConversationInfoActivity : BaseActivity() {
.build()
)
- conversationUser?.id?.let { userId ->
+ conversationUser.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
context,
conversationToken,
@@ -709,14 +702,13 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun toggleModeratorStatus(apiVersion: Int, participant: ParticipantDto) {
- val user = conversationUser ?: return
val subscriber = participantActionObserver()
if (participant.type == ParticipantDto.ParticipantType.MODERATOR ||
participant.type == ParticipantDto.ParticipantType.GUEST_MODERATOR
) {
ncApi.demoteAttendeeFromModerator(
credentials,
- ApiUtils.getUrlForRoomModerators(apiVersion, user.baseUrl!!, conversationToken),
+ ApiUtils.getUrlForRoomModerators(apiVersion, conversationUser.baseUrl!!, conversationToken),
participant.attendeeId,
null
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(subscriber)
@@ -725,7 +717,7 @@ class ConversationInfoActivity : BaseActivity() {
) {
ncApi.promoteAttendeeToModerator(
credentials,
- ApiUtils.getUrlForRoomModerators(apiVersion, user.baseUrl!!, conversationToken),
+ ApiUtils.getUrlForRoomModerators(apiVersion, conversationUser.baseUrl!!, conversationToken),
participant.attendeeId,
null
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(subscriber)
@@ -733,18 +725,17 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun toggleModeratorStatusLegacy(apiVersion: Int, participant: ParticipantDto) {
- val user = conversationUser ?: return
val subscriber = participantActionObserver()
if (participant.type == ParticipantDto.ParticipantType.MODERATOR) {
ncApi.demoteModeratorToUser(
credentials,
- ApiUtils.getUrlForRoomModerators(apiVersion, user.baseUrl!!, conversationToken),
+ ApiUtils.getUrlForRoomModerators(apiVersion, conversationUser.baseUrl!!, conversationToken),
participant.userId
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(subscriber)
} else if (participant.type == ParticipantDto.ParticipantType.USER) {
ncApi.promoteUserToModerator(
credentials,
- ApiUtils.getUrlForRoomModerators(apiVersion, user.baseUrl!!, conversationToken),
+ ApiUtils.getUrlForRoomModerators(apiVersion, conversationUser.baseUrl!!, conversationToken),
participant.userId
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(subscriber)
}
@@ -754,7 +745,7 @@ class ConversationInfoActivity : BaseActivity() {
object : Observer {
override fun onSubscribe(d: Disposable) { /* unused */ }
override fun onNext(genericOverall: GenericOverall) {
- conversationUser?.let { viewModel.loadParticipants(it, conversationToken) }
+ viewModel.loadParticipants(conversationUser, conversationToken)
}
@SuppressLint("LongLogTag")
@@ -766,12 +757,11 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun removeAttendeeFromConversation(apiVersion: Int, participant: ParticipantDto) {
- val user = conversationUser ?: return
val observer = participantActionObserver()
if (apiVersion >= ApiUtils.API_V4) {
ncApi.removeAttendeeFromConversation(
credentials,
- ApiUtils.getUrlForAttendees(apiVersion, user.baseUrl!!, conversationToken),
+ ApiUtils.getUrlForAttendees(apiVersion, conversationUser.baseUrl!!, conversationToken),
participant.attendeeId
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(observer)
} else {
@@ -780,13 +770,21 @@ class ConversationInfoActivity : BaseActivity() {
) {
ncApi.removeParticipantFromConversation(
credentials,
- ApiUtils.getUrlForRemovingParticipantFromConversation(user.baseUrl!!, conversationToken, true),
+ ApiUtils.getUrlForRemovingParticipantFromConversation(
+ conversationUser.baseUrl!!,
+ conversationToken,
+ true
+ ),
participant.sessionId
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(observer)
} else {
ncApi.removeParticipantFromConversation(
credentials,
- ApiUtils.getUrlForRemovingParticipantFromConversation(user.baseUrl!!, conversationToken, false),
+ ApiUtils.getUrlForRemovingParticipantFromConversation(
+ conversationUser.baseUrl!!,
+ conversationToken,
+ false
+ ),
participant.userId
)?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(observer)
}
@@ -794,7 +792,7 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun banActor(actorType: String, actorId: String, internalNote: String) {
- conversationUser?.let { viewModel.banActor(it, conversationToken, actorType, actorId, internalNote) }
+ viewModel.banActor(conversationUser, conversationToken, actorType, actorId, internalNote)
}
private fun handleParticipantClick(model: ParticipantModel) {
@@ -807,9 +805,8 @@ class ConversationInfoActivity : BaseActivity() {
val conv = state.conversation ?: return
val caps = state.spreedCapabilities ?: return
if (!ConversationUtils.canModerate(conv, caps)) return
- val user = conversationUser ?: return
val participant = model.participant
- val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1))
+ val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
when (action) {
ParticipantOpsAction.PromoteToModerator,
ParticipantOpsAction.DemoteFromModerator ->
@@ -866,8 +863,7 @@ class ConversationInfoActivity : BaseActivity() {
promote: Boolean,
participantType: Int
) {
- val user = conversationUser ?: return
- val url = ApiUtils.getUrlForRoomModerators(apiVersion, user.baseUrl!!, conversationToken)
+ val url = ApiUtils.getUrlForRoomModerators(apiVersion, conversationUser.baseUrl!!, conversationToken)
val call = if (promote) {
ncApi.promoteAttendeeToModerator(credentials, url, participant.attendeeId, participantType)
} else {
@@ -879,12 +875,11 @@ class ConversationInfoActivity : BaseActivity() {
}
private fun handleBan(participant: ParticipantDto) {
- val user = conversationUser ?: return
- val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1))
+ val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
val dialogBinding = DialogBanParticipantBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(this).setView(dialogBinding.root).create()
dialogBinding.avatarImage.loadUserAvatar(
- user,
+ conversationUser,
participant.actorId!!,
requestBigSize = true,
ignoreCache = false
diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt
index 529728d890..52a24a0cba 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt
@@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource
import androidx.core.net.toFile
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import com.github.dhaval2404.imagepicker.ImagePicker
import com.nextcloud.talk.activities.BaseActivity
@@ -33,9 +32,6 @@ import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditVie
import com.nextcloud.talk.utils.PickImage
import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.launch
@AutoInjector(NextcloudTalkApplication::class)
class ConversationInfoEditActivity : BaseActivity() {
@@ -75,26 +71,17 @@ class ConversationInfoEditActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ val user = setUpBoundUserOrFinish() ?: return
+ // Created right away, so results of the camera or an image picker are handled even when the activity was
+ // recreated after process death and the conversation is still loading.
+ pickImage = PickImage(this, user)
val roomToken = intent.extras?.getString(BundleKeys.KEY_ROOM_TOKEN)!!
conversationInfoEditViewModel =
ViewModelProvider(this, viewModelFactory)[ConversationInfoEditViewModel::class.java]
- conversationInfoEditViewModel.initialize(roomToken)
-
- conversationInfoEditViewModel.uiState.value.conversationUser?.let { user ->
- pickImage = PickImage(this, user)
- }
-
- if (pickImage == null) {
- lifecycleScope.launch {
- val user = conversationInfoEditViewModel.uiState
- .mapNotNull { it.conversationUser }
- .first()
- pickImage = PickImage(this@ConversationInfoEditActivity, user)
- }
- }
+ conversationInfoEditViewModel.initialize(user, roomToken)
setupCompose()
}
diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt
index 1f17dc70d4..944b1a3339 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt
@@ -16,7 +16,6 @@ import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -27,7 +26,6 @@ import javax.inject.Inject
class ConversationInfoEditViewModel @Inject constructor(
private val conversationInfoEditRepository: ConversationInfoEditRepository,
- private val currentUserProvider: CurrentUserProvider,
private val logger: Logger
) : ViewModel() {
@@ -38,9 +36,10 @@ class ConversationInfoEditViewModel @Inject constructor(
private var initialized = false
private var currentUser: User? = null
- fun initialize(token: String) {
+ fun initialize(user: User, token: String) {
if (initialized) return
initialized = true
+ currentUser = user
roomToken = token
loadRoom()
}
@@ -66,8 +65,7 @@ class ConversationInfoEditViewModel @Inject constructor(
_uiState.update { it.copy(isLoading = true) }
viewModelScope.launch {
try {
- val user = currentUserProvider.getCurrentUser().getOrThrow()
- currentUser = user
+ val user = checkNotNull(currentUser)
val roomData = conversationInfoEditRepository.getRoom(user, roomToken)
val conversationModel = roomData.conversation
val isEvent = conversationModel.objectType == ConversationEnums.ObjectType.EVENT
@@ -178,10 +176,9 @@ class ConversationInfoEditViewModel @Inject constructor(
}
@Suppress("Detekt.TooGenericExceptionCaught")
- fun renameRoom(token: String, newName: String) {
+ fun renameRoom(user: User, token: String, newName: String) {
viewModelScope.launch {
try {
- val user = currentUser ?: currentUserProvider.getCurrentUser().getOrThrow()
conversationInfoEditRepository.renameConversation(user, token, newName)
_uiState.update { it.copy(navigateBack = true) }
} catch (exception: Exception) {
diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
index d27efe8209..645dcc9c25 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
@@ -11,6 +11,7 @@ import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.ContentResolver
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -30,10 +31,10 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -79,7 +80,6 @@ import com.nextcloud.talk.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.MENTION
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.UNREAD
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.BrandingUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
@@ -130,8 +130,8 @@ import javax.inject.Inject
@Suppress("LargeClass", "TooManyFunctions", "NestedBlockDepth")
class ConversationsListActivity : BaseActivity() {
- @Inject
- lateinit var userManager: UserManager
+ // Started without an account on app start, when sharing from other apps and after login.
+ override val allowsDefaultAccount = true
@Inject
lateinit var ncApiCoroutines: NcApiCoroutines
@@ -140,19 +140,28 @@ class ConversationsListActivity : BaseActivity() {
lateinit var platformPermissionUtil: PlatformPermissionUtil
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var conversationsListViewModelFactory: ConversationsListViewModel.Factory
+
+ @Inject
+ lateinit var conversationTagsViewModelFactory: ConversationTagsViewModel.Factory
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
- lateinit var contactsViewModel: ContactsViewModel
+ lateinit var contactsViewModelFactory: ContactsViewModel.Factory
- lateinit var conversationsListViewModel: ConversationsListViewModel
+ val contactsViewModel: ContactsViewModel by assistedViewModels { contactsViewModelFactory.build(currentUser) }
- lateinit var conversationTagsViewModel: ConversationTagsViewModel
+ val conversationsListViewModel: ConversationsListViewModel by assistedViewModels {
+ conversationsListViewModelFactory.build(currentUser)
+ }
- private var currentUser: User? = null
+ val conversationTagsViewModel: ConversationTagsViewModel by assistedViewModels {
+ conversationTagsViewModelFactory.build(currentUser)
+ }
+
+ private lateinit var currentUser: User
private val snackbarHostState = SnackbarHostState()
private val isMaintenanceModeState = MutableStateFlow(false)
private val showUnreadBubbleState = MutableStateFlow(false)
@@ -196,18 +205,14 @@ class ConversationsListActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ currentUser = setUpBoundUserOrFinish() ?: return
ecosystemManager = EcosystemManager(this@ConversationsListActivity)
- val targetUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
- currentUser = if (targetUserId != 0L) {
- runBlocking { userManager.getUserWithId(targetUserId) }!!
- } else {
- currentUserProviderOld.currentUser.blockingGet()
+ if (!currentUser.current) {
+ // The shown account becomes the last used one, which the account switcher and status views rely on.
+ lifecycleScope.launch { userManager.setUserAsActive(currentUser) }
}
- conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java]
- conversationTagsViewModel = ViewModelProvider(this, viewModelFactory)[ConversationTagsViewModel::class.java]
-
setSupportActionBar(null)
forwardMessageState.value = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false)
if (savedInstanceState != null) {
@@ -237,6 +242,12 @@ class ConversationsListActivity : BaseActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
+ val newUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
+ if (newUserId != 0L && newUserId != currentUser.id) {
+ relaunchForAccount(newUserId, intent)
+ return
+ }
+ currentUser.id?.let { intent.putExtra(KEY_INTERNAL_USER_ID, it) }
setIntent(intent)
forwardMessageState.value = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false)
conversationsListViewModel.setHideRoomToken(intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM))
@@ -310,12 +321,20 @@ class ConversationsListActivity : BaseActivity() {
bundle.putBoolean(KEY_SCROLL_TO_NOTIFICATION_CATEGORY, true)
val settingsIntent = Intent(context, SettingsActivity::class.java)
settingsIntent.putExtras(bundle)
+ settingsIntent.putExtra(KEY_INTERNAL_USER_ID, currentUser.id)
startActivity(settingsIntent)
},
- onFederationHintClick = { startActivity(Intent(context, InvitationsActivity::class.java)) },
+ onFederationHintClick = {
+ val intent = Intent(context, InvitationsActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, currentUser.id)
+ startActivity(intent)
+ },
onFilterClick = {
FilterConversationFragment
- .newInstance(conversationsListViewModel.filterStateFlow.value.toMutableMap())
+ .newInstance(
+ conversationsListViewModel.filterStateFlow.value.toMutableMap(),
+ conversationsListViewModel.currentUser
+ )
.show(supportFragmentManager, FilterConversationFragment.TAG)
},
onThreadsClick = { openFollowedThreadsOverview() },
@@ -323,12 +342,14 @@ class ConversationsListActivity : BaseActivity() {
if (resources.getBoolean(R.bool.multiaccount_support)) {
showChooseAccountDialog()
} else {
- startActivity(Intent(context, SettingsActivity::class.java))
+ val settingsIntent = Intent(context, SettingsActivity::class.java)
+ settingsIntent.putExtra(KEY_INTERNAL_USER_ID, currentUser.id)
+ startActivity(settingsIntent)
}
},
onNavigateBack = { onBackPressedDispatcher.onBackPressed() },
onAccountChooserClick = {
- ChooseAccountShareToDialogFragment.newInstance()
+ ChooseAccountShareToDialogFragment.newInstance(currentUser)
.show(supportFragmentManager, ChooseAccountShareToDialogFragment.TAG)
},
onNewConversation = { showNewConversationsScreen() },
@@ -350,10 +371,8 @@ class ConversationsListActivity : BaseActivity() {
val user = users.firstOrNull { user ->
user.username == trimmedAccountName && baseUrl == user.baseUrl?.toUri()?.host
}
- if (user != null) {
- userManager.setUserAsActive(user)
- val intent = Intent(context, ConversationsListActivity::class.java)
- startActivity(intent)
+ if (user != null && userManager.setUserAsActive(user)) {
+ startActivity(createAccountSwitchIntent(context, user.id!!))
} else {
showSnackbar(getString(R.string.nc_no_account_found))
}
@@ -390,6 +409,18 @@ class ConversationsListActivity : BaseActivity() {
return usesPush
}
+ override fun onRestart() {
+ super.onRestart()
+ // The list shows the default account. If another account became the default while the list was in the
+ // background (e.g. by joining a call for it), switch to that account. Not while forwarding or sharing, which
+ // would lose the pending message.
+ val defaultUserId = defaultAccountProvider.getDefaultUserBlocking()?.id ?: return
+ val isPickingConversation = forwardMessage || hasActivityActionSendIntent()
+ if (defaultUserId != currentUser.id && !isPickingConversation) {
+ startActivity(createAccountSwitchIntent(this, defaultUserId))
+ }
+ }
+
override fun onResume() {
super.onResume()
@@ -415,27 +446,22 @@ class ConversationsListActivity : BaseActivity() {
eventBus.register(this)
}
- if (currentUser != null) {
- if (isServerEOL(currentUser!!.serverVersion?.major)) {
- showServerEOLDialog()
- return
- }
- credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+ if (isServerEOL(currentUser.serverVersion?.major)) {
+ showServerEOLDialog()
+ return
+ }
+ credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
- if (currentUser!!.id != appPreferences.getConversationListLastUserId()) {
- appPreferences.setConversationListPositionAndOffset(0, 0)
- }
+ if (currentUser.id != appPreferences.getConversationListLastUserId()) {
+ appPreferences.setConversationListPositionAndOffset(0, 0)
+ }
- lifecycleScope.launch {
- hasMultipleAccountsState.value = userManager.getUsers().size > 1
- }
- conversationsListViewModel.setHideRoomToken(intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM))
- fetchRooms()
- fetchPendingInvitations()
- } else {
- logger.e(TAG, "currentUser was null")
- showSnackbar(getString(R.string.nc_common_error_sorry))
+ lifecycleScope.launch {
+ hasMultipleAccountsState.value = userManager.getUsers().size > 1
}
+ conversationsListViewModel.setHideRoomToken(intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM))
+ fetchRooms()
+ fetchPendingInvitations()
conversationsListViewModel.checkIfThreadsExist()
conversationsListViewModel.reloadFilterFromStorage(UserIdUtils.getIdForUser(currentUser))
@@ -448,7 +474,7 @@ class ConversationsListActivity : BaseActivity() {
val firstOffset = state.layoutInfo.visibleItemsInfo.firstOrNull()?.offset ?: 0
appPreferences.setConversationListPositionAndOffset(state.firstVisibleItemIndex, firstOffset)
}
- appPreferences.setConversationListLastUserId(currentUser?.id ?: -1L)
+ appPreferences.setConversationListLastUserId(currentUser.id ?: -1L)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@@ -482,14 +508,8 @@ class ConversationsListActivity : BaseActivity() {
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
// Update Direct Share targets
- if (currentUser != null) {
- lifecycleScope.launch {
- DirectShareHelper.publishShareTargetShortcuts(
- context,
- currentUser!!,
- list
- )
- }
+ lifecycleScope.launch {
+ DirectShareHelper.publishShareTargetShortcuts(context, currentUser, list)
}
// check for Direct Share
@@ -504,9 +524,7 @@ class ConversationsListActivity : BaseActivity() {
}
// Update dynamic shortcuts for frequent/favorite conversations
- currentUser?.let { user ->
- ShortcutManagerHelper.updateDynamicShortcuts(context, list, user)
- }
+ ShortcutManagerHelper.updateDynamicShortcuts(context, list, currentUser)
if (!scrollPositionRestored) {
scrollPositionRestored = true
@@ -522,13 +540,12 @@ class ConversationsListActivity : BaseActivity() {
contactsViewModel.roomViewState.onEach { state ->
when (state) {
is ContactsViewModel.RoomUiState.Success -> {
- val conversation = state.conversation
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, conversation?.token)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
- chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- startActivity(chatIntent)
+ state.conversation?.token?.let { token ->
+ val chatIntent = ChatActivity.createIntent(context, state.userId, token)
+ chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ startActivity(chatIntent)
+ }
+ contactsViewModel.clearRoomState()
}
else -> {}
@@ -599,11 +616,8 @@ class ConversationsListActivity : BaseActivity() {
private fun handleNoteToSelfShortcut(noteToSelfAvailable: Boolean, noteToSelfToken: String) {
if (noteToSelfAvailable) {
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, noteToSelfToken)
- bundle.putBoolean(BundleKeys.KEY_FOCUS_INPUT, true)
- val intent = Intent(context, ChatActivity::class.java)
- intent.putExtras(bundle)
+ val intent = ChatActivity.createIntent(context, currentUser.id!!, noteToSelfToken)
+ intent.putExtra(BundleKeys.KEY_FOCUS_INPUT, true)
intent.action = Intent.ACTION_VIEW
val openNotesString = resources.getString(R.string.open_notes)
@@ -646,8 +660,7 @@ class ConversationsListActivity : BaseActivity() {
return
}
val threadId = result.threadId?.toLongOrNull()?.takeIf { it > 0L }
- val intent = Intent(context, ChatActivity::class.java).apply {
- putExtra(KEY_ROOM_TOKEN, result.conversationToken)
+ val intent = ChatActivity.createIntent(context, currentUser.id!!, result.conversationToken).apply {
putExtra(BundleKeys.KEY_MESSAGE_ID, messageId)
putExtra(BundleKeys.KEY_SEARCH_QUERY, result.searchTerm)
threadId?.let { putExtra(BundleKeys.KEY_THREAD_ID, it) }
@@ -663,6 +676,28 @@ class ConversationsListActivity : BaseActivity() {
showAccountDialogState.value = true
}
+ /**
+ * Replaces this list by a new one for the account with the internal id [userId], e.g. for an intent for another
+ * account or to continue a pending share with another account.
+ *
+ * The list is bound to its account, so it is not recreated, which would keep the account and its view models.
+ * The new list gets the action, data and extras of [source], including shared content and the read access to
+ * shared files, which ends with this activity. The activity is exported, so [source] can come from another app:
+ * the new intent only targets this activity and takes over no other flags.
+ */
+ fun relaunchForAccount(userId: Long, source: Intent = intent) {
+ val accountIntent = Intent(this, ConversationsListActivity::class.java).apply {
+ action = source.action
+ setDataAndType(source.data, source.type)
+ source.extras?.let { putExtras(it) }
+ clipData = source.clipData
+ putExtra(KEY_INTERNAL_USER_ID, userId)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(accountIntent)
+ finish()
+ }
+
private fun hasActivityActionSendIntent(): Boolean =
Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action
@@ -671,11 +706,11 @@ class ConversationsListActivity : BaseActivity() {
}
fun fetchRooms() {
- conversationsListViewModel.getRooms(currentUser!!)
+ conversationsListViewModel.getRooms(currentUser)
}
private fun fetchPendingInvitations() {
- if (hasSpreedFeatureCapability(currentUser?.capabilities?.spreedCapability, SpreedFeatures.FEDERATION_V1)) {
+ if (hasSpreedFeatureCapability(currentUser.capabilities?.spreedCapability, SpreedFeatures.FEDERATION_V1)) {
conversationsListViewModel.getFederationInvitations()
}
}
@@ -780,6 +815,7 @@ class ConversationsListActivity : BaseActivity() {
private fun showNewConversationsScreen() {
val intent = Intent(context, ContactsActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, currentUser.id)
startActivity(intent)
}
@@ -792,7 +828,7 @@ class ConversationsListActivity : BaseActivity() {
selectedConversation = conversation
if (selectedConversation != null) {
val hasChatPermission = ParticipantPermissions(
- currentUser?.capabilities?.spreedCapability,
+ currentUser.capabilities?.spreedCapability,
selectedConversation!!
)
.hasChatPermission()
@@ -822,11 +858,11 @@ class ConversationsListActivity : BaseActivity() {
private fun shouldShowLobby(conversation: ConversationModel): Boolean {
val participantPermissions = ParticipantPermissions(
- currentUser?.capabilities?.spreedCapability,
+ currentUser.capabilities?.spreedCapability,
selectedConversation!!
)
return conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
- !ConversationUtils.canModerate(conversation, currentUser?.capabilities?.spreedCapability) &&
+ !ConversationUtils.canModerate(conversation, currentUser.capabilities?.spreedCapability) &&
!participantPermissions.canIgnoreLobby()
}
@@ -1037,7 +1073,7 @@ class ConversationsListActivity : BaseActivity() {
val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this)
val serverNotificationAppInstalled =
- currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true
+ currentUser.capabilities?.notificationsCapability?.features?.isNotEmpty() == true
val settingsOfUserAreWrong = notificationPermissionNotGranted ||
batteryOptimizationNotIgnored ||
@@ -1058,12 +1094,13 @@ class ConversationsListActivity : BaseActivity() {
private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
selectedConversation?.let {
- val pp = ParticipantPermissions(currentUser?.capabilities?.spreedCapability, it)
+ val pp = ParticipantPermissions(currentUser.capabilities?.spreedCapability, it)
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, it.token)
+ bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
- bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, currentUser?.baseUrl!!)
+ bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, currentUser.baseUrl!!)
bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
bundle.putInt(KEY_RECORDING_STATE, it.callRecording)
bundle.putBoolean(KEY_IS_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(it))
@@ -1099,30 +1136,26 @@ class ConversationsListActivity : BaseActivity() {
private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
selectedConversation?.let {
val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token)
bundle.putString(KEY_SHARED_TEXT, textToPaste)
if (selectedMessageId != null) {
bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId)
selectedMessageId = null
}
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = ChatActivity.createIntent(context, currentUser.id!!, it.token, bundle)
- if (currentUser != null) {
- if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(currentUser?.capabilities?.spreedCapability)) {
- showSnackbar(context.getString(R.string.nc_call_e2ee_not_supported))
- return@let
- }
- val pp = ParticipantPermissions(currentUser?.capabilities?.spreedCapability, it)
- if (!pp.canStartCall() && selectedConversation?.hasCall == false) {
- Log.e(TAG, "Error starting call from conversations list: call is forbidden")
- } else {
- ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
- val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
- if (callIntent != null) {
- startActivities(arrayOf(chatIntent, callIntent))
- }
+ if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(currentUser.capabilities?.spreedCapability)) {
+ showSnackbar(context.getString(R.string.nc_call_e2ee_not_supported))
+ return@let
+ }
+ val pp = ParticipantPermissions(currentUser.capabilities?.spreedCapability, it)
+ if (!pp.canStartCall() && selectedConversation?.hasCall == false) {
+ Log.e(TAG, "Error starting call from conversations list: call is forbidden")
+ } else {
+ ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
+ val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
+ if (callIntent != null) {
+ startActivities(arrayOf(chatIntent, callIntent))
}
}
}
@@ -1130,14 +1163,13 @@ class ConversationsListActivity : BaseActivity() {
private fun openConversation(textToPaste: String? = "") {
if (CallActivity.active &&
- selectedConversation!!.token != ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken
+ !ApplicationWideCurrentRoomHolder.getInstance().isCurrentRoom(selectedConversation!!.token, currentUser.id)
) {
showSnackbar(context.getString(R.string.restrict_join_other_room_while_call))
return
}
val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token)
bundle.putString(KEY_SHARED_TEXT, textToPaste)
if (!filesToShare.isNullOrEmpty()) {
bundle.putStringArrayList(BundleKeys.KEY_SHARED_FILE_PATHS, filesToShare)
@@ -1147,8 +1179,7 @@ class ConversationsListActivity : BaseActivity() {
selectedMessageId = null
}
- val intent = Intent(context, ChatActivity::class.java)
- intent.putExtras(bundle)
+ val intent = ChatActivity.createIntent(context, currentUser.id!!, selectedConversation!!.token, bundle)
startActivity(intent)
clearIntentAction()
@@ -1156,7 +1187,7 @@ class ConversationsListActivity : BaseActivity() {
@Subscribe(sticky = true, threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(eventStatus: EventStatus) {
- if (currentUser != null && eventStatus.userId == currentUser!!.id) {
+ if (eventStatus.userId == currentUser.id) {
when (eventStatus.eventType) {
EventStatus.EventType.CONVERSATION_UPDATE -> if (eventStatus.isAllGood && !isRefreshingState.value) {
fetchRooms()
@@ -1192,18 +1223,17 @@ class ConversationsListActivity : BaseActivity() {
}
private fun shareConversationLink(conversation: ConversationModel) {
- val canGeneratePrettyURL = CapabilitiesUtil.canGeneratePrettyURL(currentUser!!)
+ val canGeneratePrettyURL = CapabilitiesUtil.canGeneratePrettyURL(currentUser)
ShareUtils.shareConversationLink(
this,
- currentUser?.baseUrl,
+ currentUser.baseUrl,
conversation.token,
canGeneratePrettyURL
)
}
private fun addConversationToHomeScreen(conversation: ConversationModel) {
- val user = currentUser ?: return
- val success = ShortcutManagerHelper.requestPinShortcut(this, conversation, user)
+ val success = ShortcutManagerHelper.requestPinShortcut(this, conversation, currentUser)
if (success) {
showSnackbar(resources.getString(R.string.nc_shortcut_created))
} else {
@@ -1235,7 +1265,7 @@ class ConversationsListActivity : BaseActivity() {
private fun renameConversation(conversation: ConversationModel) {
if (!TextUtils.isEmpty(conversation.token)) {
RenameConversationDialogFragment
- .newInstance(conversation.token!!, conversation.displayName!!)
+ .newInstance(currentUser, conversation.token!!, conversation.displayName!!)
.show(supportFragmentManager, RenameConversationDialogFragment::class.simpleName)
}
}
@@ -1267,7 +1297,7 @@ class ConversationsListActivity : BaseActivity() {
val token = conversation.token ?: return
val data = Data.Builder()
.putString(KEY_ROOM_TOKEN, token)
- .putLong(KEY_INTERNAL_USER_ID, currentUser?.id!!)
+ .putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
.build()
val worker = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java)
.setInputData(data)
@@ -1277,7 +1307,7 @@ class ConversationsListActivity : BaseActivity() {
WorkManager.getInstance(this).getWorkInfoByIdLiveData(worker.id).observeForever { workInfo ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> {
- currentUser?.id?.let { userId ->
+ currentUser.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
this,
token,
@@ -1360,8 +1390,9 @@ class ConversationsListActivity : BaseActivity() {
.setNegativeButton(R.string.nc_settings_reauthorize) { _, _ ->
val intent = Intent(context, BrowserLoginActivity::class.java)
val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl!!)
+ bundle.putString(BundleKeys.KEY_BASE_URL, currentUser.baseUrl!!)
bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true)
+ bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
intent.putExtras(bundle)
startActivity(intent)
}
@@ -1376,7 +1407,7 @@ class ConversationsListActivity : BaseActivity() {
private fun deleteUserAndRestartApp() {
lifecycleScope.launch {
- userManager.scheduleUserForDeletionWithId(currentUser!!.id!!)
+ userManager.scheduleUserForDeletionWithId(currentUser.id!!)
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java)
.setExpeditedIfSupported()
.build()
@@ -1389,7 +1420,7 @@ class ConversationsListActivity : BaseActivity() {
WorkInfo.State.SUCCEEDED -> {
val text = String.format(
context.resources.getString(R.string.nc_deleted_user),
- currentUser!!.displayName
+ currentUser.displayName
)
Toast.makeText(
context,
@@ -1400,7 +1431,7 @@ class ConversationsListActivity : BaseActivity() {
}
WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
- logger.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId)
+ logger.e(TAG, "something went wrong when deleting user with id " + currentUser.userId)
Toast.makeText(
context,
@@ -1514,7 +1545,7 @@ class ConversationsListActivity : BaseActivity() {
val data = Data.Builder()
data.putLong(
KEY_INTERNAL_USER_ID,
- currentUser?.id!!
+ currentUser.id!!
)
data.putString(KEY_ROOM_TOKEN, conversation.token)
@@ -1530,7 +1561,7 @@ class ConversationsListActivity : BaseActivity() {
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
- currentUser?.id?.let { userId ->
+ currentUser.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
context,
conversation.token,
@@ -1561,7 +1592,7 @@ class ConversationsListActivity : BaseActivity() {
fun openFollowedThreadsOverview() {
val threadsUrl = ApiUtils.getUrlForSubscribedThreads(
version = 1,
- baseUrl = currentUser!!.baseUrl
+ baseUrl = currentUser.baseUrl
)
val bundle = Bundle()
@@ -1569,11 +1600,27 @@ class ConversationsListActivity : BaseActivity() {
bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl)
val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java)
threadsOverviewIntent.putExtras(bundle)
+ threadsOverviewIntent.putExtra(KEY_INTERNAL_USER_ID, currentUser.id)
startActivity(threadsOverviewIntent)
}
companion object {
private val TAG = ConversationsListActivity::class.java.simpleName
+
+ /**
+ * Creates an intent that shows the conversation list of the account with the internal id [userId].
+ * A conversation list already in the task is recreated for that account and all screens above it are
+ * closed, so no screen of a previously shown account stays in the back stack.
+ *
+ * FLAG_ACTIVITY_NEW_TASK must not be used here: activities have an empty task affinity, so it would start
+ * a second task instead of reusing the current one.
+ */
+ fun createAccountSwitchIntent(context: Context, userId: Long): Intent =
+ Intent(context, ConversationsListActivity::class.java).apply {
+ putExtra(KEY_INTERNAL_USER_ID, userId)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+
const val BOTTOM_SHEET_DELAY: Long = 2500
const val SEARCH_DEBOUNCE_INTERVAL_MS = 300
const val HTTP_UNAUTHORIZED = 401
diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt
index 72d9efe613..ee32eb0c7d 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt
@@ -16,11 +16,12 @@ import kotlinx.coroutines.flow.Flow
interface OfflineConversationsRepository {
/**
- * Live stream of the observed account's conversations, for use in the conversation list.
- * Backed by the local database: it re-emits whenever conversation rows change (room list
- * sync, background catch-up, optimistic updates), with unchanged lists deduplicated.
+ * Live stream of the conversations of the account with the internal id [accountId], for use in
+ * the conversation list. Backed by the local database: it re-emits whenever conversation rows
+ * change (room list sync, background catch-up, optimistic updates), with unchanged lists
+ * deduplicated.
*/
- val roomListFlow: Flow>
+ fun roomListFlow(accountId: Long): Flow>
/**
* Emits when [getRooms] fails to sync with the server (e.g. a dropped/reset connection on a
@@ -38,9 +39,8 @@ interface OfflineConversationsRepository {
val conversationFlow: Flow
/**
- * Selects the account observed by [roomListFlow] and synchronizes its conversations with
- * the server (when online). The synced changes surface through [roomListFlow], which
- * observes the database.
+ * Synchronizes the conversations of [user] with the server (when online). The synced changes
+ * surface through [roomListFlow], which observes the database.
*/
@Deprecated("use observeConversation")
fun getRooms(user: User): Job
diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt
index 91738cb1a9..4f85af9f25 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt
@@ -32,15 +32,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -59,24 +55,15 @@ class OfflineFirstConversationsRepository @Inject constructor(
private val context: Context,
private val logger: Logger
) : OfflineConversationsRepository {
- private val observedAccountId = MutableStateFlow(null)
-
/**
* The conversation list as a live view of the local database — the single source of truth.
* Every write to the conversations table (room list sync, background message catch-up,
- * optimistic read state, drafts) reaches collectors reactively; [getRooms] only selects the
- * account to observe and triggers the background sync, which stays in place as the authority
- * and self-healing safeguard.
+ * optimistic read state, drafts) reaches collectors reactively; [getRooms] only triggers the
+ * background sync, which stays in place as the authority and self-healing safeguard.
*/
- @OptIn(ExperimentalCoroutinesApi::class)
- override val roomListFlow: Flow> =
- observedAccountId
- .filterNotNull()
- .distinctUntilChanged()
- .flatMapLatest { accountId ->
- dao.getConversationsForUser(accountId)
- .map { entities -> entities.map(ConversationEntity::toDomainModel) }
- }
+ override fun roomListFlow(accountId: Long): Flow> =
+ dao.getConversationsForUser(accountId)
+ .map { entities -> entities.map(ConversationEntity::toDomainModel) }
.distinctUntilChanged()
override val conversationFlow: Flow
@@ -109,8 +96,6 @@ class OfflineFirstConversationsRepository @Inject constructor(
override fun getRooms(user: User): Job =
scope.launch {
- observedAccountId.value = user.id!!
-
if (networkMonitor.isOnline.value) {
getRoomsFromServer(user)
}
diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
index ecb3ab613d..2b44f18b98 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
@@ -42,8 +42,10 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.UserIdUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.withRetry
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -66,13 +68,11 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-@Suppress("LongParameterList", "TooManyFunctions")
-class ConversationsListViewModel @Inject constructor(
+@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
+class ConversationsListViewModel @AssistedInject constructor(
private val repository: OfflineConversationsRepository,
private val threadsRepository: ThreadsRepository,
- private val currentUserProvider: CurrentUserProviderOld,
private val openConversationsRepository: OpenConversationsRepository,
private val contactsRepository: ContactsRepository,
private val unifiedSearchRepository: UnifiedSearchRepository,
@@ -81,12 +81,11 @@ class ConversationsListViewModel @Inject constructor(
var userManager: UserManager,
private val conversationsRepository: ConversationsRepository,
private val conversationListUpdater: ConversationListUpdater,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted val currentUser: User
) : ViewModel() {
- private val _currentUser = currentUserProvider.currentUser.blockingGet()
- val currentUser: User = _currentUser
- val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: ""
+ val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) ?: ""
private val searchHelper = MessageSearchHelper(unifiedSearchRepository, currentUser)
@@ -145,7 +144,7 @@ class ConversationsListViewModel @Inject constructor(
val getRoomsViewState: LiveData
get() = _getRoomsViewState
- val getRoomsFlow = repository.roomListFlow
+ val getRoomsFlow = repository.roomListFlow(currentUser.id!!)
.onEach { list ->
_getRoomsViewState.value = GetRoomsSuccessState(list.isNotEmpty())
}.catch {
@@ -167,7 +166,7 @@ class ConversationsListViewModel @Inject constructor(
val isLoadingRooms: StateFlow = _isLoadingRooms.asStateFlow()
val getRoomsStateFlow = repository
- .roomListFlow
+ .roomListFlow(currentUser.id!!)
.catch { throwable ->
Log.e(TAG, "Error observing the conversation list", throwable)
_getRoomsViewState.value = GetRoomsErrorState(throwable)
@@ -576,7 +575,7 @@ class ConversationsListViewModel @Inject constructor(
fun checkIfThreadsExist() {
val limitForFollowedThreadsExistenceCheck = 1
- val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet())
+ val accountId = UserIdUtils.getIdForUser(currentUser)
fun isLastCheckTooOld(lastCheckDate: Long): Boolean {
val currentTimeMillis = System.currentTimeMillis()
@@ -944,8 +943,6 @@ class ConversationsListViewModel @Inject constructor(
}
override fun onNext(invitationsModel: InvitationsModel) {
- val currentUser = currentUserProvider.currentUser.blockingGet()
-
if (invitationsModel.user.userId?.equals(currentUser.userId) == true &&
invitationsModel.user.baseUrl?.equals(currentUser.baseUrl) == true
) {
@@ -966,6 +963,11 @@ class ConversationsListViewModel @Inject constructor(
}
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ConversationsListViewModel
+ }
+
companion object {
private val TAG = ConversationsListViewModel::class.java.simpleName
const val FOLLOWED_THREADS_EXIST_LAST_CHECK = "FOLLOWED_THREADS_EXIST_LAST_CHECK"
diff --git a/app/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.kt
index 7077f74f5f..c5dbff4ce1 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationtags/viewmodels/ConversationTagsViewModel.kt
@@ -22,7 +22,9 @@ import com.nextcloud.talk.models.json.tags.ConversationTagErrorOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.SpreedFeatures
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -30,18 +32,16 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
-import javax.inject.Inject
@Suppress("TooManyFunctions")
-class ConversationTagsViewModel @Inject constructor(
+class ConversationTagsViewModel @AssistedInject constructor(
private val conversationTagsRepository: ConversationTagsRepository,
private val repository: OfflineConversationsRepository,
- private val currentUserProvider: CurrentUserProviderOld,
private val conversationListUpdater: ConversationListUpdater,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted private val currentUser: User
) : ViewModel() {
- private val currentUser: User = currentUserProvider.currentUser.blockingGet()
private val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) ?: ""
private val _conversationTagsFlow = MutableStateFlow>(emptyList())
@@ -218,6 +218,11 @@ class ConversationTagsViewModel @Inject constructor(
private fun List?.toDisplayTags(): List =
this?.filter { it.type != ConversationTagDto.TYPE_OTHER }?.sortedBy { it.sortOrder } ?: emptyList()
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ConversationTagsViewModel
+ }
+
companion object {
private val TAG = ConversationTagsViewModel::class.java.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
index c98fd2e82f..c4c0e36255 100644
--- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
@@ -17,6 +17,7 @@
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.users.UserManager;
import com.nextcloud.talk.utils.ApiUtils;
+import com.nextcloud.talk.utils.CredentialsCookieInterceptor;
import com.nextcloud.talk.utils.RemoteWipeInterceptor;
import com.nextcloud.talk.utils.LoggingUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
@@ -223,6 +224,8 @@ OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences,
httpClient.addInterceptor(new HeadersInterceptor());
httpClient.addInterceptor(new RemoteWipeInterceptor(userManager, context, sslSocketFactoryCompat, trustManager));
httpClient.addInterceptor(loggingHttpInterceptor);
+ // Session cookies of one account must not authenticate the requests of another one on the same server.
+ httpClient.addNetworkInterceptor(new CredentialsCookieInterceptor());
return httpClient.build();
}
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
index 421eb093b1..6e0c542db7 100644
--- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
@@ -7,37 +7,25 @@
*/
package com.nextcloud.talk.dagger.modules
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel
import com.nextcloud.talk.activities.CallViewModel
import com.nextcloud.talk.attachmentpreview.FileAttachmentPreviewViewModel
-import com.nextcloud.talk.chat.viewmodels.ScheduledMessagesViewModel
-import com.nextcloud.talk.chooseaccount.viewmodel.StatusMessageViewModel
-import com.nextcloud.talk.chooseaccount.viewmodel.StatusViewModel
-import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.contextchat.ContextChatViewModel
-import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
-import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel
-import com.nextcloud.talk.conversationtags.viewmodels.ConversationTagsViewModel
-import com.nextcloud.talk.diagnosis.DiagnosisViewModel
import com.nextcloud.talk.logger.ui.LogsViewModel
import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel
import com.nextcloud.talk.mediaviewer.viewmodels.MediaViewerViewModel
-import com.nextcloud.talk.messagesearch.MessageSearchViewModel
-import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel
-import com.nextcloud.talk.ui.chooseaccount.ChooseAccountShareToViewModel
import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel
-import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
-import com.nextcloud.talk.threadsoverview.viewmodels.ThreadsOverviewViewModel
-import com.nextcloud.talk.translate.viewmodels.TranslateViewModel
import com.nextcloud.talk.viewmodels.CallRecordingViewModel
import com.nextcloud.talk.location.viewmodels.LocationPickerViewModel
import dagger.Binds
@@ -62,6 +50,13 @@ class ViewModelFactoryWithParams(private val modelClass: Class
}
}
+/**
+ * Lazily creates a view model with [create] on first access, scoped to this activity like [viewModels]. For view
+ * models that take arguments, e.g. via assisted injection. Everything [create] uses must be set before that access.
+ */
+inline fun ComponentActivity.assistedViewModels(noinline create: () -> VM): Lazy =
+ viewModels { ViewModelFactoryWithParams(VM::class.java, create) }
+
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
@@ -84,11 +79,6 @@ abstract class ViewModelModule {
@ViewModelKey(MediaViewerViewModel::class)
abstract fun mediaViewerViewModel(viewModel: MediaViewerViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(MessageSearchViewModel::class)
- abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(PollMainViewModel::class)
@@ -109,11 +99,6 @@ abstract class ViewModelModule {
@ViewModelKey(PollCreateViewModel::class)
abstract fun pollCreateViewModel(viewModel: PollCreateViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(RemoteFileBrowserItemsViewModel::class)
- abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(CallRecordingViewModel::class)
@@ -129,31 +114,6 @@ abstract class ViewModelModule {
@ViewModelKey(RaiseHandViewModel::class)
abstract fun raiseHandViewModel(viewModel: RaiseHandViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(TranslateViewModel::class)
- abstract fun translateViewModel(viewModel: TranslateViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(OpenConversationsViewModel::class)
- abstract fun openConversationsViewModel(viewModel: OpenConversationsViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(ConversationsListViewModel::class)
- abstract fun conversationsListViewModel(viewModel: ConversationsListViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(ConversationTagsViewModel::class)
- abstract fun conversationTagsViewModel(viewModel: ConversationTagsViewModel): ViewModel
-
- // @Binds
- // @IntoMap
- // @ViewModelKey(ChatViewModel::class)
- // abstract fun chatViewModel(viewModel: ChatViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(ConversationInfoViewModel::class)
@@ -169,31 +129,11 @@ abstract class ViewModelModule {
@ViewModelKey(InvitationsViewModel::class)
abstract fun invitationsViewModel(viewModel: InvitationsViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(ContactsViewModel::class)
- abstract fun contactsViewModel(viewModel: ContactsViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(ConversationCreationViewModel::class)
- abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(DiagnosisViewModel::class)
- abstract fun diagnosisViewModel(viewModel: DiagnosisViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(LogsViewModel::class)
abstract fun logsViewModel(viewModel: LogsViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(ThreadsOverviewViewModel::class)
- abstract fun threadsOverviewViewModel(viewModel: ThreadsOverviewViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(BrowserLoginActivityViewModel::class)
@@ -209,36 +149,8 @@ abstract class ViewModelModule {
@ViewModelKey(CallViewModel::class)
abstract fun callViewModel(viewModel: CallViewModel): ViewModel
- @Binds
- @IntoMap
- @ViewModelKey(StatusViewModel::class)
- abstract fun statusRepositoryViewModel(viewModel: StatusViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(StatusMessageViewModel::class)
- abstract fun statusMessageViewModel(viewModel: StatusMessageViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(ScheduledMessagesViewModel::class)
- abstract fun scheduledMessagesViewModel(viewModel: ScheduledMessagesViewModel): ViewModel
-
- @Binds
- @IntoMap
- @ViewModelKey(ChooseAccountShareToViewModel::class)
- abstract fun chooseAccountShareToViewModel(viewModel: ChooseAccountShareToViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(FileAttachmentPreviewViewModel::class)
internal abstract fun fileAttachmentPreviewViewModel(viewModel: FileAttachmentPreviewViewModel): ViewModel
}
-
-// @Module
-// interface ChatViewModelAssistedModule {
-// @Binds
-// fun bindChatViewModelFactory(
-// factory: ChatViewModel.Factory
-// ): ChatViewModel.Factory
-// }
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
index 27f7019a1c..cc5c5e5204 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
@@ -13,7 +13,12 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
+import com.nextcloud.talk.data.user.model.UserCapabilitiesUpdate
+import com.nextcloud.talk.data.user.model.UserClientCertificateUpdate
+import com.nextcloud.talk.data.user.model.UserCredentialsUpdate
+import com.nextcloud.talk.data.user.model.UserDisplayNameUpdate
import com.nextcloud.talk.data.user.model.UserEntity
+import com.nextcloud.talk.data.user.model.UserExternalSignalingServerUpdate
import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.coroutines.flow.Flow
@@ -52,6 +57,18 @@ interface UsersDao {
@Query("SELECT * FROM User where id = :id")
suspend fun getUserWithId(id: Long): UserEntity?
+ @Query("SELECT * FROM User where id = :id")
+ fun getUserWithIdFlow(id: Long): Flow
+
+ // If several users are marked as active, keeps only the one with the highest id, matching getActiveUser().
+ // A single statement, so it cannot interleave with setUserAsActiveWithId(). SQLite evaluates the uncorrelated
+ // subqueries once, before any row is changed.
+ @Query(
+ "UPDATE User SET current = CASE WHEN id = (SELECT MAX(id) FROM User WHERE current = 1) THEN 1 ELSE 0 END " +
+ "WHERE (SELECT COUNT(*) FROM User WHERE current = 1) > 1"
+ )
+ suspend fun repairMultipleActiveUsers(): Int
+
@Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1")
suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity?
@@ -78,6 +95,25 @@ interface UsersDao {
@Query("Update User SET pushConfigurationState = :state WHERE id == :id")
suspend fun updatePushState(id: Long, state: PushConfigurationState): Int
+ // The following updates only write the columns of the given partial entity. Writing back a whole user row that
+ // was read earlier (e.g. before a network request) would reset columns that changed in the meantime, like
+ // "current".
+
+ @Update(entity = UserEntity::class)
+ suspend fun updateCapabilities(update: UserCapabilitiesUpdate): Int
+
+ @Update(entity = UserEntity::class)
+ suspend fun updateExternalSignalingServer(update: UserExternalSignalingServerUpdate): Int
+
+ @Update(entity = UserEntity::class)
+ suspend fun updateDisplayName(update: UserDisplayNameUpdate): Int
+
+ @Update(entity = UserEntity::class)
+ suspend fun updateClientCertificate(update: UserClientCertificateUpdate): Int
+
+ @Update(entity = UserEntity::class)
+ suspend fun updateCredentials(update: UserCredentialsUpdate): Int
+
companion object {
const val TAG = "UsersDao"
}
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
index ce15a57077..bf79cf82a9 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
@@ -8,6 +8,9 @@
package com.nextcloud.talk.data.user
import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
+import com.nextcloud.talk.models.json.capabilities.ServerVersionDto
import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.coroutines.flow.Flow
@@ -17,6 +20,8 @@ interface UsersRepository {
fun getActiveUserFlow(): Flow
suspend fun getUsers(): List
suspend fun getUserWithId(id: Long): User?
+ fun getUserWithIdFlow(id: Long): Flow
+ suspend fun repairMultipleActiveUsers(): Int
suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User?
suspend fun getUserWithUserId(userId: String): User?
suspend fun getUsersScheduledForDeletion(): List
@@ -27,4 +32,9 @@ interface UsersRepository {
suspend fun setUserAsActiveWithId(id: Long): Boolean
suspend fun deleteUser(user: User): Int
suspend fun updatePushState(id: Long, state: PushConfigurationState): Int
+ suspend fun updateCapabilities(id: Long, capabilities: CapabilitiesDto?, serverVersion: ServerVersionDto?): Int
+ suspend fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer?): Int
+ suspend fun updateDisplayName(id: Long, displayName: String?): Int
+ suspend fun updateClientCertificate(id: Long, clientCertificate: String?): Int
+ suspend fun updateCredentials(id: Long, token: String?, clientCertificate: String?): Int
}
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
index 5a53c3bbf8..e995d3726d 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
@@ -9,6 +9,14 @@ package com.nextcloud.talk.data.user
import android.util.Log
import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.data.user.model.UserCapabilitiesUpdate
+import com.nextcloud.talk.data.user.model.UserClientCertificateUpdate
+import com.nextcloud.talk.data.user.model.UserCredentialsUpdate
+import com.nextcloud.talk.data.user.model.UserDisplayNameUpdate
+import com.nextcloud.talk.data.user.model.UserExternalSignalingServerUpdate
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
+import com.nextcloud.talk.models.json.capabilities.ServerVersionDto
import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -16,11 +24,7 @@ import kotlinx.coroutines.flow.map
@Suppress("TooManyFunctions")
class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
- override suspend fun getActiveUser(): User? {
- val entity = usersDao.getActiveUser() ?: return null
- setUserAsActiveWithId(entity.id)
- return UserMapper.toModel(entity)
- }
+ override suspend fun getActiveUser(): User? = UserMapper.toModel(usersDao.getActiveUser())
override fun getActiveUserFlow(): Flow =
usersDao.getActiveUserFlow().map {
@@ -31,6 +35,13 @@ class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
override suspend fun getUserWithId(id: Long): User? = UserMapper.toModel(usersDao.getUserWithId(id))
+ override fun getUserWithIdFlow(id: Long): Flow =
+ usersDao.getUserWithIdFlow(id).map {
+ UserMapper.toModel(it)
+ }
+
+ override suspend fun repairMultipleActiveUsers(): Int = usersDao.repairMultipleActiveUsers()
+
override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User? =
UserMapper.toModel(usersDao.getUserWithIdNotScheduledForDeletion(id))
@@ -61,6 +72,26 @@ class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
override suspend fun updatePushState(id: Long, state: PushConfigurationState): Int =
usersDao.updatePushState(id, state)
+ override suspend fun updateCapabilities(
+ id: Long,
+ capabilities: CapabilitiesDto?,
+ serverVersion: ServerVersionDto?
+ ): Int = usersDao.updateCapabilities(UserCapabilitiesUpdate(id, capabilities, serverVersion))
+
+ override suspend fun updateExternalSignalingServer(
+ id: Long,
+ externalSignalingServer: ExternalSignalingServer?
+ ): Int = usersDao.updateExternalSignalingServer(UserExternalSignalingServerUpdate(id, externalSignalingServer))
+
+ override suspend fun updateDisplayName(id: Long, displayName: String?): Int =
+ usersDao.updateDisplayName(UserDisplayNameUpdate(id, displayName))
+
+ override suspend fun updateClientCertificate(id: Long, clientCertificate: String?): Int =
+ usersDao.updateClientCertificate(UserClientCertificateUpdate(id, clientCertificate))
+
+ override suspend fun updateCredentials(id: Long, token: String?, clientCertificate: String?): Int =
+ usersDao.updateCredentials(UserCredentialsUpdate(id, token, clientCertificate))
+
companion object {
private val TAG = UsersRepositoryImpl::class.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/UserPartialUpdates.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/UserPartialUpdates.kt
new file mode 100644
index 0000000000..336f94c1bf
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/data/user/model/UserPartialUpdates.kt
@@ -0,0 +1,42 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.data.user.model
+
+import androidx.room.ColumnInfo
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
+import com.nextcloud.talk.models.json.capabilities.ServerVersionDto
+
+// Partial entities of UserEntity for Room's @Update(entity = UserEntity::class): each writes only its own columns
+// of the user with the given id.
+
+data class UserCapabilitiesUpdate(
+ @ColumnInfo(name = "id") val id: Long,
+ @ColumnInfo(name = "capabilities") val capabilities: CapabilitiesDto?,
+ @ColumnInfo(name = "serverVersion") val serverVersion: ServerVersionDto?
+)
+
+data class UserExternalSignalingServerUpdate(
+ @ColumnInfo(name = "id") val id: Long,
+ @ColumnInfo(name = "externalSignalingServer") val externalSignalingServer: ExternalSignalingServer?
+)
+
+data class UserDisplayNameUpdate(
+ @ColumnInfo(name = "id") val id: Long,
+ @ColumnInfo(name = "displayName") val displayName: String?
+)
+
+data class UserClientCertificateUpdate(
+ @ColumnInfo(name = "id") val id: Long,
+ @ColumnInfo(name = "clientCertificate") val clientCertificate: String?
+)
+
+data class UserCredentialsUpdate(
+ @ColumnInfo(name = "id") val id: Long,
+ @ColumnInfo(name = "token") val token: String?,
+ @ColumnInfo(name = "clientCertificate") val clientCertificate: String?
+)
diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt
index ebd4d0c0ed..920583a214 100644
--- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt
@@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import com.nextcloud.talk.R
@@ -39,10 +38,11 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.components.StandardAppBar
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.data.network.NetworkMonitor
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.errorhandling.saveLogsAsZip
import com.nextcloud.talk.logger.LogsRepository
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.UnifiedPushUtils
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
@@ -57,14 +57,11 @@ class DiagnosisActivity : BaseActivity() {
lateinit var arbitraryStorageManager: ArbitraryStorageManager
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: DiagnosisViewModel.Factory
@Inject
lateinit var ncApi: NcApi
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var networkMonitor: NetworkMonitor
@@ -74,6 +71,9 @@ class DiagnosisActivity : BaseActivity() {
@Inject
lateinit var logsRepository: LogsRepository
+ private lateinit var user: User
+
+ private val diagnosisViewModel: DiagnosisViewModel by assistedViewModels { viewModelFactory.build(user) }
private val diagnosisData = mutableListOf()
private val diagnosisDataState = mutableStateOf(emptyList())
@@ -92,10 +92,7 @@ class DiagnosisActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- val diagnosisViewModel = ViewModelProvider(
- this,
- viewModelFactory
- )[DiagnosisViewModel::class.java]
+ user = setUpBoundUserOrFinish() ?: return
val colorScheme = viewThemeUtils.getColorScheme(this)
val isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
@@ -128,7 +125,8 @@ class DiagnosisActivity : BaseActivity() {
userManager,
appPreferences,
arbitraryStorageManager,
- logsRepository
+ logsRepository,
+ diagnosisViewModel.currentUser
)
)
diagnosisDataState.value = diagnosisData.toList()
diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt
index 081ea411f8..c161d9b877 100644
--- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt
+++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt
@@ -15,6 +15,7 @@ import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.logger.LogsRepository
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ClosedInterfaceImpl
@@ -56,13 +57,14 @@ private const val PUSH_TOKEN_PREFIX_END: Int = 5
private val TAG = DiagnosisActivity::class.java.simpleName
-@Suppress("LongMethod", "TooGenericExceptionCaught", "CyclomaticComplexMethod")
+@Suppress("LongMethod", "TooGenericExceptionCaught", "CyclomaticComplexMethod", "LongParameterList")
fun buildDiagnosisElements(
context: Context,
userManager: UserManager,
appPreferences: AppPreferences,
arbitraryStorageManager: ArbitraryStorageManager? = null,
- logsRepository: LogsRepository
+ logsRepository: LogsRepository,
+ accountUser: User? = null
): List {
val data = mutableListOf()
@@ -201,7 +203,7 @@ fun buildDiagnosisElements(
// Account
try {
- val user = runBlocking { userManager.getCurrentUser() } ?: return data
+ val user = accountUser ?: runBlocking { userManager.getDefaultUser() } ?: return data
addHeadline(context.getString(R.string.nc_diagnosis_account_category_title))
addEntry(context.getString(R.string.nc_diagnosis_account_server), user.baseUrl ?: "")
addEntry(context.getString(R.string.nc_diagnosis_account_user_name), user.displayName ?: "")
diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.kt
index cb5d7564cf..1bebc8cca9 100644
--- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisViewModel.kt
@@ -14,21 +14,20 @@ import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.logger.Logger
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import javax.inject.Inject
@Suppress("TooGenericExceptionCaught")
-class DiagnosisViewModel @Inject constructor(
+class DiagnosisViewModel @AssistedInject constructor(
private val ncApiCoroutines: NcApiCoroutines,
- private val currentUserProvider: CurrentUserProviderOld,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted val currentUser: User
) : ViewModel() {
- private val _currentUser = currentUserProvider.currentUser.blockingGet()
- val currentUser: User = _currentUser
- val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: ""
+ val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) ?: ""
private val _notificationViewState = MutableStateFlow(NotificationUiState.None)
val notificationViewState: StateFlow = _notificationViewState
@@ -46,7 +45,7 @@ class DiagnosisViewModel @Inject constructor(
val response = ncApiCoroutines.testPushNotifications(
credentials,
ApiUtils
- .getUrlForTestPushNotifications(_currentUser.baseUrl ?: "")
+ .getUrlForTestPushNotifications(currentUser.baseUrl ?: "")
)
val notificationMessage = response.ocs?.data?.message
_notificationViewState.value = NotificationUiState.Success(notificationMessage)
@@ -64,6 +63,11 @@ class DiagnosisViewModel @Inject constructor(
_showDialog.value = false
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): DiagnosisViewModel
+ }
+
companion object {
private val TAG = DiagnosisViewModel::class.java.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
index 26bc6fd8e4..88929ce086 100644
--- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
+++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
@@ -49,6 +49,7 @@ import com.nextcloud.talk.utils.ActorAvatar
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CharacterAvatarUtils
import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.UriUtils
import kotlin.math.min
private const val ROUNDING_PIXEL = 16f
@@ -230,7 +231,7 @@ fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.D
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
requestBuilder.placeholder(LayerDrawable(layers))
- if (url.startsWith(user.baseUrl!!) &&
+ if (UriUtils.isOnServer(url, user.baseUrl!!) &&
(url.contains("index.php/core/preview") || url.contains("/avatar/") || url.contains("remote.php/dav/"))
) {
requestBuilder.addHeader(
@@ -256,7 +257,7 @@ fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null):
.error(finalPlaceholder)
.transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL))
- if (url.startsWith(user.baseUrl!!) &&
+ if (UriUtils.isOnServer(url, user.baseUrl!!) &&
(url.contains("index.php/core/preview") || url.contains("/avatar/") || url.contains("remote.php/dav/"))
) {
requestBuilder.addHeader(
diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt
index 8a4d9e432e..7f3e118e04 100644
--- a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt
@@ -13,7 +13,6 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.core.content.FileProvider
import androidx.core.net.toUri
@@ -21,36 +20,33 @@ import androidx.fragment.app.DialogFragment
import autodagger.AutoInjector
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
-import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype.TEXT_PREFIX_GENERIC
-import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
import java.io.File
-import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
-class FullScreenTextViewerActivity : AppCompatActivity() {
+class FullScreenTextViewerActivity : BaseActivity() {
- @Inject
- lateinit var viewThemeUtils: ViewThemeUtils
private lateinit var textFile: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ val user = setUpBoundUserOrFinish() ?: return
val fileName = intent.getStringExtra("FILE_NAME").orEmpty()
val isMarkdown = intent.getBooleanExtra("IS_MARKDOWN", false)
val fileId = intent.getStringExtra("FILE_ID").orEmpty()
val link = intent.getStringExtra("LINK")
- val username = intent.getStringExtra("USERNAME").orEmpty()
- val baseUrl = intent.getStringExtra("BASE_URL").orEmpty()
+ val username = user.username.orEmpty()
+ val baseUrl = user.baseUrl.orEmpty()
textFile = FileUtils.resolveSharedAttachmentFile(applicationContext.cacheDir, fileName) ?: run {
Log.e(TAG, "Invalid text filename: $fileName")
finish()
@@ -58,8 +54,6 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
}
val text = readFile(textFile)
- adjustUIForAPILevel35()
-
setContent {
val colorScheme = viewThemeUtils.getColorScheme(this)
MaterialTheme(colorScheme = colorScheme) {
diff --git a/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt
index ea954a6ed9..e7766da05c 100644
--- a/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt
@@ -49,6 +49,7 @@ class InvitationsActivity : BaseActivity() {
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val intent = Intent(this@InvitationsActivity, ConversationsListActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, currentUser.id)
startActivity(intent)
}
}
@@ -56,10 +57,10 @@ class InvitationsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ currentUser = setUpBoundUserOrFinish() ?: return
invitationsViewModel = ViewModelProvider(this, viewModelFactory)[InvitationsViewModel::class.java]
- currentUser = currentUserProviderOld.currentUser.blockingGet()
invitationsViewModel.fetchInvitations(currentUser)
binding = ActivityInvitationsBinding.inflate(layoutInflater)
@@ -136,12 +137,11 @@ class InvitationsActivity : BaseActivity() {
is InvitationsViewModel.InvitationActionSuccessState -> {
if (state.action == ActionEnum.ACCEPT) {
- val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, state.invitation.localToken)
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
- chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- startActivity(chatIntent)
+ state.invitation.localToken?.let { token ->
+ val chatIntent = ChatActivity.createIntent(context, currentUser.id!!, token)
+ chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ startActivity(chatIntent)
+ }
} else {
// adapter.currentList.remove(state.invitation)
// adapter.notifyDataSetChanged() // leads to UnsupportedOperationException ?!
diff --git a/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt
index 104ea73352..56c6748664 100644
--- a/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt
+++ b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt
@@ -19,6 +19,7 @@ import com.nextcloud.talk.databinding.RvItemInvitationBinding
import com.nextcloud.talk.invitation.InvitationsActivity
import com.nextcloud.talk.invitation.data.Invitation
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -64,6 +65,7 @@ class InvitationsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InvitationsViewHolder {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(parent.context, viewThemeUtils)
return InvitationsViewHolder(
RvItemInvitationBinding.inflate(
LayoutInflater.from(parent.context),
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.kt b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.kt
index 85886ec93d..57c1b33400 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesFetcher.kt
@@ -84,7 +84,7 @@ class CapabilitiesFetcher(
user.serverVersion = capabilitiesOverall.ocs?.data?.serverVersion
return try {
- val success = userManager.updateOrCreateUser(user) > 0
+ val success = userManager.updateCapabilities(user.id!!, user.capabilities, user.serverVersion) > 0
if (!success) {
Log.w(TAG, "Error updating user")
}
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
index 70a0666ab4..30abc74e85 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
@@ -34,11 +34,11 @@ import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall
+import com.nextcloud.talk.users.DefaultAccountProvider
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ContactUtils
import com.nextcloud.talk.utils.DateConstants
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -59,7 +59,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
lateinit var userManager: UserManager
@Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
+ lateinit var defaultAccountProvider: DefaultAccountProvider
@Inject
lateinit var appPreferences: AppPreferences
@@ -71,7 +71,8 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
override fun doWork(): Result {
sharedApplication!!.componentApplication.inject(this)
- val currentUser = currentUserProvider.currentUser.blockingGet()
+ // The phone book integration is not bound to an account, so it syncs for the default account.
+ val currentUser = defaultAccountProvider.getDefaultUserBlocking()
accountName = context.getString(R.string.nc_app_product_name)
accountType = BuildConfig.APPLICATION_ID
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
index db0f9039f2..2c9f00d9cd 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
@@ -19,8 +19,9 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.FileUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.preferences.AppPreferences
+import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import java.io.BufferedInputStream
import java.io.File
@@ -41,9 +42,6 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var appPreferences: AppPreferences
@@ -55,9 +53,7 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
}
try {
- val currentUser = currentUserProvider.currentUser.blockingGet()
- val baseUrl = inputData.getString(KEY_BASE_URL)
- val userId = inputData.getString(KEY_USER_ID)
+ val currentUser = runBlocking { userManager.getUserWithId(inputData.getLong(KEY_INTERNAL_USER_ID, 0L)) }
val attachmentFolder = inputData.getString(KEY_ATTACHMENT_FOLDER)
val fileId = inputData.getString(KEY_FILE_ID)
val fileName = inputData.getString(KEY_FILE_NAME)
@@ -65,8 +61,8 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
totalFileSize = (inputData.getLong(KEY_FILE_SIZE, -1))
checkNotNull(currentUser)
- checkNotNull(baseUrl)
- checkNotNull(userId)
+ val baseUrl = checkNotNull(currentUser.baseUrl)
+ val userId = checkNotNull(currentUser.userId)
checkNotNull(attachmentFolder)
checkNotNull(fileName)
checkNotNull(remotePath)
@@ -147,8 +143,6 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
companion object {
const val TAG = "DownloadFileToCache"
- const val KEY_BASE_URL = "KEY_BASE_URL"
- const val KEY_USER_ID = "KEY_USER_ID"
const val KEY_ATTACHMENT_FOLDER = "KEY_ATTACHMENT_FOLDER"
const val KEY_FILE_ID = "KEY_FILE_ID"
const val KEY_FILE_NAME = "KEY_FILE_NAME"
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt
index da17ac617f..ab512b8409 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt
@@ -26,11 +26,11 @@ import com.nextcloud.talk.utils.ApiUtils.getConversationApiVersion
import com.nextcloud.talk.utils.ApiUtils.getCredentials
import com.nextcloud.talk.utils.ApiUtils.getUrlForParticipantsSelf
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
+import kotlinx.coroutines.runBlocking
import retrofit2.HttpException
import javax.inject.Inject
@@ -45,15 +45,13 @@ class LeaveConversationWorker(context: Context, workerParams: WorkerParameters)
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
private val result = SettableFuture.create()
override fun startWork(): ListenableFuture {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
val conversationToken = inputData.getString(BundleKeys.KEY_ROOM_TOKEN)
- val currentUser = currentUserProvider.currentUser.blockingGet()
+ val userId = inputData.getLong(BundleKeys.KEY_INTERNAL_USER_ID, 0L)
+ val currentUser = runBlocking { userManager.getUserWithId(userId) }
if (currentUser != null && conversationToken != null) {
val credentials = getCredentials(currentUser.username, currentUser.token)
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
index 546f87ad86..6b2042243e 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
@@ -392,8 +392,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
null
}
- if (conversation != null && runBlocking { userManager.setUserAsActive(userBeingCalled!!) }) {
- if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled?.capabilities?.spreedCapability)) {
+ if (conversation != null && userBeingCalled != null) {
+ if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled.capabilities?.spreedCapability)) {
showEndToEndEncryptionUnsupportedNotification(conversation)
} else {
prepareCallNotificationScreen(conversation)
@@ -1062,6 +1062,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val dismissIntent = Intent(context, DismissRecordingAvailableReceiver::class.java)
dismissIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+ dismissIntent.putExtra(KEY_INTERNAL_USER_ID, user.id)
dismissIntent.putExtra(KEY_DISMISS_RECORDING_URL, dismissRecordingUrl)
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -1095,6 +1096,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val shareRecordingIntent = Intent(context, ShareRecordingToChatReceiver::class.java)
shareRecordingIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+ shareRecordingIntent.putExtra(KEY_INTERNAL_USER_ID, user.id)
shareRecordingIntent.putExtra(KEY_SHARE_RECORDING_TO_CHAT_URL, shareToChatUrl)
shareRecordingIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
index 1f11f65b3d..fecb112a72 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
+++ b/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
@@ -116,7 +116,10 @@ public void onNext(SignalingSettingsOverall signalingSettingsOverall) {
try {
int rows = BuildersKt.runBlocking(
EmptyCoroutineContext.INSTANCE,
- (scope, continuation) -> userManager.saveUser(user, continuation));
+ (scope, continuation) -> userManager.updateExternalSignalingServer(
+ user.getId(),
+ externalSignalingServer,
+ continuation));
saved = rows > 0;
} catch (Exception e) {
saved = false;
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
index 1725c70c19..985bfdaa6b 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
@@ -50,7 +50,7 @@ import com.nextcloud.talk.utils.ImageCompressor
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.RemoteFileUtils
import com.nextcloud.talk.utils.VideoCompressor
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.Observable
@@ -85,9 +85,6 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
@Inject
lateinit var ncApiCoroutines: NcApiCoroutines
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var appPreferences: AppPreferences
@@ -102,7 +99,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
lateinit var fileName: String
- private var mNotifyManager: NotificationManager? = null
+ private val notificationManager by lazy {
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
lateinit var roomToken: String
lateinit var conversationName: String
@@ -142,15 +141,21 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
)
private fun doUpload(): Result {
return try {
- currentUser = currentUserProvider.currentUser.blockingGet()
+ // Read first, so a failure below can still mark the placeholder message as failed.
+ referenceId = inputData.getString(KEY_REFERENCE_ID)
+ internalConversationId = inputData.getString(KEY_INTERNAL_CONVERSATION_ID)
+
+ val userId = inputData.getLong(KEY_INTERNAL_USER_ID, 0L)
+ currentUser = runBlocking { userManager.getUserWithId(userId) } ?: run {
+ // E.g. the account was removed while the upload waited for a retry.
+ Log.e(TAG, "No user found for id $userId")
+ return failUpload()
+ }
val sourceFile = inputData.getString(DEVICE_SOURCE_FILE)
roomToken = inputData.getString(ROOM_TOKEN)!!
conversationName = inputData.getString(CONVERSATION_NAME)!!
val metaData = inputData.getString(META_DATA)
- referenceId = inputData.getString(KEY_REFERENCE_ID)
- internalConversationId = inputData.getString(KEY_INTERNAL_CONVERSATION_ID)
- checkNotNull(currentUser)
checkNotNull(sourceFile)
require(sourceFile.isNotEmpty())
checkNotNull(roomToken)
@@ -159,8 +164,6 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
fileName = FileUtils.getFileName(sourceFileUri, context)
file = FileUtils.getFileFromUri(context, sourceFileUri)
- initNotificationSetup()
-
if (inputData.getBoolean(COMPRESS_IMAGES, false)) {
sourceFileUri = compressMediaIfPossible(sourceFileUri)
}
@@ -430,15 +433,11 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
super.onStopped()
}
- private fun initNotificationSetup() {
- mNotifyManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- }
-
private fun showFailedToUploadNotification() {
val failureTitle = getResourceString(context, R.string.nc_upload_failed_notification_title)
val failureText = String.format(
getResourceString(context, R.string.nc_upload_failed_notification_text),
- fileName
+ failedFileName()
)
val failureNotification = NotificationCompat.Builder(
context,
@@ -452,9 +451,17 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
.setOngoing(false)
.build()
- mNotifyManager!!.notify(SystemClock.uptimeMillis().toInt(), failureNotification)
+ notificationManager.notify(SystemClock.uptimeMillis().toInt(), failureNotification)
}
+ // The upload can fail before the file name is known, e.g. when its account does not exist anymore.
+ private fun failedFileName(): String =
+ if (::fileName.isInitialized) {
+ fileName
+ } else {
+ inputData.getString(DEVICE_SOURCE_FILE)?.toUri()?.lastPathSegment.orEmpty()
+ }
+
private fun getResourceString(context: Context, resourceId: Int): String = context.resources.getString(resourceId)
companion object {
@@ -527,6 +534,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
@Suppress("LongParameterList")
fun upload(
+ userId: Long,
fileUri: String,
roomToken: String,
conversationName: String,
@@ -537,6 +545,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
allowUpdate: Boolean = false
): UUID {
val data: Data = Data.Builder()
+ .putLong(KEY_INTERNAL_USER_ID, userId)
.putString(DEVICE_SOURCE_FILE, fileUri)
.putString(ROOM_TOKEN, roomToken)
.putString(CONVERSATION_NAME, conversationName)
diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt
index b8a1e019f2..acea84e0ff 100644
--- a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt
@@ -41,6 +41,7 @@ class GeocodingActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ setUpBoundUserOrFinish() ?: return
roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
chatApiVersion = intent.getIntExtra(BundleKeys.KEY_CHAT_API_VERSION, 1)
diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt
index 80b3aaa0b1..bda0ae596e 100644
--- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt
@@ -18,12 +18,14 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.components.ColoredStatusBar
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.getParcelableExtraProvider
import com.nextcloud.talk.location.components.LocationPickerScreen
+import com.nextcloud.talk.location.viewmodels.LocationPickerViewModel
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CHAT_API_VERSION
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_GEOCODING_RESULT
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
-import com.nextcloud.talk.location.viewmodels.LocationPickerViewModel
import javax.inject.Inject
private const val MAP_CENTER_LAT_KEY = "mapCenterLat"
@@ -34,6 +36,8 @@ private const val MOVE_TO_CURRENT_LOCATION_KEY = "moveToCurrentLocation"
private const val GEOCODING_RESULT_KEY = "geocodingResult"
+private const val TAG = "LocationPickerActivity"
+
@AutoInjector(NextcloudTalkApplication::class)
class LocationPickerActivity : BaseActivity() {
@@ -41,6 +45,7 @@ class LocationPickerActivity : BaseActivity() {
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewModel: LocationPickerViewModel
+ private lateinit var user: User
private lateinit var roomToken: String
private var chatApiVersion: Int = 1
@@ -66,6 +71,7 @@ class LocationPickerActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ user = setUpBoundUserOrFinish() ?: return
roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
chatApiVersion = intent.getIntExtra(KEY_CHAT_API_VERSION, 1)
@@ -84,6 +90,7 @@ class LocationPickerActivity : BaseActivity() {
viewModel.initialize(
LocationPickerViewModel.LocationPickerInitParams(
+ user = user,
roomToken = roomToken,
chatApiVersion = chatApiVersion,
geocodingResult = geocodingResult,
@@ -122,6 +129,7 @@ class LocationPickerActivity : BaseActivity() {
private fun navigateToGeocoding() {
val intent = Intent(this, GeocodingActivity::class.java)
+ intent.putExtra(KEY_INTERNAL_USER_ID, user.id)
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
intent.putExtra(KEY_CHAT_API_VERSION, chatApiVersion)
geocodingLauncher.launch(intent)
diff --git a/app/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.kt b/app/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.kt
index 31d5a8b896..745dc9a434 100644
--- a/app/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModel.kt
@@ -11,11 +11,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.location.GeocodingResult
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import fr.dudie.nominatim.client.TalkJsonNominatimClient
import fr.dudie.nominatim.model.Address
import io.reactivex.Observer
@@ -35,7 +35,6 @@ import javax.inject.Inject
@Suppress("TooManyFunctions")
class LocationPickerViewModel @Inject constructor(
private val ncApi: NcApi,
- private val currentUserProviderOld: CurrentUserProviderOld,
private val okHttpClient: OkHttpClient,
private val logger: Logger
) : ViewModel() {
@@ -61,6 +60,7 @@ class LocationPickerViewModel @Inject constructor(
)
data class LocationPickerInitParams(
+ val user: User,
val roomToken: String,
val chatApiVersion: Int,
val geocodingResult: GeocodingResult?,
@@ -78,6 +78,7 @@ class LocationPickerViewModel @Inject constructor(
private var isStateInitialized = false
+ private lateinit var user: User
private var roomToken: String = ""
private var chatApiVersion: Int = 1
@@ -88,6 +89,7 @@ class LocationPickerViewModel @Inject constructor(
if (isStateInitialized) return
isStateInitialized = true
+ user = params.user
roomToken = params.roomToken
chatApiVersion = params.chatApiVersion
nominatimClient = TalkJsonNominatimClient(params.geocoderBaseUrl, okHttpClient, params.geocoderEmail)
@@ -247,11 +249,9 @@ class LocationPickerViewModel @Inject constructor(
"\"latitude\":\"$selectedLat\"," +
"\"longitude\":\"$selectedLon\",\"name\":\"$locationNameToShare\"}"
- val currentUser = currentUserProviderOld.currentUser.blockingGet()
-
ncApi.sendLocation(
- ApiUtils.getCredentials(currentUser.username, currentUser.token),
- ApiUtils.getUrlToSendLocation(chatApiVersion, currentUser.baseUrl!!, roomToken),
+ ApiUtils.getCredentials(user.username, user.token),
+ ApiUtils.getUrlToSendLocation(chatApiVersion, user.baseUrl!!, roomToken),
"geo-location",
objectId,
metaData
diff --git a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt
index 827a9a67e8..f97d175ffe 100644
--- a/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/logger/ui/LogsActivity.kt
@@ -66,7 +66,6 @@ import com.nextcloud.talk.errorhandling.saveLogsAsZip
import com.nextcloud.talk.logger.Level
import com.nextcloud.talk.logger.LogEntry
import com.nextcloud.talk.logger.LogsRepository
-import com.nextcloud.talk.users.UserManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
@@ -79,9 +78,6 @@ class LogsActivity : BaseActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var logsRepository: LogsRepository
diff --git a/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt
index d0f94f8396..13b8bb7b74 100644
--- a/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerActivity.kt
@@ -34,9 +34,13 @@ import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX_GENERIC
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import java.io.File
import javax.inject.Inject
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.ui.LocalImageAuthHeader
+import androidx.compose.runtime.CompositionLocalProvider
/**
* Swipeable, group-aware media viewer - the entry point for every image/video tap in chat. See
@@ -54,13 +58,14 @@ class MediaViewerActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ val user = setUpBoundUserOrFinish() ?: return
+ val imageAuthHeader = ApiUtils.getCredentials(user.username, user.token)
val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)
val seedItems = intent.getParcelableArrayListExtraProvider(EXTRA_SEED_ITEMS)
val startMessageId = intent.getLongExtra(EXTRA_START_MESSAGE_ID, -1L)
- val user = currentUserProviderOld.currentUser.blockingGet()
- if (roomToken == null || seedItems.isNullOrEmpty() || user == null) {
+ if (roomToken == null || seedItems.isNullOrEmpty()) {
Log.e(TAG, "Missing data to open the media viewer")
finish()
return
@@ -81,14 +86,16 @@ class MediaViewerActivity : BaseActivity() {
setContent {
val colorScheme = viewThemeUtils.getColorScheme(this@MediaViewerActivity)
MaterialTheme(colorScheme = colorScheme) {
- MediaViewerScreen(
- viewModel = viewModel,
- onShare = ::shareFile,
- onSave = ::showSaveDialog,
- onControlsVisibilityChanged = { visible ->
- if (visible) exitImmersiveMode() else enterImmersiveMode()
- }
- )
+ CompositionLocalProvider(LocalImageAuthHeader provides imageAuthHeader) {
+ MediaViewerScreen(
+ viewModel = viewModel,
+ onShare = ::shareFile,
+ onSave = ::showSaveDialog,
+ onControlsVisibilityChanged = { visible ->
+ if (visible) exitImmersiveMode() else enterImmersiveMode()
+ }
+ )
+ }
}
}
}
@@ -135,11 +142,13 @@ class MediaViewerActivity : BaseActivity() {
fun newIntent(
context: Context,
+ userId: Long,
roomToken: String,
seedItems: List,
startMessageId: Long
): Intent =
Intent(context, MediaViewerActivity::class.java).apply {
+ putExtra(BundleKeys.KEY_INTERNAL_USER_ID, userId)
putExtra(KEY_ROOM_TOKEN, roomToken)
putParcelableArrayListExtra(EXTRA_SEED_ITEMS, ArrayList(seedItems))
putExtra(EXTRA_START_MESSAGE_ID, startMessageId)
diff --git a/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerScreen.kt b/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerScreen.kt
index 60894e910a..31218c2847 100644
--- a/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerScreen.kt
+++ b/app/src/main/java/com/nextcloud/talk/mediaviewer/activities/MediaViewerScreen.kt
@@ -90,6 +90,8 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DrawableUtils
import com.nextcloud.talk.utils.Mimetype
import com.nextcloud.talk.utils.MimetypeUtils
+import com.nextcloud.talk.ui.LocalImageAuthHeader
+import com.nextcloud.talk.ui.withAuthHeader
import java.io.File
import kotlinx.coroutines.launch
import pl.droidsonroids.gif.GifDrawable
@@ -330,7 +332,10 @@ private fun MediaPage(
private fun PreviewPlaceholder(item: MediaViewerItem, onToggleControls: () -> Unit) {
if (item.previewUrl != null) {
AsyncImage(
- model = item.previewUrl,
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(item.previewUrl)
+ .withAuthHeader(LocalImageAuthHeader.current)
+ .build(),
contentDescription = item.fileName,
modifier = Modifier.fillMaxSize().clickable(onClick = onToggleControls),
contentScale = ContentScale.Fit
@@ -512,7 +517,10 @@ private fun ThumbnailTile(item: MediaViewerItem, isSelected: Boolean, onClick: (
) {
if (item.previewUrl != null) {
AsyncImage(
- model = item.previewUrl,
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(item.previewUrl)
+ .withAuthHeader(LocalImageAuthHeader.current)
+ .build(),
contentDescription = item.fileName,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
diff --git a/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt b/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt
index 27563729fe..694c6f7e54 100644
--- a/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt
@@ -12,6 +12,7 @@ import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -82,7 +83,8 @@ class MediaViewerViewModel @Inject constructor(private val sharedItemsRepository
fun initialize(user: User, roomToken: String, seedItems: List, startMessageId: Long) {
this.user = user
repositoryParameters = SharedItemsRepository.Parameters(
- user.userId!!,
+ // The login name, as it authenticates the requests. The user id can differ from it.
+ user.username!!,
user.token!!,
user.baseUrl!!,
roomToken
@@ -137,8 +139,7 @@ class MediaViewerViewModel @Inject constructor(private val sharedItemsRepository
_uiState.update { it.copy(downloadingMessageIds = it.downloadingMessageIds + item.messageId) }
val data = Data.Builder()
- .putString(DownloadFileToCacheWorker.KEY_BASE_URL, user.baseUrl)
- .putString(DownloadFileToCacheWorker.KEY_USER_ID, user.userId)
+ .putLong(BundleKeys.KEY_INTERNAL_USER_ID, user.id!!)
.putString(
DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
CapabilitiesUtil.getAttachmentFolder(user.capabilities!!.spreedCapability!!)
diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
deleted file mode 100644
index 7b8ac75a34..0000000000
--- a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.messagesearch
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import androidx.activity.OnBackPressedCallback
-import androidx.appcompat.widget.SearchView
-import androidx.lifecycle.ViewModelProvider
-import autodagger.AutoInjector
-import com.google.android.material.snackbar.Snackbar
-import com.nextcloud.talk.R
-import com.nextcloud.talk.activities.BaseActivity
-import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
-import com.nextcloud.talk.adapters.items.MessageResultItem
-import com.nextcloud.talk.application.NextcloudTalkApplication
-import com.nextcloud.talk.conversationlist.ConversationsListActivity
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.databinding.ActivityMessageSearchBinding
-import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.viewholders.FlexibleViewHolder
-import io.reactivex.Observable
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.disposables.Disposable
-import io.reactivex.schedulers.Schedulers
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@AutoInjector(NextcloudTalkApplication::class)
-class MessageSearchActivity : BaseActivity() {
-
- @Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
-
- private lateinit var binding: ActivityMessageSearchBinding
- private lateinit var searchView: SearchView
-
- private lateinit var user: User
-
- private lateinit var viewModel: MessageSearchViewModel
-
- private var searchViewDisposable: Disposable? = null
- private var adapter: FlexibleAdapter>? = null
-
- private val onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- setResult(Activity.RESULT_CANCELED)
- finish()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
-
- binding = ActivityMessageSearchBinding.inflate(layoutInflater)
- setupActionBar()
- setContentView(binding.root)
- initSystemBars()
-
- viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java]
- user = currentUserProviderOld.currentUser.blockingGet()
- val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
- viewModel.initialize(roomToken)
- setupStateObserver()
-
- binding.swipeRefreshLayout.setOnRefreshListener {
- viewModel.refresh(searchView.query?.toString())
- }
-
- onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
- }
-
- private fun setupActionBar() {
- setSupportActionBar(binding.messageSearchToolbar)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME)
- supportActionBar?.title = conversationName
- viewThemeUtils.material.themeToolbar(binding.messageSearchToolbar)
- }
-
- private fun setupStateObserver() {
- viewModel.state.observe(this) { state ->
- when (state) {
- MessageSearchViewModel.InitialState -> showInitial()
- MessageSearchViewModel.EmptyState -> showEmpty()
- is MessageSearchViewModel.LoadedState -> showLoaded(state)
- MessageSearchViewModel.LoadingState -> showLoading()
- MessageSearchViewModel.ErrorState -> showError()
- is MessageSearchViewModel.FinishedState -> onFinish()
- }
- }
- }
-
- private fun showError() {
- displayLoading(false)
- Snackbar.make(binding.root, "Error while searching", Snackbar.LENGTH_SHORT).show()
- }
-
- private fun showLoading() {
- displayLoading(true)
- }
-
- private fun displayLoading(loading: Boolean) {
- binding.swipeRefreshLayout.isRefreshing = loading
- }
-
- private fun showLoaded(state: MessageSearchViewModel.LoadedState) {
- displayLoading(false)
- binding.emptyContainer.emptyListView.visibility = View.GONE
- binding.messageSearchRecycler.visibility = View.VISIBLE
- setAdapterItems(state)
- }
-
- private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) {
- val loadMoreItems = if (state.hasMore) {
- listOf(LoadMoreResultsItem)
- } else {
- emptyList()
- }
- val newItems =
- state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems
-
- if (adapter != null) {
- adapter!!.updateDataSet(newItems)
- } else {
- createAdapter(newItems)
- }
- }
-
- private fun createAdapter(items: List>) {
- adapter = FlexibleAdapter(items)
- binding.messageSearchRecycler.adapter = adapter
- adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener {
- override fun onItemClick(view: View?, position: Int): Boolean {
- val item = adapter!!.getItem(position)
- when (item) {
- is LoadMoreResultsItem -> {
- viewModel.loadMore()
- }
- is MessageResultItem -> {
- viewModel.selectMessage(item.messageEntry)
- }
- }
- return false
- }
- })
- }
-
- private fun onFinish() {
- val state = viewModel.state.value
- if (state is MessageSearchViewModel.FinishedState) {
- val resultIntent = Intent().apply {
- putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
- putExtra(RESULT_KEY_THREAD_ID, state.selectedThreadId)
- }
- setResult(Activity.RESULT_OK, resultIntent)
- finish()
- }
- }
-
- private fun showInitial() {
- displayLoading(false)
- binding.messageSearchRecycler.visibility = View.GONE
- binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing)
- binding.emptyContainer.emptyListView.visibility = View.VISIBLE
- }
-
- private fun showEmpty() {
- displayLoading(false)
- binding.messageSearchRecycler.visibility = View.GONE
- binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty)
- binding.emptyContainer.emptyListView.visibility = View.VISIBLE
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_search, menu)
- return true
- }
-
- override fun onPrepareOptionsMenu(menu: Menu): Boolean {
- val menuItem = menu.findItem(R.id.action_search)
- searchView = menuItem.actionView as SearchView
- setupSearchView()
- menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
- override fun onMenuItemActionExpand(item: MenuItem): Boolean {
- searchView.requestFocus()
- return true
- }
-
- override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
- onBackPressedDispatcher.onBackPressed()
- return false
- }
- })
- menuItem.expandActionView()
- return true
- }
-
- private fun setupSearchView() {
- searchView.queryHint = getString(R.string.message_search_hint)
- searchViewDisposable = observeSearchView(searchView)
- .debounce { query ->
- when {
- TextUtils.isEmpty(query) -> Observable.empty()
- else -> Observable.timer(
- ConversationsListActivity.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(),
- TimeUnit.MILLISECONDS
- )
- }
- }
- .distinctUntilChanged()
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe { newText -> viewModel.onQueryTextChange(newText) }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- searchViewDisposable?.dispose()
- }
-
- companion object {
- const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
- const val RESULT_KEY_THREAD_ID = "MessageSearchActivity.result.thread"
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt
deleted file mode 100644
index a91acf734f..0000000000
--- a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.messagesearch
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.logger.Logger
-import com.nextcloud.talk.models.domain.SearchMessageEntry
-import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-/**
- * Install PlantUML plugin to render this state diagram
- * @startuml
- * hide empty description
- * [*] --> InitialState
- * InitialState --> LoadingState
- * LoadingState --> EmptyState
- * LoadingState --> LoadedState
- * LoadingState --> LoadingState
- * LoadedState --> LoadingState
- * EmptyState --> LoadingState
- * LoadingState --> ErrorState
- * ErrorState --> LoadingState
- * @enduml
- */
-class MessageSearchViewModel @Inject constructor(
- private val unifiedSearchRepository: UnifiedSearchRepository,
- private val currentUserProvider: CurrentUserProviderOld,
- private val logger: Logger
-) : ViewModel() {
-
- sealed class ViewState
- object InitialState : ViewState()
- object LoadingState : ViewState()
- object EmptyState : ViewState()
- object ErrorState : ViewState()
- class LoadedState(val results: List, val hasMore: Boolean) : ViewState()
- class FinishedState(val selectedMessageId: String, val selectedThreadId: String?) : ViewState()
-
- private lateinit var messageSearchHelper: MessageSearchHelper
-
- private val _state: MutableLiveData = MutableLiveData(InitialState)
- val state: LiveData
- get() = _state
-
- private var currentUser: User = currentUserProvider.currentUser.blockingGet()
- private var searchJob: Job? = null
-
- fun initialize(roomToken: String) {
- messageSearchHelper = MessageSearchHelper(
- unifiedSearchRepository,
- currentUser,
- roomToken
- )
- }
-
- @Suppress("Detekt.TooGenericExceptionCaught")
- fun onQueryTextChange(newText: String) {
- if (newText.length >= MIN_CHARS_FOR_SEARCH) {
- _state.value = LoadingState
- searchJob?.cancel()
- searchJob = viewModelScope.launch {
- try {
- val results = messageSearchHelper.startMessageSearch(newText)
- onReceiveResults(results)
- } catch (e: Exception) {
- onError(e)
- }
- }
- }
- }
-
- fun loadMore() {
- _state.value = LoadingState
- searchJob?.cancel()
- searchJob = viewModelScope.launch {
- try {
- val results = messageSearchHelper.loadMore()
- results?.let { onReceiveResults(it) }
- } catch (e: Exception) {
- onError(e)
- }
- }
- }
-
- private fun onReceiveResults(results: MessageSearchHelper.MessageSearchResults) {
- if (results.messages.isEmpty()) {
- _state.value = EmptyState
- } else {
- _state.value = LoadedState(results.messages, results.hasMore)
- }
- }
-
- private fun onError(throwable: Throwable) {
- logger.e(TAG, "onError:", throwable)
- _state.value = ErrorState
- }
-
- fun refresh(query: String?) {
- query?.let { onQueryTextChange(it) }
- }
-
- fun selectMessage(messageEntry: SearchMessageEntry) {
- _state.value = FinishedState(messageEntry.messageId!!, messageEntry.threadId)
- }
-
- companion object {
- private val TAG = MessageSearchViewModel::class.java.simpleName
- private const val MIN_CHARS_FOR_SEARCH = 2
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt
index baa006739d..8b7dc48de3 100644
--- a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt
@@ -11,37 +11,42 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
+import com.nextcloud.talk.dagger.modules.assistedViewModels
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.ConversationDto
import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel
import com.nextcloud.talk.utils.adjustUIForAPILevel35
-import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.ui.LocalImageAuthHeader
+import androidx.compose.runtime.CompositionLocalProvider
@AutoInjector(NextcloudTalkApplication::class)
class ListOpenConversationsActivity : BaseActivity() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: OpenConversationsViewModel.Factory
- private lateinit var openConversationsViewModel: OpenConversationsViewModel
+ private lateinit var user: User
+
+ private val openConversationsViewModel: OpenConversationsViewModel by assistedViewModels {
+ viewModelFactory.build(user)
+ }
override fun onCreate(savedInstanceState: Bundle?) {
adjustUIForAPILevel35()
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ user = setUpBoundUserOrFinish() ?: return
- openConversationsViewModel = ViewModelProvider(this, viewModelFactory)[OpenConversationsViewModel::class.java]
openConversationsViewModel.fetchConversations()
- val user = currentUserProviderOld.currentUser.blockingGet()
-
setContent {
val colorScheme = viewThemeUtils.getColorScheme(this)
val viewState by openConversationsViewModel.viewState.collectAsStateWithLifecycle()
@@ -49,29 +54,29 @@ class ListOpenConversationsActivity : BaseActivity() {
MaterialTheme(colorScheme = colorScheme) {
ColoredStatusBar()
- OpenConversationsScreen(
- viewState = viewState,
- searchTerm = searchTerm,
- userBaseUrl = user?.baseUrl,
- listenerInput = OpenConversationsScreenListenerInput(
- onSearchTermChange = { term ->
- openConversationsViewModel.updateSearchTerm(term)
- openConversationsViewModel.fetchConversations()
- },
- onConversationClick = { conversation -> navigateToChat(conversation) },
- onBackClick = { onBackPressedDispatcher.onBackPressed() }
+ CompositionLocalProvider(
+ LocalImageAuthHeader provides ApiUtils.getCredentials(user.username, user.token)
+ ) {
+ OpenConversationsScreen(
+ viewState = viewState,
+ searchTerm = searchTerm,
+ userBaseUrl = user.baseUrl,
+ listenerInput = OpenConversationsScreenListenerInput(
+ onSearchTermChange = { term ->
+ openConversationsViewModel.updateSearchTerm(term)
+ openConversationsViewModel.fetchConversations()
+ },
+ onConversationClick = { conversation -> navigateToChat(user.id!!, conversation) },
+ onBackClick = { onBackPressedDispatcher.onBackPressed() }
+ )
)
- )
+ }
}
}
}
- private fun navigateToChat(conversation: ConversationDto) {
- val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token)
-
- val chatIntent = Intent(this, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ private fun navigateToChat(userId: Long, conversation: ConversationDto) {
+ val chatIntent = ChatActivity.createIntent(this, userId, conversation.token)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/OpenConversationsScreen.kt b/app/src/main/java/com/nextcloud/talk/openconversations/OpenConversationsScreen.kt
index 94d0db1f70..480611f892 100644
--- a/app/src/main/java/com/nextcloud/talk/openconversations/OpenConversationsScreen.kt
+++ b/app/src/main/java/com/nextcloud/talk/openconversations/OpenConversationsScreen.kt
@@ -90,6 +90,8 @@ import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewMode
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import kotlinx.coroutines.flow.distinctUntilChanged
+import com.nextcloud.talk.ui.withAuthHeader
+import com.nextcloud.talk.ui.LocalImageAuthHeader
private val TAG = ListOpenConversationsActivity::class.java.simpleName
@@ -352,6 +354,7 @@ private fun OpenConversationItem(
}
val imageRequest = ImageRequest.Builder(context)
.data(avatarUrl)
+ .withAuthHeader(LocalImageAuthHeader.current)
.transformations(CircleCropTransformation())
.error(errorPlaceholder)
.placeholder(errorPlaceholder)
diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt b/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt
index a9cb741444..33ea9fe78b 100644
--- a/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt
@@ -9,18 +9,20 @@ package com.nextcloud.talk.openconversations.viewmodels
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.ConversationDto
import com.nextcloud.talk.openconversations.data.OpenConversationsRepository
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import javax.inject.Inject
-class OpenConversationsViewModel @Inject constructor(
+class OpenConversationsViewModel @AssistedInject constructor(
private val repository: OpenConversationsRepository,
- private val currentUserProvider: CurrentUserProvider
+ @Assisted private val user: User
) : ViewModel() {
sealed interface ViewState
@@ -42,34 +44,31 @@ class OpenConversationsViewModel @Inject constructor(
_viewState.value = FetchConversationsStartState
viewModelScope.launch {
- currentUserProvider.getCurrentUser()
- .onSuccess {
- val apiVersion = ApiUtils.getConversationApiVersion(
- it,
- intArrayOf(
- ApiUtils.API_V4,
- ApiUtils.API_V3,
- 1
- )
- )
- val url = ApiUtils.getUrlForOpenConversations(apiVersion, it.baseUrl!!)
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ user,
+ intArrayOf(
+ ApiUtils.API_V4,
+ ApiUtils.API_V3,
+ 1
+ )
+ )
+ val url = ApiUtils.getUrlForOpenConversations(apiVersion, user.baseUrl!!)
- repository.fetchConversations(
- it,
- url,
- _searchTerm.value
- )
- .onSuccess { conversations ->
- if (conversations.isEmpty()) {
- _viewState.value = FetchConversationsEmptyState
- } else {
- _viewState.value = FetchConversationsSuccessState(conversations)
- }
- }
- .onFailure { exception ->
- Log.e(TAG, "Failed to fetch conversations", exception)
- _viewState.value = FetchConversationsErrorState
- }
+ repository.fetchConversations(
+ user,
+ url,
+ _searchTerm.value
+ )
+ .onSuccess { conversations ->
+ if (conversations.isEmpty()) {
+ _viewState.value = FetchConversationsEmptyState
+ } else {
+ _viewState.value = FetchConversationsSuccessState(conversations)
+ }
+ }
+ .onFailure { exception ->
+ Log.e(TAG, "Failed to fetch conversations", exception)
+ _viewState.value = FetchConversationsErrorState
}
}
}
@@ -78,6 +77,11 @@ class OpenConversationsViewModel @Inject constructor(
_searchTerm.value = newTerm
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): OpenConversationsViewModel
+ }
+
companion object {
private val TAG = OpenConversationsViewModel::class.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
index 3307b1c52b..24a37ca5f9 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt
@@ -15,6 +15,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
+import androidx.core.os.BundleCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
@@ -23,6 +24,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogPollCreateBinding
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.polls.adapters.PollCreateOptionItem
@@ -30,6 +32,7 @@ import com.nextcloud.talk.polls.adapters.PollCreateOptionsAdapter
import com.nextcloud.talk.polls.adapters.PollCreateOptionsItemListener
import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -54,10 +57,12 @@ class PollCreateDialogFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel = ViewModelProvider(this, viewModelFactory)[PollCreateViewModel::class.java]
val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
- viewModel.setData(roomToken)
+ val user = BundleCompat.getParcelable(requireArguments(), KEY_USER, User::class.java)!!
+ viewModel.setData(user, roomToken)
}
@SuppressLint("InflateParams")
@@ -187,10 +192,12 @@ class PollCreateDialogFragment :
companion object {
private val TAG = PollCreateDialogFragment::class.java.simpleName
private const val KEY_ROOM_TOKEN = "keyRoomToken"
+ private const val KEY_USER = "keyUser"
@JvmStatic
- fun newInstance(roomTokenParam: String): PollCreateDialogFragment {
+ fun newInstance(user: User, roomTokenParam: String): PollCreateDialogFragment {
val args = Bundle()
+ args.putParcelable(KEY_USER, user)
args.putString(KEY_ROOM_TOKEN, roomTokenParam)
val fragment = PollCreateDialogFragment()
fragment.arguments = args
diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt
index 75c65a7395..089082b04c 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt
@@ -16,6 +16,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogPollLoadingBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -29,6 +30,7 @@ class PollLoadingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
index 3c0a5f6154..ab1e69ed20 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt
@@ -12,6 +12,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
@@ -23,7 +24,7 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogPollMainBinding
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -35,9 +36,6 @@ class PollMainDialogFragment : DialogFragment() {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
- var currentUserProvider: CurrentUserProviderOld? = null
- @Inject set
-
private lateinit var binding: DialogPollMainBinding
private lateinit var viewModel: PollMainViewModel
@@ -46,10 +44,11 @@ class PollMainDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java]
- user = currentUserProvider?.currentUser?.blockingGet()!!
+ user = BundleCompat.getParcelable(requireArguments(), KEY_USER_ENTITY, User::class.java)!!
val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
val isOwnerOrModerator = arguments?.getBoolean(KEY_OWNER_OR_MODERATOR)!!
diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
index ae925cac65..fce36d3275 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt
@@ -25,6 +25,7 @@ import com.nextcloud.talk.polls.adapters.PollResultsAdapter
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -48,6 +49,7 @@ class PollResultsFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel = ViewModelProvider(this, viewModelFactory)[PollResultsViewModel::class.java]
parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java]
}
diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
index 73976fc58e..72bbe32d25 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt
@@ -29,6 +29,7 @@ import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -51,6 +52,7 @@ class PollVoteFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
viewModel = ViewModelProvider(this, viewModelFactory)[PollVoteViewModel::class.java]
parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java]
@@ -99,7 +101,7 @@ class PollVoteFragment : Fragment() {
}
binding.pollVoteSubmitButton.setOnClickListener {
- viewModel.vote(parentViewModel.roomToken, parentViewModel.pollId)
+ viewModel.vote(parentViewModel.user, parentViewModel.roomToken, parentViewModel.pollId)
}
binding.pollVoteEditDismiss.setOnClickListener {
diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
index 3fda1f2b1d..b2cb603a61 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt
@@ -10,21 +10,18 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.polls.adapters.PollCreateOptionItem
import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.repositories.PollRepository
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
-class PollCreateViewModel @Inject constructor(
- private val repository: PollRepository,
- private val currentUserProvider: CurrentUserProviderOld
-) : ViewModel() {
+class PollCreateViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
private lateinit var roomToken: String
@@ -33,7 +30,7 @@ class PollCreateViewModel @Inject constructor(
object PollCreatedState : ViewState
object PollCreationFailedState : ViewState
- private val currentUser = currentUserProvider.currentUser.blockingGet()
+ private lateinit var currentUser: User
private val _viewState: MutableLiveData = MutableLiveData(
PollCreationState(
@@ -68,7 +65,8 @@ class PollCreateViewModel @Inject constructor(
addOption()
}
- fun setData(roomToken: String) {
+ fun setData(user: User, roomToken: String) {
+ this.currentUser = user
this.roomToken = roomToken
updateCreationState()
}
diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
index 3aa89919e5..04dc4dace8 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt
@@ -15,17 +15,13 @@ import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.repositories.PollRepository
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
-class PollMainViewModel @Inject constructor(
- private val repository: PollRepository,
- private val currentUserProvider: CurrentUserProviderOld
-) : ViewModel() {
+class PollMainViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
@Inject
lateinit var userManager: UserManager
@@ -63,8 +59,8 @@ class PollMainViewModel @Inject constructor(
private var disposable: Disposable? = null
- private val currentUser = currentUserProvider.currentUser.blockingGet()
- private val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+ private val credentials: String?
+ get() = ApiUtils.getCredentials(user.username, user.token)
fun setData(user: User, roomToken: String, isOwnerOrModerator: Boolean, pollId: String, pollTitle: String) {
this.user = user
@@ -93,7 +89,7 @@ class PollMainViewModel @Inject constructor(
_viewState.value = LoadingState
val url = ApiUtils.getUrlForPoll(
- currentUser.baseUrl!!,
+ user.baseUrl!!,
roomToken,
pollId
)
@@ -113,7 +109,7 @@ class PollMainViewModel @Inject constructor(
_viewState.value = LoadingState
val url = ApiUtils.getUrlForPoll(
- currentUser.baseUrl!!,
+ user.baseUrl!!,
roomToken,
pollId
)
@@ -187,7 +183,7 @@ class PollMainViewModel @Inject constructor(
poll.resultMode == Poll.RESULT_MODE_PUBLIC &&
poll.votedSelf?.isNotEmpty() == true
- private fun isPollCreatedByCurrentUser(poll: Poll): Boolean = currentUser.userId == poll.actorId
+ private fun isPollCreatedByCurrentUser(poll: Poll): Boolean = user.userId == poll.actorId
fun dismissDialog() {
_viewState.value = DismissDialogState
diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt
index c717ef2d03..7b2877e374 100644
--- a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt
@@ -11,20 +11,17 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.repositories.PollRepository
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
-class PollVoteViewModel @Inject constructor(
- private val repository: PollRepository,
- private val currentUserProvider: CurrentUserProviderOld
-) : ViewModel() {
+class PollVoteViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() {
sealed interface ViewState
object InitialState : ViewState
@@ -50,9 +47,6 @@ class PollVoteViewModel @Inject constructor(
val selectedOptions: List
get() = _selectedOptions
- private val currentUser = currentUserProvider.currentUser.blockingGet()
- private val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
-
fun initVotedOptions(selectedOptions: List) {
_votedOptions = selectedOptions
_selectedOptions = selectedOptions
@@ -70,18 +64,18 @@ class PollVoteViewModel @Inject constructor(
_selectedOptions = _selectedOptions.minus(option)
}
- fun vote(roomToken: String, pollId: String) {
+ fun vote(user: User, roomToken: String, pollId: String) {
if (_selectedOptions.isNotEmpty()) {
_submitButtonEnabled.value = false
val url = ApiUtils.getUrlForPoll(
- currentUser.baseUrl!!,
+ user.baseUrl!!,
roomToken,
pollId
)
repository.vote(
- credentials,
+ ApiUtils.getCredentials(user.username, user.token),
url,
roomToken,
pollId,
diff --git a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
index b98cad526d..7b28dff502 100644
--- a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
+++ b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
@@ -20,10 +20,10 @@
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.models.json.mention.MentionDto;
import com.nextcloud.talk.models.json.mention.MentionOverall;
+import com.nextcloud.talk.ui.theme.HostViewThemeUtils;
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import com.nextcloud.talk.users.UserManager;
import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld;
import com.otaliastudios.autocomplete.RecyclerViewPresenter;
import java.util.ArrayList;
@@ -52,9 +52,6 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter()
@@ -114,6 +111,7 @@ class ProfileActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ setUpBoundUserOrFinish() ?: return
val restoredEdit = savedInstanceState?.getBoolean(KEY_EDIT_MODE) ?: false
profileUiState = profileUiState.copy(isEditMode = restoredEdit)
@@ -172,7 +170,11 @@ class ProfileActivity : BaseActivity() {
override fun onResume() {
super.onResume()
- currentUser = currentUserProviderOld.currentUser.blockingGet()
+ currentUser = runBlocking { userManager.getUserWithId(resolveUserIdFromIntent()) } ?: run {
+ // The account was removed while this screen was in the background.
+ finish()
+ return
+ }
val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
pickImage = PickImage(this, currentUser)
diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt b/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt
index 3c6a670859..1da3539e20 100644
--- a/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt
@@ -10,22 +10,19 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.raisehand.RequestAssistanceModel
import com.nextcloud.talk.raisehand.RequestAssistanceRepository
import com.nextcloud.talk.raisehand.WithdrawRequestAssistanceModel
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
-class RaiseHandViewModel @Inject constructor(
- private val repository: RequestAssistanceRepository,
- private val currentUserProvider: CurrentUserProviderOld
-) : ViewModel() {
+class RaiseHandViewModel @Inject constructor(private val repository: RequestAssistanceRepository) : ViewModel() {
@Inject
lateinit var userManager: UserManager
@@ -39,7 +36,7 @@ class RaiseHandViewModel @Inject constructor(
object LoweredHandState : ViewState
object ErrorState : ViewState
- private val currentUser = currentUserProvider.currentUser.blockingGet()
+ private lateinit var currentUser: User
private val _viewState: MutableLiveData = MutableLiveData(LoweredHandState)
val viewState: LiveData
@@ -101,7 +98,8 @@ class RaiseHandViewModel @Inject constructor(
}
}
- fun setData(roomToken: String, isBreakoutRoom: Boolean) {
+ fun setData(user: User, roomToken: String, isBreakoutRoom: Boolean) {
+ this.currentUser = user
this.roomToken = roomToken
this.isBreakoutRoom = isBreakoutRoom
}
diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
index 42873ec647..fca8cacb07 100644
--- a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
+++ b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
@@ -34,7 +34,6 @@ import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.message.SendMessageUtils
import io.reactivex.Observer
import io.reactivex.Single
@@ -50,9 +49,6 @@ class DirectReplyReceiver : BroadcastReceiver() {
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var ncApi: NcApi
@@ -74,8 +70,11 @@ class DirectReplyReceiver : BroadcastReceiver() {
systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0)
roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)
- val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!)
- currentUser = runBlocking { userManager.getUserWithId(id) }!!
+ val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
+ currentUser = runBlocking { userManager.getUserWithId(id) } ?: run {
+ Log.e(TAG, "No user found for id $id")
+ return
+ }
replyMessage = getMessageText(intent)
sendDirectReply()
diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt
index e9fba339af..cdfa161082 100644
--- a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt
+++ b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt
@@ -21,7 +21,6 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -35,9 +34,6 @@ class DismissRecordingAvailableReceiver : BroadcastReceiver() {
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var ncApi: NcApi
@@ -58,8 +54,11 @@ class DismissRecordingAvailableReceiver : BroadcastReceiver() {
systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0)
link = intent.getStringExtra(BundleKeys.KEY_DISMISS_RECORDING_URL)
- val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!)
- currentUser = runBlocking { userManager.getUserWithId(id) }!!
+ val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
+ currentUser = runBlocking { userManager.getUserWithId(id) } ?: run {
+ Log.e(TAG, "No user found for id $id")
+ return
+ }
dismissNcRecordingAvailableNotification()
}
diff --git a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt
index fdaa213d52..f83da396e6 100644
--- a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt
+++ b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt
@@ -23,7 +23,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -37,9 +36,6 @@ class MarkAsReadReceiver : BroadcastReceiver() {
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var ncApi: NcApi
@@ -62,8 +58,11 @@ class MarkAsReadReceiver : BroadcastReceiver() {
roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)
messageId = intent.getIntExtra(KEY_MESSAGE_ID, 0)
- val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!)
- currentUser = runBlocking { userManager.getUserWithId(id) }!!
+ val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
+ currentUser = runBlocking { userManager.getUserWithId(id) } ?: run {
+ Log.e(TAG, "No user found for id $id")
+ return
+ }
markAsRead()
}
diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt
index 1c48a6cad8..9ec1630ee9 100644
--- a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt
+++ b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt
@@ -24,7 +24,6 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -38,9 +37,6 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() {
@Inject
lateinit var userManager: UserManager
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var ncApi: NcApi
@@ -58,8 +54,11 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() {
systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0)
link = intent.getStringExtra(BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL)
- val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!)
- currentUser = runBlocking { userManager.getUserWithId(id) }!!
+ val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L)
+ currentUser = runBlocking { userManager.getUserWithId(id) } ?: run {
+ Log.e(TAG, "No user found for id $id")
+ return
+ }
shareRecordingToChat()
}
diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
index bff575ea17..0d4a461278 100644
--- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
@@ -10,58 +10,50 @@ package com.nextcloud.talk.remotefilebrowser.activities
import android.app.Activity
import android.content.Intent
-import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment
-import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import autodagger.AutoInjector
import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.dagger.modules.assistedViewModels
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityRemoteFileBrowserBinding
import com.nextcloud.talk.remotefilebrowser.SelectionInterface
import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter
import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment
-import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileSortOrder
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class RemoteFileBrowserActivity :
- AppCompatActivity(),
+ BaseActivity(),
SelectionInterface,
SwipeRefreshLayout.OnRefreshListener {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: RemoteFileBrowserItemsViewModel.Factory
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
- @Inject
- lateinit var viewThemeUtils: ViewThemeUtils
+ private lateinit var user: User
@Inject
lateinit var dateUtils: DateUtils
private lateinit var binding: ActivityRemoteFileBrowserBinding
- private lateinit var viewModel: RemoteFileBrowserItemsViewModel
+ private val viewModel: RemoteFileBrowserItemsViewModel by assistedViewModels { viewModelFactory.build(user) }
private var filesSelectionDoneMenuItem: MenuItem? = null
@@ -75,6 +67,7 @@ class RemoteFileBrowserActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ user = setUpBoundUserOrFinish() ?: return
binding = ActivityRemoteFileBrowserBinding.inflate(layoutInflater)
setSupportActionBar(binding.remoteFileBrowserItemsToolbar)
@@ -112,8 +105,6 @@ class RemoteFileBrowserActivity :
}
private fun initViewModel(mimeTypeSelectionFilter: String?) {
- viewModel = ViewModelProvider(this, viewModelFactory)[RemoteFileBrowserItemsViewModel::class.java]
-
viewModel.viewState.observe(this) { state ->
clearEmptyLoading()
when (state) {
@@ -167,7 +158,7 @@ class RemoteFileBrowserActivity :
val adapter = RemoteFileBrowserItemsAdapter(
showGrid = showGrid,
mimeTypeSelectionFilter = mimeTypeSelectionFilter,
- user = currentUserProvider.currentUser.blockingGet(),
+ user = user,
selectionInterface = this,
viewThemeUtils = viewThemeUtils,
dateUtils = dateUtils,
@@ -246,21 +237,6 @@ class RemoteFileBrowserActivity :
binding.recyclerView.visibility = View.VISIBLE
}
- fun initSystemBars() {
- val decorView = window.decorView
- decorView.setOnApplyWindowInsetsListener { view, insets ->
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- val systemBars = insets.getInsets(
- WindowInsetsCompat.Type.systemBars() or
- WindowInsetsCompat.Type.displayCutout()
- )
- view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
- }
- insets
- }
- ViewCompat.requestApplyInsets(decorView)
- }
-
override fun onRefresh() {
refreshCurrentPath()
}
diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt
index 7615d7dc74..10cd907c17 100644
--- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt
@@ -12,12 +12,15 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nextcloud.talk.R
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository
import com.nextcloud.talk.utils.FileSortOrder
import com.nextcloud.talk.utils.Mimetype.FOLDER
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -28,7 +31,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.io.File
-import javax.inject.Inject
/**
* @startuml
@@ -44,12 +46,10 @@ import javax.inject.Inject
* @enduml
*/
@OptIn(ExperimentalCoroutinesApi::class)
-class RemoteFileBrowserItemsViewModel
-@Inject
-constructor(
+class RemoteFileBrowserItemsViewModel @AssistedInject constructor(
private val repository: RemoteFileBrowserItemsRepository,
private val appPreferences: AppPreferencesImpl,
- private val currentUserProvider: CurrentUserProviderOld
+ @Assisted private val currentUser: User
) : ViewModel() {
sealed interface ViewState
@@ -59,8 +59,6 @@ constructor(
class LoadedState(val items: List) : ViewState
class FinishState(val selectedPaths: Set) : ViewState
- private val currentUser = currentUserProvider.currentUser.blockingGet()
-
private val initialSortOrder = FileSortOrder.getFileSortOrder(appPreferences.sorting)
private var sortingFlow: Flow
@@ -221,6 +219,11 @@ constructor(
fun isPathSelected(path: String): Boolean =
selectedPaths.value?.contains(path) == true || shouldPathBeSelectedDueToParent(path)
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): RemoteFileBrowserItemsViewModel
+ }
+
companion object {
private val TAG = RemoteFileBrowserItemsViewModel::class.simpleName
private const val ROOT_PATH = "/"
diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
index 7bf5bda7f8..48ac99d0b6 100644
--- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
@@ -43,6 +43,7 @@ import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -80,7 +81,6 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.profile.ProfileActivity
import com.nextcloud.talk.ui.dialog.SetPhoneNumberDialogFragment
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.ClosedInterfaceImpl
@@ -134,9 +134,6 @@ class SettingsActivity :
@Inject
lateinit var ncApiCoroutines: NcApiCoroutines
- @Inject
- lateinit var userManager: UserManager
-
@Inject
lateinit var platformPermissionUtil: PlatformPermissionUtil
@@ -175,6 +172,7 @@ class SettingsActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ setUpBoundUserOrFinish() ?: return
networkMonitor.isOnlineLiveData.observe(this) { online ->
isOnline.value = online
handleNetworkChange(isOnline.value)
@@ -187,7 +185,7 @@ class SettingsActivity :
binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") }
- getCurrentUser()
+ if (!getCurrentUser()) return
handleIntent(intent)
setupLicenceSetting(isOnline.value)
@@ -277,6 +275,7 @@ class SettingsActivity :
binding.settingsName.visibility = View.VISIBLE
binding.settingsName.setOnClickListener {
val intent = Intent(this, ProfileActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id)
startActivity(intent)
}
@@ -316,8 +315,7 @@ class SettingsActivity :
WorkManager.getInstance(context).getWorkInfoByIdLiveData(capabilitiesWork.id)
.observe(this) { workInfo ->
- if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
- getCurrentUser()
+ if (workInfo?.state == WorkInfo.State.SUCCEEDED && getCurrentUser()) {
setupCheckables(isOnline)
}
}
@@ -335,9 +333,17 @@ class SettingsActivity :
viewThemeUtils.material.themeToolbar(binding.settingsToolbar)
}
- private fun getCurrentUser() {
- currentUser = currentUserProviderOld.currentUser.blockingGet()
- credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+ /**
+ * Loads the account of this screen again. Finishes the activity and returns false if it was removed meanwhile.
+ */
+ private fun getCurrentUser(): Boolean {
+ val user = runBlocking { userManager.getUserWithId(resolveUserIdFromIntent()) } ?: run {
+ finish()
+ return false
+ }
+ currentUser = user
+ credentials = ApiUtils.getCredentials(user.username, user.token)
+ return true
}
private fun setupPhoneBookIntegration(isOnline: Boolean) {
@@ -631,6 +637,7 @@ class SettingsActivity :
}
.setNeutralButton(R.string.nc_diagnosis_dialog_open_diagnosis) { _, _ ->
val intent = Intent(context, DiagnosisActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id)
startActivity(intent)
}
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
@@ -656,14 +663,14 @@ class SettingsActivity :
private fun setupServerNotificationAppCheck() {
val serverNotificationAppInstalled =
- currentUserProviderOld.currentUser.blockingGet()
+ currentUser!!
.capabilities?.notificationsCapability?.features?.isNotEmpty()
?: false
if (!serverNotificationAppInstalled) {
binding.settingsServerNotificationAppWrapper.visibility = View.VISIBLE
val description = context.getString(R.string.nc_settings_contact_admin_of) + LINEBREAK +
- currentUserProviderOld.currentUser.blockingGet().baseUrl!!
+ currentUser!!.baseUrl!!
binding.settingsServerNotificationAppDescription.text = description
if (openedByNotificationWarning) {
@@ -692,7 +699,9 @@ class SettingsActivity :
private fun setupDiagnosis() {
binding.diagnosisWrapper.setOnClickListener {
- startActivity(Intent(context, DiagnosisActivity::class.java))
+ val intent = Intent(context, DiagnosisActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id)
+ startActivity(intent)
}
binding.logsWrapper.setOnClickListener {
startActivity(Intent(context, LogsActivity::class.java))
@@ -776,8 +785,9 @@ class SettingsActivity :
}
Log.d(TAG, "host: $host and port: $port")
currentUser!!.clientCertificate = finalAlias
+ val userId = currentUser!!.id!!
lifecycleScope.launch {
- userManager.updateOrCreateUser(currentUser!!)
+ userManager.updateClientCertificate(userId, finalAlias)
}
},
arrayOf("RSA", "EC"),
@@ -1094,8 +1104,9 @@ class SettingsActivity :
}
if ((!TextUtils.isEmpty(displayName) && !(displayName == currentUser!!.displayName))) {
currentUser!!.displayName = displayName
+ val userId = currentUser!!.id!!
lifecycleScope.launch {
- userManager.updateOrCreateUser(currentUser!!)
+ userManager.updateDisplayName(userId, displayName)
}
binding.nameText.text = currentUser!!.displayName
}
diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
index d80d0dd40e..ac080d75cc 100644
--- a/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
@@ -25,7 +25,7 @@ import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.contextchat.ContextChatViewModel
-import com.nextcloud.talk.dagger.modules.ViewModelFactoryWithParams
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
import com.nextcloud.talk.shareditems.adapters.SharedItemsAdapter
@@ -46,6 +46,8 @@ class SharedItemsActivity : BaseActivity() {
@Inject
lateinit var chatViewModelFactory: ChatViewModel.ChatViewModelFactory
+ private lateinit var conversationUser: User
+
val roomToken: String by lazy {
intent.getStringExtra(KEY_ROOM_TOKEN)
?: error("roomToken missing")
@@ -59,17 +61,15 @@ class SharedItemsActivity : BaseActivity() {
}
}
- val chatViewModel: ChatViewModel by viewModels {
- ViewModelFactoryWithParams(ChatViewModel::class.java) {
- chatViewModelFactory.build(
- roomToken,
- conversationThreadId
- )
- }
+ val chatViewModel: ChatViewModel by assistedViewModels {
+ chatViewModelFactory.build(
+ conversationUser,
+ roomToken,
+ conversationThreadId
+ )
}
- @Inject
- lateinit var contextChatViewModel: ContextChatViewModel
+ private val contextChatViewModel: ContextChatViewModel by viewModels { viewModelFactory }
private lateinit var binding: ActivitySharedItemsBinding
private lateinit var viewModel: SharedItemsViewModel
@@ -77,10 +77,9 @@ class SharedItemsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ conversationUser = setUpBoundUserOrFinish() ?: return
val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME)
- val user = currentUserProviderOld.currentUser.blockingGet()
-
val isUserConversationOwnerOrModerator = intent.getBooleanExtra(KEY_USER_IS_OWNER_OR_MODERATOR, false)
val isOne2One = intent.getBooleanExtra(KEY_IS_ONE_2_ONE, false)
@@ -99,7 +98,7 @@ class SharedItemsActivity : BaseActivity() {
viewModel = ViewModelProvider(this, viewModelFactory)[SharedItemsViewModel::class.java]
viewModel.viewState.observe(this) { state ->
- handleModelChange(state, user, roomToken, isUserConversationOwnerOrModerator, isOne2One)
+ handleModelChange(state, conversationUser, roomToken, isUserConversationOwnerOrModerator, isOne2One)
}
binding.imageRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@@ -111,7 +110,7 @@ class SharedItemsActivity : BaseActivity() {
}
})
- viewModel.initialize(user, roomToken)
+ viewModel.initialize(conversationUser, roomToken)
}
private fun handleModelChange(
@@ -162,18 +161,11 @@ class SharedItemsActivity : BaseActivity() {
viewThemeUtils.material.themeTabLayoutOnSurface(binding.sharedItemsTabs)
}
- fun startContextChatWindowForMessage(
- credentials: String?,
- baseUrl: String?,
- roomToken: String,
- messageId: String?,
- threadId: String?
- ) {
+ fun startContextChatWindowForMessage(user: User, roomToken: String, messageId: String?, threadId: String?) {
binding.genericComposeView.apply {
setContent {
contextChatViewModel.getContextForChatMessages(
- credentials = credentials!!,
- baseUrl = baseUrl!!,
+ user = user,
token = roomToken,
threadId = threadId,
messageId = messageId!!,
@@ -181,7 +173,7 @@ class SharedItemsActivity : BaseActivity() {
)
// Context chat Compose integration is pending for this screen.
// ContextChatView(
- // user = currentUserProviderOld.currentUser.blockingGet(),
+ // user = user,
// context,
// viewThemeUtils = viewThemeUtils,
// contextChatViewModel
diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
index d62b965d8c..fa360a8d62 100644
--- a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
+++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
@@ -8,7 +8,6 @@
package com.nextcloud.talk.shareditems.adapters
import android.content.Context
-import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@@ -125,23 +124,21 @@ class SharedItemsAdapter(
.sortedBy { it.messageId }
.map { it.toMediaViewerItem() }
.capSeedAroundMessage(item.messageId)
- context.startActivity(MediaViewerActivity.newIntent(context, roomToken, seedItems, item.messageId))
+ context.startActivity(
+ MediaViewerActivity.newIntent(context, user.id!!, roomToken, seedItems, item.messageId)
+ )
}
private fun openInChat(item: SharedFileItem, context: Context) {
- val intent = Intent(context, ChatActivity::class.java).apply {
- putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+ val intent = ChatActivity.createIntent(context, user.id!!, roomToken).apply {
putExtra(BundleKeys.KEY_MESSAGE_ID, item.messageId)
}
context.startActivity(intent)
}
private fun openMessage(item: SharedItem, context: Context) {
- val credentials = ApiUtils.getCredentials(user.username, user.token)
- val baseUrl = user.baseUrl
(context as SharedItemsActivity).startContextChatWindowForMessage(
- credentials,
- baseUrl,
+ user,
roomToken,
item.id,
null
diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt
index bd4f64442e..eed91f3b87 100644
--- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt
+++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt
@@ -19,5 +19,8 @@ interface SharedItemsRepository {
fun availableTypes(parameters: Parameters): Observable>
+ /**
+ * [userName] is the login name of the account, which authenticates the requests together with [userToken].
+ */
data class Parameters(val userName: String, val userToken: String, val baseUrl: String, val roomToken: String)
}
diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt
index d083ca9dde..68ebf27368 100644
--- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt
+++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt
@@ -26,6 +26,7 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateConstants
import com.nextcloud.talk.utils.DateUtils
import io.reactivex.Observable
+import retrofit2.HttpException
import retrofit2.Response
import java.util.Locale
import javax.inject.Inject
@@ -65,6 +66,9 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
}
+ if (!response.isSuccessful) {
+ throw HttpException(response)
+ }
val mediaItems = response.body()!!.ocs!!.data
if (mediaItems != null) {
for (it in mediaItems) {
diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt
index 3de476c563..cd2f672013 100644
--- a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt
@@ -41,7 +41,8 @@ class SharedItemsViewModel @Inject constructor(private val repository: SharedIte
fun initialize(user: User, roomToken: String) {
repositoryParameters = SharedItemsRepository.Parameters(
- user.userId!!,
+ // The login name, as it authenticates the requests. The user id can differ from it.
+ user.username!!,
user.token!!,
user.baseUrl!!,
roomToken
diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt
index a457002283..18280571ac 100644
--- a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt
@@ -7,7 +7,6 @@
package com.nextcloud.talk.threadsoverview
-import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import androidx.activity.compose.setContent
@@ -36,7 +35,6 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
@@ -45,12 +43,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.components.StandardAppBar
+import com.nextcloud.talk.dagger.modules.assistedViewModels
import com.nextcloud.talk.data.database.mappers.toDomainModel
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.threads.ThreadInfoDto
import com.nextcloud.talk.threadsoverview.components.ThreadRow
import com.nextcloud.talk.threadsoverview.viewmodels.ThreadsOverviewViewModel
-import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
import javax.inject.Inject
@@ -58,15 +56,16 @@ import javax.inject.Inject
class ThreadsOverviewActivity : BaseActivity() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var threadsOverviewViewModelFactory: ThreadsOverviewViewModel.Factory
@Inject
lateinit var ncApi: NcApi
- @Inject
- lateinit var userManager: UserManager
+ private lateinit var user: User
- lateinit var threadsOverviewViewModel: ThreadsOverviewViewModel
+ val threadsOverviewViewModel: ThreadsOverviewViewModel by assistedViewModels {
+ threadsOverviewViewModelFactory.build(user)
+ }
var threadsSourceUrl: String = ""
var appbarTitle: String = ""
@@ -74,10 +73,7 @@ class ThreadsOverviewActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- threadsOverviewViewModel = ViewModelProvider(
- this,
- viewModelFactory
- )[ThreadsOverviewViewModel::class.java]
+ user = setUpBoundUserOrFinish() ?: return
val colorScheme = viewThemeUtils.getColorScheme(this)
@@ -124,11 +120,8 @@ class ThreadsOverviewActivity : BaseActivity() {
}
private fun navigateToChatActivity(roomToken: String, threadId: Int) {
- val bundle = Bundle()
- bundle.putString(KEY_ROOM_TOKEN, roomToken)
- bundle.putLong(KEY_THREAD_ID, threadId.toLong())
- val chatIntent = Intent(context, ChatActivity::class.java)
- chatIntent.putExtras(bundle)
+ val chatIntent = ChatActivity.createIntent(context, resolveUserIdFromIntent(), roomToken)
+ chatIntent.putExtra(KEY_THREAD_ID, threadId.toLong())
startActivity(chatIntent)
}
diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt
index 0bc6c1bc50..c45a7fe9e0 100644
--- a/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt
@@ -19,22 +19,21 @@ import com.nextcloud.talk.models.json.threads.ThreadsOverall
import com.nextcloud.talk.threadsoverview.data.ThreadsRepository
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.UserIdUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import javax.inject.Inject
@Suppress("TooGenericExceptionCaught")
-class ThreadsOverviewViewModel @Inject constructor(
+class ThreadsOverviewViewModel @AssistedInject constructor(
private val threadsRepository: ThreadsRepository,
- private val currentUserProvider: CurrentUserProviderOld,
private val arbitraryStorageManager: ArbitraryStorageManager,
- private val logger: Logger
+ private val logger: Logger,
+ @Assisted val currentUser: User
) : ViewModel() {
- private val _currentUser = currentUserProvider.currentUser.blockingGet()
- val currentUser: User = _currentUser
- val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: ""
+ val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) ?: ""
private val _threadsListState = MutableStateFlow(ThreadsListUiState.None)
val threadsListState: StateFlow = _threadsListState
@@ -59,7 +58,7 @@ class ThreadsOverviewViewModel @Inject constructor(
private fun updateFollowedThreadsIndicator(url: String, threads: ThreadsOverall) {
val subscribedThreadsEndpoint = "subscribed-threads"
if (url.contains(subscribedThreadsEndpoint) && threads.ocs?.data?.isEmpty() == true) {
- val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet())
+ val accountId = UserIdUtils.getIdForUser(currentUser)
arbitraryStorageManager.storeStorageSetting(
accountId,
FOLLOWED_THREADS_EXIST,
@@ -81,6 +80,11 @@ class ThreadsOverviewViewModel @Inject constructor(
data class Error(val exception: Exception) : ThreadsListUiState()
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ThreadsOverviewViewModel
+ }
+
companion object {
private val TAG = ThreadsOverviewViewModel::class.java.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt
index 494ac7d60e..9b6b83763d 100644
--- a/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt
@@ -18,16 +18,16 @@ import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.core.graphics.drawable.toDrawable
-import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.dagger.modules.assistedViewModels
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityTranslateBinding
import com.nextcloud.talk.translate.repositories.model.LanguageDto
import com.nextcloud.talk.translate.viewmodels.TranslateViewModel
-import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.bundle.BundleKeys
import java.util.Locale
import javax.inject.Inject
@@ -37,12 +37,11 @@ import javax.inject.Inject
class TranslateActivity : BaseActivity() {
@Inject
- lateinit var userManager: UserManager
+ lateinit var viewModelFactory: TranslateViewModel.Factory
- @Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ private lateinit var user: User
- lateinit var viewModel: TranslateViewModel
+ val viewModel: TranslateViewModel by assistedViewModels { viewModelFactory.build(user) }
lateinit var binding: ActivityTranslateBinding
private var toLanguages: Array? = null
@@ -53,9 +52,9 @@ class TranslateActivity : BaseActivity() {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ user = setUpBoundUserOrFinish() ?: return
binding = ActivityTranslateBinding.inflate(layoutInflater)
- viewModel = ViewModelProvider(this, viewModelFactory)[TranslateViewModel::class.java]
viewModel.viewState.observe(this) { state ->
when (state) {
diff --git a/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt
index 6e561fc703..c19476f7ba 100644
--- a/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt
@@ -16,16 +16,17 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.translate.repositories.TranslateRepository
import com.nextcloud.talk.translate.repositories.model.LanguageDto
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
-import javax.inject.Inject
-class TranslateViewModel @Inject constructor(
+class TranslateViewModel @AssistedInject constructor(
private val repository: TranslateRepository,
- private val currentUserProvider: CurrentUserProviderOld
+ @Assisted private val currentUser: User
) : ViewModel() {
sealed interface ViewState
@@ -44,7 +45,6 @@ class TranslateViewModel @Inject constructor(
get() = _viewState
fun translateMessage(toLanguage: String, fromLanguage: String?, text: String) {
- val currentUser: User = currentUserProvider.currentUser.blockingGet()
val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
val url: String = ApiUtils.getUrlForTranslation(currentUser.baseUrl!!)
val calculatedFromLanguage =
@@ -67,7 +67,6 @@ class TranslateViewModel @Inject constructor(
}
fun getLanguages() {
- val currentUser: User = currentUserProvider.currentUser.blockingGet()
val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
val url: String = ApiUtils.getUrlForLanguages(currentUser.baseUrl!!)
Log.d(TAG, "URL is: $url")
@@ -113,6 +112,12 @@ class TranslateViewModel @Inject constructor(
// nothing?
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): TranslateViewModel
+ }
+
companion object {
private val TAG = TranslateViewModel::class.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/ImageAuthHeader.kt b/app/src/main/java/com/nextcloud/talk/ui/ImageAuthHeader.kt
new file mode 100644
index 0000000000..ffcadc3c55
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/ui/ImageAuthHeader.kt
@@ -0,0 +1,22 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.ui
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import coil.request.ImageRequest
+
+/**
+ * Credentials of the account a screen belongs to, for images it loads from that account's server, like file previews
+ * or conversation avatars. Requests with credentials don't use session cookies, so these images need them.
+ */
+val LocalImageAuthHeader = staticCompositionLocalOf { null }
+
+/**
+ * Adds [authHeader] as Authorization header, if there is one.
+ */
+fun ImageRequest.Builder.withAuthHeader(authHeader: String?): ImageRequest.Builder =
+ apply { authHeader?.let { addHeader("Authorization", it) } }
diff --git a/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt
index 348138e191..53a69b407a 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt
@@ -11,6 +11,7 @@ import android.content.Context
import android.util.AttributeSet
import autodagger.AutoInjector
import com.google.android.material.button.MaterialButton
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import java.util.Locale
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.ui.theme.ViewThemeUtils
@@ -35,6 +36,7 @@ class PlaybackSpeedControl @JvmOverloads constructor(
init {
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+ viewThemeUtils = hostViewThemeUtils(context, viewThemeUtils)
text = currentSpeed.label
viewThemeUtils.material.colorMaterialButtonText(this)
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageScaffold.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageScaffold.kt
index 0b071dcbdc..2b6584749a 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageScaffold.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageScaffold.kt
@@ -99,6 +99,8 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.TextMatchers
import java.time.LocalDate
+import coil.request.ImageRequest
+import com.nextcloud.talk.ui.withAuthHeader
private val regularTextSize = 16.sp
private val timeTextSize = 12.sp
@@ -907,7 +909,10 @@ private fun QuoteMediaRow(message: ChatMessageUi, content: MessageTypeContent.Me
) {
if (content.previewUrl != null) {
AsyncImage(
- model = content.previewUrl,
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(content.previewUrl)
+ .withAuthHeader(content.authHeader)
+ .build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize(),
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/LinkMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/LinkMessage.kt
index c8ef6abb15..3998c90941 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chat/LinkMessage.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chat/LinkMessage.kt
@@ -34,6 +34,7 @@ import com.nextcloud.talk.chat.ui.model.MessageTypeContent
import com.nextcloud.talk.contacts.load
import com.nextcloud.talk.models.json.opengraph.OpenGraphObjectDto
import com.nextcloud.talk.ui.theme.LocalOpenGraphFetcher
+import com.nextcloud.talk.utils.UriUtils
import androidx.core.net.toUri
private val previewImageHeight = 120.dp
@@ -70,7 +71,8 @@ fun LinkMessage(
LinkPreviewCard(
og = og,
url = og.link?.takeIf { it.isNotBlank() } ?: typeContent.url,
- highlightSearchTerm = highlightSearchTerm
+ highlightSearchTerm = highlightSearchTerm,
+ typeContent = typeContent
)
}
}
@@ -79,7 +81,12 @@ fun LinkMessage(
}
@Composable
-private fun LinkPreviewCard(og: OpenGraphObjectDto, url: String, highlightSearchTerm: String?) {
+private fun LinkPreviewCard(
+ og: OpenGraphObjectDto,
+ url: String,
+ highlightSearchTerm: String?,
+ typeContent: MessageTypeContent.LinkPreview
+) {
val context = LocalContext.current
Surface(
shape = MaterialTheme.shapes.small,
@@ -97,7 +104,7 @@ private fun LinkPreviewCard(og: OpenGraphObjectDto, url: String, highlightSearch
og = og,
highlightSearchTerm = highlightSearchTerm
)
- LinkPreviewImage(thumbUrl = og.thumb, context = context)
+ LinkPreviewImage(thumbUrl = og.thumb, context = context, typeContent = typeContent)
}
}
}
@@ -140,12 +147,19 @@ private fun LinkPreviewTexts(og: OpenGraphObjectDto, highlightSearchTerm: String
}
@Composable
-private fun LinkPreviewImage(thumbUrl: String?, context: android.content.Context) {
+private fun LinkPreviewImage(
+ thumbUrl: String?,
+ context: android.content.Context,
+ typeContent: MessageTypeContent.LinkPreview
+) {
thumbUrl?.takeIf { it.isNotBlank() }?.let {
+ // Credentials only for images of the account's own server, never for external ones.
+ val isOwnServer = typeContent.serverBaseUrl?.let { baseUrl -> UriUtils.isOnServer(it, baseUrl) } == true
val loadedImage = load(
imageUri = it,
context = context,
- errorPlaceholderImage = R.drawable.ic_mimetype_image
+ errorPlaceholderImage = R.drawable.ic_mimetype_image,
+ authHeader = if (isOwnServer) typeContent.authHeader else null
)
AsyncImage(
model = loadedImage,
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaGroupMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaGroupMessage.kt
index 5c7b571b5f..285ba26466 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaGroupMessage.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaGroupMessage.kt
@@ -434,7 +434,8 @@ private fun SyncedMediaTileContent(
imageUri = typeContent.previewUrl,
context = context,
errorPlaceholderImage = typeContent.drawableResourceId,
- animated = typeContent.animateGif
+ animated = typeContent.animateGif,
+ authHeader = typeContent.authHeader
)
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt
index ba56c5a954..31f1a6b742 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt
@@ -266,7 +266,8 @@ fun MediaMessage(
imageUri = retryAwarePreviewUrl,
context = context,
errorPlaceholderImage = typeContent.drawableResourceId,
- animated = typeContent.animateGif
+ animated = typeContent.animateGif,
+ authHeader = typeContent.authHeader
)
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.kt
index f2a36063e1..0f17072a9d 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToDialogFragment.kt
@@ -17,6 +17,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.core.os.BundleCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
@@ -25,10 +26,13 @@ import androidx.lifecycle.repeatOnLifecycle
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.conversationlist.ConversationsListActivity
+import com.nextcloud.talk.dagger.modules.ViewModelFactoryWithParams
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.ui.chooseaccount.model.LoadUsersSuccessStateChooseAccountShareTo
import com.nextcloud.talk.ui.chooseaccount.model.SwitchUserSuccessStateChooseAccountShareTo
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.net.CookieManager
@@ -38,7 +42,7 @@ import javax.inject.Inject
class ChooseAccountShareToDialogFragment : DialogFragment() {
@Inject
- lateinit var viewModelFactory: ViewModelProvider.Factory
+ lateinit var viewModelFactory: ChooseAccountShareToViewModel.Factory
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@@ -62,7 +66,13 @@ class ChooseAccountShareToDialogFragment : DialogFragment() {
super.onViewCreated(view, savedInstanceState)
NextcloudTalkApplication.Companion.sharedApplication!!.componentApplication.inject(this)
- viewModel = ViewModelProvider(this, viewModelFactory)[ChooseAccountShareToViewModel::class.java]
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
+ // The account of the conversation list this chooser belongs to, not the default account.
+ val user = BundleCompat.getParcelable(requireArguments(), KEY_USER, User::class.java)!!
+ viewModel = ViewModelProvider(
+ this,
+ ViewModelFactoryWithParams(ChooseAccountShareToViewModel::class.java) { viewModelFactory.build(user) }
+ )[ChooseAccountShareToViewModel::class.java]
val otherUsers = mutableStateOf>(emptyList())
@@ -75,7 +85,7 @@ class ChooseAccountShareToDialogFragment : DialogFragment() {
}
is SwitchUserSuccessStateChooseAccountShareTo -> {
cookieManager.cookieStore.removeAll()
- activity?.recreate()
+ (activity as? ConversationsListActivity)?.relaunchForAccount(state.user.id!!)
dismiss()
}
else -> {}
@@ -110,6 +120,11 @@ class ChooseAccountShareToDialogFragment : DialogFragment() {
companion object {
const val TAG = "ChooseAccountShareToDialogFragment"
- fun newInstance(): ChooseAccountShareToDialogFragment = ChooseAccountShareToDialogFragment()
+ private const val KEY_USER = "KEY_USER"
+
+ fun newInstance(user: User): ChooseAccountShareToDialogFragment =
+ ChooseAccountShareToDialogFragment().apply {
+ arguments = Bundle().apply { putParcelable(KEY_USER, user) }
+ }
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt
index dbd8a25d01..fadc244d7f 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt
@@ -17,21 +17,22 @@ import com.nextcloud.talk.ui.chooseaccount.model.LoadUsersSuccessStateChooseAcco
import com.nextcloud.talk.ui.chooseaccount.model.SwitchUserErrorStateChooseAccountShareTo
import com.nextcloud.talk.ui.chooseaccount.model.SwitchUserSuccessStateChooseAccountShareTo
import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import javax.inject.Inject
-class ChooseAccountShareToViewModel @Inject constructor(
+/**
+ * Lets the conversation list of [currentUser] continue a pending share with another account.
+ */
+class ChooseAccountShareToViewModel @AssistedInject constructor(
private val userManager: UserManager,
- currentUserProvider: CurrentUserProvider
+ @Assisted val currentUser: User
) : ViewModel() {
- val currentUser: User? = runBlocking { currentUserProvider.getCurrentUser() }.getOrNull()
-
private val _chooseAccountShareToViewState: MutableStateFlow =
MutableStateFlow(LoadUsersStartStateChooseAccountShareTo)
val chooseAccountShareToViewState: StateFlow =
@@ -44,7 +45,7 @@ class ChooseAccountShareToViewModel @Inject constructor(
try {
val users = userManager.getUsers()
_chooseAccountShareToViewState.value =
- LoadUsersSuccessStateChooseAccountShareTo(users.filter { !it.current })
+ LoadUsersSuccessStateChooseAccountShareTo(users.filter { it.id != currentUser.id })
} catch (e: Exception) {
Log.e(TAG, "Error loading users", e)
_chooseAccountShareToViewState.value = LoadUsersSuccessStateChooseAccountShareTo(emptyList())
@@ -59,7 +60,7 @@ class ChooseAccountShareToViewModel @Inject constructor(
val success = userManager.setUserAsActive(user)
_chooseAccountShareToViewState.value =
if (success) {
- SwitchUserSuccessStateChooseAccountShareTo
+ SwitchUserSuccessStateChooseAccountShareTo(user)
} else {
SwitchUserErrorStateChooseAccountShareTo
}
@@ -70,6 +71,11 @@ class ChooseAccountShareToViewModel @Inject constructor(
}
}
+ @AssistedFactory
+ interface Factory {
+ fun build(user: User): ChooseAccountShareToViewModel
+ }
+
companion object {
private val TAG = ChooseAccountShareToViewModel::class.simpleName
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.kt b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.kt
index ec79369ab2..c33043c910 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/model/ChooseAccountShareToViewState.kt
@@ -13,5 +13,5 @@ sealed interface ChooseAccountShareToViewState
object LoadUsersStartStateChooseAccountShareTo : ChooseAccountShareToViewState
open class LoadUsersSuccessStateChooseAccountShareTo(val users: List) : ChooseAccountShareToViewState
-object SwitchUserSuccessStateChooseAccountShareTo : ChooseAccountShareToViewState
+class SwitchUserSuccessStateChooseAccountShareTo(val user: User) : ChooseAccountShareToViewState
object SwitchUserErrorStateChooseAccountShareTo : ChooseAccountShareToViewState
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
index 1f0cf9512f..a1cdb1e73c 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
@@ -20,6 +20,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.databinding.DialogAttachmentBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.SpreedFeatures
import javax.inject.Inject
@@ -35,6 +36,7 @@ class AttachmentDialog(val activity: Activity, var chatActivity: ChatActivity) :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
dialogAttachmentBinding = DialogAttachmentBinding.inflate(layoutInflater)
setContentView(dialogAttachmentBinding.root)
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt
index 659599fc28..e1eab531df 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt
@@ -19,6 +19,7 @@ import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogAudioOutputBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.webrtc.WebRtcAudioManager
import javax.inject.Inject
@@ -33,6 +34,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+ viewThemeUtils = hostViewThemeUtils(callActivity, viewThemeUtils)
dialogAudioOutputBinding = DialogAudioOutputBinding.inflate(layoutInflater)
setContentView(dialogAudioOutputBinding.root)
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt
index 3842ff06d6..f2baf8b63c 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt
@@ -63,10 +63,11 @@ import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.ui.dialog.DateTimeCompose.Companion.HALF_WEIGHT
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
import java.time.DayOfWeek
import java.time.Instant
@@ -79,21 +80,17 @@ import java.time.temporal.TemporalAdjusters.nextOrSame
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
-class DateTimeCompose(val bundle: Bundle, val chatViewModel: ChatViewModel) {
+class DateTimeCompose(val bundle: Bundle, val chatViewModel: ChatViewModel, val user: User) {
private var timeState = mutableStateOf(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.MIN))
init {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
- val user = currentUserProvider.currentUser.blockingGet()
val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!!
val messageId = bundle.getInt(BundleKeys.KEY_MESSAGE_ID)
val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION)
chatViewModel.getReminder(user, roomToken, messageId.toString(), apiVersion)
}
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@@ -103,7 +100,7 @@ class DateTimeCompose(val bundle: Bundle, val chatViewModel: ChatViewModel) {
return
}
- val colorScheme = viewThemeUtils.getColorScheme(context)
+ val colorScheme = hostViewThemeUtils(context, viewThemeUtils).getColorScheme(context)
val isCollapsed = remember { mutableStateOf(true) }
MaterialTheme(colorScheme = colorScheme) {
@@ -141,7 +138,6 @@ class DateTimeCompose(val bundle: Bundle, val chatViewModel: ChatViewModel) {
) {
TextButton(
onClick = {
- val user = currentUserProvider.currentUser.blockingGet()
val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!!
val messageId = bundle.getInt(BundleKeys.KEY_MESSAGE_ID)
val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION)
@@ -169,7 +165,6 @@ class DateTimeCompose(val bundle: Bundle, val chatViewModel: ChatViewModel) {
TextButton(
onClick = {
- val user = currentUserProvider.currentUser.blockingGet()
val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!!
val messageId = bundle.getInt(BundleKeys.KEY_MESSAGE_ID)
val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION)
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt
index 6647a03ed4..ab7ea2f153 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt
@@ -12,6 +12,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
+import androidx.core.os.BundleCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment
@@ -26,11 +27,17 @@ import com.nextcloud.talk.databinding.BanItemListBinding
import com.nextcloud.talk.databinding.FragmentDialogBanListBinding
import com.nextcloud.talk.models.json.participants.TalkBanDto
import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
-class DialogBanListFragment(val roomToken: String) : DialogFragment() {
+class DialogBanListFragment : DialogFragment() {
+
+ // Read from the arguments, so the fragment can be recreated by the system after rotation or process death.
+ private val roomToken: String by lazy { requireArguments().getString(ROOM_TOKEN_ARG)!! }
+ private val conversationUser: User by lazy {
+ BundleCompat.getParcelable(requireArguments(), USER_ARG, User::class.java)!!
+ }
lateinit var binding: FragmentDialogBanListBinding
@@ -40,11 +47,7 @@ class DialogBanListFragment(val roomToken: String) : DialogFragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
-
lateinit var viewModel: ConversationInfoViewModel
- private lateinit var conversationUser: User
private val adapter = object : BaseAdapter() {
private var bans: List = mutableListOf()
@@ -87,10 +90,10 @@ class DialogBanListFragment(val roomToken: String) : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
binding = FragmentDialogBanListBinding.inflate(layoutInflater)
viewModel =
ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
- conversationUser = currentUserProvider.currentUser.blockingGet()
themeView()
initObservers()
@@ -144,8 +147,17 @@ class DialogBanListFragment(val roomToken: String) : DialogFragment() {
}
companion object {
+ private const val ROOM_TOKEN_ARG = "ROOM_TOKEN_ARG"
+ private const val USER_ARG = "USER_ARG"
+
@JvmStatic
- fun newInstance(roomToken: String) = DialogBanListFragment(roomToken)
+ fun newInstance(roomToken: String, user: User) =
+ DialogBanListFragment().apply {
+ arguments = Bundle().apply {
+ putString(ROOM_TOKEN_ARG, roomToken)
+ putParcelable(USER_ARG, user)
+ }
+ }
const val ONE_SEC = 1000L
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt
index aa6fc1245d..f5ac1f34fe 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt
@@ -13,6 +13,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.os.BundleCompat
import androidx.fragment.app.DialogFragment
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -20,12 +21,13 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.conversationlist.ConversationsListActivity
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogFilterConversationBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.UserIdUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -33,9 +35,7 @@ class FilterConversationFragment : DialogFragment() {
lateinit var binding: DialogFilterConversationBinding
private var dialogView: View? = null
private lateinit var filterState: HashMap
-
- @Inject
- lateinit var currentUserProvider: CurrentUserProviderOld
+ private lateinit var user: User
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@@ -50,11 +50,13 @@ class FilterConversationFragment : DialogFragment() {
} else {
arguments?.getSerializable(FILTER_STATE_ARG) as HashMap
}
+ user = BundleCompat.getParcelable(requireArguments(), USER_ARG, User::class.java)!!
return MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
setUpColors()
setUpListeners()
return inflater.inflate(R.layout.dialog_filter_conversation, container, false)
@@ -117,7 +119,7 @@ class FilterConversationFragment : DialogFragment() {
binding.mentionedFilterChip.isChecked = filterState[MENTION]!!
binding.archivedFilterChip.visibility = View.GONE
- currentUserProvider.currentUser.blockingGet().capabilities?.spreedCapability?.let {
+ user.capabilities?.spreedCapability?.let {
if (hasSpreedFeatureCapability(it, SpreedFeatures.ARCHIVE_CONVERSATIONS)) {
binding.archivedFilterChip.visibility = View.VISIBLE
binding.archivedFilterChip.isChecked = filterState[ARCHIVE]!!
@@ -127,7 +129,7 @@ class FilterConversationFragment : DialogFragment() {
private fun processSubmit() {
// store
- val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet())
+ val accountId = UserIdUtils.getIdForUser(user)
val mentionValue = filterState[MENTION] == true
val unreadValue = filterState[UNREAD] == true
val archivedValue = filterState[ARCHIVE] == true
@@ -153,12 +155,14 @@ class FilterConversationFragment : DialogFragment() {
companion object {
private const val FILTER_STATE_ARG = "FILTER_STATE_ARG"
+ private const val USER_ARG = "USER_ARG"
@JvmStatic
- fun newInstance(savedFilterState: MutableMap): FilterConversationFragment {
+ fun newInstance(savedFilterState: MutableMap, user: User): FilterConversationFragment {
val filterConversationFragment = FilterConversationFragment()
val args = Bundle()
args.putSerializable(FILTER_STATE_ARG, HashMap(savedFilterState))
+ args.putParcelable(USER_ARG, user)
filterConversationFragment.arguments = args
return filterConversationFragment
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt
index bfc10a712b..453212ffef 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt
@@ -24,6 +24,7 @@ import com.nextcloud.talk.camera.ImageSegmenterHelper
import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding
import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.viewmodels.CallRecordingViewModel
@@ -40,6 +41,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+ viewThemeUtils = hostViewThemeUtils(callActivity, viewThemeUtils)
binding = DialogMoreCallActionsBinding.inflate(layoutInflater)
setContentView(binding.root)
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt
index 3c43eb5196..e68d91fedc 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt
@@ -16,6 +16,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import com.nextcloud.talk.utils.setExpeditedIfSupported
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -40,6 +41,7 @@ class SaveToStorageDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
fileName = arguments?.getString(KEY_FILE_NAME)!!
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt
index e6913c4009..11d4f6e6e8 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt
@@ -23,6 +23,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogSetPhoneNumberBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.ui.theme.hostViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@@ -37,6 +38,7 @@ class SetPhoneNumberDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
+ viewThemeUtils = hostViewThemeUtils(activity, viewThemeUtils)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java
index c23a60a880..ac7f380d70 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java
@@ -21,6 +21,7 @@
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.databinding.SortingOrderFragmentBinding;
+import com.nextcloud.talk.ui.theme.HostViewThemeUtils;
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import com.nextcloud.talk.utils.FileSortOrder;
import com.nextcloud.talk.utils.preferences.AppPreferences;
@@ -98,6 +99,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
+ viewThemeUtils = HostViewThemeUtils.hostViewThemeUtils(getActivity(), viewThemeUtils);
setupDialogElements();
setupListeners();
diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/HostViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/HostViewThemeUtils.kt
new file mode 100644
index 0000000000..68e4ef8ef9
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/ui/theme/HostViewThemeUtils.kt
@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+@file:JvmName("HostViewThemeUtils")
+
+package com.nextcloud.talk.ui.theme
+
+import android.content.Context
+import android.content.ContextWrapper
+import com.nextcloud.talk.activities.BaseActivity
+
+/**
+ * Returns the [ViewThemeUtils] of the [BaseActivity] that [context] belongs to, which is themed for the account that
+ * activity was started for, or [fallback] if [context] does not belong to a [BaseActivity].
+ */
+fun hostViewThemeUtils(context: Context?, fallback: ViewThemeUtils): ViewThemeUtils {
+ var current = context
+ while (current is ContextWrapper) {
+ if (current is BaseActivity) {
+ return current.viewThemeUtils
+ }
+ current = current.baseContext
+ }
+ return fallback
+}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt
index a1a57ad03c..93160d7f01 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt
@@ -14,5 +14,9 @@ import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
interface MaterialSchemesProvider {
fun getMaterialSchemesForUser(user: User?): MaterialSchemes
fun getMaterialSchemesForCapabilities(capabilities: CapabilitiesDto?): MaterialSchemes
- fun getMaterialSchemesForCurrentUser(): MaterialSchemes
+
+ /**
+ * Schemes of the default account, for code without an account context of its own.
+ */
+ fun getMaterialSchemesForDefaultUser(): MaterialSchemes
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt
index 3442203746..871482f4f0 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt
@@ -11,40 +11,34 @@ import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.MaterialSchemes
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
+import com.nextcloud.talk.models.json.capabilities.ThemingCapabilityDto
+import com.nextcloud.talk.users.DefaultAccountProvider
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
internal class MaterialSchemesProviderImpl @Inject constructor(
- private val userProvider: CurrentUserProviderOld,
+ private val defaultAccountProvider: DefaultAccountProvider,
private val colorUtil: ColorUtil
) : MaterialSchemesProvider {
- private val themeCache: ConcurrentHashMap = ConcurrentHashMap()
+ // Keyed by the theming capability instead of the server: accounts on the same server can have different colors
+ // (e.g. a personal primary color), and changed colors are used after the next capabilities sync.
+ private val themeCache: ConcurrentHashMap = ConcurrentHashMap()
override fun getMaterialSchemesForUser(user: User?): MaterialSchemes {
- val url: String = if (user?.baseUrl != null) {
- user.baseUrl!!
- } else {
- FALLBACK_URL
- }
-
- if (!themeCache.containsKey(url)) {
- themeCache[url] = getMaterialSchemesForCapabilities(user?.capabilities)
- }
-
- return themeCache[url]!!
+ val capabilities = user?.capabilities
+ // A copy, so a later change of the capability object cannot change the key of a cached entry.
+ val key = ThemeKey(capabilities?.themingCapability?.copy())
+ return themeCache.getOrPut(key) { getMaterialSchemesForCapabilities(capabilities) }
}
- override fun getMaterialSchemesForCurrentUser(): MaterialSchemes =
- getMaterialSchemesForUser(userProvider.currentUser.blockingGet())
+ override fun getMaterialSchemesForDefaultUser(): MaterialSchemes =
+ getMaterialSchemesForUser(defaultAccountProvider.getDefaultUserBlocking())
override fun getMaterialSchemesForCapabilities(capabilities: CapabilitiesDto?): MaterialSchemes {
val serverTheme = ServerThemeImpl(capabilities?.themingCapability, colorUtil)
return MaterialSchemes.fromServerTheme(serverTheme)
}
- companion object {
- const val FALLBACK_URL = "NULL"
- }
+ private data class ThemeKey(val themingCapability: ThemingCapabilityDto?)
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt
index 296c34c294..eeaebadd80 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt
@@ -24,7 +24,7 @@ internal abstract class ThemeModule {
companion object {
@Provides
- fun provideCurrentMaterialSchemes(schemesProvider: MaterialSchemesProvider): MaterialSchemes =
- schemesProvider.getMaterialSchemesForCurrentUser()
+ fun provideDefaultMaterialSchemes(schemesProvider: MaterialSchemesProvider): MaterialSchemes =
+ schemesProvider.getMaterialSchemesForDefaultUser()
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtilsFactory.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtilsFactory.kt
new file mode 100644
index 0000000000..263e530ee5
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtilsFactory.kt
@@ -0,0 +1,33 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.ui.theme
+
+import com.nextcloud.android.common.ui.color.ColorUtil
+import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils
+import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils
+import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils
+import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils
+import com.nextcloud.talk.data.user.model.User
+import javax.inject.Inject
+
+/**
+ * Creates [ViewThemeUtils] themed with the server colors of a given account.
+ */
+class ViewThemeUtilsFactory @Inject constructor(
+ private val schemesProvider: MaterialSchemesProvider,
+ private val colorUtil: ColorUtil
+) {
+ fun forUser(user: User): ViewThemeUtils {
+ val schemes = schemesProvider.getMaterialSchemesForUser(user)
+ val android = AndroidViewThemeUtils(schemes, colorUtil)
+ val material = MaterialViewThemeUtils(schemes, colorUtil)
+ val androidx = AndroidXViewThemeUtils(schemes, android)
+ val talk = TalkSpecificViewThemeUtils(schemes, androidx)
+ val dialog = DialogViewThemeUtils(schemes)
+ return ViewThemeUtils(schemes, android, material, androidx, talk, dialog)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/users/DefaultAccountProvider.kt b/app/src/main/java/com/nextcloud/talk/users/DefaultAccountProvider.kt
new file mode 100644
index 0000000000..3e7ca306c7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/users/DefaultAccountProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.users
+
+import com.nextcloud.talk.data.user.model.User
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+/**
+ * Provides the default account, i.e. the last used one.
+ *
+ * Only for code without an account context of its own, like app entry points, share-to, the account switcher and the
+ * phone book integration. Screens, workers and receivers must use the account they were started for.
+ */
+class DefaultAccountProvider @Inject constructor(private val userManager: UserManager) {
+
+ suspend fun getDefaultUser(): User? = userManager.getDefaultUser()
+
+ /**
+ * Returns the default account without suspending. Only loads it from the database if it is not known yet.
+ */
+ fun getDefaultUserBlocking(): User? = userManager.defaultUserFlow.value ?: runBlocking { getDefaultUser() }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
index 5f1e454efd..1497c6d4d2 100644
--- a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
+++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
@@ -19,8 +19,10 @@ import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
@@ -32,10 +34,31 @@ class UserManager internal constructor(private val userRepository: UsersReposito
suspend fun getUsersScheduledForDeletion(): List = userRepository.getUsersScheduledForDeletion()
+ suspend fun getUsersNotScheduledForDeletion(): List = userRepository.getUsersNotScheduledForDeletion()
+
+ /**
+ * The default account, i.e. the last active user, or - if none is active - any user not scheduled for deletion,
+ * which is then set as active.
+ *
+ * Only for entry points that have no account context (app launch, share-to, deep links without account).
+ * Screens, workers and receivers must use the user they were started for, see [getUserWithId] and [userFlow].
+ */
+ suspend fun getDefaultUser(): User? = userRepository.getActiveUser() ?: getAnyUserAndSetAsActive()
+
/**
- * The active user, or - if none is active - any user not scheduled for deletion, which is then set as active.
+ * Emits the user with the given internal id whenever its row changes, or null if it does not exist (anymore).
*/
- suspend fun getCurrentUser(): User? = userRepository.getActiveUser() ?: getAnyUserAndSetAsActive()
+ fun userFlow(id: Long): Flow = userRepository.getUserWithIdFlow(id).distinctUntilChanged()
+
+ /**
+ * Ensures that at most one user is marked as active. If several are, the one with the highest id is kept,
+ * matching the user returned by [getDefaultUser].
+ */
+ suspend fun repairMultipleActiveUsers() {
+ if (userRepository.repairMultipleActiveUsers() > 0) {
+ Log.w(TAG, "Multiple active users found, kept the one with the highest id as active")
+ }
+ }
/**
* Backed by [activeUserStateFlow] rather than [UsersRepository.getActiveUserFlow] directly, so that
@@ -46,8 +69,10 @@ class UserManager internal constructor(private val userRepository: UsersReposito
* knows about it - e.g. launching a screen for the new user before its avatar/data had actually updated.
* Room's own [UsersRepository.getActiveUserFlow] is still relied on underneath to seed this and to catch
* any change to the `current` flag that doesn't go through [setUserAsActive].
+ *
+ * Screens must not use this but the account they were started for, see [userFlow].
*/
- val currentUserFlow: StateFlow
+ val defaultUserFlow: StateFlow
get() = activeUserStateFlow
private val activeUserStateFlow: MutableStateFlow by lazy {
@@ -71,7 +96,10 @@ class UserManager internal constructor(private val userRepository: UsersReposito
suspend fun getUserWithInternalId(id: Long): User? = userRepository.getUserWithIdNotScheduledForDeletion(id)
suspend fun checkIfUserExists(username: String, server: String): Boolean =
- userRepository.getUserWithUsernameAndServer(username, server) != null
+ getUserWithUsernameAndServer(username, server) != null
+
+ suspend fun getUserWithUsernameAndServer(username: String, server: String): User? =
+ userRepository.getUserWithUsernameAndServer(username, server)
/**
* Don't ask
@@ -99,25 +127,38 @@ class UserManager internal constructor(private val userRepository: UsersReposito
}
}
+ // The following updates only write the given fields, so they cannot reset fields that changed since the user
+ // was read, like the default account flag. Use them instead of saving a whole user that was read earlier.
+
suspend fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Int {
- val user = userRepository.getUserWithId(id) ?: throw NoSuchElementException()
- user.externalSignalingServer = externalSignalingServer
- return userRepository.updateUser(user)
+ val updated = userRepository.updateExternalSignalingServer(id, externalSignalingServer)
+ if (updated == 0) throw NoSuchElementException()
+ return updated
}
+ suspend fun updateCapabilities(id: Long, capabilities: CapabilitiesDto?, serverVersion: ServerVersionDto?): Int =
+ userRepository.updateCapabilities(id, capabilities, serverVersion)
+
+ suspend fun updateDisplayName(id: Long, displayName: String?): Int =
+ userRepository.updateDisplayName(id, displayName)
+
+ suspend fun updateClientCertificate(id: Long, clientCertificate: String?): Int =
+ userRepository.updateClientCertificate(id, clientCertificate)
+
+ suspend fun updateCredentials(id: Long, token: String?, clientCertificate: String?): Int =
+ userRepository.updateCredentials(id, token, clientCertificate)
+
suspend fun updateOrCreateUser(user: User): Int =
when (user.id) {
null -> userRepository.insertUser(user).toInt()
else -> userRepository.updateUser(user)
}
- suspend fun saveUser(user: User): Int = userRepository.updateUser(user)
-
suspend fun setUserAsActive(user: User): Boolean {
Log.d(TAG, "setUserAsActive:" + user.id!!)
val success = userRepository.setUserAsActiveWithId(user.id!!)
if (success) {
- activeUserStateFlow.value = user
+ activeUserStateFlow.value = userRepository.getUserWithId(user.id!!) ?: user
}
return success
}
@@ -210,7 +251,6 @@ class UserManager internal constructor(private val userRepository: UsersReposito
companion object {
const val TAG = "UserManager"
- private const val NO_ACTIVE_USER_ID = -1L
}
data class UserAttributes(
diff --git a/app/src/main/java/com/nextcloud/talk/utils/CredentialsCookieInterceptor.kt b/app/src/main/java/com/nextcloud/talk/utils/CredentialsCookieInterceptor.kt
new file mode 100644
index 0000000000..4502ff4770
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/CredentialsCookieInterceptor.kt
@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.utils
+
+import okhttp3.Interceptor
+import okhttp3.Response
+
+/**
+ * Keeps server session cookies out of requests that authenticate with their own credentials.
+ *
+ * All accounts share one HTTP client with one cookie store, which keeps cookies per server only. A session cookie of
+ * one account would be sent along with the requests of another account on the same server, and the server would
+ * treat those requests as the first account. Requests with an Authorization header therefore neither send nor store
+ * cookies, so they are authenticated by their credentials alone.
+ *
+ * Must be added as a network interceptor, as OkHttp adds the cookies of the cookie jar and stores received cookies
+ * around the network interceptors.
+ */
+class CredentialsCookieInterceptor : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ if (request.header(AUTHORIZATION) == null) {
+ return chain.proceed(request)
+ }
+ val response = chain.proceed(request.newBuilder().removeHeader(COOKIE).build())
+ return response.newBuilder().removeHeader(SET_COOKIE).build()
+ }
+
+ private companion object {
+ const val AUTHORIZATION = "Authorization"
+ const val COOKIE = "Cookie"
+ const val SET_COOKIE = "Set-Cookie"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
index fbbf8f969e..f820cdcce5 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
@@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX
import com.nextcloud.talk.utils.Mimetype.VIDEO_QUICKTIME
import com.nextcloud.talk.utils.Mimetype.VIDEO_WEBM
import com.nextcloud.talk.utils.MimetypeUtils.isMarkdown
+import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
import java.util.concurrent.ExecutionException
@@ -117,7 +118,9 @@ class FileViewerUtils(private val context: Context, private val user: User) {
actorDisplayName = message.actorDisplayName.orEmpty(),
timestamp = message.timestamp
)
- context.startActivity(MediaViewerActivity.newIntent(context, roomToken, listOf(item), item.messageId))
+ context.startActivity(
+ MediaViewerActivity.newIntent(context, user.id!!, roomToken, listOf(item), item.messageId)
+ )
}
fun openFile(
@@ -267,8 +270,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype))
fullScreenTextViewerIntent.putExtra("FILE_ID", fileId)
fullScreenTextViewerIntent.putExtra("LINK", link)
- fullScreenTextViewerIntent.putExtra("USERNAME", user.username)
- fullScreenTextViewerIntent.putExtra("BASE_URL", user.baseUrl)
+ fullScreenTextViewerIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, user.id)
context.startActivity(fullScreenTextViewerIntent)
}
@@ -319,8 +321,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
}
val data: Data = Data.Builder()
- .putString(DownloadFileToCacheWorker.KEY_BASE_URL, user.baseUrl)
- .putString(DownloadFileToCacheWorker.KEY_USER_ID, user.userId)
+ .putLong(BundleKeys.KEY_INTERNAL_USER_ID, user.id!!)
.putString(
DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
CapabilitiesUtil.getAttachmentFolder(user.capabilities!!.spreedCapability!!)
diff --git a/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt
index 3d148e2f39..a831767198 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt
@@ -82,6 +82,7 @@ class PickImage(private val activity: Activity, private var currentUser: User?)
val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java)
avatarIntent.putExtras(bundle)
+ currentUser?.id?.let { avatarIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it) }
startSelectRemoteFilesIntentForResult.launch(avatarIntent)
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt b/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt
index 68b273268e..d7ab1301f1 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt
@@ -60,7 +60,7 @@ class RemoteWipeInterceptor(
return response
}
- val candidate = resolveWipeCandidate(chain.request().url.toString()) ?: return response
+ val candidate = resolveWipeCandidate(chain.request()) ?: return response
if (!handledUserIds.add(candidate.userId)) {
Log.d(TAG, "User ${candidate.userId} was already handled, ignoring")
return response
@@ -72,27 +72,36 @@ class RemoteWipeInterceptor(
return response
}
- private fun resolveWipeCandidate(requestUrl: String): WipeCandidate? {
- val user = runBlocking { userManager.getUsers() }
- .firstOrNull { it.baseUrl != null && requestUrl.startsWith(it.baseUrl!!) }
- if (user == null) {
- Log.d(TAG, "No known user matches base URL of $requestUrl, ignoring")
- return null
+ /**
+ * The account whose credentials the server rejected: the one with exactly the credentials of [request], on the
+ * server of its URL. A 401 for a request without credentials, or with credentials of no stored account, says
+ * nothing about an account, e.g. a request that relied on a session cookie, and is ignored. Several accounts can
+ * be on the same server, so the server alone does not identify the account.
+ */
+ private fun resolveWipeCandidate(request: Request): WipeCandidate? {
+ val user = findUserWithCredentialsOf(request)
+ val token = user?.token
+ val userId = user?.id
+
+ return if (user != null && token != null && userId != null) {
+ WipeCandidate(user, token, userId)
+ } else {
+ Log.d(TAG, "401 for a request without the credentials of a known account, ignoring: ${request.url}")
+ null
}
+ }
- val token = user.token
- if (token == null) {
- Log.d(TAG, "User ${user.id} has no token, ignoring")
- return null
- }
+ /**
+ * The stored account on the server of [request] whose credentials the request carries, if any.
+ */
+ private fun findUserWithCredentialsOf(request: Request): User? {
+ val credentials = request.header(AUTHORIZATION) ?: return null
+ val requestUrl = request.url.toString()
- val userId = user.id
- if (userId == null) {
- Log.d(TAG, "User has no id, ignoring")
- return null
+ return runBlocking { userManager.getUsers() }.firstOrNull { user ->
+ val isOnServerOfRequest = user.baseUrl?.let { UriUtils.isOnServer(requestUrl, it) } == true
+ isOnServerOfRequest && ApiUtils.getCredentials(user.username, user.token) == credentials
}
-
- return WipeCandidate(user, token, userId)
}
private fun isWipeRequestedByServer(candidate: WipeCandidate): Boolean {
@@ -155,6 +164,7 @@ class RemoteWipeInterceptor(
companion object {
private const val TAG = "RemoteWipeInterceptor"
private const val HTTP_UNAUTHORIZED = 401
+ private const val AUTHORIZATION = "Authorization"
private const val WIPE_PATH = "wipe"
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
index 955d836259..c56f8d4b09 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
@@ -8,11 +8,29 @@
package com.nextcloud.talk.utils
import androidx.core.net.toUri
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
class UriUtils {
companion object {
fun hasHttpProtocolPrefixed(uri: String): Boolean = uri.startsWith("http://") || uri.startsWith("https://")
+ /**
+ * Whether [url] points to the server at [baseUrl]: same scheme, host and port, and within its path. Unlike a
+ * prefix check, this rejects e.g. `https://cloud.example.com.attacker.test` for `https://cloud.example.com`,
+ * so it can decide whether to send the account's credentials.
+ */
+ fun isOnServer(url: String, baseUrl: String): Boolean {
+ val target = url.toHttpUrlOrNull()
+ val server = baseUrl.toHttpUrlOrNull()
+ val serverPath = server?.encodedPath?.trimEnd('/').orEmpty()
+ return target != null &&
+ server != null &&
+ target.scheme == server.scheme &&
+ target.host == server.host &&
+ target.port == server.port &&
+ (target.encodedPath == serverPath || target.encodedPath.startsWith("$serverPath/"))
+ }
+
fun extractInstanceInternalFileFileId(url: String): String {
// https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41
return url.toUri().getQueryParameter("fileid").toString()
diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.kt
deleted file mode 100644
index 965d2387a0..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProvider.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.utils.database.user
-
-import com.nextcloud.talk.data.user.model.User
-import kotlinx.coroutines.flow.Flow
-
-interface CurrentUserProvider {
- val currentUserFlow: Flow
- suspend fun getCurrentUser(timeout: Long = 5000L): Result
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt
deleted file mode 100644
index abc2ee7820..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.utils.database.user
-
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.users.UserManager
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.withTimeoutOrNull
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class CurrentUserProviderImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProvider {
-
- // only emit non-null users
- override val currentUserFlow: Flow = userManager.currentUserFlow.filterNotNull()
-
- // function for safe one-shot access
- override suspend fun getCurrentUser(timeout: Long): Result {
- val user = withTimeoutOrNull(timeout) {
- currentUserFlow.first()
- }
-
- return if (user != null) {
- Result.success(user)
- } else {
- Result.failure(IllegalStateException("No current user available"))
- }
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.kt
deleted file mode 100644
index 1663cad1a8..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOld.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.utils.database.user
-
-import com.nextcloud.talk.data.user.model.User
-import io.reactivex.Maybe
-
-@Deprecated("Use CurrentUserProvider instead")
-interface CurrentUserProviderOld {
- val currentUser: Maybe
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.kt
deleted file mode 100644
index 7d525b7355..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderOldImpl.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.utils.database.user
-
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.users.UserManager
-import io.reactivex.Maybe
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/**
- * Listens to changes in the database and provides the current user without needing to query the database everytime.
- */
-@Deprecated("Use CurrentUserProvider instead")
-@Singleton
-class CurrentUserProviderOldImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProviderOld {
-
- private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
-
- private var _currentUser: User? = null
-
- // synchronized to avoid multiple observers initialized from different threads
- @get:Synchronized
- @set:Synchronized
- private var currentUserObserver: Job? = null
-
- @Deprecated("Use currentUserProvider instead")
- override val currentUser: Maybe
- get() {
- if (_currentUser == null) {
- // immediately get a result synchronously
- _currentUser = runBlocking { userManager.getCurrentUser() }
- if (currentUserObserver == null) {
- currentUserObserver = scope.launch {
- userManager.currentUserFlow.filterNotNull().collect {
- _currentUser = it
- }
- }
- }
- }
- return _currentUser?.let { Maybe.just(it) } ?: Maybe.empty()
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt
index 07036cc858..4ff6b378e9 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt
@@ -11,7 +11,6 @@ package com.nextcloud.talk.utils.database.user
import com.nextcloud.talk.dagger.modules.DatabaseModule
import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.users.UserManager
-import dagger.Binds
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@@ -19,14 +18,6 @@ import javax.inject.Singleton
@Module(includes = [DatabaseModule::class])
abstract class UserModule {
- @Binds
- abstract fun bindCurrentUserProviderOld(
- currentUserProviderOldImpl: CurrentUserProviderOldImpl
- ): CurrentUserProviderOld
-
- @Binds
- abstract fun bindCurrentUserProvider(currentUserProviderImpl: CurrentUserProviderImpl): CurrentUserProvider
-
companion object {
@Provides
@Singleton
diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt
index 9b7da27ae0..729c2f51ef 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt
@@ -44,6 +44,7 @@ import com.nextcloud.talk.data.network.NetworkMonitorImpl
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.repositories.passwordpolicy.PasswordPolicyRepositoryImpl
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl
@@ -54,10 +55,8 @@ import com.nextcloud.talk.threadsoverview.data.ThreadsRepositoryImpl
import com.nextcloud.talk.ui.theme.MaterialSchemesProviderImpl
import com.nextcloud.talk.ui.theme.TalkSpecificViewThemeUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.users.DefaultAccountProvider
import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderImpl
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOldImpl
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl
@@ -66,6 +65,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
@@ -95,14 +95,17 @@ class ComposePreviewUtils private constructor(context: Context) {
val userManager: UserManager
get() = UserManager(userRepository)
- val userProvider: CurrentUserProviderOld
- get() = CurrentUserProviderOldImpl(userManager)
+ val defaultAccountProvider: DefaultAccountProvider
+ get() = DefaultAccountProvider(userManager)
+
+ val currentUser: User
+ get() = runBlocking { userManager.getDefaultUser() }!!
val colorUtil: ColorUtil
get() = ColorUtil(mContext)
val materialScheme: MaterialSchemes
- get() = MaterialSchemesProviderImpl(userProvider, colorUtil).getMaterialSchemesForCurrentUser()
+ get() = MaterialSchemesProviderImpl(defaultAccountProvider, colorUtil).getMaterialSchemesForDefaultUser()
val viewThemeUtils: ViewThemeUtils
get() {
@@ -209,9 +212,6 @@ class ComposePreviewUtils private constructor(context: Context) {
val audioFocusRequestManager: AudioFocusRequestManager
get() = AudioFocusRequestManager(mContext)
- val currentUserProvider: CurrentUserProviderImpl
- get() = CurrentUserProviderImpl(userManager)
-
object TestLogger : Logger {
override fun d(tag: String, message: String) = Unit
override fun d(tag: String, message: String, t: Throwable) = Unit
@@ -233,8 +233,9 @@ class ComposePreviewUtils private constructor(context: Context) {
unifiedSearchRepository = unifiedSearchRepository,
mediaRecorderManager = mediaRecorderManager,
audioFocusRequestManager = audioFocusRequestManager,
- currentUserProvider = currentUserProvider,
+ userManager = userManager,
appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
+ initialUser = currentUser,
chatRoomToken = "",
conversationThreadId = null,
logger = TestLogger
@@ -244,7 +245,7 @@ class ComposePreviewUtils private constructor(context: Context) {
get() = ContactsRepositoryImpl(ncApiCoroutines)
val contactsViewModel: ContactsViewModel
- get() = ContactsViewModel(contactsRepository, currentUserProvider, TestLogger)
+ get() = ContactsViewModel(contactsRepository, TestLogger, currentUser)
val conversationCreationViewModel: ConversationCreationViewModel
get() = ConversationCreationRepositoryImpl(ncApiCoroutines).let { repository ->
@@ -252,7 +253,7 @@ class ComposePreviewUtils private constructor(context: Context) {
repository,
ConversationCreator(repository, TestLogger),
PasswordPolicyRepositoryImpl(ncApiCoroutines),
- currentUserProvider
+ currentUser
)
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt
index 1dc27b7788..1c00bfb971 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt
@@ -14,7 +14,12 @@ import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.user.UsersDao
+import com.nextcloud.talk.data.user.model.UserCapabilitiesUpdate
+import com.nextcloud.talk.data.user.model.UserClientCertificateUpdate
+import com.nextcloud.talk.data.user.model.UserCredentialsUpdate
+import com.nextcloud.talk.data.user.model.UserDisplayNameUpdate
import com.nextcloud.talk.data.user.model.UserEntity
+import com.nextcloud.talk.data.user.model.UserExternalSignalingServerUpdate
import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@@ -205,6 +210,10 @@ class DummyUserDaoImpl : UsersDao {
override suspend fun getUserWithId(id: Long): UserEntity? = dummyUsers.find { it.id == id }
+ override fun getUserWithIdFlow(id: Long): Flow = flowOf(dummyUsers.find { it.id == id })
+
+ override suspend fun repairMultipleActiveUsers(): Int = 0
+
override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity? =
dummyUsers.find { it.id == id && !it.scheduledForDeletion }
@@ -237,6 +246,30 @@ class DummyUserDaoImpl : UsersDao {
0
}
}
+
+ override suspend fun updateCapabilities(update: UserCapabilitiesUpdate): Int =
+ updateDummyUser(update.id) {
+ it.copy(capabilities = update.capabilities, serverVersion = update.serverVersion)
+ }
+
+ override suspend fun updateExternalSignalingServer(update: UserExternalSignalingServerUpdate): Int =
+ updateDummyUser(update.id) { it.copy(externalSignalingServer = update.externalSignalingServer) }
+
+ override suspend fun updateDisplayName(update: UserDisplayNameUpdate): Int =
+ updateDummyUser(update.id) { it.copy(displayName = update.displayName) }
+
+ override suspend fun updateClientCertificate(update: UserClientCertificateUpdate): Int =
+ updateDummyUser(update.id) { it.copy(clientCertificate = update.clientCertificate) }
+
+ override suspend fun updateCredentials(update: UserCredentialsUpdate): Int =
+ updateDummyUser(update.id) { it.copy(token = update.token, clientCertificate = update.clientCertificate) }
+
+ private fun updateDummyUser(id: Long, update: (UserEntity) -> UserEntity): Int {
+ val index = dummyUsers.indexOfFirst { it.id == id }
+ if (index == -1) return 0
+ dummyUsers[index] = update(dummyUsers[index])
+ return 1
+ }
}
class DummyConversationDaoImpl : ConversationsDao {
diff --git a/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt b/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt
deleted file mode 100644
index 5097ff8465..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Nextcloud Talk - Android Client
- *
- * SPDX-FileCopyrightText: 2022 Álvaro Brey
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.talk.utils.rx
-
-import androidx.appcompat.widget.SearchView
-import io.reactivex.Observable
-import io.reactivex.subjects.PublishSubject
-
-class SearchViewObservable {
-
- companion object {
- @JvmStatic
- fun observeSearchView(searchView: SearchView): Observable {
- val subject: PublishSubject = PublishSubject.create()
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- searchView.clearFocus()
- subject.onComplete()
- return true
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- subject.onNext(newText)
- return true
- }
- })
- return subject
- }
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java
index 515bae0633..2bb5895af9 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java
@@ -41,6 +41,14 @@ public String getCurrentRoomToken() {
return currentRoomToken;
}
+ /**
+ * Whether the current room is the room with the given token of the account with the given internal id. The token
+ * alone is not enough: accounts on the same server share the tokens of their common conversations.
+ */
+ public boolean isCurrentRoom(String roomToken, Long userId) {
+ return currentRoomToken.equals(roomToken) && userInRoom.getId() != null && userInRoom.getId().equals(userId);
+ }
+
public void setCurrentRoomToken(String currentRoomToken) {
this.currentRoomToken = currentRoomToken;
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelector.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelector.kt
new file mode 100644
index 0000000000..c1c143ef7a
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelector.kt
@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.utils.ssl
+
+import com.nextcloud.talk.data.user.model.User
+import java.net.URI
+import java.net.URISyntaxException
+
+/**
+ * Chooses the client certificate of the account a TLS connection is for.
+ *
+ * The TLS handshake only knows the server, not the account of the request. So the certificate is chosen by the
+ * server: the one of the accounts on it. Only if accounts on the same server use different certificates, the
+ * default account decides, as the connection may then be reused by all of them.
+ */
+object ClientCertificateSelector {
+
+ /**
+ * Returns the alias of the client certificate for a connection to [host], or null if no account on it uses one.
+ * Without a [host], the certificate of [defaultUser] is used.
+ */
+ @JvmStatic
+ fun selectAlias(host: String?, users: List, defaultUser: User?): String? {
+ val usersOnHost = users.filter { host != null && hostOf(it).equals(host, ignoreCase = true) }
+ val aliases = usersOnHost.mapNotNull { aliasOf(it) }.toSet()
+ return when {
+ host == null -> aliasOf(defaultUser)
+ aliases.size <= 1 -> aliases.firstOrNull()
+ else -> defaultUser?.takeIf { default -> usersOnHost.any { it.id == default.id } }?.let { aliasOf(it) }
+ }
+ }
+
+ private fun aliasOf(user: User?): String? = user?.clientCertificate?.takeIf { it.isNotEmpty() }
+
+ private fun hostOf(user: User): String? =
+ try {
+ user.baseUrl?.let { URI(it).host }
+ } catch (e: URISyntaxException) {
+ null
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java b/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java
index cf867f74ba..885e3cd9a6 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java
@@ -27,6 +27,8 @@
import java.util.List;
import java.util.Set;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
import javax.net.ssl.X509KeyManager;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlinx.coroutines.BuildersKt;
@@ -49,20 +51,28 @@ public KeyManager(X509KeyManager keyManager, UserManager userManager, AppPrefere
context = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
}
+ /**
+ * Chooses the certificate of the accounts on the server of the connection, see {@link ClientCertificateSelector}. The
+ * handshake doesn't know the account of the request, so the default account alone would offer the wrong
+ * certificate for the requests of other accounts.
+ */
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
String alias;
- User currentUser = null;
+ List users = new ArrayList<>();
+ User defaultUser = null;
try {
- currentUser = BuildersKt.runBlocking(
+ users = BuildersKt.runBlocking(
EmptyCoroutineContext.INSTANCE,
- (scope, continuation) -> userManager.getCurrentUser(continuation));
+ (scope, continuation) -> userManager.getUsers(continuation));
+ defaultUser = BuildersKt.runBlocking(
+ EmptyCoroutineContext.INSTANCE,
+ (scope, continuation) -> userManager.getDefaultUser(continuation));
} catch (InterruptedException e) {
- Log.e(TAG, "Interrupted while getting the current user: " + e.getLocalizedMessage());
+ Log.e(TAG, "Interrupted while getting the users: " + e.getLocalizedMessage());
Thread.currentThread().interrupt();
}
- if ((currentUser != null &&
- !TextUtils.isEmpty(alias = currentUser.getClientCertificate())) ||
+ if (!TextUtils.isEmpty(alias = ClientCertificateSelector.selectAlias(getPeerHost(socket), users, defaultUser)) ||
!TextUtils.isEmpty(alias = appPreferences.getTemporaryClientCertAlias())
&& new ArrayList<>(Arrays.asList(getClientAliases())).contains(alias)) {
return alias;
@@ -71,6 +81,20 @@ public String chooseClientAlias(String[] strings, Principal[] principals, Socket
return null;
}
+ @Nullable
+ private static String getPeerHost(@Nullable Socket socket) {
+ if (socket instanceof SSLSocket) {
+ SSLSession session = ((SSLSocket) socket).getHandshakeSession();
+ if (session != null && session.getPeerHost() != null) {
+ return session.getPeerHost();
+ }
+ }
+ if (socket != null && socket.getInetAddress() != null) {
+ return socket.getInetAddress().getHostName();
+ }
+ return null;
+ }
+
@Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return null;
diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt
index 60f879e505..c61b5c1d78 100644
--- a/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt
@@ -15,19 +15,16 @@ import com.nextcloud.talk.models.domain.StartCallRecordingModel
import com.nextcloud.talk.models.domain.StopCallRecordingModel
import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository
import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
-class CallRecordingViewModel @Inject constructor(
- private val repository: CallRecordingRepository,
- private val currentUserProvider: CurrentUserProviderOld
-) : ViewModel() {
+class CallRecordingViewModel @Inject constructor(private val repository: CallRecordingRepository) : ViewModel() {
lateinit var roomToken: String
+ private lateinit var currentUser: User
sealed interface ViewState
open class RecordingStartedState(val hasVideo: Boolean, val showStartedInfo: Boolean) : ViewState
@@ -44,8 +41,8 @@ class CallRecordingViewModel @Inject constructor(
private var disposable: Disposable? = null
- private var currentUser: User = currentUserProvider.currentUser.blockingGet()
- val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
+ private val credentials: String
+ get() = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
fun clickRecordButton() {
when (viewState.value) {
@@ -117,7 +114,8 @@ class CallRecordingViewModel @Inject constructor(
disposable?.dispose()
}
- fun setData(roomToken: String) {
+ fun setData(user: User, roomToken: String) {
+ this.currentUser = user
this.roomToken = roomToken
}
diff --git a/app/src/main/res/layout/activity_message_search.xml b/app/src/main/res/layout/activity_message_search.xml
deleted file mode 100644
index e077575275..0000000000
--- a/app/src/main/res/layout/activity_message_search.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/rv_item_load_more.xml b/app/src/main/res/layout/rv_item_load_more.xml
deleted file mode 100644
index 7e05fe57ba..0000000000
--- a/app/src/main/res/layout/rv_item_load_more.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/rv_item_search_message.xml b/app/src/main/res/layout/rv_item_search_message.xml
deleted file mode 100644
index 020d276c5c..0000000000
--- a/app/src/main/res/layout/rv_item_search_message.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml
deleted file mode 100644
index 970712def4..0000000000
--- a/app/src/main/res/menu/menu_search.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index ca9a240d29..ba77a21a5e 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -21,7 +21,6 @@
8dp
- 16dp
40dp
96dp
52dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6022720eb5..8ab569cb88 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,6 +32,7 @@ How to translate with transifex:
Set
Dismiss
Sorry, something went wrong!
+ You logged in with a different account. Log in with the account you want to reauthorize.
Create
Unknown
Disabled
@@ -885,8 +886,6 @@ How to translate with transifex:
Messages
Load more results
Search …
- Start typing to search …
- No search results
Previous search result
Next search result
No more messages found
diff --git a/app/src/test/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModelTest.kt
new file mode 100644
index 0000000000..0d089501bd
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModelTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.account.viewmodels
+
+import com.nextcloud.talk.account.data.LoginRepository
+import com.nextcloud.talk.account.data.model.LoginResponse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verifyBlocking
+import org.mockito.kotlin.wheneverBlocking
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class BrowserLoginActivityViewModelTest {
+
+ private val repository: LoginRepository = mock()
+ private lateinit var viewModel: BrowserLoginActivityViewModel
+
+ private val loginResponse = LoginResponse(token = "token", pollUrl = "https://example.com/poll", loginUrl = URL)
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(UnconfinedTestDispatcher())
+ viewModel = BrowserLoginActivityViewModel(repository)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `starting the login again continues the started login`() {
+ wheneverBlocking { repository.startLoginFlow(any(), any(), anyOrNull()) }.thenReturn(loginResponse)
+
+ viewModel.startWebBrowserLogin(BASE_URL)
+ viewModel.startWebBrowserLogin(BASE_URL)
+
+ verifyBlocking(repository, times(1)) { startLoginFlow(any(), any(), anyOrNull()) }
+ assertEquals(
+ BrowserLoginActivityViewModel.InitialLoginViewState.InitialLoginRequestSuccess(URL),
+ viewModel.initialLoginRequestState.value
+ )
+ }
+
+ @Test
+ fun `a QR code login is not started twice either`() {
+ wheneverBlocking { repository.startOTPLoginFlow(any(), any(), anyOrNull()) }.thenReturn(null)
+
+ viewModel.loginWithOTPQR("qr")
+ viewModel.loginWithOTPQR("qr")
+
+ verifyBlocking(repository, times(1)) { startOTPLoginFlow(any(), any(), anyOrNull()) }
+ }
+
+ companion object {
+ private const val BASE_URL = "https://example.com"
+ private const val URL = "https://example.com/login"
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt
index 55c3b2f1bd..76f9799ca7 100644
--- a/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt
@@ -16,8 +16,6 @@ import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderImpl
import com.nextcloud.talk.utils.preview.DummyUserDaoImpl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -33,6 +31,7 @@ import org.junit.Test
import org.mockito.kotlin.mock
@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("TooManyFunctions")
class ContactsViewModelTest {
private lateinit var viewModel: ContactsViewModel
private val repository: ContactsRepository = FakeRepositorySuccess()
@@ -49,19 +48,11 @@ class ContactsViewModelTest {
val userManager: UserManager
get() = UserManager(userRepository)
- val userProvider: CurrentUserProvider
- get() = CurrentUserProviderImpl(userManager)
-
private var viewModelCount = 0
private fun createViewModel(repo: ContactsRepository): ContactsViewModel {
- val provider = userProvider
- // CurrentUserProviderImpl feeds its currentUserFlow from a real Dispatchers.IO
- // scope, independent of the test dispatcher's virtual time. Waiting for it here
- // (real blocking wait, since this isn't inside runTest) ensures the value is
- // already cached by the time the ViewModel's viewModelScope collects it.
- runBlocking { provider.getCurrentUser() }
- return ContactsViewModel(repo, provider, mock()).also {
+ val user = runBlocking { userManager.getDefaultUser() }!!
+ return ContactsViewModel(repo, mock(), user).also {
viewModelStore.put("contactsViewModel${viewModelCount++}", it)
}
}
@@ -136,6 +127,14 @@ class ContactsViewModelTest {
assert(successState.conversation == FakeItem.roomOverall.ocs!!.data)
}
+ @Test
+ fun `a handled room state is cleared`() =
+ runTest {
+ viewModel.createRoom("1", "users", "s@gmail.com", null)
+ viewModel.clearRoomState()
+ assert(viewModel.roomViewState.value is ContactsViewModel.RoomUiState.None)
+ }
+
@Test
fun `test failure room state`() =
runTest {
diff --git a/app/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.kt
index 31d7439ba5..c1be03efce 100644
--- a/app/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModelTest.kt
@@ -16,11 +16,8 @@ import com.nextcloud.talk.models.json.capabilities.PasswordApiDto
import com.nextcloud.talk.models.json.capabilities.PasswordPolicyDto
import com.nextcloud.talk.passwordpolicy.FakePasswordPolicyRepository
import com.nextcloud.talk.utils.SpreedFeatures
-import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
@@ -35,28 +32,21 @@ import org.junit.Test
import org.mockito.kotlin.mock
/**
- * What the creation screen's ViewModel does before and after the current user is known.
+ * What the creation screen's ViewModel does depending on the capabilities of its user.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class ConversationCreationViewModelTest {
- private val users = MutableSharedFlow(replay = 1)
private val repository = FakeConversationCreationRepository()
- private val userProvider = object : CurrentUserProvider {
- override val currentUserFlow: Flow = users
- override suspend fun getCurrentUser(timeout: Long): Result =
- Result.failure(UnsupportedOperationException())
- }
-
private val passwordPolicyRepository = FakePasswordPolicyRepository()
- private fun viewModel() =
+ private fun viewModel(user: User) =
ConversationCreationViewModel(
repository,
ConversationCreator(repository, mock()),
passwordPolicyRepository,
- userProvider
+ user
)
private fun user(vararg spreedFeatures: SpreedFeatures, passwordEnforced: Boolean = false) =
@@ -89,32 +79,9 @@ class ConversationCreationViewModelTest {
}
@Test
- fun `nothing is asked of the server while the current user is unknown`() =
- runTest {
- val model = viewModel()
-
- assertNull(model.currentUser.value)
- assertEquals(0, repository.presetCalls)
- assertTrue(model.isLoadingPresets)
- }
-
- @Test
- fun `creating is refused while the current user is unknown`() =
+ fun `the presets are requested when the server supports them`() =
runTest {
- val model = viewModel()
-
- model.createRoomAndAddParticipants()
-
- assertNull(repository.bodyRequest)
- assertNull(repository.formRequest)
- }
-
- @Test
- fun `the presets are requested once the user arrives`() =
- runTest {
- val model = viewModel()
-
- users.emit(user(SpreedFeatures.CONVERSATION_PRESETS))
+ val model = viewModel(user(SpreedFeatures.CONVERSATION_PRESETS))
assertEquals(1, repository.presetCalls)
assertTrue(model.presets.value is PresetsUiState.Success)
@@ -125,8 +92,7 @@ class ConversationCreationViewModelTest {
fun `opening a conversation to guests generates the password the server enforces`() =
runTest {
passwordPolicyRepository.generatedPassword = "generated"
- val model = viewModel()
- users.emit(user(passwordEnforced = true))
+ val model = viewModel(user(passwordEnforced = true))
model.allowGuests(true)
@@ -137,8 +103,7 @@ class ConversationCreationViewModelTest {
fun `a password the user typed is left alone`() =
runTest {
passwordPolicyRepository.generatedPassword = "generated"
- val model = viewModel()
- users.emit(user(passwordEnforced = true))
+ val model = viewModel(user(passwordEnforced = true))
model.updatePassword("hunter2")
model.allowGuests(true)
@@ -150,8 +115,7 @@ class ConversationCreationViewModelTest {
fun `no password is generated where the server does not enforce one`() =
runTest {
passwordPolicyRepository.generatedPassword = "generated"
- val model = viewModel()
- users.emit(user())
+ val model = viewModel(user())
model.allowGuests(true)
@@ -162,9 +126,7 @@ class ConversationCreationViewModelTest {
@Test
fun `a server without the capability is never asked for presets`() =
runTest {
- val model = viewModel()
-
- users.emit(user())
+ val model = viewModel(user())
assertEquals(0, repository.presetCalls)
assertFalse(model.showPresetSelection)
diff --git a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt
index 99fc651fae..c3a1254e98 100644
--- a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt
@@ -379,7 +379,7 @@ class ConversationListFreshnessIntegrationTest {
runBlocking {
val emissions = mutableListOf>()
val collector = launch(Dispatchers.IO) {
- repository.roomListFlow.collect { emissions.add(it) }
+ repository.roomListFlow(user.id!!).collect { emissions.add(it) }
}
repository.getRooms(user).join()
@@ -392,6 +392,21 @@ class ConversationListFreshnessIntegrationTest {
}
}
+ @Test
+ fun `room list flow only emits the conversations of the requested account`() {
+ val repository = repository()
+
+ runBlocking {
+ db.usersDao().saveUser(
+ UserEntity(id = OTHER_ACCOUNT_ID, userId = "other", username = "other", baseUrl = BASE_URL)
+ )
+ seedConversation(lastActivity = 10, lastReadMessage = 1, unreadMessages = 0)
+
+ assertEquals(1, repository.roomListFlow(ACCOUNT_ID).first().size)
+ assertTrue(repository.roomListFlow(OTHER_ACCOUNT_ID).first().isEmpty())
+ }
+ }
+
private fun repository(): OfflineFirstConversationsRepository =
OfflineFirstConversationsRepository(
db.conversationsDao(),
@@ -493,6 +508,7 @@ class ConversationListFreshnessIntegrationTest {
companion object {
private const val ACCOUNT_ID = 1L
+ private const val OTHER_ACCOUNT_ID = 2L
private const val BASE_URL = "https://server.example.com"
private const val ROOM_TOKEN = "room1"
private const val INTERNAL_CONVERSATION_ID = "$ACCOUNT_ID@$ROOM_TOKEN"
diff --git a/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoPartialUpdateTest.kt b/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoPartialUpdateTest.kt
new file mode 100644
index 0000000000..9532a5b833
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoPartialUpdateTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.data.user
+
+import android.app.Application
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import com.nextcloud.talk.data.source.local.TalkDatabase
+import com.nextcloud.talk.data.user.model.UserCapabilitiesUpdate
+import com.nextcloud.talk.data.user.model.UserClientCertificateUpdate
+import com.nextcloud.talk.data.user.model.UserCredentialsUpdate
+import com.nextcloud.talk.data.user.model.UserDisplayNameUpdate
+import com.nextcloud.talk.data.user.model.UserEntity
+import com.nextcloud.talk.data.user.model.UserExternalSignalingServerUpdate
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesDto
+import com.nextcloud.talk.models.json.capabilities.ServerVersionDto
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/**
+ * The partial updates must only write their own columns, so a user that was read before the default account
+ * changed cannot reset it.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [33])
+class UsersDaoPartialUpdateTest {
+
+ private lateinit var db: TalkDatabase
+ private lateinit var dao: UsersDao
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = db.usersDao()
+ runBlocking {
+ dao.saveUser(UserEntity(id = USER_A, username = "a", baseUrl = BASE_URL, token = "tokenA", current = true))
+ dao.saveUser(UserEntity(id = USER_B, username = "b", baseUrl = BASE_URL, token = "tokenB"))
+ }
+ }
+
+ @After
+ fun tearDown() {
+ db.close()
+ }
+
+ @Test
+ fun `partial updates of a previously active user keep the new default account`() =
+ runBlocking {
+ dao.setUserAsActiveWithId(USER_B)
+
+ dao.updateCapabilities(UserCapabilitiesUpdate(USER_A, CapabilitiesDto(), ServerVersionDto(major = 30)))
+ dao.updateExternalSignalingServer(
+ UserExternalSignalingServerUpdate(USER_A, ExternalSignalingServer(externalSignalingServer = "wss://s"))
+ )
+ dao.updateDisplayName(UserDisplayNameUpdate(USER_A, "Alice"))
+ dao.updateClientCertificate(UserClientCertificateUpdate(USER_A, "alias"))
+ dao.updateCredentials(UserCredentialsUpdate(USER_A, "newToken", "alias"))
+
+ val userA = dao.getUserWithId(USER_A)!!
+ assertFalse(userA.current)
+ assertTrue(dao.getUserWithId(USER_B)!!.current)
+ assertEquals(USER_B, dao.getActiveUser()!!.id)
+ assertEquals(30, userA.serverVersion?.major)
+ assertEquals("wss://s", userA.externalSignalingServer?.externalSignalingServer)
+ assertEquals("Alice", userA.displayName)
+ assertEquals("newToken", userA.token)
+ assertEquals("alias", userA.clientCertificate)
+ }
+
+ companion object {
+ private const val USER_A = 1L
+ private const val USER_B = 2L
+ private const val BASE_URL = "https://example.com"
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoRepairTest.kt b/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoRepairTest.kt
new file mode 100644
index 0000000000..8a419140c1
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/data/user/UsersDaoRepairTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.data.user
+
+import android.app.Application
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import com.nextcloud.talk.data.source.local.TalkDatabase
+import com.nextcloud.talk.data.user.model.UserEntity
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [33])
+class UsersDaoRepairTest {
+
+ private lateinit var db: TalkDatabase
+ private lateinit var dao: UsersDao
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = db.usersDao()
+ }
+
+ @After
+ fun tearDown() {
+ db.close()
+ }
+
+ @Test
+ fun `several active users are reduced to the one with the highest id`() =
+ runBlocking {
+ saveUsers(activeIds = setOf(1L, 3L))
+
+ val updated = dao.repairMultipleActiveUsers()
+
+ assertEquals(USER_IDS.size, updated)
+ assertEquals(listOf(3L), activeIds())
+ }
+
+ @Test
+ fun `a single active user is left unchanged`() =
+ runBlocking {
+ saveUsers(activeIds = setOf(1L))
+
+ val updated = dao.repairMultipleActiveUsers()
+
+ assertEquals(0, updated)
+ assertEquals(listOf(1L), activeIds())
+ }
+
+ private suspend fun saveUsers(activeIds: Set) {
+ USER_IDS.forEach { id ->
+ dao.saveUser(UserEntity(id = id, username = "user$id", baseUrl = BASE_URL, current = id in activeIds))
+ }
+ }
+
+ private suspend fun activeIds(): List = dao.getUsers().filter { it.current }.map { it.id }
+
+ companion object {
+ private val USER_IDS = listOf(1L, 2L, 3L)
+ private const val BASE_URL = "https://example.com"
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/data/user/UsersRepositoryImplTest.kt b/app/src/test/java/com/nextcloud/talk/data/user/UsersRepositoryImplTest.kt
new file mode 100644
index 0000000000..634b628927
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/data/user/UsersRepositoryImplTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.data.user
+
+import com.nextcloud.talk.data.user.model.UserEntity
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verifyBlocking
+import org.mockito.kotlin.wheneverBlocking
+
+class UsersRepositoryImplTest {
+
+ private val usersDao: UsersDao = mock()
+ private val repository = UsersRepositoryImpl(usersDao)
+
+ @Test
+ fun `getActiveUser only reads the database`() =
+ runTest {
+ val entity = UserEntity(id = 1L, username = "userA", baseUrl = "https://example.com", current = true)
+ wheneverBlocking { usersDao.getActiveUser() }.thenReturn(entity)
+
+ val result = repository.getActiveUser()
+
+ assertEquals(1L, result?.id)
+ verifyBlocking(usersDao, never()) { setUserAsActiveWithId(any()) }
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.kt b/app/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.kt
index 5b1fd29782..5610b0d4cc 100644
--- a/app/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/jobs/CapabilitiesFetcherTest.kt
@@ -22,6 +22,8 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
@@ -51,7 +53,7 @@ class CapabilitiesFetcherTest {
val testUser = user()
val capabilities = CapabilitiesDto()
val serverVersion = ServerVersionDto(major = 30)
- wheneverBlocking { userManager.updateOrCreateUser(testUser) }.thenReturn(1)
+ wheneverBlocking { userManager.updateCapabilities(USER_ID, capabilities, serverVersion) }.thenReturn(1)
val result = fetcher.updateUser(overall(capabilities, serverVersion), testUser)
@@ -65,7 +67,7 @@ class CapabilitiesFetcherTest {
fun `returns false and posts failure when no row was updated`() =
runTest {
val testUser = user()
- wheneverBlocking { userManager.updateOrCreateUser(testUser) }.thenReturn(0)
+ wheneverBlocking { userManager.updateCapabilities(any(), anyOrNull(), anyOrNull()) }.thenReturn(0)
val result = fetcher.updateUser(overall(CapabilitiesDto()), testUser)
@@ -77,7 +79,8 @@ class CapabilitiesFetcherTest {
fun `returns false and posts failure when persisting the user throws`() =
runTest {
val testUser = user()
- wheneverBlocking { userManager.updateOrCreateUser(testUser) }.thenThrow(RuntimeException("db error"))
+ wheneverBlocking { userManager.updateCapabilities(any(), anyOrNull(), anyOrNull()) }
+ .thenThrow(RuntimeException("db error"))
val result = fetcher.updateUser(overall(CapabilitiesDto()), testUser)
@@ -94,7 +97,7 @@ class CapabilitiesFetcherTest {
assertFalse(result)
verify(eventBus).post(EventStatus(USER_ID, EventStatus.EventType.CAPABILITIES_FETCH, false))
- verifyBlocking(userManager, never()) { updateOrCreateUser(testUser) }
+ verifyBlocking(userManager, never()) { updateCapabilities(any(), anyOrNull(), anyOrNull()) }
}
@Test
@@ -106,7 +109,7 @@ class CapabilitiesFetcherTest {
assertFalse(result)
verify(eventBus).post(EventStatus(USER_ID, EventStatus.EventType.CAPABILITIES_FETCH, false))
- verifyBlocking(userManager, never()) { updateOrCreateUser(testUser) }
+ verifyBlocking(userManager, never()) { updateCapabilities(any(), anyOrNull(), anyOrNull()) }
}
@Test
@@ -118,7 +121,7 @@ class CapabilitiesFetcherTest {
assertFalse(result)
verify(eventBus).post(EventStatus(USER_ID, EventStatus.EventType.CAPABILITIES_FETCH, false))
- verifyBlocking(userManager, never()) { updateOrCreateUser(testUser) }
+ verifyBlocking(userManager, never()) { updateCapabilities(any(), anyOrNull(), anyOrNull()) }
}
companion object {
diff --git a/app/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.kt
index 571d08110c..7bb71d483d 100644
--- a/app/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/location/viewmodels/LocationPickerViewModelTest.kt
@@ -7,9 +7,9 @@
package com.nextcloud.talk.location.viewmodels
import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.location.GeocodingResult
import com.nextcloud.talk.logger.Logger
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -30,11 +30,11 @@ class LocationPickerViewModelTest {
private lateinit var viewModel: LocationPickerViewModel
private val ncApi: NcApi = mock()
- private val userProvider: CurrentUserProviderOld = mock()
private val okHttpClient: OkHttpClient = mock()
private val logger: Logger = mock()
private val initParams = LocationPickerViewModel.LocationPickerInitParams(
+ user = User(id = 1L, username = "user1", baseUrl = "https://server1", token = "token"),
roomToken = "testRoom",
chatApiVersion = 1,
geocodingResult = null,
@@ -48,7 +48,7 @@ class LocationPickerViewModelTest {
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
- viewModel = LocationPickerViewModel(ncApi, userProvider, okHttpClient, logger)
+ viewModel = LocationPickerViewModel(ncApi, okHttpClient, logger)
viewModel.initialize(initParams)
}
diff --git a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt
index 70bb649bef..1e74b30740 100644
--- a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt
@@ -7,7 +7,6 @@
package com.nextcloud.talk.login.data
-import android.os.Bundle
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.work.WorkInfo
@@ -30,6 +29,7 @@ import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -402,7 +402,7 @@ class LoginRepositoryTest {
val result = repo.parseAndLogin(loginData)
// Assert
- assertNull(result)
+ assertEquals(LoginRepository.LoginResult.ExistingAccount, result)
verify(localLoginDataSource).startAccountRemovalWorker()
}
@@ -420,8 +420,8 @@ class LoginRepositoryTest {
val result = repo.parseAndLogin(loginData)
// Assert
- assertNull(result)
- verify(localLoginDataSource, never()).updateUser(any())
+ assertEquals(LoginRepository.LoginResult.ExistingAccount, result)
+ verify(localLoginDataSource, never()).updateUser(any(), anyOrNull())
}
@Test
@@ -436,13 +436,15 @@ class LoginRepositoryTest {
.thenReturn(false)
whenever(localLoginDataSource.checkIfUserExists(loginData))
.thenReturn(true)
+ whenever(localLoginDataSource.updateUser(loginData, null))
+ .thenReturn(true)
// Act
val result = repo.parseAndLogin(loginData)
// Assert
- assertNull(result)
- verify(localLoginDataSource).updateUser(loginData)
+ assertEquals(LoginRepository.LoginResult.ExistingAccount, result)
+ verify(localLoginDataSource).updateUser(loginData, null)
}
@Test
@@ -459,8 +461,7 @@ class LoginRepositoryTest {
val result = repo.parseAndLogin(loginData)
// Assert
- assertNotNull(result)
- assertTrue(result is Bundle)
+ assertTrue(result is LoginRepository.LoginResult.NewAccount)
}
@Test
@@ -477,8 +478,7 @@ class LoginRepositoryTest {
val result = repo.parseAndLogin(loginData)
// Assert
- assertNotNull(result)
- assertTrue(result is Bundle)
+ assertTrue(result is LoginRepository.LoginResult.NewAccount)
}
@Test
@@ -495,8 +495,28 @@ class LoginRepositoryTest {
val result = repo.parseAndLogin(loginData)
// Assert
- assertNotNull(result)
- assertTrue(result is Bundle)
+ assertTrue(result is LoginRepository.LoginResult.NewAccount)
+ }
+
+ @Test
+ fun `parseAndLogin reports a different account for an account that is not set up during a reauthorization`() =
+ runTest {
+ // Arrange - reauthorize the account with the internal id 7
+ val qrData = "nc://login/server:https%3A//example.com"
+ repo.startLoginFlowFromQR(qrData, reAuth = true, accountToReauthorize = 7L)
+
+ val loginData = LoginCompletion(200, "https://server.com", "newuser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(false)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertEquals(LoginRepository.LoginResult.DifferentAccount, result)
+ verify(localLoginDataSource, never()).updateUser(any(), anyOrNull())
}
// ========== LocalLoginDataSource Integration Tests ==========
@@ -534,7 +554,7 @@ class LoginRepositoryTest {
// Assert
verify(localLoginDataSource).checkIfUserExists(loginData)
- verify(localLoginDataSource, never()).updateUser(any())
+ verify(localLoginDataSource, never()).updateUser(any(), anyOrNull())
}
@Test
@@ -549,12 +569,37 @@ class LoginRepositoryTest {
.thenReturn(false)
whenever(localLoginDataSource.checkIfUserExists(loginData))
.thenReturn(true)
+ whenever(localLoginDataSource.updateUser(loginData, null))
+ .thenReturn(true)
// Act
repo.parseAndLogin(loginData)
// Assert
- verify(localLoginDataSource).updateUser(loginData)
+ verify(localLoginDataSource).updateUser(loginData, null)
+ }
+
+ @Test
+ fun `parseAndLogin passes the account to reauthorize and reports a different account`() =
+ runTest {
+ // Arrange - reauthorize the account with the internal id 7
+ val qrData = "nc://login/server:https%3A//example.com"
+ repo.startLoginFlowFromQR(qrData, reAuth = true, accountToReauthorize = 7L)
+
+ val loginData = LoginCompletion(200, "https://server.com", "otheruser", "apppass123")
+ whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData))
+ .thenReturn(false)
+ whenever(localLoginDataSource.checkIfUserExists(loginData))
+ .thenReturn(true)
+ whenever(localLoginDataSource.updateUser(loginData, 7L))
+ .thenReturn(false)
+
+ // Act
+ val result = repo.parseAndLogin(loginData)
+
+ // Assert
+ assertEquals(LoginRepository.LoginResult.DifferentAccount, result)
+ verify(localLoginDataSource).updateUser(loginData, 7L)
}
// ========== Edge Cases and Error Handling ==========
diff --git a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
index ea64bcc39c..cd7ce92e3c 100644
--- a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt
@@ -10,13 +10,13 @@ package com.nextcloud.talk.messagesearch
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.SearchMessageEntry
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
import com.nextcloud.talk.test.fakes.FakeUnifiedSearchRepository
import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOldImpl
import com.nextcloud.talk.utils.preview.DummyUserDaoImpl
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
@@ -36,8 +36,8 @@ class MessageSearchHelperTest {
val userManager: UserManager
get() = UserManager(userRepository)
- val userProvider: CurrentUserProviderOld
- get() = CurrentUserProviderOldImpl(userManager)
+ val currentUser: User
+ get() = runBlocking { userManager.getDefaultUser() }!!
@Suppress("LongParameterList")
private fun createMessageEntry(
@@ -64,7 +64,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val result = sut.startMessageSearch("foo")
@@ -80,7 +80,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val result = sut.startMessageSearch("foo")
@@ -96,7 +96,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val result = sut.startMessageSearch("foo")
@@ -115,7 +115,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val result = sut.startMessageSearch("foo")
@@ -140,7 +140,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val result = sut.startMessageSearch("foo")
@@ -158,7 +158,7 @@ class MessageSearchHelperTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
repeat(5) {
@@ -173,7 +173,7 @@ class MessageSearchHelperTest {
runTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
Assert.assertEquals(null, sut.loadMore())
}
@@ -183,7 +183,7 @@ class MessageSearchHelperTest {
runTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val firstPageEntries = (1..5).map { createMessageEntry() }
@@ -214,7 +214,7 @@ class MessageSearchHelperTest {
runTest {
val sut = MessageSearchHelper(
repository,
- currentUser = userProvider.currentUser.blockingGet()
+ currentUser = currentUser
)
val firstPageEntries = listOf(
diff --git a/app/src/test/java/com/nextcloud/talk/users/DefaultAccountProviderTest.kt b/app/src/test/java/com/nextcloud/talk/users/DefaultAccountProviderTest.kt
new file mode 100644
index 0000000000..19d6edf230
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/users/DefaultAccountProviderTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.users
+
+import com.nextcloud.talk.data.user.UsersRepository
+import com.nextcloud.talk.data.user.model.User
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verifyBlocking
+import org.mockito.kotlin.wheneverBlocking
+
+class DefaultAccountProviderTest {
+
+ private val usersRepository: UsersRepository = mock()
+ private val userManager = UserManager(usersRepository)
+ private val provider = DefaultAccountProvider(userManager)
+
+ private val userA = User(id = 1, username = "userA", baseUrl = "https://example.com", current = true)
+
+ @Before
+ fun setUp() {
+ wheneverBlocking { usersRepository.getActiveUserFlow() }.thenReturn(emptyFlow())
+ }
+
+ @Test
+ fun `getDefaultUserBlocking loads the active user when none is known yet`() {
+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(userA)
+
+ assertEquals(userA, provider.getDefaultUserBlocking())
+ }
+
+ @Test
+ fun `getDefaultUserBlocking returns the known default user without querying the database`() =
+ runTest {
+ wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(true)
+ wheneverBlocking { usersRepository.getUserWithId(1L) }.thenReturn(userA)
+ userManager.setUserAsActive(userA)
+
+ assertEquals(userA, provider.getDefaultUserBlocking())
+ verifyBlocking(usersRepository, never()) { getActiveUser() }
+ }
+
+ @Test
+ fun `getDefaultUserBlocking is null when there is no account`() {
+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null)
+ wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList())
+
+ assertNull(provider.getDefaultUserBlocking())
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt
index 448edbdaf3..c67a447b79 100644
--- a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt
@@ -10,6 +10,8 @@ import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.ExternalSignalingServer
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -44,19 +46,54 @@ class UserManagerTest {
}
@Test
- fun `getCurrentUser returns the active user without touching any fallback`() =
+ fun `getDefaultUser returns the active user without touching any fallback`() =
runTest {
val active = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true)
wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(active)
- val result = userManager.getCurrentUser()
+ val result = userManager.getDefaultUser()
assertEquals(active, result)
verifyBlocking(usersRepository, never()) { getUsersNotScheduledForDeletion() }
}
@Test
- fun `getCurrentUser falls back to any non-deleted user and sets it active when none is active`() =
+ fun `getDefaultUser does not write to the database when an active user exists`() =
+ runTest {
+ val active = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true)
+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(active)
+
+ userManager.getDefaultUser()
+
+ verifyBlocking(usersRepository, never()) { setUserAsActiveWithId(any()) }
+ }
+
+ @Test
+ fun `userFlow emits the user with the given id and skips unchanged rows`() =
+ runTest {
+ val userA = user(id = 1, username = "userA", baseUrl = "https://example.com")
+ val renamedUserA = userA.copy(displayName = "New Name")
+ wheneverBlocking { usersRepository.getUserWithIdFlow(1L) }
+ .thenReturn(flowOf(userA, userA.copy(), renamedUserA, null))
+
+ val emissions = userManager.userFlow(1L).toList()
+
+ assertEquals(listOf(userA, renamedUserA, null), emissions)
+ }
+
+ @Test
+ fun `repairMultipleActiveUsers repairs in a single repository call`() =
+ runTest {
+ wheneverBlocking { usersRepository.repairMultipleActiveUsers() }.thenReturn(2)
+
+ userManager.repairMultipleActiveUsers()
+
+ verifyBlocking(usersRepository) { repairMultipleActiveUsers() }
+ verifyBlocking(usersRepository, never()) { setUserAsActiveWithId(any()) }
+ }
+
+ @Test
+ fun `getDefaultUser falls back to any non-deleted user and sets it active when none is active`() =
runTest {
val fallback = user(id = 1, username = "userA", baseUrl = "https://example.com")
wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(listOf(fallback))
@@ -65,18 +102,18 @@ class UserManagerTest {
// now reporting the freshly-activated row.
wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null, fallback)
- val result = userManager.getCurrentUser()
+ val result = userManager.getDefaultUser()
assertEquals(fallback, result)
verifyBlocking(usersRepository) { setUserAsActiveWithId(fallback.id!!) }
}
@Test
- fun `getCurrentUser is null when there is no active user and none to fall back to`() =
+ fun `getDefaultUser is null when there is no active user and none to fall back to`() =
runTest {
wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList())
- assertNull(userManager.getCurrentUser())
+ assertNull(userManager.getDefaultUser())
}
@Test
@@ -183,10 +220,11 @@ class UserManagerTest {
@Test
fun `updateExternalSignalingServer throws when the user does not exist`() =
runTest {
- wheneverBlocking { usersRepository.getUserWithId(7L) }.thenReturn(null)
+ val server = ExternalSignalingServer()
+ wheneverBlocking { usersRepository.updateExternalSignalingServer(7L, server) }.thenReturn(0)
try {
- userManager.updateExternalSignalingServer(7L, ExternalSignalingServer())
+ userManager.updateExternalSignalingServer(7L, server)
fail("Expected NoSuchElementException")
} catch (expected: NoSuchElementException) {
// expected
@@ -196,15 +234,13 @@ class UserManagerTest {
@Test
fun `updateExternalSignalingServer updates the matching user`() =
runTest {
- val existing = user(id = 7, username = "userA", baseUrl = "https://example.com")
val server = ExternalSignalingServer(externalSignalingServer = "https://signaling.example.com")
- wheneverBlocking { usersRepository.getUserWithId(7L) }.thenReturn(existing)
- wheneverBlocking { usersRepository.updateUser(existing) }.thenReturn(1)
+ wheneverBlocking { usersRepository.updateExternalSignalingServer(7L, server) }.thenReturn(1)
val result = userManager.updateExternalSignalingServer(7L, server)
assertEquals(1, result)
- assertEquals(server, existing.externalSignalingServer)
+ verify(usersRepository, never()).updateUser(any())
}
@Test
@@ -232,7 +268,7 @@ class UserManagerTest {
}
@Test
- fun `setUserAsActive publishes the new user on currentUserFlow only when it succeeds`() =
+ fun `setUserAsActive publishes the new user on defaultUserFlow only when it succeeds`() =
runTest {
val target = user(id = 1, username = "userA", baseUrl = "https://example.com")
wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(true)
@@ -240,11 +276,24 @@ class UserManagerTest {
val result = userManager.setUserAsActive(target)
assertTrue(result)
- assertEquals(target, userManager.currentUserFlow.value)
+ assertEquals(target, userManager.defaultUserFlow.value)
+ }
+
+ @Test
+ fun `setUserAsActive publishes the stored row instead of the passed user`() =
+ runTest {
+ val passed = user(id = 1, username = "userA", baseUrl = "https://example.com")
+ val stored = passed.copy(current = true, displayName = "Stored Name")
+ wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(true)
+ wheneverBlocking { usersRepository.getUserWithId(1L) }.thenReturn(stored)
+
+ userManager.setUserAsActive(passed)
+
+ assertEquals(stored, userManager.defaultUserFlow.value)
}
@Test
- fun `setUserAsActive leaves currentUserFlow untouched when it fails`() =
+ fun `setUserAsActive leaves defaultUserFlow untouched when it fails`() =
runTest {
val target = user(id = 1, username = "userA", baseUrl = "https://example.com")
wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(false)
@@ -252,7 +301,7 @@ class UserManagerTest {
val result = userManager.setUserAsActive(target)
assertFalse(result)
- assertNull(userManager.currentUserFlow.value)
+ assertNull(userManager.defaultUserFlow.value)
}
@Test
diff --git a/app/src/test/java/com/nextcloud/talk/utils/CredentialsCookieInterceptorTest.kt b/app/src/test/java/com/nextcloud/talk/utils/CredentialsCookieInterceptorTest.kt
new file mode 100644
index 0000000000..aa1e79e34e
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/utils/CredentialsCookieInterceptorTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.utils
+
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.net.CookieManager
+
+class CredentialsCookieInterceptorTest {
+
+ private val server = MockWebServer()
+ private val cookieManager = CookieManager()
+ private lateinit var client: OkHttpClient
+
+ @Before
+ fun setUp() {
+ server.start()
+ client = OkHttpClient.Builder()
+ .cookieJar(JavaNetCookieJar(cookieManager))
+ .addNetworkInterceptor(CredentialsCookieInterceptor())
+ .build()
+ }
+
+ @After
+ fun tearDown() {
+ server.shutdown()
+ }
+
+ @Test
+ fun `a request with credentials doesn't send a stored session cookie`() {
+ server.enqueue(MockResponse().addHeader("Set-Cookie", "$SESSION_COOKIE; Path=/"))
+ server.enqueue(MockResponse())
+
+ // An anonymous request stores the session cookie.
+ execute(authorization = null)
+ assertEquals(1, cookieManager.cookieStore.cookies.size)
+
+ execute(authorization = CREDENTIALS)
+
+ server.takeRequest()
+ assertNull(server.takeRequest().getHeader("Cookie"))
+ }
+
+ @Test
+ fun `a response to a request with credentials doesn't store its session cookie`() {
+ server.enqueue(MockResponse().addHeader("Set-Cookie", "$SESSION_COOKIE; Path=/"))
+
+ execute(authorization = CREDENTIALS)
+
+ assertTrue(cookieManager.cookieStore.cookies.isEmpty())
+ }
+
+ @Test
+ fun `a request without credentials keeps using cookies`() {
+ server.enqueue(MockResponse().addHeader("Set-Cookie", "$SESSION_COOKIE; Path=/"))
+ server.enqueue(MockResponse())
+
+ execute(authorization = null)
+ execute(authorization = null)
+
+ server.takeRequest()
+ assertEquals(SESSION_COOKIE, server.takeRequest().getHeader("Cookie"))
+ }
+
+ private fun execute(authorization: String?) {
+ val request = Request.Builder()
+ .url(server.url("/ocs/v2.php/apps/spreed/api/v4/room"))
+ .apply { authorization?.let { header("Authorization", it) } }
+ .build()
+ client.newCall(request).execute().close()
+ }
+
+ companion object {
+ private const val SESSION_COOKIE = "nc_session_id=abc"
+ private const val CREDENTIALS = "Basic bWFyY2VsMjp0b2tlbg=="
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/utils/RemoteWipeInterceptorTest.kt b/app/src/test/java/com/nextcloud/talk/utils/RemoteWipeInterceptorTest.kt
new file mode 100644
index 0000000000..9ef533203d
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/utils/RemoteWipeInterceptorTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.utils
+
+import android.content.Context
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat
+import com.nextcloud.talk.utils.ssl.TrustManager
+import okhttp3.Credentials
+import okhttp3.Interceptor
+import okhttp3.Protocol
+import okhttp3.Request
+import okhttp3.Response
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verifyBlocking
+import org.mockito.kotlin.wheneverBlocking
+
+/**
+ * A 401 must only remove the account whose credentials the request carried. These cases must never remove one.
+ */
+class RemoteWipeInterceptorTest {
+
+ private val userManager: UserManager = mock()
+ private val interceptor = RemoteWipeInterceptor(
+ userManager,
+ mock(),
+ mock(),
+ mock()
+ )
+
+ private val marcel = User(id = 1, username = "marcel", token = "tokenA", baseUrl = BASE_URL)
+ private val marcel2 = User(id = 2, username = "marcel2", token = "tokenB", baseUrl = BASE_URL)
+
+ @Test
+ fun `a 401 for a request without credentials removes no account`() {
+ wheneverBlocking { userManager.getUsers() } doReturn listOf(marcel, marcel2)
+
+ val response = interceptor.intercept(chainAnswering401(Request.Builder().url(PREVIEW_URL).build()))
+
+ assertEquals(HTTP_UNAUTHORIZED, response.code)
+ verifyBlocking(userManager, never()) { scheduleUserForDeletionWithId(any()) }
+ }
+
+ @Test
+ fun `a 401 for credentials of no stored account removes no account`() {
+ wheneverBlocking { userManager.getUsers() } doReturn listOf(marcel, marcel2)
+ val request = Request.Builder()
+ .url(PREVIEW_URL)
+ .header("Authorization", Credentials.basic("marcel2", "outdatedToken"))
+ .build()
+
+ interceptor.intercept(chainAnswering401(request))
+
+ verifyBlocking(userManager, never()) { scheduleUserForDeletionWithId(any()) }
+ }
+
+ private fun chainAnswering401(request: Request): Interceptor.Chain {
+ val response = Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(HTTP_UNAUTHORIZED)
+ .message("Unauthorized")
+ .build()
+ return mock {
+ on { request() } doReturn request
+ on { proceed(any()) } doReturn response
+ }
+ }
+
+ companion object {
+ private const val BASE_URL = "https://cloud.example.com"
+ private const val PREVIEW_URL = "$BASE_URL/index.php/core/preview?fileId=1"
+ private const val HTTP_UNAUTHORIZED = 401
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/utils/UriUtilsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/UriUtilsTest.kt
index 37ba993f26..065d56597c 100644
--- a/app/src/test/java/com/nextcloud/talk/utils/UriUtilsTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/utils/UriUtilsTest.kt
@@ -28,4 +28,30 @@ class UriUtilsTest {
val link = UriUtils.getMessageLink("https://example.com/nextcloud", "token1", 7)
Assert.assertEquals("https://example.com/nextcloud/call/token1#message_7", link)
}
+
+ @Test
+ fun `isOnServer accepts URLs of the server`() {
+ Assert.assertTrue(UriUtils.isOnServer("$SERVER/core/preview?x=1", SERVER))
+ Assert.assertTrue(UriUtils.isOnServer("$SERVER/", "$SERVER/"))
+ Assert.assertTrue(UriUtils.isOnServer("$SUBFOLDER_SERVER/index.php", SUBFOLDER_SERVER))
+ }
+
+ @Test
+ fun `isOnServer rejects other hosts that start with the server`() {
+ Assert.assertFalse(UriUtils.isOnServer("https://cloud.example.com.attacker.test/image", SERVER))
+ Assert.assertFalse(UriUtils.isOnServer("https://cloud.example.com@attacker.test/image", SERVER))
+ }
+
+ @Test
+ fun `isOnServer rejects another scheme, port or path`() {
+ Assert.assertFalse(UriUtils.isOnServer("http://cloud.example.com/image", SERVER))
+ Assert.assertFalse(UriUtils.isOnServer("https://cloud.example.com:8443/image", SERVER))
+ Assert.assertFalse(UriUtils.isOnServer("${SUBFOLDER_SERVER}2/image", SUBFOLDER_SERVER))
+ Assert.assertFalse(UriUtils.isOnServer("not a url", SERVER))
+ }
+
+ companion object {
+ private const val SERVER = "https://cloud.example.com"
+ private const val SUBFOLDER_SERVER = "https://example.com/nextcloud"
+ }
}
diff --git a/app/src/test/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelectorTest.kt b/app/src/test/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelectorTest.kt
new file mode 100644
index 0000000000..a1d8a38f9a
--- /dev/null
+++ b/app/src/test/java/com/nextcloud/talk/utils/ssl/ClientCertificateSelectorTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.utils.ssl
+
+import com.nextcloud.talk.data.user.model.User
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class ClientCertificateSelectorTest {
+
+ private val withCertA = User(id = 1, baseUrl = "https://a.example.com", clientCertificate = "certA")
+ private val withoutCertB = User(id = 2, baseUrl = "https://b.example.com")
+ private val withCertB = User(id = 3, baseUrl = "https://b.example.com", clientCertificate = "certB")
+ private val otherCertB = User(id = 4, baseUrl = "https://b.example.com", clientCertificate = "otherB")
+
+ @Test
+ fun `the certificate of the server's account is used, not the default account's`() {
+ val alias = ClientCertificateSelector.selectAlias(
+ "a.example.com",
+ listOf(withCertA, withoutCertB),
+ withoutCertB
+ )
+
+ assertEquals("certA", alias)
+ }
+
+ @Test
+ fun `no certificate for a server whose accounts use none, even if the default account has one`() {
+ val alias = ClientCertificateSelector.selectAlias("b.example.com", listOf(withCertA, withoutCertB), withCertA)
+
+ assertNull(alias)
+ }
+
+ @Test
+ fun `accounts on the same server with one certificate share it`() {
+ val alias = ClientCertificateSelector.selectAlias(
+ "b.example.com",
+ listOf(withoutCertB, withCertB),
+ withoutCertB
+ )
+
+ assertEquals("certB", alias)
+ }
+
+ @Test
+ fun `different certificates on the same server are decided by the default account`() {
+ val users = listOf(withCertB, otherCertB, withCertA)
+
+ assertEquals("otherB", ClientCertificateSelector.selectAlias("b.example.com", users, otherCertB))
+ assertNull(ClientCertificateSelector.selectAlias("b.example.com", users, withCertA))
+ }
+
+ @Test
+ fun `without a host the default account's certificate is used`() {
+ assertEquals("certA", ClientCertificateSelector.selectAlias(null, listOf(withCertA, withCertB), withCertA))
+ }
+}
diff --git a/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt
index 7efd5c9a15..e619c8ebf4 100644
--- a/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt
+++ b/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt
@@ -6,14 +6,8 @@
*/
package com.nextcloud.talk.viewmodels
-import com.nextcloud.talk.data.user.UsersDao
-import com.nextcloud.talk.data.user.UsersRepository
-import com.nextcloud.talk.data.user.UsersRepositoryImpl
+import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.test.fakes.FakeCallRecordingRepository
-import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderOldImpl
-import com.nextcloud.talk.utils.preview.DummyUserDaoImpl
import com.vividsolutions.jts.util.Assert
import org.junit.Before
import org.junit.Test
@@ -23,17 +17,7 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
private val repository = FakeCallRecordingRepository()
- val usersDao: UsersDao
- get() = DummyUserDaoImpl()
-
- val userRepository: UsersRepository
- get() = UsersRepositoryImpl(usersDao)
-
- val userManager: UserManager
- get() = UserManager(userRepository)
-
- val userProvider: CurrentUserProviderOld
- get() = CurrentUserProviderOldImpl(userManager)
+ private val user = User(id = 1L, username = "user1", baseUrl = "https://server1", token = "token")
@Before
fun setUp() {
@@ -42,8 +26,8 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
@Test
fun testCallRecordingViewModel_clickStartRecord() {
- val viewModel = CallRecordingViewModel(repository, userProvider)
- viewModel.setData("foo")
+ val viewModel = CallRecordingViewModel(repository)
+ viewModel.setData(user, "foo")
viewModel.clickRecordButton()
Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartingState)
@@ -56,8 +40,8 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
@Test
fun testCallRecordingViewModel_clickStopRecord() {
- val viewModel = CallRecordingViewModel(repository, userProvider)
- viewModel.setData("foo")
+ val viewModel = CallRecordingViewModel(repository)
+ viewModel.setData(user, "foo")
viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE)
Assert.equals(true, (viewModel.viewState.value as CallRecordingViewModel.RecordingStartedState).showStartedInfo)
@@ -73,8 +57,8 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
@Test
fun testCallRecordingViewModel_keepConfirmState() {
- val viewModel = CallRecordingViewModel(repository, userProvider)
- viewModel.setData("foo")
+ val viewModel = CallRecordingViewModel(repository)
+ viewModel.setData(user, "foo")
viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE)
Assert.equals(true, (viewModel.viewState.value as CallRecordingViewModel.RecordingStartedState).showStartedInfo)
@@ -90,8 +74,8 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
@Test
fun testCallRecordingViewModel_continueRecordingWhenDismissStopDialog() {
- val viewModel = CallRecordingViewModel(repository, userProvider)
- viewModel.setData("foo")
+ val viewModel = CallRecordingViewModel(repository)
+ viewModel.setData(user, "foo")
viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE)
viewModel.clickRecordButton()
@@ -109,8 +93,8 @@ class CallRecordingViewModelTest : AbstractViewModelTest() {
@Test
fun testSetRecordingStateDirectly() {
- val viewModel = CallRecordingViewModel(repository, userProvider)
- viewModel.setData("foo")
+ val viewModel = CallRecordingViewModel(repository)
+ viewModel.setData(user, "foo")
viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STOPPED_CODE)
Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStoppedState)