Migrate Threads and notify user

This commit is contained in:
ariskotsomitopoulos 2022-03-17 18:51:54 +01:00
parent 99b43fd771
commit 2ca3387ab3
12 changed files with 177 additions and 12 deletions

View file

@ -60,7 +60,11 @@ data class MatrixConfiguration(
/** /**
* RoomDisplayNameFallbackProvider to provide default room display name. * RoomDisplayNameFallbackProvider to provide default room display name.
*/ */
val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
/**
* Thread messages default enable/disabled value
*/
val threadMessagesEnabledDefault: Boolean = false,
) { ) {
/** /**

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.lightweight
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.matrix.android.sdk.api.MatrixConfiguration
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -27,7 +28,10 @@ import javax.inject.Inject
* not for large data sets * not for large data sets
*/ */
class LightweightSettingsStorage @Inject constructor(context: Context) { class LightweightSettingsStorage @Inject constructor(
context: Context,
val matrixConfiguration: MatrixConfiguration
) {
private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
@ -38,7 +42,7 @@ class LightweightSettingsStorage @Inject constructor(context: Context) {
} }
fun areThreadMessagesEnabled(): Boolean { 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 { companion object {

View file

@ -75,7 +75,7 @@ internal class SyncResponseHandler @Inject constructor(
suspend fun handleResponse(syncResponse: SyncResponse, suspend fun handleResponse(syncResponse: SyncResponse,
fromToken: String?, fromToken: String?,
reporter: ProgressReporter?) { reporter: ProgressReporter?) {
val isInitialSync = fromToken == null var isInitialSync = fromToken == null
Timber.v("Start handling sync, is InitialSync: $isInitialSync") Timber.v("Start handling sync, is InitialSync: $isInitialSync")
measureTimeMillis { measureTimeMillis {

View file

@ -36,8 +36,9 @@
<!-- Level 1: Security and Privacy --> <!-- Level 1: Security and Privacy -->
<!-- Level 1: Labs --> <!-- Level 1: Labs -->
<bool name="settings_labs_thread_messages_default">false</bool>
<!-- Level 1: Advcanced settings --> <!-- Level 1: Advanced settings -->
<!-- Level 1: Help and about --> <!-- Level 1: Help and about -->

View file

@ -46,6 +46,7 @@ import im.vector.app.features.navigation.Navigator
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider 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.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -113,10 +114,13 @@ object VectorStaticModule {
} }
@Provides @Provides
fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration { fun providesMatrixConfiguration(
vectorPreferences: VectorPreferences,
vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
return MatrixConfiguration( return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION, applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider,
threadMessagesEnabledDefault = vectorPreferences.areThreadMessagesEnabled()
) )
} }

View file

@ -241,7 +241,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
// We have a session. // We have a session.
// Check it can be opened // Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) { if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this) HomeActivity.newIntent(this, existingSession = true)
} else { } else {
// The token is still invalid // The token is still invalid
navigator.softLogout(this) navigator.softLogout(this)

View file

@ -90,6 +90,7 @@ import javax.inject.Inject
data class HomeActivityArgs( data class HomeActivityArgs(
val clearNotification: Boolean, val clearNotification: Boolean,
val accountCreation: Boolean, val accountCreation: Boolean,
val existingSession: Boolean = false,
val inviteNotificationRoomId: String? = null val inviteNotificationRoomId: String? = null
) : Parcelable ) : Parcelable
@ -253,6 +254,8 @@ class HomeActivity :
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
}.exhaustive }.exhaustive
} }
homeActivityViewModel.onEach { renderState(it) } homeActivityViewModel.onEach { renderState(it) }
@ -269,6 +272,49 @@ class HomeActivity :
navigator.openAnalyticsOptIn(this) 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<HomeActivityArgs>(Mavericks.KEY_ARG)
if (args?.existingSession == 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.threadsMigrated()
}
} 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.threadsMigrated()
MainActivity.restartApp(this, MainActivityArgs(clearCache = true))
}
private fun handleNotifyUserForThreadsMigration() {
MaterialAlertDialogBuilder(this)
.setTitle("Threads, no longer experimental")
.setMessage("All \uD83C\uDF89 \uD83C\uDF89 threads created during experimental period will\n\n now be rendered as regular replies. This will be an one-off transition, as threads are now part of the matrix specification")
.setCancelable(true)
.setPositiveButton(R.string.ok) { _, _ -> }
.show()
}
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {
intent?.dataString?.let { deepLink -> intent?.dataString?.let { deepLink ->
val resolvedLink = when { val resolvedLink = when {
@ -546,11 +592,13 @@ class HomeActivity :
fun newIntent(context: Context, fun newIntent(context: Context,
clearNotification: Boolean = false, clearNotification: Boolean = false,
accountCreation: Boolean = false, accountCreation: Boolean = false,
existingSession: Boolean = false,
inviteNotificationRoomId: String? = null inviteNotificationRoomId: String? = null
): Intent { ): Intent {
val args = HomeActivityArgs( val args = HomeActivityArgs(
clearNotification = clearNotification, clearNotification = clearNotification,
accountCreation = accountCreation, accountCreation = accountCreation,
existingSession = existingSession,
inviteNotificationRoomId = inviteNotificationRoomId inviteNotificationRoomId = inviteNotificationRoomId
) )

View file

@ -25,4 +25,7 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents
object NotifyUserForThreadsMigration : HomeActivityViewEvents
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
} }

View file

@ -51,6 +51,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap 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 org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -62,6 +63,7 @@ class HomeActivityViewModel @AssistedInject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@ -84,6 +86,7 @@ class HomeActivityViewModel @AssistedInject constructor(
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
observeAnalytics() observeAnalytics()
initThreadsMigration()
} }
private fun observeAnalytics() { private fun observeAnalytics() {
@ -130,6 +133,49 @@ class HomeActivityViewModel @AssistedInject constructor(
.launchIn(viewModelScope) .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() {
// Notify users
if (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()
return
}
// Migrate users with enabled lab settings
if (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))
return
}
// Enable all users
// When we would to enable threads for all
// if(vectorPreferences.shouldMigrateThreads) -->
// vectorPreferences.setThreadMessagesEnabled() &&
// lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
if(vectorPreferences.shouldMigrateThreads() && vectorPreferences.areThreadMessagesEnabled()){
Timber.i("----> Try to migrate threads")
_viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = true))
return
}
}
private fun observeInitialSync() { private fun observeInitialSync() {
val session = activeSessionHolder.getSafeActiveSession() ?: return val session = activeSessionHolder.getSafeActiveSession() ?: return

View file

@ -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 TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" 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 // Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
@ -1006,7 +1012,55 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true)
} }
/**
* Indicates whether or not thread messages are enabled
*/
fun areThreadMessagesEnabled(): Boolean { 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 threadsMigrated() {
defaultPrefs
.edit()
.putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, false)
.apply()
} }
} }

View file

@ -42,6 +42,7 @@ class VectorSettingsLabsFragment @Inject constructor(
// clear cache // clear cache
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let { findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener { it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
vectorPreferences.threadsMigrated() // Manual actions should disable the ato enable mechanism
lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
displayLoadingView() displayLoadingView()
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true))

View file

@ -52,8 +52,8 @@
<!--</im.vector.app.core.preference.VectorPreferenceCategory>--> <!--</im.vector.app.core.preference.VectorPreferenceCategory>-->
<im.vector.app.core.preference.VectorSwitchPreference <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="@bool/settings_labs_thread_messages_default"
android:key="SETTINGS_LABS_ENABLE_THREAD_MESSAGES" android:key="SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
android:summary="@string/labs_enable_thread_messages_desc" android:summary="@string/labs_enable_thread_messages_desc"
android:title="@string/labs_enable_thread_messages" /> android:title="@string/labs_enable_thread_messages" />