diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 7d421a256a..1ca834bc66 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -139,6 +139,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) + val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value) val roomName: Async by remember { derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized } } @@ -219,6 +220,7 @@ class MessagesPresenter @AssistedInject constructor( roomAvatar = roomAvatar, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToRedact = userHasPermissionToRedact, + userHasPermissionToSendReaction = userHasPermissionToSendReaction, composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 7d342ab107..c7ef5f320f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -37,6 +37,7 @@ data class MessagesState( val roomAvatar: Async, val userHasPermissionToSendMessage: Boolean, val userHasPermissionToRedact: Boolean, + val userHasPermissionToSendReaction: Boolean, val composerState: MessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 00c15fb410..2984e4e087 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -87,6 +87,7 @@ fun aMessagesState() = MessagesState( roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), userHasPermissionToSendMessage = true, userHasPermissionToRedact = false, + userHasPermissionToSendReaction = true, composerState = aMessageComposerState().copy( richTextEditorState = RichTextEditorState("Hello", initialFocus = true), isFullScreen = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index dd728aa274..d6556c9a8c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -159,6 +159,7 @@ fun MessagesView( event = event, canRedact = state.userHasPermissionToRedact, canSendMessage = state.userHasPermissionToSendMessage, + canSendReaction = state.userHasPermissionToSendReaction, ) ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt index 7c8fad6c7c..6339716ccf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt @@ -24,5 +24,6 @@ sealed interface ActionListEvents { val event: TimelineItem.Event, val canRedact: Boolean, val canSendMessage: Boolean, + val canSendReaction: Boolean, ) : ActionListEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 645c903158..d3a4d44246 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -59,6 +59,7 @@ class ActionListPresenter @Inject constructor( timelineItem = event.event, userCanRedact = event.canRedact, userCanSendMessage = event.canSendMessage, + userCanSendReaction = event.canSendReaction, isDeveloperModeEnabled = isDeveloperModeEnabled, target = target, ) @@ -75,6 +76,7 @@ class ActionListPresenter @Inject constructor( timelineItem: TimelineItem.Event, userCanRedact: Boolean, userCanSendMessage: Boolean, + userCanSendReaction: Boolean, isDeveloperModeEnabled: Boolean, target: MutableState ) = launch { @@ -169,7 +171,9 @@ class ActionListPresenter @Inject constructor( } } } - val displayEmojiReactions = timelineItem.isRemote && timelineItem.content.canReact() + val displayEmojiReactions = userCanSendReaction && + timelineItem.isRemote + && timelineItem.content.canReact() if (actions.isNotEmpty() || displayEmojiReactions) { target.value = ActionListState.Target.Success( event = timelineItem, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 1fcd873d5a..14ae8f1b59 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -62,7 +62,7 @@ class ActionListPresenterTest { }.test { val initialState = awaitItem() val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -88,7 +88,7 @@ class ActionListPresenterTest { }.test { val initialState = awaitItem() val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -117,7 +117,7 @@ class ActionListPresenterTest { isMine = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -150,7 +150,7 @@ class ActionListPresenterTest { isMine = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -182,7 +182,7 @@ class ActionListPresenterTest { isMine = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -203,6 +203,38 @@ class ActionListPresenterTest { } } + @Test + fun `present - compute for others message and cannot send reaction`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = false, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = false)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + displayEmojiReactions = false, + actions = persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.Copy, + TimelineItemAction.ViewSource, + 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 = createActionListPresenter(isDeveloperModeEnabled = true) @@ -214,7 +246,7 @@ class ActionListPresenterTest { isMine = true, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -248,7 +280,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemImageContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -280,7 +312,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemStateEventContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -310,7 +342,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemStateEventContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -339,7 +371,7 @@ class ActionListPresenterTest { isMine = true, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() @@ -377,10 +409,10 @@ class ActionListPresenterTest { content = TimelineItemRedactedContent, ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) awaitItem().run { assertThat(target).isEqualTo(ActionListState.Target.None) } @@ -400,7 +432,7 @@ class ActionListPresenterTest { content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -428,7 +460,7 @@ class ActionListPresenterTest { isEditable = true, content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = false)), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -457,7 +489,7 @@ class ActionListPresenterTest { isEditable = false, content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = true)), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -485,7 +517,7 @@ class ActionListPresenterTest { isEditable = false, content = aTimelineItemPollContent(isEnded = true), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -511,7 +543,7 @@ class ActionListPresenterTest { isMine = true, content = aTimelineItemVoiceContent(), ) - initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success(