Skip to content

Commit 05a3b64

Browse files
authored
Merge pull request #1271 from vector-im/feature/bma/replyCondition
Reply action: harmonize condition
2 parents dd134c6 + 664caf3 commit 05a3b64

File tree

10 files changed

+87
-27
lines changed

10 files changed

+87
-27
lines changed

changelog.d/1173.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reply action: harmonize conditions in bottom sheet and swipe to reply.

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@ fun MessagesView(
123123
fun onMessageLongClicked(event: TimelineItem.Event) {
124124
Timber.v("OnMessageLongClicked= ${event.id}")
125125
localView.hideKeyboard()
126-
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
126+
state.actionListState.eventSink(
127+
ActionListEvents.ComputeForMessage(
128+
event = event,
129+
canRedact = state.userHasPermissionToRedact,
130+
canSendMessage = state.userHasPermissionToSendMessage,
131+
)
132+
)
127133
}
128134

129135
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
@@ -203,8 +209,8 @@ fun MessagesView(
203209
CustomReactionBottomSheet(
204210
state = state.customReactionState,
205211
onEmojiSelected = { eventId, emoji ->
206-
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
207-
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
212+
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
213+
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
208214
}
209215
)
210216

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
2020

2121
sealed interface ActionListEvents {
2222
data object Clear : ActionListEvents
23-
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
23+
data class ComputeForMessage(
24+
val event: TimelineItem.Event,
25+
val canRedact: Boolean,
26+
val canSendMessage: Boolean,
27+
) : ActionListEvents
2428
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class ActionListPresenter @Inject constructor(
6262
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
6363
timelineItem = event.event,
6464
userCanRedact = event.canRedact,
65+
userCanSendMessage = event.canSendMessage,
6566
target = target,
6667
)
6768
}
@@ -77,6 +78,7 @@ class ActionListPresenter @Inject constructor(
7778
private fun CoroutineScope.computeForMessage(
7879
timelineItem: TimelineItem.Event,
7980
userCanRedact: Boolean,
81+
userCanSendMessage: Boolean,
8082
target: MutableState<ActionListState.Target>
8183
) = launch {
8284
target.value = ActionListState.Target.Loading(timelineItem)
@@ -101,7 +103,8 @@ class ActionListPresenter @Inject constructor(
101103
buildList {
102104
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
103105

104-
// TODO Poll: Reply to poll
106+
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
107+
// when touching this
105108
// if (timelineItem.isRemote) {
106109
// // Can only reply or forward messages already uploaded to the server
107110
// add(TimelineItemAction.Reply)
@@ -126,7 +129,9 @@ class ActionListPresenter @Inject constructor(
126129
else -> buildList<TimelineItemAction> {
127130
if (timelineItem.isRemote) {
128131
// Can only reply or forward messages already uploaded to the server
129-
add(TimelineItemAction.Reply)
132+
if (userCanSendMessage) {
133+
add(TimelineItemAction.Reply)
134+
}
130135
add(TimelineItemAction.Forward)
131136
}
132137
if (timelineItem.isMine && timelineItem.isTextMessage) {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class TimelinePresenter @Inject constructor(
116116

117117
return TimelineState(
118118
highlightedEventId = highlightedEventId.value,
119-
canReply = userHasPermissionToSendMessage,
119+
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
120120
paginationState = paginationState,
121121
timelineItems = timelineItems,
122122
hasNewItems = hasNewItems.value,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
2626
data class TimelineState(
2727
val timelineItems: ImmutableList<TimelineItem>,
2828
val highlightedEventId: EventId?,
29-
val canReply: Boolean,
29+
val userHasPermissionToSendMessage: Boolean,
3030
val paginationState: MatrixTimeline.PaginationState,
3131
val hasNewItems: Boolean,
3232
val eventSink: (TimelineEvents) -> Unit

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
4444
timelineItems = timelineItems,
4545
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
4646
highlightedEventId = null,
47-
canReply = true,
47+
userHasPermissionToSendMessage = true,
4848
hasNewItems = false,
4949
eventSink = {},
5050
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
6363
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
6464
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
6565
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
66+
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
6667
import io.element.android.libraries.designsystem.preview.DayNightPreviews
6768
import io.element.android.libraries.designsystem.preview.ElementPreview
6869
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
@@ -119,7 +120,7 @@ fun TimelineView(
119120
TimelineItemRow(
120121
timelineItem = timelineItem,
121122
highlightedItem = state.highlightedEventId?.value,
122-
canReply = state.canReply,
123+
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
123124
onClick = onMessageClicked,
124125
onLongClick = onMessageLongClicked,
125126
onUserDataClick = onUserDataClicked,
@@ -156,7 +157,7 @@ fun TimelineView(
156157
fun TimelineItemRow(
157158
timelineItem: TimelineItem,
158159
highlightedItem: String?,
159-
canReply: Boolean,
160+
userHasPermissionToSendMessage: Boolean,
160161
onUserDataClick: (UserId) -> Unit,
161162
onClick: (TimelineItem.Event) -> Unit,
162163
onLongClick: (TimelineItem.Event) -> Unit,
@@ -189,7 +190,7 @@ fun TimelineItemRow(
189190
TimelineItemEventRow(
190191
event = timelineItem,
191192
isHighlighted = highlightedItem == timelineItem.identifier(),
192-
canReply = canReply,
193+
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
193194
onClick = { onClick(timelineItem) },
194195
onLongClick = { onLongClick(timelineItem) },
195196
onUserDataClick = onUserDataClick,
@@ -228,7 +229,7 @@ fun TimelineItemRow(
228229
TimelineItemRow(
229230
timelineItem = subGroupEvent,
230231
highlightedItem = highlightedItem,
231-
canReply = false,
232+
userHasPermissionToSendMessage = false,
232233
onClick = onClick,
233234
onLongClick = onLongClick,
234235
inReplyToClick = inReplyToClick,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ fun TimelineItemEventContent.canBeCopied(): Boolean =
3434
else -> false
3535
}
3636

37+
/**
38+
* Determine if the event content can be replied to.
39+
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
40+
*/
41+
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
42+
when (this) {
43+
is TimelineItemRedactedContent,
44+
is TimelineItemStateContent,
45+
is TimelineItemPollContent -> false
46+
else -> true
47+
}
48+
3749
/**
3850
* Return true if user can react (i.e. send a reaction) on the event content.
3951
*/

features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class ActionListPresenterTest {
6464
}.test {
6565
val initialState = awaitItem()
6666
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
67-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
67+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
6868
// val loadingState = awaitItem()
6969
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
7070
val successState = awaitItem()
@@ -89,7 +89,7 @@ class ActionListPresenterTest {
8989
}.test {
9090
val initialState = awaitItem()
9191
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
92-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
92+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
9393
// val loadingState = awaitItem()
9494
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
9595
val successState = awaitItem()
@@ -117,7 +117,7 @@ class ActionListPresenterTest {
117117
isMine = false,
118118
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
119119
)
120-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
120+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
121121
// val loadingState = awaitItem()
122122
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
123123
val successState = awaitItem()
@@ -138,6 +138,37 @@ class ActionListPresenterTest {
138138
}
139139
}
140140

141+
@Test
142+
fun `present - compute for others message cannot sent message`() = runTest {
143+
val presenter = anActionListPresenter(isBuildDebuggable = true)
144+
moleculeFlow(RecompositionMode.Immediate) {
145+
presenter.present()
146+
}.test {
147+
val initialState = awaitItem()
148+
val messageEvent = aMessageEvent(
149+
isMine = false,
150+
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
151+
)
152+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false))
153+
// val loadingState = awaitItem()
154+
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
155+
val successState = awaitItem()
156+
assertThat(successState.target).isEqualTo(
157+
ActionListState.Target.Success(
158+
messageEvent,
159+
persistentListOf(
160+
TimelineItemAction.Forward,
161+
TimelineItemAction.Copy,
162+
TimelineItemAction.Developer,
163+
TimelineItemAction.ReportContent,
164+
)
165+
)
166+
)
167+
initialState.eventSink.invoke(ActionListEvents.Clear)
168+
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
169+
}
170+
}
171+
141172
@Test
142173
fun `present - compute for others message and can redact`() = runTest {
143174
val presenter = anActionListPresenter(isBuildDebuggable = true)
@@ -149,7 +180,7 @@ class ActionListPresenterTest {
149180
isMine = false,
150181
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
151182
)
152-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
183+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true))
153184
val successState = awaitItem()
154185
assertThat(successState.target).isEqualTo(
155186
ActionListState.Target.Success(
@@ -180,7 +211,7 @@ class ActionListPresenterTest {
180211
isMine = true,
181212
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
182213
)
183-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
214+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
184215
// val loadingState = awaitItem()
185216
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
186217
val successState = awaitItem()
@@ -213,7 +244,7 @@ class ActionListPresenterTest {
213244
isMine = true,
214245
content = aTimelineItemImageContent(),
215246
)
216-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
247+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
217248
// val loadingState = awaitItem()
218249
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
219250
val successState = awaitItem()
@@ -244,7 +275,7 @@ class ActionListPresenterTest {
244275
isMine = true,
245276
content = aTimelineItemStateEventContent(),
246277
)
247-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
278+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
248279
// val loadingState = awaitItem()
249280
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
250281
val successState = awaitItem()
@@ -273,7 +304,7 @@ class ActionListPresenterTest {
273304
isMine = true,
274305
content = aTimelineItemStateEventContent(),
275306
)
276-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
307+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
277308
// val loadingState = awaitItem()
278309
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
279310
val successState = awaitItem()
@@ -301,7 +332,7 @@ class ActionListPresenterTest {
301332
isMine = true,
302333
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
303334
)
304-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
335+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
305336
// val loadingState = awaitItem()
306337
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
307338
val successState = awaitItem()
@@ -338,10 +369,10 @@ class ActionListPresenterTest {
338369
content = TimelineItemRedactedContent,
339370
)
340371

341-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
372+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
342373
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
343374

344-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
375+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true))
345376
awaitItem().run {
346377
assertThat(target).isEqualTo(ActionListState.Target.None)
347378
assertThat(displayEmojiReactions).isFalse()
@@ -362,7 +393,7 @@ class ActionListPresenterTest {
362393
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
363394
)
364395

365-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
396+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
366397
val successState = awaitItem()
367398
assertThat(successState.target).isEqualTo(
368399
ActionListState.Target.Success(
@@ -389,7 +420,7 @@ class ActionListPresenterTest {
389420
isMine = true,
390421
content = aTimelineItemPollContent(),
391422
)
392-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
423+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
393424
val successState = awaitItem()
394425
assertThat(successState.target).isEqualTo(
395426
ActionListState.Target.Success(
@@ -415,7 +446,7 @@ class ActionListPresenterTest {
415446
isMine = true,
416447
content = aTimelineItemPollContent(isEnded = true),
417448
)
418-
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
449+
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
419450
val successState = awaitItem()
420451
assertThat(successState.target).isEqualTo(
421452
ActionListState.Target.Success(

0 commit comments

Comments
 (0)