Skip to content

Allow user with enough power level to redact other's messages (#969) #991

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

Merged
merged 2 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -109,6 +110,7 @@ class MessagesPresenter @AssistedInject constructor(

val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
var roomName: Async<String> by remember { mutableStateOf(Async.Uninitialized) }
var roomAvatar: Async<AvatarData> by remember { mutableStateOf(Async.Uninitialized) }
LaunchedEffect(syncUpdateFlow.value) {
Expand Down Expand Up @@ -165,6 +167,7 @@ class MessagesPresenter @AssistedInject constructor(
roomName = roomName,
roomAvatar = roomAvatar,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedact = userHasPermissionToRedact,
composerState = composerState,
timelineState = timelineState,
actionListState = actionListState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ data class MessagesState(
val roomName: Async<String>,
val roomAvatar: Async<AvatarData>,
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToRedact: Boolean,
val composerState: MessageComposerState,
val timelineState: TimelineState,
val actionListState: ActionListState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fun aMessagesState() = MessagesState(
roomName = Async.Success("Room name"),
roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true,
userHasPermissionToRedact = false,
composerState = aMessageComposerState().copy(
text = "Hello",
isFullScreen = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fun MessagesView(
fun onMessageLongClicked(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${event.id}")
localView.hideKeyboard()
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
}

fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem

sealed interface ActionListEvents {
object Clear : ActionListEvents
data class ComputeForMessage(val event: TimelineItem.Event) : ActionListEvents
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ class ActionListPresenter @Inject constructor(
fun handleEvents(event: ActionListEvents) {
when (event) {
ActionListEvents.Clear -> target.value = ActionListState.Target.None
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(event.event, target)
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
timelineItem = event.event,
userCanRedact = event.canRedact,
target = target,
)
}
}

Expand All @@ -65,7 +69,11 @@ class ActionListPresenter @Inject constructor(
)
}

private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
private fun CoroutineScope.computeForMessage(
timelineItem: TimelineItem.Event,
userCanRedact: Boolean,
target: MutableState<ActionListState.Target>
) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
val actions =
when (timelineItem.content) {
Expand Down Expand Up @@ -102,7 +110,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (timelineItem.isMine) {
if (timelineItem.isMine || userCanRedact) {
add(TimelineItemAction.Redact)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.media.MediaSource
Expand Down Expand Up @@ -83,9 +86,16 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.roomName).isEqualTo(Async.Success(""))
assertThat(initialState.roomAvatar).isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", size = AvatarSize.TimelineRoom)))
assertThat(initialState.userHasPermissionToSendMessage).isTrue()
assertThat(initialState.userHasPermissionToRedact).isFalse()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(Async.Uninitialized)
assertThat(initialState.showReinvitePrompt).isFalse()
}
}

Expand Down Expand Up @@ -531,6 +541,19 @@ class MessagesPresenterTest {
}
}

@Test
fun `present - permission to redact`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedact = true)
val presenter = createMessagePresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedact }.last()
assertThat(initialState.userHasPermissionToRedact).isTrue()
cancelAndIgnoreRemainingEvents()
}
}

private fun TestScope.createMessagePresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand All @@ -81,7 +81,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -109,7 +109,7 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand All @@ -130,6 +130,37 @@ class ActionListPresenterTest {
}
}

@Test
fun `present - compute for others message and can redact`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
)
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
}
}

@Test
fun `present - compute for my message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
Expand All @@ -141,7 +172,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -174,7 +205,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -205,7 +236,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -234,7 +265,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -262,7 +293,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
Expand Down Expand Up @@ -299,10 +330,10 @@ class ActionListPresenterTest {
content = TimelineItemRedactedContent,
)

initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)

initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
awaitItem().run {
assertThat(target).isEqualTo(ActionListState.Target.None)
assertThat(displayEmojiReactions).isFalse()
Expand All @@ -323,7 +354,7 @@ class ActionListPresenterTest {
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
)

initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ interface MatrixRoom : Closeable {

suspend fun canUserInvite(userId: UserId): Result<Boolean>

suspend fun canUserRedact(userId: UserId): Result<Boolean>

suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>

suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result<Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ suspend fun MatrixRoom.canSendState(type: StateEventType): Result<Boolean> = can
* Shortcut for calling [MatrixRoom.canUserSendMessage] with our own user.
*/
suspend fun MatrixRoom.canSendMessage(type: MessageEventType): Result<Boolean> = canUserSendMessage(sessionId, type)

/**
* Shortcut for calling [MatrixRoom.canUserRedact] with our own user.
*/
suspend fun MatrixRoom.canRedact(): Result<Boolean> = canUserRedact(sessionId)

Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ class RustMatrixRoom(
}
}

override suspend fun canUserRedact(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserRedact(userId.value)
}
}

override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> {
return runCatching {
innerRoom.canUserSendState(userId.value, type.map())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class FakeMatrixRoom(
override val joinedMemberCount: Long = 123L,
override val activeMemberCount: Long = 234L,
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
canRedact: Boolean = false,
) : MatrixRoom {

private var ignoreResult: Result<Unit> = Result.success(Unit)
Expand All @@ -66,6 +67,7 @@ class FakeMatrixRoom(
private var joinRoomResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
private var canRedactResult = Result.success(canRedact)
private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>()
private val canSendEventResults = mutableMapOf<MessageEventType, Result<Boolean>>()
private var sendMediaResult = Result.success(Unit)
Expand Down Expand Up @@ -207,6 +209,10 @@ class FakeMatrixRoom(
return canInviteResult
}

override suspend fun canUserRedact(userId: UserId): Result<Boolean> {
return canRedactResult
}

override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> {
return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.powerlevels.canRedact
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage

@Composable
Expand All @@ -30,3 +31,10 @@ fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): S
}
}

@Composable
fun MatrixRoom.canRedactAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canRedact().getOrElse { false }
}
}