Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete Events from ignored users and trigger a clear cache request dialog when unignoring user(s) #5772

Merged
merged 10 commits into from
Apr 21, 2022
1 change: 1 addition & 0 deletions changelog.d/5772.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve management of ignored users
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()

/**
* The SDK requires the app (which should request the user) to perform an initial sync.
*/
data class InitialSyncRequest(val reason: InitialSyncRequestReason) : GlobalError()
object ExpiredAccount : GlobalError()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.matrix.android.sdk.api.failure

/**
* This enum provide the reason why the SDK request an initial sync to the application
*/
enum class InitialSyncRequestReason {
/**
* The list of ignored users has changed, and at least one user who was ignored is not ignored anymore
*/
IGNORED_USERS_LIST_CHANGE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ interface Session :
* Possible cases:
* - The access token is not valid anymore,
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
* See [GlobalError] for all the possible cases
*/
fun onGlobalError(session: Session, globalError: GlobalError) = Unit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.matrix.android.sdk.api.session.room.timeline

// TODO Move to internal, strange?
data class TimelineEventFilters(
/**
* A flag to filter edit events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ data class TimelineSettings(
/**
* The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
*/
val rootThreadEventId: String? = null) {
val rootThreadEventId: String? = null,
) {

/**
* Returns true if this is a thread timeline or false otherwise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ internal class DefaultTimeline(private val roomId: String,
threadsAwarenessHandler = threadsAwarenessHandler,
lightweightSettingsStorage = lightweightSettingsStorage,
onEventsUpdated = this::sendSignalToPostSnapshot,
onEventsDeleted = this::onEventsDeleted,
onLimitedTimeline = this::onLimitedTimeline,
onNewTimelineEvents = this::onNewTimelineEvents
)
Expand Down Expand Up @@ -304,6 +305,12 @@ internal class DefaultTimeline(private val roomId: String,
}
}

private fun onEventsDeleted() {
// Some event have been deleted, for instance when a user has been ignored.
// Restart the timeline (live)
restartWithEventId(null)
}

private suspend fun postSnapshot() {
val snapshot = strategy.buildSnapshot()
Timber.v("Post snapshot of ${snapshot.size} events")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ internal class LoadTimelineStrategy(
val threadsAwarenessHandler: ThreadsAwarenessHandler,
val lightweightSettingsStorage: LightweightSettingsStorage,
val onEventsUpdated: (Boolean) -> Unit,
val onEventsDeleted: () -> Unit,
val onLimitedTimeline: () -> Unit,
val onNewTimelineEvents: (List<String>) -> Unit
)
Expand Down Expand Up @@ -302,7 +303,8 @@ internal class LoadTimelineStrategy(
threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
lightweightSettingsStorage = dependencies.lightweightSettingsStorage,
initialEventId = mode.originEventId(),
onBuiltEvents = dependencies.onEventsUpdated
onBuiltEvents = dependencies.onEventsUpdated,
onEventsDeleted = dependencies.onEventsDeleted,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
private val realmConfiguration: RealmConfiguration,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val uiEchoManager: UIEchoManager? = null,
private val uiEchoManager: UIEchoManager?,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val initialEventId: String?,
private val onBuiltEvents: (Boolean) -> Unit) {
private val onBuiltEvents: (Boolean) -> Unit,
private val onEventsDeleted: () -> Unit,
) {

private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward)
Expand Down Expand Up @@ -505,6 +507,11 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
onBuiltEvents(true)
}

val deletions = changeSet.deletions
if (deletions.isNotEmpty()) {
onEventsDeleted()
}
}

private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
Expand Down Expand Up @@ -543,7 +550,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
threadsAwarenessHandler = threadsAwarenessHandler,
lightweightSettingsStorage = lightweightSettingsStorage,
initialEventId = null,
onBuiltEvents = this.onBuiltEvents
onBuiltEvents = this.onBuiltEvents,
onEventsDeleted = this.onEventsDeleted
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmList
import io.realm.kotlin.where
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.InitialSyncRequestReason
import org.matrix.android.sdk.api.pushrules.RuleScope
import org.matrix.android.sdk.api.pushrules.RuleSetKey
import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse
Expand All @@ -31,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.api.session.sync.model.UserAccountDataSync
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
Expand All @@ -39,14 +42,19 @@ import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity
import org.matrix.android.sdk.internal.database.model.PushRulesEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.query.getDirectRooms
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionListeners
import org.matrix.android.sdk.internal.session.dispatchTo
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
Expand All @@ -65,7 +73,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(
private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val roomAvatarResolver: RoomAvatarResolver,
private val roomDisplayNameResolver: RoomDisplayNameResolver
private val roomDisplayNameResolver: RoomDisplayNameResolver,
@SessionId private val sessionId: String,
private val sessionManager: SessionManager,
private val sessionListeners: SessionListeners
) {

fun handle(realm: Realm, accountData: UserAccountDataSync?) {
Expand Down Expand Up @@ -184,12 +195,39 @@ internal class UserAccountDataSyncHandler @Inject constructor(

private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) {
val userIds = event.content.toModel<IgnoredUsersContent>()?.ignoredUsers?.keys ?: return
realm.where(IgnoredUserEntity::class.java)
.findAll()
.deleteAllFromRealm()
val currentIgnoredUsers = realm.where(IgnoredUserEntity::class.java).findAll()
val currentIgnoredUserIds = currentIgnoredUsers.map { it.userId }
// Delete the previous list
currentIgnoredUsers.deleteAllFromRealm()
// And save the new received list
userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } }
// TODO If not initial sync, we should execute a init sync

// Delete all the TimelineEvents for all the ignored users
// See https://spec.matrix.org/latest/client-server-api/#client-behaviour-22 :
// "Once ignored, the client will no longer receive events sent by that user, with the exception of state events"
// So just delete all non-state events from our local storage.
realm.where(TimelineEventEntity::class.java)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to extract this functionality in TimelineEventEntityQueries.kt or TimelineEventHelper.kt. Not a blocker

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Will be handled in a dedicated PR later.

.`in`(TimelineEventEntityFields.ROOT.SENDER, userIds.toTypedArray())
.isNull(TimelineEventEntityFields.ROOT.STATE_KEY)
.findAll()
.also { Timber.d("Deleting ${it.size} TimelineEventEntity from ignored users") }
.forEach {
it.deleteOnCascade(true)
}

// Handle the case when some users are unignored from another session
val mustRefreshCache = currentIgnoredUserIds.any { currentIgnoredUserId -> currentIgnoredUserId !in userIds }
if (mustRefreshCache) {
Timber.d("A user has been unignored from another session, an initial sync should be performed")
dispatchMustRefresh()
}
}

private fun dispatchMustRefresh() {
val session = sessionManager.getSessionComponent(sessionId)?.session()
session.dispatchTo(sessionListeners) { safeSession, listener ->
listener.onGlobalError(safeSession, GlobalError.InitialSyncRequest(InitialSyncRequestReason.IGNORED_USERS_LIST_CHANGE))
}
}

private fun handleBreadcrumbs(realm: Realm, event: UserAccountDataEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSaveIgnoredUsersTask
import org.matrix.android.sdk.internal.session.user.accountdata.DefaultUpdateIgnoredUserIdsTask
import org.matrix.android.sdk.internal.session.user.accountdata.SaveIgnoredUsersTask
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask
import org.matrix.android.sdk.internal.session.user.model.DefaultSearchUserTask
import org.matrix.android.sdk.internal.session.user.model.SearchUserTask
Expand All @@ -48,9 +46,6 @@ internal abstract class UserModule {
@Binds
abstract fun bindSearchUserTask(task: DefaultSearchUserTask): SearchUserTask

@Binds
abstract fun bindSaveIgnoredUsersTask(task: DefaultSaveIgnoredUsersTask): SaveIgnoredUsersTask

@Binds
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ internal interface UpdateIgnoredUserIdsTask : Task<UpdateIgnoredUserIdsTask.Para
internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor(
private val accountDataApi: AccountDataAPI,
@SessionDatabase private val monarchy: Monarchy,
private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
@UserId private val userId: String,
private val globalErrorReceiver: GlobalErrorReceiver
) : UpdateIgnoredUserIdsTask {
Expand Down Expand Up @@ -66,8 +65,5 @@ internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor(
executeRequest(globalErrorReceiver) {
accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body)
}

// Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update)
saveIgnoredUsersTask.execute(SaveIgnoredUsersTask.Params(list))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import com.bumptech.glide.util.Util
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.EntryPointAccessors
import im.vector.app.BuildConfig
Expand Down Expand Up @@ -86,6 +87,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.InitialSyncRequestReason
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
import javax.inject.Inject
Expand Down Expand Up @@ -266,9 +268,27 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
is GlobalError.CertificateError ->
handleCertificateError(globalError)
GlobalError.ExpiredAccount -> Unit // TODO Handle account expiration
is GlobalError.InitialSyncRequest -> handleInitialSyncRequest(globalError)
}
}

private fun handleInitialSyncRequest(initialSyncRequest: GlobalError.InitialSyncRequest) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.initial_sync_request_title)
.setMessage(
getString(R.string.initial_sync_request_content, getString(
when (initialSyncRequest.reason) {
InitialSyncRequestReason.IGNORED_USERS_LIST_CHANGE -> R.string.initial_sync_request_reason_unignored_users
}
))
)
.setPositiveButton(R.string.ok) { _, _ ->
MainActivity.restartApp(this, MainActivityArgs(clearCache = true))
}
.setNegativeButton(R.string.later, null)
.show()
}

private fun handleCertificateError(certificateError: GlobalError.CertificateError) {
singletonEntryPoint()
.unrecognizedCertificateDialog()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ import im.vector.app.core.utils.startInstallFromSourceIntent
import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentTimelineBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.analytics.plan.MobileScreen
Expand Down Expand Up @@ -1730,14 +1728,10 @@ class TimelineFragment @Inject constructor(
dismissLoadingDialog()
views.composerLayout.setTextIfDifferent("")
when (parsedCommand) {
is ParsedCommand.SetMarkdown -> {
is ParsedCommand.SetMarkdown -> {
showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
}
is ParsedCommand.UnignoreUser -> {
// A user has been un-ignored, perform a initial sync
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true))
}
else -> Unit
else -> Unit
}
}

Expand Down
Loading