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 b0029fe4ee..388780d384 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 @@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus @@ -90,6 +91,7 @@ class MessagesPresenter @AssistedInject constructor( private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, + private val timelineProtectionPresenter: Presenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, @@ -123,6 +125,7 @@ class MessagesPresenter @AssistedInject constructor( val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() val timelineState = timelinePresenter.present() + val timelineProtectionState = timelineProtectionPresenter.present() val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() @@ -182,6 +185,7 @@ class MessagesPresenter @AssistedInject constructor( composerState = composerState, enableTextFormatting = composerState.showTextFormatting, timelineState = timelineState, + timelineProtectionState = timelineProtectionState, ) } is MessagesEvents.ToggleReaction -> { @@ -213,6 +217,7 @@ class MessagesPresenter @AssistedInject constructor( userEventPermissions = userEventPermissions, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, + timelineProtectionState = timelineProtectionState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, @@ -262,6 +267,7 @@ class MessagesPresenter @AssistedInject constructor( action: TimelineItemAction, targetEvent: TimelineItem.Event, composerState: MessageComposerState, + timelineProtectionState: TimelineProtectionState, enableTextFormatting: Boolean, timelineState: TimelineState, ) = launch { @@ -271,7 +277,7 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting) TimelineItemAction.Reply, - TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState) + TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState) TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) @@ -385,11 +391,18 @@ class MessagesPresenter @AssistedInject constructor( } } - private suspend fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { + private suspend fun handleActionReply( + targetEvent: TimelineItem.Event, + composerState: MessageComposerState, + timelineProtectionState: TimelineProtectionState, + ) { if (targetEvent.eventId == null) return timelineController.invokeOnCurrentTimeline { val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser) - val composerMode = MessageComposerMode.Reply(replyToDetails = replyToDetails) + val composerMode = MessageComposerMode.Reply( + replyToDetails = replyToDetails, + hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId), + ) composerState.eventSink( MessageComposerEvents.SetMode(composerMode) ) 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 2c5bae6d3b..2e03cbdb9d 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 @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -32,6 +33,7 @@ data class MessagesState( val composerState: MessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, + val timelineProtectionState: TimelineProtectionState, val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, 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 2c1a487440..985471c641 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 @@ -26,6 +26,8 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState 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.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState @@ -103,6 +105,7 @@ fun aMessagesState( // Render a focused event for an event with sender information displayed focusedEventIndex = 2, ), + timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(), actionListState: ActionListState = anActionListState(), customReactionState: CustomReactionState = aCustomReactionState(), @@ -121,6 +124,7 @@ fun aMessagesState( userEventPermissions = userEventPermissions, composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, + timelineProtectionState = timelineProtectionState, timelineState = timelineState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, 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 067d0da87b..36bf0bc5fb 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 @@ -379,6 +379,7 @@ private fun MessagesViewContent( val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior() TimelineView( state = state.timelineState, + timelineProtectionState = state.timelineProtectionState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onMessageClick = onMessageClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index a6cbd68cf6..1a5aa1c7d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -14,6 +14,8 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionPresenter +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.libraries.architecture.Presenter @@ -30,4 +32,7 @@ interface MessagesModule { @Binds fun bindTypingNotificationPresenter(presenter: TypingNotificationPresenter): Presenter + + @Binds + fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index aabeaa96a0..8529732b06 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -585,10 +585,18 @@ class MessageComposerPresenter @Inject constructor( content = htmlText ?: markdownText ) is ComposerDraftType.Reply -> { - messageComposerContext.composerMode = MessageComposerMode.Reply(InReplyToDetails.Loading(draftType.eventId)) + messageComposerContext.composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(draftType.eventId), + // I guess it's fine to always render the image when restoring a draft + hideImage = false + ) timelineController.invokeOnCurrentTimeline { val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser) - run { messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails) } + messageComposerContext.composerMode = MessageComposerMode.Reply( + replyToDetails = replyToDetails, + // I guess it's fine to always render the image when restoring a draft + hideImage = false + ) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 1e557f84bd..4ccef0f23d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -60,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private val room: MatrixRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, private val timelineProvider: PinnedEventsTimelineProvider, + private val timelineProtectionPresenter: Presenter, private val snackbarDispatcher: SnackbarDispatcher, actionListPresenterFactory: ActionListPresenter.Factory, private val appCoroutineScope: CoroutineScope, @@ -97,14 +99,13 @@ class PinnedMessagesListPresenter @AssistedInject constructor( ) ) } - + val timelineProtectionState = timelineProtectionPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) var pinnedMessageItems by remember { mutableStateOf>>(AsyncData.Uninitialized) } - PinnedMessagesListEffect( onItemsChange = { newItems -> pinnedMessageItems = newItems @@ -119,6 +120,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( return pinnedMessagesListState( timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, userEventPermissions = userEventPermissions, timelineItems = pinnedMessageItems, eventSink = ::handleEvents @@ -214,6 +216,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( @Composable private fun pinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo, + timelineProtectionState: TimelineProtectionState, userEventPermissions: UserEventPermissions, timelineItems: AsyncData>, eventSink: (PinnedMessagesListEvents) -> Unit @@ -228,6 +231,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( val actionListState = actionListPresenter.present() PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, userEventPermissions = userEventPermissions, timelineItems = timelineItems.data, actionListState = actionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index 82105a2e35..3a15c9f8bc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -26,6 +27,7 @@ sealed interface PinnedMessagesListState { data object Empty : PinnedMessagesListState data class Filled( val timelineRoomInfo: TimelineRoomInfo, + val timelineProtectionState: TimelineProtectionState, val userEventPermissions: UserEventPermissions, val timelineItems: ImmutableList, val actionListState: ActionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt index d394b6efad..bd51652cb4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt @@ -22,6 +22,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -83,12 +85,14 @@ fun anEmptyPinnedMessagesListState() = PinnedMessagesListState.Empty fun aLoadedPinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), + timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), timelineItems: List = emptyList(), actionListState: ActionListState = anActionListState(), aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT, eventSink: (PinnedMessagesListEvents) -> Unit = {} ) = PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, timelineItems = timelineItems.toImmutableList(), actionListState = actionListState, userEventPermissions = aUserEventPermissions, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index ad62ad616a..3c7f76e507 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -32,6 +32,8 @@ import io.element.android.features.messages.impl.timeline.components.event.Timel import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.poll.api.pollcontent.PollTitleView import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton @@ -77,8 +79,8 @@ fun PinnedMessagesListView( onLinkClick = onLinkClick, onErrorDismiss = onBackClick, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), + .padding(padding) + .consumeWindowInsets(padding), ) } ) @@ -208,6 +210,7 @@ private fun PinnedMessagesListLoaded( timelineItem = timelineItem, timelineRoomInfo = state.timelineRoomInfo, renderReadReceipts = false, + timelineProtectionState = state.timelineProtectionState, isLastOutgoingMessage = false, focusedEventId = null, onUserDataClick = onUserDataClick, @@ -225,6 +228,7 @@ private fun PinnedMessagesListLoaded( eventContentView = { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentViewWrapper( event = event, + timelineProtectionState = state.timelineProtectionState, onLinkClick = onLinkClick, modifier = contentModifier, onContentLayoutChange = onContentLayoutChange @@ -238,6 +242,7 @@ private fun PinnedMessagesListLoaded( @Composable private fun TimelineItemEventContentViewWrapper( event: TimelineItem.Event, + timelineProtectionState: TimelineProtectionState, onLinkClick: (String) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, @@ -251,6 +256,8 @@ private fun TimelineItemEventContentViewWrapper( } else { TimelineItemEventContentView( content = event.content, + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = { }, modifier = modifier, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index d83e526694..c55750dac5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -57,7 +57,7 @@ class TimelineController @Inject constructor( return detachedTimeline.map { !it.isPresent } } - suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) { + suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Unit)) { currentTimelineFlow.value.run { block(this) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 819f7a1f3a..b40e24b88a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -243,8 +243,8 @@ class TimelinePresenter @AssistedInject constructor( } } return TimelineState( - timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems, + timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, newEventState = newEventState.value, isLive = isLive, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 0781cc3ece..f0e976e368 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -57,6 +57,8 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState 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 @@ -65,13 +67,13 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter 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 @Composable fun TimelineView( state: TimelineState, + timelineProtectionState: TimelineProtectionState, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onMessageClick: (TimelineItem.Event) -> Unit, @@ -114,10 +116,6 @@ 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) { @@ -137,6 +135,7 @@ fun TimelineView( TimelineItemRow( timelineItem = timelineItem, timelineRoomInfo = state.timelineRoomInfo, + timelineProtectionState = timelineProtectionState, renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()), focusedEventId = state.focusedEventId, @@ -320,6 +319,7 @@ internal fun TimelineViewPreview( ), focusedEventIndex = 0, ), + timelineProtectionState = aTimelineProtectionState(), onUserDataClick = {}, onLinkClick = {}, onMessageClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index 4566bf88bc..806a81b7fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -14,6 +14,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr 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.timeline.protection.aTimelineProtectionState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import kotlinx.collections.immutable.toImmutableList @@ -35,6 +36,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { timelineItems = items.toImmutableList(), messageShield = messageShield, ), + timelineProtectionState = aTimelineProtectionState(), onUserDataClick = {}, onLinkClick = {}, onMessageClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 0db7a8ac52..1fa5e7f9a1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -11,6 +11,8 @@ import androidx.compose.runtime.Composable import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState // For previews @Composable @@ -20,10 +22,12 @@ internal fun ATimelineItemEventRow( renderReadReceipts: Boolean = false, isLastOutgoingMessage: Boolean = false, isHighlighted: Boolean = false, + timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), ) = TimelineItemEventRow( event = event, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, + timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = isHighlighted, onClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 5a5fd6d470..67342203bd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -70,6 +70,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -108,6 +110,7 @@ private val BUBBLE_INCOMING_OFFSET = 16.dp fun TimelineItemEventRow( event: TimelineItem.Event, timelineRoomInfo: TimelineRoomInfo, + timelineProtectionState: TimelineProtectionState, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, isHighlighted: Boolean, @@ -126,6 +129,8 @@ fun TimelineItemEventRow( eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -164,6 +169,7 @@ fun TimelineItemEventRow( } TimelineItemEventRowContent( event = event, + timelineProtectionState = timelineProtectionState, isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, @@ -197,6 +203,7 @@ fun TimelineItemEventRow( } else { TimelineItemEventRowContent( event = event, + timelineProtectionState = timelineProtectionState, isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, @@ -252,6 +259,7 @@ private fun SwipeSensitivity( @Composable private fun TimelineItemEventRowContent( event: TimelineItem.Event, + timelineProtectionState: TimelineProtectionState, isHighlighted: Boolean, timelineRoomInfo: TimelineRoomInfo, interactionSource: MutableInteractionSource, @@ -330,6 +338,7 @@ private fun TimelineItemEventRowContent( ) { MessageEventBubbleContent( event = event, + timelineProtectionState = timelineProtectionState, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, eventSink = eventSink, @@ -411,6 +420,7 @@ private fun MessageSenderInformation( @Composable private fun MessageEventBubbleContent( event: TimelineItem.Event, + timelineProtectionState: TimelineProtectionState, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @@ -560,7 +570,11 @@ private fun MessageEventBubbleContent( .clip(RoundedCornerShape(6.dp)) // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent .clickable(onClick = inReplyToClick) - InReplyToView(inReplyTo, modifier = inReplyToModifier) + InReplyToView( + inReplyTo = inReplyTo, + hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()), + modifier = inReplyToModifier, + ) } if (inReplyToDetails != null) { // Use SubComposeLayout only if necessary as it can have consequences on the performance. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 75f1ccc02c..d280efa62c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -25,6 +25,9 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId @@ -34,6 +37,7 @@ import io.element.android.libraries.matrix.api.core.UserId fun TimelineItemGroupedEventsRow( timelineItem: TimelineItem.GroupedEvents, timelineRoomInfo: TimelineRoomInfo, + timelineProtectionState: TimelineProtectionState, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, focusedEventId: EventId?, @@ -52,6 +56,8 @@ fun TimelineItemGroupedEventsRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -70,6 +76,7 @@ fun TimelineItemGroupedEventsRow( onExpandGroupClick = ::onExpandGroupClick, timelineItem = timelineItem, timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, focusedEventId = focusedEventId, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, @@ -94,6 +101,7 @@ private fun TimelineItemGroupedEventsRowContent( onExpandGroupClick: () -> Unit, timelineItem: TimelineItem.GroupedEvents, timelineRoomInfo: TimelineRoomInfo, + timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, @@ -112,6 +120,8 @@ private fun TimelineItemGroupedEventsRowContent( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -136,6 +146,7 @@ private fun TimelineItemGroupedEventsRowContent( TimelineItemRow( timelineItem = subGroupEvent, timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, @@ -178,6 +189,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi onExpandGroupClick = {}, timelineItem = events, timelineRoomInfo = aTimelineRoomInfo(), + timelineProtectionState = aTimelineProtectionState(), focusedEventId = events.events.first().eventId, renderReadReceipts = true, isLastOutgoingMessage = false, @@ -202,6 +214,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi onExpandGroupClick = {}, timelineItem = aGroupedEvents(withReadReceipts = true), timelineRoomInfo = aTimelineRoomInfo(), + timelineProtectionState = aTimelineProtectionState(), focusedEventId = null, renderReadReceipts = true, isLastOutgoingMessage = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 56f509f9fb..c7c1cb5350 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -26,6 +26,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.mustBeProtected import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.matrix.api.core.EventId @@ -37,6 +40,7 @@ internal fun TimelineItemRow( timelineRoomInfo: TimelineRoomInfo, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, + timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, @@ -55,6 +59,8 @@ internal fun TimelineItemRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, modifier = contentModifier, @@ -109,9 +115,14 @@ internal fun TimelineItemRow( event = timelineItem, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, + timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = timelineItem.isEvent(focusedEventId), - onClick = { onClick(timelineItem) }, + onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) { + {} + } else { + { onClick(timelineItem) } + }, onLongClick = { onLongClick(timelineItem) }, onLinkClick = onLinkClick, onUserDataClick = onUserDataClick, @@ -133,6 +144,7 @@ internal fun TimelineItemRow( TimelineItemGroupedEventsRow( timelineItem = timelineItem, timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index ffb398a0d1..f023a63c35 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -71,6 +71,8 @@ fun TimelineItemStateEventRow( TimelineItemEventContentView( content = event.content, onLinkClick = {}, + hideMediaContent = false, + onShowClick = {}, eventSink = eventSink, modifier = Modifier.defaultTimelineContentPadding() ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index 48072ce5bf..76777f4ae1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -25,12 +25,14 @@ fun TimelineItemAspectRatioBox( aspectRatio: Float?, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, + minHeight: Int = MIN_HEIGHT_IN_DP, + maxHeight: Int = MAX_HEIGHT_IN_DP, content: @Composable (BoxScope.() -> Unit), ) { val safeAspectRatio = aspectRatio ?: DEFAULT_ASPECT_RATIO Box( modifier = modifier - .heightIn(min = MIN_HEIGHT_IN_DP.dp, max = MAX_HEIGHT_IN_DP.dp) + .heightIn(min = minHeight.dp, max = maxHeight.dp) .aspectRatio(safeAspectRatio, false), contentAlignment = contentAlignment, content = content diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index 49b8731ae0..b24a5ca0be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt @@ -35,6 +35,8 @@ import io.element.android.libraries.architecture.Presenter @Composable fun TimelineItemEventContentView( content: TimelineItemEventContent, + hideMediaContent: Boolean, + onShowClick: () -> Unit, onLinkClick: (url: String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, @@ -69,15 +71,21 @@ fun TimelineItemEventContentView( ) is TimelineItemImageContent -> TimelineItemImageView( content = content, + hideMediaContent = hideMediaContent, + onShowClick = onShowClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) is TimelineItemStickerContent -> TimelineItemStickerView( content = content, + hideMediaContent = hideMediaContent, + onShowClick = onShowClick, modifier = modifier, ) is TimelineItemVideoContent -> TimelineItemVideoView( content = content, + hideMediaContent = hideMediaContent, + onShowClick = onShowClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 9f9222f458..85b2b7f678 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -58,6 +59,8 @@ import io.element.android.wysiwyg.compose.EditorStyledText @Composable fun TimelineItemImageView( content: TimelineItemImageContent, + hideMediaContent: Boolean, + onShowClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { @@ -76,23 +79,28 @@ fun TimelineItemImageView( modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f), aspectRatio = content.aspectRatio, ) { - var isLoaded by remember { mutableStateOf(false) } - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .then(if (isLoaded) Modifier.background(Color.White) else Modifier), - model = MediaRequestData( - source = content.preferredMediaSource, - kind = MediaRequestData.Kind.File( - body = content.filename ?: content.body, - mimeType = content.mimeType, + ProtectedView( + hideContent = hideMediaContent, + onShowClick = onShowClick, + ) { + var isLoaded by remember { mutableStateOf(false) } + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + model = MediaRequestData( + source = content.preferredMediaSource, + kind = MediaRequestData.Kind.File( + body = content.filename ?: content.body, + mimeType = content.mimeType, + ), ), - ), - contentScale = ContentScale.Fit, - alignment = Alignment.Center, - contentDescription = description, - onState = { isLoaded = it is AsyncImagePainter.State.Success }, - ) + contentScale = ContentScale.Fit, + alignment = Alignment.Center, + contentDescription = description, + onState = { isLoaded = it is AsyncImagePainter.State.Success }, + ) + } } if (content.showCaption) { @@ -123,7 +131,23 @@ fun TimelineItemImageView( @PreviewsDayNight @Composable internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview { - TimelineItemImageView(content, {}) + TimelineItemImageView( + content = content, + hideMediaContent = false, + onShowClick = {}, + onContentLayoutChange = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview { + TimelineItemImageView( + content = aTimelineItemImageContent(), + hideMediaContent = true, + onShowClick = {}, + onContentLayoutChange = {}, + ) } @PreviewsDayNight diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index f895eddc85..cef5acd1dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -7,44 +7,84 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider -import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage +import io.element.android.features.messages.impl.timeline.protection.ProtectedView +import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.ui.strings.CommonStrings private const val STICKER_SIZE_IN_DP = 128 @Composable fun TimelineItemStickerView( content: TimelineItemStickerContent, + hideMediaContent: Boolean, + onShowClick: () -> Unit, modifier: Modifier = Modifier, ) { - val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO - Box( - modifier = modifier - .heightIn(min = STICKER_SIZE_IN_DP.dp, max = STICKER_SIZE_IN_DP.dp) - .aspectRatio(aspectRatio, false), - contentAlignment = Alignment.TopStart, + val description = content.body.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image) + Column( + modifier = modifier.semantics { contentDescription = description }, ) { - BlurHashAsyncImage( - model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), - blurHash = content.blurhash, - ) + TimelineItemAspectRatioBox( + modifier = Modifier.blurHashBackground(content.blurhash, alpha = 0.9f), + aspectRatio = content.aspectRatio, + minHeight = STICKER_SIZE_IN_DP, + maxHeight = STICKER_SIZE_IN_DP, + ) { + ProtectedView( + hideContent = hideMediaContent, + onShowClick = onShowClick, + ) { + var isLoaded by remember { mutableStateOf(false) } + AsyncImage( + modifier = Modifier + .fillMaxSize() + .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + model = MediaRequestData( + source = content.preferredMediaSource, + kind = MediaRequestData.Kind.File( + body = content.body, + mimeType = content.mimeType, + ), + ), + contentScale = ContentScale.Fit, + alignment = Alignment.Center, + contentDescription = description, + onState = { isLoaded = it is AsyncImagePainter.State.Success }, + ) + } + } } } @PreviewsDayNight @Composable internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemStickerContentProvider::class) content: TimelineItemStickerContent) = ElementPreview { - TimelineItemStickerView(content) + TimelineItemStickerView( + content = content, + hideMediaContent = false, + onShowClick = {}, + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 7324d3368b..7815e02610 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -51,6 +51,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.preview.ElementPreview @@ -64,6 +65,8 @@ import io.element.android.wysiwyg.compose.EditorStyledText @Composable fun TimelineItemVideoView( content: TimelineItemVideoContent, + hideMediaContent: Boolean, + onShowClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { @@ -83,33 +86,38 @@ fun TimelineItemVideoView( aspectRatio = content.aspectRatio, contentAlignment = Alignment.Center, ) { - var isLoaded by remember { mutableStateOf(false) } - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .then(if (isLoaded) Modifier.background(Color.White) else Modifier), - model = MediaRequestData( - source = content.thumbnailSource, - kind = MediaRequestData.Kind.File( - body = content.filename ?: content.body, - mimeType = content.mimeType - ) - ), - contentScale = ContentScale.Fit, - alignment = Alignment.Center, - contentDescription = description, - onState = { isLoaded = it is AsyncImagePainter.State.Success }, - ) - - Box( - modifier = Modifier.roundedBackground(), - contentAlignment = Alignment.Center, + ProtectedView( + hideContent = hideMediaContent, + onShowClick = onShowClick, ) { - Image( - Icons.Default.PlayArrow, - contentDescription = stringResource(id = CommonStrings.a11y_play), - colorFilter = ColorFilter.tint(Color.White), + var isLoaded by remember { mutableStateOf(false) } + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .then(if (isLoaded) Modifier.background(Color.White) else Modifier), + model = MediaRequestData( + source = content.thumbnailSource, + kind = MediaRequestData.Kind.File( + body = content.filename ?: content.body, + mimeType = content.mimeType + ) + ), + contentScale = ContentScale.Fit, + alignment = Alignment.Center, + contentDescription = description, + onState = { isLoaded = it is AsyncImagePainter.State.Success }, ) + + Box( + modifier = Modifier.roundedBackground(), + contentAlignment = Alignment.Center, + ) { + Image( + Icons.Default.PlayArrow, + contentDescription = stringResource(id = CommonStrings.a11y_play), + colorFilter = ColorFilter.tint(Color.White), + ) + } } } @@ -141,7 +149,23 @@ fun TimelineItemVideoView( @PreviewsDayNight @Composable internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview { - TimelineItemVideoView(content, {}) + TimelineItemVideoView( + content = content, + hideMediaContent = false, + onShowClick = {}, + onContentLayoutChange = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview { + TimelineItemVideoView( + content = aTimelineItemVideoContent(), + hideMediaContent = true, + onShowClick = {}, + onContentLayoutChange = {}, + ) } @PreviewsDayNight diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 0d90ec9d09..268cda1829 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -16,22 +16,26 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider get() = sequenceOf( aTimelineItemImageContent(), - aTimelineItemImageContent().copy(aspectRatio = 1.0f), - aTimelineItemImageContent().copy(aspectRatio = 1.5f), + aTimelineItemImageContent(aspectRatio = 1.0f), + aTimelineItemImageContent(aspectRatio = 1.5f), + aTimelineItemImageContent(blurhash = null), ) } -fun aTimelineItemImageContent() = TimelineItemImageContent( +fun aTimelineItemImageContent( + aspectRatio: Float = 0.5f, + blurhash: String? = A_BLUR_HASH, +) = TimelineItemImageContent( body = "a body", formatted = null, filename = null, mediaSource = MediaSource(""), thumbnailSource = null, mimeType = MimeTypes.IMAGE_JPEG, - blurhash = A_BLUR_HASH, + blurhash = blurhash, width = null, height = 300, - aspectRatio = 0.5f, + aspectRatio = aspectRatio, formattedFileSize = "4MB", fileExtension = "jpg" ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt index adff977e32..bf231bd7fa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt @@ -16,20 +16,24 @@ open class TimelineItemStickerContentProvider : PreviewParameterProvider get() = sequenceOf( aTimelineItemStickerContent(), - aTimelineItemStickerContent().copy(aspectRatio = 1.0f), - aTimelineItemStickerContent().copy(aspectRatio = 1.5f), + aTimelineItemStickerContent(aspectRatio = 1.0f), + aTimelineItemStickerContent(aspectRatio = 1.5f), + aTimelineItemStickerContent(blurhash = null), ) } -fun aTimelineItemStickerContent() = TimelineItemStickerContent( +fun aTimelineItemStickerContent( + aspectRatio: Float = 0.5f, + blurhash: String? = A_BLUR_HASH, +) = TimelineItemStickerContent( body = "a body", mediaSource = MediaSource(""), thumbnailSource = null, mimeType = MimeTypes.IMAGE_JPEG, - blurhash = A_BLUR_HASH, + blurhash = blurhash, width = null, height = 128, - aspectRatio = 0.5f, + aspectRatio = aspectRatio, formattedFileSize = "4MB", fileExtension = "jpg" ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt index 9d494e95e9..510de5a100 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt @@ -17,18 +17,22 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider get() = sequenceOf( aTimelineItemVideoContent(), - aTimelineItemVideoContent().copy(aspectRatio = 1.0f), - aTimelineItemVideoContent().copy(aspectRatio = 1.5f), + aTimelineItemVideoContent(aspectRatio = 1.0f), + aTimelineItemVideoContent(aspectRatio = 1.5f), + aTimelineItemVideoContent(blurhash = null), ) } -fun aTimelineItemVideoContent() = TimelineItemVideoContent( +fun aTimelineItemVideoContent( + aspectRatio: Float = 0.5f, + blurhash: String? = A_BLUR_HASH, +) = TimelineItemVideoContent( body = "Video.mp4", formatted = null, filename = null, thumbnailSource = null, - blurHash = A_BLUR_HASH, - aspectRatio = 0.5f, + blurHash = blurhash, + aspectRatio = aspectRatio, duration = 100.milliseconds, videoSource = MediaSource(""), height = 300, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt new file mode 100644 index 0000000000..6a9db31682 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH +import io.element.android.libraries.ui.strings.CommonStrings + +@SuppressWarnings("ModifierClickableOrder") +@Composable +fun ProtectedView( + hideContent: Boolean, + onShowClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + if (hideContent) { + Box( + modifier = modifier + .fillMaxSize() + .background(Color(0x99000000)), + contentAlignment = Alignment.Center, + ) { + ElementTheme(darkTheme = false) { + // Not using a button to be able to have correct size + Text( + modifier = Modifier + .clip(RoundedCornerShape(percent = 50)) + .clickable( + onClick = onShowClick, + role = Role.Button, + ) + .padding(4.dp) + .border( + width = 1.dp, + color = ElementTheme.colors.borderInteractiveSecondary, + shape = RoundedCornerShape(percent = 50), + ) + .padding( + horizontal = 16.dp, + vertical = 4.dp, + ), + text = stringResource(CommonStrings.action_show), + color = ElementTheme.colors.textOnSolidPrimary, + style = ElementTheme.typography.fontBodyLgMedium, + ) + } + } + } else { + content() + } +} + +@PreviewsDayNight +@Composable +internal fun ProtectedViewPreview() = ElementPreview { + Box( + modifier = Modifier + .size(160.dp) + .blurHashBackground(A_BLUR_HASH) + ) { + ProtectedView( + hideContent = true, + onShowClick = {}, + content = {}, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt new file mode 100644 index 0000000000..824f3843ea --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +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 +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent + +/** + * Return true if the event must be hidden by default when the setting to hide images and videos is enabled. + */ +fun TimelineItem.mustBeProtected(): Boolean { + return when (this) { + is TimelineItem.Event -> when (content) { + is TimelineItemImageContent, + is TimelineItemVideoContent, + is TimelineItemStickerContent -> true + is TimelineItemAudioContent, + is TimelineItemCallNotifyContent, + is TimelineItemEncryptedContent, + is TimelineItemFileContent, + TimelineItemLegacyCallInviteContent, + is TimelineItemLocationContent, + is TimelineItemPollContent, + TimelineItemRedactedContent, + is TimelineItemProfileChangeContent, + is TimelineItemRoomMembershipContent, + is TimelineItemStateEventContent, + is TimelineItemEmoteContent, + is TimelineItemNoticeContent, + is TimelineItemTextContent, + TimelineItemUnknownContent, + is TimelineItemVoiceContent -> false + } + is TimelineItem.Virtual -> false + is TimelineItem.GroupedEvents -> false + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt new file mode 100644 index 0000000000..78c878c3a4 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import io.element.android.libraries.matrix.api.core.EventId + +sealed interface TimelineProtectionEvent { + data class ShowContent(val eventId: EventId?) : TimelineProtectionEvent +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt new file mode 100644 index 0000000000..5d83347e67 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.collections.immutable.toImmutableSet +import javax.inject.Inject + +class TimelineProtectionPresenter @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, +) : Presenter { + @Composable + override fun present(): TimelineProtectionState { + val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) + var allowedEvents by remember { mutableStateOf>(setOf()) } + val protectionState by remember(hideMediaContent) { + derivedStateOf { + if (hideMediaContent) { + ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet()) + } else { + ProtectionState.RenderAll + } + } + } + + fun handleEvent(event: TimelineProtectionEvent) { + when (event) { + is TimelineProtectionEvent.ShowContent -> { + allowedEvents = allowedEvents + setOfNotNull(event.eventId) + } + } + } + + return TimelineProtectionState( + protectionState = protectionState, + eventSink = { event -> handleEvent(event) } + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt new file mode 100644 index 0000000000..af1f26127c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.collections.immutable.ImmutableSet + +data class TimelineProtectionState( + val protectionState: ProtectionState, + val eventSink: (TimelineProtectionEvent) -> Unit, +) { + fun hideMediaContent(eventId: EventId?) = when (protectionState) { + is ProtectionState.RenderAll -> false + is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds + } +} + +@Immutable +sealed interface ProtectionState { + data object RenderAll : ProtectionState + data class RenderOnly(val eventIds: ImmutableSet) : ProtectionState +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateProvider.kt new file mode 100644 index 0000000000..f0c2acbaa7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateProvider.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +fun aTimelineProtectionState( + protectionState: ProtectionState = ProtectionState.RenderAll, + eventSink: (TimelineProtectionEvent) -> Unit = {}, +) = TimelineProtectionState( + protectionState = protectionState, + eventSink = eventSink, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 79792f4db1..209d719b82 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer @@ -1059,12 +1060,12 @@ class MessagesPresenterTest { val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) - return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, + timelineProtectionPresenter = { aTimelineProtectionState() }, actionListPresenterFactory = FakeActionListPresenter.Factory, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 6ec4cdab6a..df0cc1bdc2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1521,4 +1521,7 @@ fun anEditMode( transactionId: TransactionId? = null, ) = MessageComposerMode.Edit(eventId, transactionId, message) -fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID)) +fun aReplyMode() = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index b7c3a09c5c..b8715568d1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -14,6 +14,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -309,6 +310,7 @@ class PinnedMessagesListPresenterTest { room = room, timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), timelineProvider = timelineProvider, + timelineProtectionPresenter = { aTimelineProtectionState() }, snackbarDispatcher = SnackbarDispatcher(), actionListPresenterFactory = FakeActionListPresenter.Factory, analyticsService = analyticsService, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 88f4edf003..315abd8302 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -17,6 +17,8 @@ import io.element.android.features.messages.impl.timeline.components.aCriticalSh import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel +import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.Timeline @@ -137,6 +139,7 @@ class TimelineViewTest { private fun AndroidComposeTestRule.setTimelineView( state: TimelineState, + timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), @@ -152,6 +155,7 @@ private fun AndroidComposeTestRule.setTimel setSafeContent { TimelineView( state = state, + timelineProtectionState = timelineProtectionState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onMessageClick = onMessageClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt new file mode 100644 index 0000000000..5d82b292bf --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.lambda.lambdaError +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ProtectedViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `when hideContent is false, the content is rendered`() { + rule.setProtectedView( + hideContent = false, + content = { + Text("Hello") + } + ) + rule.onNodeWithText("Hello").assertExists() + } + + @Test + fun `when hideContent is true, the content is not rendered, and user can reveal it`() { + ensureCalledOnce { + rule.setProtectedView( + hideContent = true, + onShowClick = it, + content = { + Text("Hello") + } + ) + rule.onNodeWithText("Hello").assertDoesNotExist() + rule.clickOn(CommonStrings.action_show) + } + } +} + +private fun AndroidComposeTestRule.setProtectedView( + hideContent: Boolean = false, + onShowClick: () -> Unit = { lambdaError() }, + content: @Composable () -> Unit = {}, +) { + setContent { + ProtectedView( + hideContent = hideContent, + onShowClick = onShowClick, + content = content + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt new file mode 100644 index 0000000000..97b97ac3d5 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class TimelineProtectionPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll) + } + } + + @Test + fun `present - protected`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true) + val presenter = createPresenter(appPreferencesStore) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf())) + // ShowContent with null should have no effect. + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null)) + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID))) + } + } + + private fun createPresenter( + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + ) = TimelineProtectionPresenter( + appPreferencesStore = appPreferencesStore, + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt new file mode 100644 index 0000000000..81187eb6d4 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.protection + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import kotlinx.collections.immutable.persistentSetOf +import org.junit.Test + +class TimelineProtectionStateTest { + @Test + fun `when protectionState is RenderAll, hideMediaContent always return null`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderAll + ) + assertThat(sut.hideMediaContent(null)).isFalse() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse() + } + + @Test + fun `when protectionState is RenderOnly with empty set, hideMediaContent always return true`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderOnly(persistentSetOf()) + ) + assertThat(sut.hideMediaContent(null)).isTrue() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isTrue() + } + + @Test + fun `when protectionState is RenderOnly with an Event, hideMediaContent can return true or false`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)) + ) + assertThat(sut.hideMediaContent(null)).isTrue() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse() + assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 70117c6162..b13e2a4fb1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -17,13 +17,12 @@ import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer +import io.element.android.features.messages.impl.messagecomposer.aReplyMode import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.core.ProgressCallback -import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -708,8 +707,6 @@ class VoiceMessageComposerPresenterTest { ) } -private fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID)) - private fun aVoiceMessageComposerEvent( isReply: Boolean = false ) = Composer( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index 77602c4e12..6d8572a516 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -13,5 +13,6 @@ sealed interface DeveloperSettingsEvents { data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents data class SetSimplifiedSlidingSyncEnabled(val isEnabled: Boolean) : DeveloperSettingsEvents + data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index af7c05f592..a113a80a1f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -71,6 +71,9 @@ class DeveloperSettingsPresenter @Inject constructor( val isSimplifiedSlidingSyncEnabled by appPreferencesStore .isSimplifiedSlidingSyncEnabledFlow() .collectAsState(initial = false) + val hideImagesAndVideos by appPreferencesStore + .doesHideImagesAndVideosFlow() + .collectAsState(initial = false) LaunchedEffect(Unit) { FeatureFlags.entries @@ -114,6 +117,9 @@ class DeveloperSettingsPresenter @Inject constructor( appPreferencesStore.setSimplifiedSlidingSyncEnabled(event.isEnabled) logoutUseCase.logout(ignoreSdkError = true) } + is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch { + appPreferencesStore.setHideImagesAndVideos(event.value) + } } } @@ -128,6 +134,7 @@ class DeveloperSettingsPresenter @Inject constructor( validator = ::customElementCallUrlValidator, ), isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, + hideImagesAndVideos = hideImagesAndVideos, eventSink = ::handleEvents ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index f4b599a504..e4c8641197 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -19,6 +19,7 @@ data class DeveloperSettingsState( val clearCacheAction: AsyncData, val customElementCallBaseUrlState: CustomElementCallBaseUrlState, val isSimpleSlidingSyncEnabled: Boolean, + val hideImagesAndVideos: Boolean, val eventSink: (DeveloperSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index 34918c6ea4..601ed2ee7a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -31,6 +31,7 @@ fun aDeveloperSettingsState( clearCacheAction: AsyncData = AsyncData.Uninitialized, customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), isSimplifiedSlidingSyncEnabled: Boolean = false, + hideImagesAndVideos: Boolean = false, eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( features = aFeatureUiModelList(), @@ -39,6 +40,7 @@ fun aDeveloperSettingsState( clearCacheAction = clearCacheAction, customElementCallBaseUrlState = customElementCallBaseUrlState, isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, + hideImagesAndVideos = hideImagesAndVideos, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index f2e427e3be..dc06036c43 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -40,9 +40,10 @@ fun DeveloperSettingsView( title = stringResource(id = CommonStrings.common_developer_options) ) { // Note: this is OK to hardcode strings in this debug screen. + SettingsCategory(state) PreferenceCategory( title = "Feature flags", - showTopDivider = false, + showTopDivider = true, ) { FeatureListContent(state) } @@ -92,6 +93,22 @@ fun DeveloperSettingsView( } } +@Composable +private fun SettingsCategory( + state: DeveloperSettingsState, +) { + PreferenceCategory(title = "Preferences", showTopDivider = false) { + PreferenceSwitch( + title = "Hide image & video previews", + subtitle = "When toggled image & video will not render in the timeline by default.", + isChecked = state.hideImagesAndVideos, + onCheckedChange = { + state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(it)) + } + ) + } +} + @Composable private fun ElementCallCategory( state: DeveloperSettingsState, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 0b1ce2cfdb..128f4aa705 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -50,6 +50,7 @@ class DeveloperSettingsPresenterTest { assertThat(initialState.customElementCallBaseUrlState).isNotNull() assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull() assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse() + assertThat(initialState.hideImagesAndVideos).isFalse() val loadedState = awaitItem() assertThat(loadedState.rageshakeState.isEnabled).isFalse() assertThat(loadedState.rageshakeState.isSupported).isTrue() @@ -179,6 +180,24 @@ class DeveloperSettingsPresenterTest { } } + @Test + fun `present - toggling hide image and video`() = runTest { + val preferences = InMemoryAppPreferencesStore() + val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.hideImagesAndVideos).isFalse() + initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) + assertThat(awaitItem().hideImagesAndVideos).isTrue() + assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue() + initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false)) + assertThat(awaitItem().hideImagesAndVideos).isFalse() + assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse() + } + } + private fun createDeveloperSettingsPresenter( featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index 82e8df6993..f6319007d4 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -45,6 +45,7 @@ class DeveloperSettingsViewTest { } } + @Config(qualifiers = "h1500dp") @Test fun `clicking on element call url open the dialogs and submit emits the expected event`() { val eventsRecorder = EventsRecorder() @@ -113,6 +114,18 @@ class DeveloperSettingsViewTest { rule.onNodeWithText("Enable Simplified Sliding Sync").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true)) } + + @Test + fun `clicking on the hide images and videos switch emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeveloperSettingsView( + state = aDeveloperSettingsState( + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Hide image & video previews").performClick() + eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) + } } private fun AndroidComposeTestRule.setDeveloperSettingsView( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt index bea0911544..0225a44750 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt @@ -145,7 +145,7 @@ data class AttachmentThumbnailInfo( @Composable internal fun AttachmentThumbnailPreview(@PreviewParameter(AttachmentThumbnailInfoProvider::class) data: AttachmentThumbnailInfo) = ElementPreview { AttachmentThumbnail( - data, + info = data, modifier = Modifier .size(36.dp) .clip(RoundedCornerShape(4.dp)) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index b7ba762ce6..1da14d3839 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -57,11 +57,11 @@ internal sealed interface InReplyToMetadata { * Metadata can be either a thumbnail with a text OR just a text. */ @Composable -internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (eventContent) { +internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetadata? = when (eventContent) { is MessageContent -> when (val type = eventContent.type) { is ImageMessageType -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource ?: type.source, + thumbnailSource = (type.info?.thumbnailSource ?: type.source).takeUnless { hideImage }, textContent = eventContent.body, type = AttachmentThumbnailType.Image, blurHash = type.info?.blurhash, @@ -69,7 +69,7 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event ) is VideoMessageType -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource, + thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage }, textContent = eventContent.body, type = AttachmentThumbnailType.Video, blurHash = type.info?.blurhash, @@ -77,7 +77,7 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event ) is FileMessageType -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource, + thumbnailSource = type.info?.thumbnailSource?.takeUnless { hideImage }, textContent = eventContent.body, type = AttachmentThumbnailType.File, ) @@ -104,7 +104,7 @@ internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (event } is StickerContent -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( - thumbnailSource = eventContent.source, + thumbnailSource = eventContent.source.takeUnless { hideImage }, textContent = eventContent.body, type = AttachmentThumbnailType.Image, blurHash = eventContent.info.blurhash, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt index 6fe11ec254..70a30c809a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun InReplyToView( inReplyTo: InReplyToDetails, + hideImage: Boolean, modifier: Modifier = Modifier, ) { when (inReplyTo) { @@ -55,7 +56,7 @@ fun InReplyToView( ReplyToReadyContent( senderId = inReplyTo.senderId, senderProfile = inReplyTo.senderProfile, - metadata = inReplyTo.metadata(), + metadata = inReplyTo.metadata(hideImage), modifier = modifier ) } @@ -191,5 +192,8 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) { @PreviewsDayNight @Composable internal fun InReplyToViewPreview(@PreviewParameter(provider = InReplyToDetailsProvider::class) inReplyTo: InReplyToDetails) = ElementPreview { - InReplyToView(inReplyTo) + InReplyToView( + inReplyTo = inReplyTo, + hideImage = false, + ) } diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 26e7df9631..9e4318b3be 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -61,7 +61,7 @@ class InReplyToMetadataKtTest { @Test fun `any message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - anInReplyToDetailsReady(eventContent = aMessageContent()).metadata() + anInReplyToDetailsReady(eventContent = aMessageContent()).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo(InReplyToMetadata.Text("textContent")) @@ -82,7 +82,7 @@ class InReplyToMetadataKtTest { info = anImageInfo(), ) ) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -99,6 +99,36 @@ class InReplyToMetadataKtTest { } } + @Test + fun `an image message content, no thumbnail`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = ImageMessageType( + body = "body", + formatted = null, + filename = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ) + ).metadata(hideImage = true) + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "body", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + @Test fun `a sticker message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { @@ -108,7 +138,7 @@ class InReplyToMetadataKtTest { info = anImageInfo(), source = aMediaSource(url = "url") ) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -125,6 +155,32 @@ class InReplyToMetadataKtTest { } } + @Test + fun `a sticker message content, no thumbnail`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetailsReady( + eventContent = StickerContent( + body = "body", + info = anImageInfo(), + source = aMediaSource(url = "url") + ) + ).metadata(hideImage = true) + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "body", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + @Test fun `a video message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { @@ -138,7 +194,7 @@ class InReplyToMetadataKtTest { info = aVideoInfo(), ) ) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -155,6 +211,36 @@ class InReplyToMetadataKtTest { } } + @Test + fun `a video message content, no thumbnail`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = VideoMessageType( + body = "body", + formatted = null, + filename = null, + source = aMediaSource(), + info = aVideoInfo(), + ) + ) + ).metadata(hideImage = true) + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "body", + type = AttachmentThumbnailType.Video, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + @Test fun `a file message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { @@ -171,7 +257,7 @@ class InReplyToMetadataKtTest { ), ) ) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -188,6 +274,39 @@ class InReplyToMetadataKtTest { } } + @Test + fun `a file message content, no thumbnail`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = FileMessageType( + body = "body", + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = aMediaSource(), + ), + ) + ) + ).metadata(hideImage = true) + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "body", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + @Test fun `a audio message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { @@ -203,7 +322,7 @@ class InReplyToMetadataKtTest { ), ) ) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -231,7 +350,7 @@ class InReplyToMetadataKtTest { description = null, ) ) - ).metadata() + ).metadata(hideImage = false) } }.test { awaitItem().let { @@ -262,7 +381,7 @@ class InReplyToMetadataKtTest { details = null, ) ) - ).metadata() + ).metadata(hideImage = false) } }.test { awaitItem().let { @@ -285,7 +404,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = aPollContent() - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo( @@ -307,7 +426,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = RedactedContent - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo(InReplyToMetadata.Redacted) @@ -320,7 +439,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = UnableToDecryptContent(UnableToDecryptContent.Data.Unknown) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isEqualTo(InReplyToMetadata.UnableToDecrypt) @@ -333,7 +452,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = FailedToParseMessageLikeContent("", "") - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -346,7 +465,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = FailedToParseStateContent("", "", "") - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -359,7 +478,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = ProfileChangeContent("", "", "", "") - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -372,7 +491,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = RoomMembershipContent(A_USER_ID, null, null) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -385,7 +504,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = StateContent("", OtherState.RoomJoinRules) - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -398,7 +517,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = UnknownContent - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() @@ -411,7 +530,7 @@ class InReplyToMetadataKtTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( eventContent = null - ).metadata() + ).metadata(hideImage = false) }.test { awaitItem().let { assertThat(it).isNull() diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index f05b56614a..ecdcca780b 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -22,5 +22,8 @@ interface AppPreferencesStore { suspend fun setSimplifiedSlidingSyncEnabled(enabled: Boolean) fun isSimplifiedSlidingSyncEnabledFlow(): Flow + suspend fun setHideImagesAndVideos(value: Boolean) + fun doesHideImagesAndVideosFlow(): Flow + suspend fun reset() } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index ab985e798d..d9cbc6cc03 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -30,6 +30,7 @@ private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") private val themeKey = stringPreferencesKey("theme") private val simplifiedSlidingSyncKey = booleanPreferencesKey("useSimplifiedSlidingSync") +private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos") @ContributesBinding(AppScope::class) class DefaultAppPreferencesStore @Inject constructor( @@ -91,6 +92,18 @@ class DefaultAppPreferencesStore @Inject constructor( } } + override suspend fun setHideImagesAndVideos(value: Boolean) { + store.edit { prefs -> + prefs[hideImagesAndVideosKey] = value + } + } + + override fun doesHideImagesAndVideosFlow(): Flow { + return store.data.map { prefs -> + prefs[hideImagesAndVideosKey] ?: false + } + } + override suspend fun reset() { store.edit { it.clear() } } diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt index c4591c9355..36ad0f9363 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt @@ -13,11 +13,13 @@ import kotlinx.coroutines.flow.MutableStateFlow class InMemoryAppPreferencesStore( isDeveloperModeEnabled: Boolean = false, + hideImagesAndVideos: Boolean = false, customElementCallBaseUrl: String? = null, theme: String? = null, simplifiedSlidingSyncEnabled: Boolean = false ) : AppPreferencesStore { private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) + private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val theme = MutableStateFlow(theme) private val simplifiedSlidingSyncEnabled = MutableStateFlow(simplifiedSlidingSyncEnabled) @@ -54,6 +56,14 @@ class InMemoryAppPreferencesStore( return simplifiedSlidingSyncEnabled } + override suspend fun setHideImagesAndVideos(value: Boolean) { + hideImagesAndVideos.value = value + } + + override fun doesHideImagesAndVideosFlow(): Flow { + return hideImagesAndVideos + } + override suspend fun reset() { // No op } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 283b28ab31..08e95fcfcf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.ui.messages.toPlainText +import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -45,6 +46,7 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.flow.first import timber.log.Timber import javax.inject.Inject @@ -69,6 +71,7 @@ class DefaultNotifiableEventResolver @Inject constructor( @ApplicationContext private val context: Context, private val permalinkParser: PermalinkParser, private val callNotificationEventResolver: CallNotificationEventResolver, + private val appPreferencesStore: AppPreferencesStore, ) : NotifiableEventResolver { override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? { // Restore session @@ -103,7 +106,7 @@ class DefaultNotifiableEventResolver @Inject constructor( timestamp = this.timestamp, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, body = messageBody, - imageUriString = fetchImageIfPresent(client)?.toString(), + imageUriString = content.fetchImageIfPresent(client)?.toString(), roomName = roomDisplayName, roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, @@ -148,7 +151,6 @@ class DefaultNotifiableEventResolver @Inject constructor( timestamp = this.timestamp, senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), body = stringProvider.getString(CommonStrings.common_call_invite), - imageUriString = fetchImageIfPresent(client)?.toString(), roomName = roomDisplayName, roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, @@ -288,22 +290,21 @@ class DefaultNotifiableEventResolver @Inject constructor( } } - private suspend fun NotificationData.fetchImageIfPresent(client: MatrixClient): Uri? { - val fileResult = when (val content = this.content) { - is NotificationContent.MessageLike.RoomMessage -> { - when (val messageType = content.messageType) { - is ImageMessageType -> notificationMediaRepoFactory.create(client) - .getMediaFile( - mediaSource = messageType.source, - mimeType = messageType.info?.mimetype, - body = messageType.body, - ) - is VideoMessageType -> null // Use the thumbnail here? - else -> null - } - } + private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? { + if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) { + return null + } + val fileResult = when (val messageType = messageType) { + is ImageMessageType -> notificationMediaRepoFactory.create(client) + .getMediaFile( + mediaSource = messageType.source, + mimeType = messageType.info?.mimetype, + body = messageType.body, + ) + is VideoMessageType -> null // Use the thumbnail here? else -> null - } ?: return null + } + ?: return null return fileResult .onFailure { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index de74d1d7a9..aae4d84d38 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -42,6 +42,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent @@ -798,6 +800,7 @@ class DefaultNotifiableEventResolverTest { private fun createDefaultNotifiableEventResolver( notificationService: FakeNotificationService? = FakeNotificationService(), notificationResult: Result = Result.success(null), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), ): DefaultNotifiableEventResolver { val context = RuntimeEnvironment.getApplication() as Context notificationService?.givenGetNotificationResult(notificationResult) @@ -821,6 +824,7 @@ class DefaultNotifiableEventResolverTest { callNotificationEventResolver = DefaultCallNotificationEventResolver( stringProvider = AndroidStringProvider(context.resources) ), + appPreferencesStore = appPreferencesStore, ) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index a8d22330d3..f9988adfba 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -48,6 +48,7 @@ internal fun ComposerModeView( ReplyToModeView( modifier = Modifier.padding(8.dp), replyToDetails = composerMode.replyToDetails, + hideImage = composerMode.hideImage, onResetComposerMode = onResetComposerMode, ) } @@ -103,6 +104,7 @@ private fun EditingModeView( @Composable private fun ReplyToModeView( replyToDetails: InReplyToDetails, + hideImage: Boolean, onResetComposerMode: () -> Unit, modifier: Modifier = Modifier, ) { @@ -112,7 +114,11 @@ private fun ReplyToModeView( .background(MaterialTheme.colorScheme.surface) .padding(4.dp) ) { - InReplyToView(inReplyTo = replyToDetails, modifier = Modifier.weight(1f)) + InReplyToView( + inReplyTo = replyToDetails, + hideImage = hideImage, + modifier = Modifier.weight(1f), + ) Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_close), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index a71461e338..1f2e041ac5 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -118,8 +118,8 @@ fun TextComposer( } val layoutModifier = modifier - .fillMaxSize() - .height(IntrinsicSize.Min) + .fillMaxSize() + .height(IntrinsicSize.Min) val composerOptionsButton: @Composable () -> Unit = remember { @Composable { @@ -316,8 +316,8 @@ private fun StandardLayout( if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { Box( modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { voiceDeleteButton() @@ -327,8 +327,8 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { voiceRecording() } @@ -341,16 +341,16 @@ private fun StandardLayout( } Box( modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { textInput() } } Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) - .size(48.dp), + Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + .size(48.dp), contentAlignment = Alignment.Center, ) { endButton() @@ -372,8 +372,8 @@ private fun TextFormattingLayout( ) { Box( modifier = Modifier - .weight(1f) - .padding(horizontal = 12.dp) + .weight(1f) + .padding(horizontal = 12.dp) ) { textInput() } @@ -417,21 +417,24 @@ private fun TextInputBox( Column( modifier = Modifier - .clip(roundedCorners) - .border(0.5.dp, borderColor, roundedCorners) - .background(color = bgColor) - .requiredHeightIn(min = 42.dp) - .fillMaxSize(), + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp) + .fillMaxSize(), ) { if (composerMode is MessageComposerMode.Special) { - ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + ComposerModeView( + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + ) } val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( modifier = Modifier - .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 42.dp) - // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail - .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), + .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 42.dp) + // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail + .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), contentAlignment = Alignment.CenterStart, ) { // Placeholder @@ -477,8 +480,8 @@ private fun TextInput( // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus), resolveMentionDisplay = resolveMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay, @@ -603,6 +606,7 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider voiceMessageState = VoiceMessageState.Idle, composerMode = MessageComposerMode.Reply( replyToDetails = inReplyToDetails, + hideImage = false, ), enableVoiceMessages = true, ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 69ff2eeb48..fafa65e64f 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -27,7 +27,8 @@ sealed interface MessageComposerMode { ) : Special data class Reply( - val replyToDetails: InReplyToDetails + val replyToDetails: InReplyToDetails, + val hideImage: Boolean, ) : Special { val eventId: EventId = replyToDetails.eventId() } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 11e1ab922a..3fb2adc584 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -104,6 +104,7 @@ "Send message" "Share" "Share link" + "Show" "Sign in again" "Sign out" "Sign out anyway" diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 4f5c2d9c2a..ac3474e7f9 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -118,6 +118,8 @@ class KonsistPreviewTest { "TimelineItemEventRowWithReplyPreview", "TimelineItemGroupedEventsRowContentCollapsePreview", "TimelineItemGroupedEventsRowContentExpandedPreview", + "TimelineItemImageViewHideMediaContentPreview", + "TimelineItemVideoViewHideMediaContentPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png new file mode 100644 index 0000000000..0103ed2548 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78ea6cbfcf12e405eca8b953b3d847e73f80121ad47beb6346563a2e9b5d567b +size 56342 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png new file mode 100644 index 0000000000..9e571c9b94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:047da66e7f7e78478b1d8442028224073e3d31493b5facf926472fc526532be1 +size 56473 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png index 78579bbd4a..40ad608500 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dac7e47f0e994457ed3db3add1a982f6dbecbd24533b4dca25b6b58b8a25a39d -size 36847 +oid sha256:bee6962a35cc2da956be0488f446c9a3d8831cec97f30e0828440068f764a0d1 +size 34241 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png index f79503d780..f1d9752b9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2615fb7e5541a393546f647e2f0534dcab5cc807eabec5d900ffbc571493932 -size 50069 +oid sha256:f08bf0cad0c94da75a0efb97dc92bc3e3c36cde4d833a6a26f9b0d63887785bf +size 47453 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png index d9ee17d120..4238d4bab8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f34ea24588048379282997eba41bc269a63a2d48d95fd9f83053adf22adc30 -size 62067 +oid sha256:0d5253e52390f9dcdd46db4f8d98d26be7cc8d05176d707a8a8c10adf4e7307b +size 57064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png index d96328629f..1db3339236 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6abd680ddf4abfeeb06968624a2d6f7468031d4774f39fccf6e6bf767d957142 -size 36814 +oid sha256:d37431be2add3d26b6fd41a94a4c2970d3303de8d39154f1b36f33ec4ae6bf44 +size 34260 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png index 593fb030c1..c6c6efe376 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ab8cf4eeb0aedb2061b7a7b2c762d3c0113a378872cfbb44d456c5c765790b5 -size 49975 +oid sha256:1a7cdbdd2fac32dcf3c08dcd2abdc8ec96c7b5d56bdfafb5b8594e84ad5da884 +size 47375 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png index 0e4582b9ed..94bac0a161 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:784d117799cee721b3efdf2eb8826642843e44ea893f2f5f2148d29b251a9537 -size 61573 +oid sha256:f0f597867f02b8c31fc094f02b39bea68a00b553cb999e3231f4d046e09da9a0 +size 56893 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png new file mode 100644 index 0000000000..0103ed2548 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78ea6cbfcf12e405eca8b953b3d847e73f80121ad47beb6346563a2e9b5d567b +size 56342 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png new file mode 100644 index 0000000000..9e571c9b94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:047da66e7f7e78478b1d8442028224073e3d31493b5facf926472fc526532be1 +size 56473 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png new file mode 100644 index 0000000000..a1719dd0a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:351b2b559fbe17a1de36c51e63171230b6807702c6b56233ba584ca65ff5eff9 +size 5015 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png new file mode 100644 index 0000000000..03bc461c8c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13e417d71098d8c863e72753263dfa27f7b398ed5ecb6461f7dde7081fd622fa +size 4801 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Day_0_en.png new file mode 100644 index 0000000000..6dd8676eee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db067468af10a72fbaa437a00937afddba100f5345317b9c23b03c8920d5cffa +size 33401 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Night_0_en.png new file mode 100644 index 0000000000..3c4052880c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0fc9f20bae5c54d807f5917844669bb8909514483d2ad29b348ef18f8f986a6 +size 33356 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png index d46d57762d..c02cae3baf 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb9971d6aa7f0734f9a30f83b25a03519f97789618219f0c79efa87d4430ca0f -size 58918 +oid sha256:0dda72bce08ceff2d577feabb88d097dc0af0de2bd6b24261adf39bed92a5157 +size 57652 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png index d46d57762d..c02cae3baf 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb9971d6aa7f0734f9a30f83b25a03519f97789618219f0c79efa87d4430ca0f -size 58918 +oid sha256:0dda72bce08ceff2d577feabb88d097dc0af0de2bd6b24261adf39bed92a5157 +size 57652 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png index 5119090213..15296e31b6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f44fc0dc3cd92839a7b176b086be75b082a238b9c8cd197f7273b33e7f01c591 -size 57495 +oid sha256:1ed1eeca50499ba467db8d2b17d334becb68cc9ea7418286eee8e19aeab3f9cb +size 56240 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png index 2294fd4c8a..78295963cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a6d6723bc89b9660a440848461c1568004d312599d7d2d2b94299d6aa47c0a -size 57037 +oid sha256:ccc5bf3169fbbd19626c775c88e295b48192f5a54b5640c99dfeca813e8e7ca5 +size 55838 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png index 2294fd4c8a..78295963cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a6d6723bc89b9660a440848461c1568004d312599d7d2d2b94299d6aa47c0a -size 57037 +oid sha256:ccc5bf3169fbbd19626c775c88e295b48192f5a54b5640c99dfeca813e8e7ca5 +size 55838 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png index 1e6522216a..d32653f9fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd7e58a29f370733af1e35531f428d5d2a732c70b9156d66ba590c9c5203f5d6 -size 55689 +oid sha256:266125ef6b0812194472e6efb5c832210523e0bc1538408f872ef5c48fc3906d +size 54434