diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index a97e7d8cbe..c4bc289b75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -64,7 +64,11 @@ data class MatrixConfiguration( /** * True to enable presence information sync (if available). False to disable regardless of server setting. */ - val presenceSyncEnabled: Boolean = true + val presenceSyncEnabled: Boolean = true, + /** + * Thread messages default enable/disabled value + */ + val threadMessagesEnabledDefault: Boolean = false, ) { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt index 700b94a985..069e539e2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt @@ -19,15 +19,19 @@ package org.matrix.android.sdk.internal.database.lightweight import android.content.Context import androidx.core.content.edit import androidx.preference.PreferenceManager +import org.matrix.android.sdk.api.MatrixConfiguration import javax.inject.Inject /** * The purpose of this class is to provide an alternative and lightweight way to store settings/data - * on the sdi without using the database. This should be used just for sdk/user preferences and + * on the sdk without using the database. This should be used just for sdk/user preferences and * not for large data sets */ -class LightweightSettingsStorage @Inject constructor(context: Context) { +class LightweightSettingsStorage @Inject constructor( + context: Context, + private val matrixConfiguration: MatrixConfiguration +) { private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) @@ -38,7 +42,7 @@ class LightweightSettingsStorage @Inject constructor(context: Context) { } fun areThreadMessagesEnabled(): Boolean { - return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false) + return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, matrixConfiguration.threadMessagesEnabledDefault) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index 7d004bc5c0..fedd7d05f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -80,8 +80,8 @@ internal class WorkManagerProvider @Inject constructor( workManager.enqueue(checkWorkerRequest) val checkWorkerLiveState = workManager.getWorkInfoByIdLiveData(checkWorkerRequest.id) val observer = object : Observer { - override fun onChanged(workInfo: WorkInfo) { - if (workInfo.state.isFinished) { + override fun onChanged(workInfo: WorkInfo?) { + if (workInfo?.state?.isFinished == true) { checkWorkerLiveState.removeObserver(this) if (workInfo.state == WorkInfo.State.FAILED) { throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index d316eed691..b596f2288e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -44,7 +44,7 @@ internal interface FetchThreadSummariesTask : Task + false - + diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index e3a84f95de..fdd6e3c2ba 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -46,6 +46,7 @@ import im.vector.app.features.navigation.Navigator import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.CoroutineScope @@ -113,10 +114,13 @@ object VectorStaticModule { } @Provides - fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration { + fun providesMatrixConfiguration( + vectorPreferences: VectorPreferences, + vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration { return MatrixConfiguration( applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider, + threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled(), presenceSyncEnabled = BuildConfig.PRESENCE_SYNC_ENABLED ) } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 33b735551c..42bd2318b3 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -241,7 +241,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - HomeActivity.newIntent(this) + HomeActivity.newIntent(this, existingSession = true) } else { // The token is still invalid navigator.softLogout(this) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 964fb6f365..2e9ab0efcb 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -90,6 +90,7 @@ import javax.inject.Inject data class HomeActivityArgs( val clearNotification: Boolean, val accountCreation: Boolean, + val hasExistingSession: Boolean = false, val inviteNotificationRoomId: String? = null ) : Parcelable @@ -253,6 +254,8 @@ class HomeActivity : HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() + HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() + is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) }.exhaustive } homeActivityViewModel.onEach { renderState(it) } @@ -269,6 +272,48 @@ class HomeActivity : navigator.openAnalyticsOptIn(this) } + /** + * Migrating from old threads io.element.thread to new m.thread needs an initial sync to + * sync and display existing messages appropriately + */ + private fun migrateThreadsIfNeeded(checkSession: Boolean) { + if (checkSession) { + // We should check session to ensure we will only clear cache if needed + val args = intent.getParcelableExtra(Mavericks.KEY_ARG) + if (args?.hasExistingSession == true) { + // existingSession --> Will be true only if we came from an existing active session + Timber.i("----> Migrating threads from an existing session..") + handleThreadsMigration() + } else { + // We came from a new session and not an existing one, + // so there is no need to migrate threads while an initial synced performed + Timber.i("----> No thread migration needed, we are ok") + vectorPreferences.setShouldMigrateThreads(shouldMigrate = false) + } + } else { + // Proceed with migration + handleThreadsMigration() + } + } + + /** + * Clear cache and restart to invoke an initial sync for threads migration + */ + private fun handleThreadsMigration() { + Timber.i("----> Threads Migration detected, clearing cache and sync...") + vectorPreferences.setShouldMigrateThreads(shouldMigrate = false) + MainActivity.restartApp(this, MainActivityArgs(clearCache = true)) + } + + private fun handleNotifyUserForThreadsMigration() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.threads_notice_migration_title) + .setMessage(R.string.threads_notice_migration_message) + .setCancelable(true) + .setPositiveButton(R.string.sas_got_it) { _, _ -> } + .show() + } + private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> val resolvedLink = when { @@ -546,11 +591,13 @@ class HomeActivity : fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false, + existingSession: Boolean = false, inviteNotificationRoomId: String? = null ): Intent { val args = HomeActivityArgs( clearNotification = clearNotification, accountCreation = accountCreation, + hasExistingSession = existingSession, inviteNotificationRoomId = inviteNotificationRoomId ) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index adc44a57bd..5efd49a579 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -25,4 +25,6 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents + object NotifyUserForThreadsMigration : HomeActivityViewEvents + data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 35c112b63a..b4af50c7ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -51,6 +51,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import kotlin.coroutines.Continuation @@ -62,6 +63,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val activeSessionHolder: ActiveSessionHolder, private val reAuthHelper: ReAuthHelper, private val analyticsStore: AnalyticsStore, + private val lightweightSettingsStorage: LightweightSettingsStorage, private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { @@ -84,6 +86,7 @@ class HomeActivityViewModel @AssistedInject constructor( checkSessionPushIsOn() observeCrossSigningReset() observeAnalytics() + initThreadsMigration() } private fun observeAnalytics() { @@ -130,6 +133,46 @@ class HomeActivityViewModel @AssistedInject constructor( .launchIn(viewModelScope) } + /** + * Handle threads migration. The migration includes: + * - Notify users that had io.element.thread enabled from labs + * - Re-Enable m.thread to those users (that they had enabled labs threads) + * - Handle migration when threads are enabled by default + */ + private fun initThreadsMigration() { + // When we would like to enable threads for all users +// if(vectorPreferences.shouldMigrateThreads()) { +// vectorPreferences.setThreadMessagesEnabled() +// lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) +// } + + when { + // Notify users + vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> { + Timber.i("----> Notify users about threads") + // Notify the user if needed that we migrated to support m.thread + // instead of io.element.thread so old thread messages will be displayed as normal timeline messages + _viewEvents.post(HomeActivityViewEvents.NotifyUserForThreadsMigration) + vectorPreferences.userNotifiedAboutThreads() + } + // Migrate users with enabled lab settings + vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.shouldMigrateThreads() -> { + Timber.i("----> Migrate threads with enabled labs") + // If user had io.element.thread enabled then enable the new thread support, + // clear cache to sync messages appropriately + vectorPreferences.setThreadMessagesEnabled() + lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) + // Clear Cache + _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false)) + } + // Enable all users + vectorPreferences.shouldMigrateThreads() && vectorPreferences.areThreadMessagesEnabled() -> { + Timber.i("----> Try to migrate threads") + _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = true)) + } + } + } + private fun observeInitialSync() { val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 352c5768fb..8d93edc0ec 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -201,7 +201,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" - const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" + + // This key will be used to identify clients with the old thread support enabled io.element.thread + const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" + + // This key will be used to identify clients with the new thread support enabled m.thread + const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" + const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 @@ -1006,7 +1012,56 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) } + /** + * Indicates whether or not thread messages are enabled + */ fun areThreadMessagesEnabled(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, false) + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, getDefault(R.bool.settings_labs_thread_messages_default)) + } + + /** + * Manually sets thread messages enabled, useful for migrating users from io.element.thread + */ + fun setThreadMessagesEnabled() { + defaultPrefs + .edit() + .putBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, true) + .apply() + } + + /** + * Indicates whether or not the user will be notified about the new thread support + * We should notify the user only if he had old thread support enabled + */ + fun shouldNotifyUserAboutThreads(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS, false) + } + + /** + * Indicates that the user have been notified about threads migration + */ + fun userNotifiedAboutThreads() { + defaultPrefs + .edit() + .putBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS, false) + .apply() + } + + /** + * Indicates whether or not we should clear cache for threads migration. + * Default value is true, for fresh installs and updates + */ + fun shouldMigrateThreads(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, true) + } + + /** + * Indicates that there no longer threads migration needed + */ + fun setShouldMigrateThreads(shouldMigrate: Boolean) { + defaultPrefs + .edit() + .putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, shouldMigrate) + .apply() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 118e820f84..003832fb97 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -42,6 +42,8 @@ class VectorSettingsLabsFragment @Inject constructor( // clear cache findPreference(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // We should migrate threads only if threads are disabled + vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) displayLoadingView() MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index aed977d687..551e5961ec 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -731,6 +731,8 @@ Tip: Long tap a message and use โ€œ%sโ€. From a Thread + Threads Approaching Beta ๐ŸŽ‰ + Weโ€™re getting closer to releasing a public Beta for Threads.\n\nAs we prepare for it, we need to make some changes: threads created before this point will be displayed as regular replies.\n\nThis will be a one-off transition as Threads are now part of the Matrix specification. Search diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 73193edfd5..5144f6fe1f 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -52,8 +52,8 @@