Skip to content

Commit

Permalink
Merge pull request #3240 from element-hq/feature/valere/message_shields
Browse files Browse the repository at this point in the history
Timeline UI | MessageShield Support
  • Loading branch information
bmarty authored Aug 16, 2024
2 parents 6f0f328 + cbc59e0 commit 21f2c5a
Show file tree
Hide file tree
Showing 48 changed files with 689 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

Expand Down Expand Up @@ -121,6 +122,16 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemPollActionList(),
),
),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent().copy(
reactionsState = reactionsState,
messageShield = MessageShield.UnknownDevice(isCritical = true)
),
displayEmojiReactions = true,
actions = aTimelineItemActionList(),
)
),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.components.MessageShieldView
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
Expand Down Expand Up @@ -181,7 +182,14 @@ private fun SheetContent(
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(14.dp))
if (target.event.messageShield != null) {
MessageShieldView(
shield = target.event.messageShield,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
} else {
Spacer(modifier = Modifier.height(14.dp))
}
HorizontalDivider()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline

import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlin.time.Duration

sealed interface TimelineEvents {
Expand All @@ -27,6 +28,9 @@ sealed interface TimelineEvents {
data object OnFocusEventRender : TimelineEvents
data object JumpToLive : TimelineEvents

data class ShowShieldDialog(val messageShield: MessageShield) : TimelineEvents
data object HideShieldDialog : TimelineEvents

/**
* Events coming from a timeline item.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
Expand Down Expand Up @@ -97,6 +98,7 @@ class TimelinePresenter @AssistedInject constructor(
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }

val newEventState = remember { mutableStateOf(NewEventState.None) }
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }

val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
Expand Down Expand Up @@ -151,6 +153,8 @@ class TimelinePresenter @AssistedInject constructor(
is TimelineEvents.JumpToLive -> {
timelineController.focusOnLive()
}
TimelineEvents.HideShieldDialog -> messageShield.value = null
is TimelineEvents.ShowShieldDialog -> messageShield.value = event.messageShield
}
}

Expand Down Expand Up @@ -226,6 +230,7 @@ class TimelinePresenter @AssistedInject constructor(
newEventState = newEventState.value,
isLive = isLive,
focusRequestState = focusRequestState.value,
messageShield = messageShield.value,
eventSink = { handleEvents(it) }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.NewEventState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration

Expand All @@ -31,6 +32,8 @@ data class TimelineState(
val newEventState: NewEventState,
val isLive: Boolean,
val focusRequestState: FocusRequestState,
// If not null, info will be rendered in a dialog
val messageShield: MessageShield?,
val eventSink: (TimelineEvents) -> Unit,
) {
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady
import kotlinx.collections.immutable.ImmutableList
Expand All @@ -50,6 +51,7 @@ fun aTimelineState(
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
focusedEventIndex: Int = -1,
isLive: Boolean = true,
messageShield: MessageShield? = null,
eventSink: (TimelineEvents) -> Unit = {},
): TimelineState {
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
Expand All @@ -65,6 +67,7 @@ fun aTimelineState(
newEventState = NewEventState.None,
isLive = isLive,
focusRequestState = focusRequestState,
messageShield = messageShield,
eventSink = eventSink,
)
}
Expand Down Expand Up @@ -138,6 +141,7 @@ internal fun aTimelineItemEvent(
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
messageShield: MessageShield? = null,
): TimelineItem.Event {
return TimelineItem.Event(
id = UUID.randomUUID().toString(),
Expand All @@ -161,7 +165,8 @@ internal fun aTimelineItemEvent(
inReplyTo = inReplyTo,
debugInfo = debugInfo,
isThreaded = isThreaded,
origin = null
origin = null,
messageShield = messageShield,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
import io.element.android.features.messages.impl.timeline.components.toText
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.focus.FocusRequestStateView
Expand All @@ -68,12 +69,14 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.typing.TypingNotificationView
import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
import kotlin.math.abs
Expand Down Expand Up @@ -124,6 +127,10 @@ fun TimelineView(
state.eventSink(TimelineEvents.FocusOnEvent(eventId))
}

fun onShieldClick(shield: MessageShield) {
state.eventSink(TimelineEvents.ShowShieldDialog(shield))
}

// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
AnimatedVisibility(visible = true, enter = fadeIn()) {
Box(modifier) {
Expand Down Expand Up @@ -154,6 +161,7 @@ fun TimelineView(
focusedEventId = state.focusedEventId,
onClick = onMessageClick,
onLongClick = onMessageLongClick,
onShieldClick = ::onShieldClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
inReplyToClick = ::inReplyToClick,
Expand Down Expand Up @@ -186,6 +194,17 @@ fun TimelineView(
)
}
}

MessageShieldDialog(state)
}

@Composable
private fun MessageShieldDialog(state: TimelineState) {
val messageShield = state.messageShield ?: return
AlertDialog(
content = messageShield.toText(),
onDismiss = { state.eventSink.invoke(TimelineEvents.HideShieldDialog) },
)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* 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 io.element.android.features.messages.impl.timeline

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import kotlinx.collections.immutable.toImmutableList

@PreviewsDayNight
@Composable
internal fun TimelineViewMessageShieldPreview() = ElementPreview {
val timelineItems = aTimelineItemList(aTimelineItemTextContent())
// For consistency, ensure that there is a message in the timeline (the last one) with an error.
val messageShield = aCriticalShield()
val items = listOf(
(timelineItems.first() as TimelineItem.Event).copy(messageShield = messageShield)
) + timelineItems.drop(1)
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
) {
TimelineView(
state = aTimelineState(
timelineItems = items.toImmutableList(),
messageShield = messageShield,
),
typingNotificationState = aTypingNotificationState(),
onUserDataClick = {},
onLinkClick = {},
onMessageClick = {},
onMessageLongClick = {},
onSwipeToReply = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onReadReceiptClick = {},
onJoinCallClick = {},
forceJumpToBottomVisibility = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal fun ATimelineItemEventRow(
isHighlighted = isHighlighted,
onClick = {},
onLongClick = {},
onShieldClick = {},
onUserDataClick = {},
onLinkClick = {},
inReplyToClick = {},
Expand Down
Loading

0 comments on commit 21f2c5a

Please sign in to comment.