From dd2a1b3388e558b38c83e8a5de992dc729040e2b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Oct 2024 19:47:44 +0200 Subject: [PATCH 01/17] Add settings to hide images and videos in the timeline. Hide images, videos and stickers in the timeline. Disable click on hidden content. It must be revealed first. Add preview without BlurHash. Also hide image in thumbnails. --- .../messages/impl/MessagesPresenter.kt | 20 ++- .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 4 + .../features/messages/impl/MessagesView.kt | 1 + .../MessageComposerPresenter.kt | 14 +- .../list/PinnedMessagesListPresenter.kt | 9 +- .../pinned/list/PinnedMessagesListState.kt | 2 + .../list/PinnedMessagesListStateProvider.kt | 4 + .../pinned/list/PinnedMessagesListView.kt | 11 +- .../impl/timeline/TimelinePresenter.kt | 2 +- .../messages/impl/timeline/TimelineView.kt | 5 + .../TimelineViewMessageShieldPreview.kt | 2 + .../components/ATimelineItemEventRow.kt | 4 + .../components/TimelineItemEventRow.kt | 16 +- .../TimelineItemGroupedEventsRow.kt | 13 ++ .../timeline/components/TimelineItemRow.kt | 14 +- .../components/TimelineItemStateEventRow.kt | 2 + .../event/TimelineItemAspectRatioBox.kt | 4 +- .../event/TimelineItemEventContentView.kt | 8 + .../components/event/TimelineItemImageView.kt | 58 +++++-- .../event/TimelineItemStickerView.kt | 72 ++++++-- .../components/event/TimelineItemVideoView.kt | 76 ++++++--- .../event/TimelineItemImageContentProvider.kt | 14 +- .../TimelineItemStickerContentProvider.kt | 14 +- .../event/TimelineItemVideoContentProvider.kt | 14 +- .../impl/timeline/protection/ProtectedView.kt | 33 ++++ .../impl/timeline/protection/TimelineItem.kt | 60 +++++++ .../protection/TimelineProtectionEvent.kt | 14 ++ .../protection/TimelineProtectionPresenter.kt | 53 ++++++ .../protection/TimelineProtectionState.kt | 28 ++++ .../TimelineProtectionStateProvider.kt | 16 ++ .../impl/developer/DeveloperSettingsEvents.kt | 1 + .../developer/DeveloperSettingsPresenter.kt | 7 + .../impl/developer/DeveloperSettingsState.kt | 1 + .../DeveloperSettingsStateProvider.kt | 2 + .../impl/developer/DeveloperSettingsView.kt | 19 ++- .../DeveloperSettingsPresenterTest.kt | 19 +++ .../developer/DeveloperSettingsViewTest.kt | 13 ++ .../ui/components/AttachmentThumbnail.kt | 2 +- .../ui/messages/reply/InReplyToMetadata.kt | 10 +- .../matrix/ui/messages/reply/InReplyToView.kt | 8 +- .../messages/reply/InReplyToMetadataKtTest.kt | 155 ++++++++++++++++-- .../api/store/AppPreferencesStore.kt | 3 + .../impl/store/DefaultAppPreferencesStore.kt | 13 ++ .../test/InMemoryAppPreferencesStore.kt | 10 ++ .../textcomposer/ComposerModeView.kt | 8 +- .../libraries/textcomposer/TextComposer.kt | 52 +++--- .../textcomposer/model/MessageComposerMode.kt | 3 +- 48 files changed, 775 insertions(+), 140 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateProvider.kt 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..274e94617d 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,8 @@ 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.TimelineProtectionPresenter +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 +92,7 @@ class MessagesPresenter @AssistedInject constructor( private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, + private val timelineProtectionPresenter: TimelineProtectionPresenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, @@ -123,6 +126,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 +186,7 @@ class MessagesPresenter @AssistedInject constructor( composerState = composerState, enableTextFormatting = composerState.showTextFormatting, timelineState = timelineState, + timelineProtectionState = timelineProtectionState, ) } is MessagesEvents.ToggleReaction -> { @@ -213,6 +218,7 @@ class MessagesPresenter @AssistedInject constructor( userEventPermissions = userEventPermissions, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, + timelineProtectionState = timelineProtectionState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, @@ -262,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor( action: TimelineItemAction, targetEvent: TimelineItem.Event, composerState: MessageComposerState, + timelineProtectionState: TimelineProtectionState, enableTextFormatting: Boolean, timelineState: TimelineState, ) = launch { @@ -271,7 +278,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 +392,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.hideContent(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/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index aabeaa96a0..6b80605401 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,20 @@ 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) } + run { + 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..7452e7b01a 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,8 @@ 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.TimelineProtectionPresenter +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 +62,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private val room: MatrixRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, private val timelineProvider: PinnedEventsTimelineProvider, + private val timelineProtectionPresenter: TimelineProtectionPresenter, private val snackbarDispatcher: SnackbarDispatcher, actionListPresenterFactory: ActionListPresenter.Factory, private val appCoroutineScope: CoroutineScope, @@ -97,14 +100,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 +121,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( return pinnedMessagesListState( timelineRoomInfo = timelineRoomInfo, + timelineProtectionState = timelineProtectionState, userEventPermissions = userEventPermissions, timelineItems = pinnedMessageItems, eventSink = ::handleEvents @@ -214,6 +217,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( @Composable private fun pinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo, + timelineProtectionState: TimelineProtectionState, userEventPermissions: UserEventPermissions, timelineItems: AsyncData>, eventSink: (PinnedMessagesListEvents) -> Unit @@ -228,6 +232,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..fc561d57e9 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, + hideContent = timelineProtectionState.hideContent(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/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..63a913284d 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 @@ -72,6 +74,7 @@ import kotlinx.coroutines.launch @Composable fun TimelineView( state: TimelineState, + timelineProtectionState: TimelineProtectionState, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onMessageClick: (TimelineItem.Event) -> Unit, @@ -137,6 +140,7 @@ fun TimelineView( TimelineItemRow( timelineItem = timelineItem, timelineRoomInfo = state.timelineRoomInfo, + timelineProtectionState = timelineProtectionState, renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()), focusedEventId = state.focusedEventId, @@ -320,6 +324,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..bc710aeb92 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, + hideContent = timelineProtectionState.hideContent(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.hideContent(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..eec5adcb4e 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, + hideContent = timelineProtectionState.hideContent(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, + hideContent = timelineProtectionState.hideContent(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..35199837cc 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, + hideContent = timelineProtectionState.hideContent(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.hideContent(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..2e3812a09d 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 = {}, + hideContent = 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..c855714d14 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, + hideContent: 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, + hideContent = hideContent, + onShowClick = onShowClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) is TimelineItemStickerContent -> TimelineItemStickerView( content = content, + hideContent = hideContent, + onShowClick = onShowClick, modifier = modifier, ) is TimelineItemVideoContent -> TimelineItemVideoView( content = content, + hideContent = hideContent, + 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..e7cacede9e 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, + hideContent: 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 = hideContent, + 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, + hideContent = false, + onShowClick = {}, + onContentLayoutChange = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemImageViewHideContentPreview() = ElementPreview { + TimelineItemImageView( + content = aTimelineItemImageContent(), + hideContent = 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..d41b985d86 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, + hideContent: 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 = hideContent, + 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, + hideContent = 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..9c28172c2e 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, + hideContent: 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 = hideContent, + 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, + hideContent = false, + onShowClick = {}, + onContentLayoutChange = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemVideoViewHideContentPreview() = ElementPreview { + TimelineItemVideoView( + content = aTimelineItemVideoContent(), + hideContent = 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..d7e8d94963 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt @@ -0,0 +1,33 @@ +/* + * 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.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.element.android.libraries.designsystem.theme.components.Button + +@Composable +fun BoxScope.ProtectedView( + hideContent: Boolean, + onShowClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + if (hideContent) { + // TODO Update design, wording for video? + Button( + modifier = modifier.align(Alignment.Center), + text = "Show", + onClick = onShowClick, + ) + } else { + 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..46ac375aa4 --- /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 hideContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) + var allowedEvents by remember { mutableStateOf>(setOf()) } + val protectionState by remember(hideContent) { + derivedStateOf { + if (hideContent) { + 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..66ebb279f5 --- /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 hideContent(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/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/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() } From fd142c16d9bad2cdcdf1dbded6c9face93dd0a37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Oct 2024 21:05:43 +0200 Subject: [PATCH 02/17] Improve extension to fix a copy paste issue. --- .../DefaultNotifiableEventResolver.kt | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) 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..cfbac8de2e 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 @@ -103,7 +103,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 +148,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 +287,18 @@ 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? { + 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 { From f544f31761240be20095c6fbb10f4554f8829a75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Oct 2024 21:10:06 +0200 Subject: [PATCH 03/17] Do not render images in notification if the setting to hide image is enabled. --- .../impl/notifications/DefaultNotifiableEventResolver.kt | 6 ++++++ .../notifications/DefaultNotifiableEventResolverTest.kt | 4 ++++ 2 files changed, 10 insertions(+) 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 cfbac8de2e..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 @@ -288,6 +291,9 @@ class DefaultNotifiableEventResolver @Inject constructor( } 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( 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, ) } } From 0244b71795d323e9ce30960b8aa9e9419bdf6495 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Oct 2024 23:24:29 +0200 Subject: [PATCH 04/17] Fix test compilation --- .../android/features/messages/impl/MessagesPresenterTest.kt | 6 ++++++ .../impl/messagecomposer/MessageComposerPresenterTest.kt | 5 ++++- .../impl/pinned/list/PinnedMessagesListPresenterTest.kt | 4 ++++ .../features/messages/impl/timeline/TimelineViewTest.kt | 4 ++++ .../composer/VoiceMessageComposerPresenterTest.kt | 5 +---- 5 files changed, 19 insertions(+), 5 deletions(-) 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..60ffcd01d8 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.TimelineProtectionPresenter 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 @@ -93,6 +94,7 @@ import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.model.MessageComposerMode @@ -1059,12 +1061,16 @@ class MessagesPresenterTest { val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) + val timelineProtectionPresenter = TimelineProtectionPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(), + ) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, + timelineProtectionPresenter = timelineProtectionPresenter, 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..3ac9995ec7 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.TimelineProtectionPresenter 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 @@ -31,6 +32,7 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.assert @@ -303,12 +305,14 @@ class PinnedMessagesListPresenterTest { initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) ) ) + val timelineProtectionPresenter = TimelineProtectionPresenter(InMemoryAppPreferencesStore()) timelineProvider.launchIn(backgroundScope) return PinnedMessagesListPresenter( navigator = navigator, room = room, timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), timelineProvider = timelineProvider, + timelineProtectionPresenter = timelineProtectionPresenter, 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/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( From d2bd1b7cabdb016fe5576ff026015dffd454c245 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 00:51:47 +0200 Subject: [PATCH 05/17] Fix test --- .../io/element/android/tests/konsist/KonsistPreviewTest.kt | 2 ++ 1 file changed, 2 insertions(+) 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..1776b070d4 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", + "TimelineItemImageViewHideContentPreview", + "TimelineItemVideoViewHideContentPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", From 8ffdfff8396b12de0c0c7d28f5494a1d9b53144a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 11:05:58 +0200 Subject: [PATCH 06/17] Add unit test on TimelineProtectionPresenter --- .../TimelineProtectionPresenterTest.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt 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, + ) +} From 73bdf0e1ce60436097c5fa0cb40e8d1d6cce01d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 11:31:01 +0200 Subject: [PATCH 07/17] Add unit test on ProtectedView and TimelineProtectionState --- .../timeline/protection/ProtectedViewTest.kt | 71 +++++++++++++++++++ .../protection/TimelineProtectionStateTest.kt | 44 ++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt 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..a1e876f328 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt @@ -0,0 +1,71 @@ +/* + * 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.foundation.layout.Box +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.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.theme.components.Text +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.onNodeWithText("Show").performClick() + } + } +} + +private fun AndroidComposeTestRule.setProtectedView( + hideContent: Boolean = false, + onShowClick: () -> Unit = { lambdaError() }, + content: @Composable () -> Unit = {}, +) { + setContent { + Box { + ProtectedView( + hideContent = hideContent, + onShowClick = onShowClick, + content = content + ) + } + } +} 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..0a05636df1 --- /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, hideContent always return null`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderAll + ) + assertThat(sut.hideContent(null)).isFalse() + assertThat(sut.hideContent(AN_EVENT_ID)).isFalse() + } + + @Test + fun `when protectionState is RenderOnly with empty set, hideContent always return true`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderOnly(persistentSetOf()) + ) + assertThat(sut.hideContent(null)).isTrue() + assertThat(sut.hideContent(AN_EVENT_ID)).isTrue() + } + + @Test + fun `when protectionState is RenderOnly with an Event, hideContent always return true`() { + val sut = aTimelineProtectionState( + protectionState = ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)) + ) + assertThat(sut.hideContent(null)).isTrue() + assertThat(sut.hideContent(AN_EVENT_ID)).isFalse() + assertThat(sut.hideContent(AN_EVENT_ID_2)).isTrue() + } +} From a465b0c0807f54f3648e3fda9d11cd8bbf9a2cf4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 11:36:01 +0200 Subject: [PATCH 08/17] Remove dead code. --- .../android/features/messages/impl/timeline/TimelineView.kt | 5 ----- 1 file changed, 5 deletions(-) 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 63a913284d..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 @@ -67,7 +67,6 @@ 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 @@ -117,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) { From 5285696b0ab7b16a3555e5f11428f4181bc51f26 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 12:14:17 +0200 Subject: [PATCH 09/17] Fix test name. --- .../impl/timeline/protection/TimelineProtectionStateTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0a05636df1..cc42b4fd9b 100644 --- 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 @@ -33,7 +33,7 @@ class TimelineProtectionStateTest { } @Test - fun `when protectionState is RenderOnly with an Event, hideContent always return true`() { + fun `when protectionState is RenderOnly with an Event, hideContent can return true or false`() { val sut = aTimelineProtectionState( protectionState = ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)) ) From 8b282f27e5a2efcd854b1609561e24a20c74ad39 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 17:00:48 +0200 Subject: [PATCH 10/17] Hide images: iterate on design. --- .../impl/timeline/protection/ProtectedView.kt | 77 ++++++++++++++++--- .../timeline/protection/ProtectedViewTest.kt | 18 ++--- .../src/main/res/values/localazy.xml | 1 + 3 files changed, 77 insertions(+), 19 deletions(-) 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 index d7e8d94963..0761a3fdfb 100644 --- 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 @@ -7,27 +7,86 @@ package io.element.android.features.messages.impl.timeline.protection -import androidx.compose.foundation.layout.BoxScope +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 io.element.android.libraries.designsystem.theme.components.Button +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 @Composable -fun BoxScope.ProtectedView( +fun ProtectedView( hideContent: Boolean, onShowClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { if (hideContent) { - // TODO Update design, wording for video? - Button( - modifier = modifier.align(Alignment.Center), - text = "Show", - onClick = onShowClick, - ) + 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 PreviewProtectedView() = ElementPreview { + Box( + modifier = Modifier + .size(160.dp) + .blurHashBackground(A_BLUR_HASH) + ) { + ProtectedView( + hideContent = true, + onShowClick = {}, + content = {}, + ) + } +} 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 index a1e876f328..5d82b292bf 100644 --- 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 @@ -8,14 +8,14 @@ package io.element.android.features.messages.impl.timeline.protection import androidx.activity.ComponentActivity -import androidx.compose.foundation.layout.Box 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.compose.ui.test.performClick 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 @@ -49,7 +49,7 @@ class ProtectedViewTest { } ) rule.onNodeWithText("Hello").assertDoesNotExist() - rule.onNodeWithText("Show").performClick() + rule.clickOn(CommonStrings.action_show) } } } @@ -60,12 +60,10 @@ private fun AndroidComposeTestRule.setProte content: @Composable () -> Unit = {}, ) { setContent { - Box { - ProtectedView( - hideContent = hideContent, - onShowClick = onShowClick, - content = content - ) - } + ProtectedView( + hideContent = hideContent, + onShowClick = onShowClick, + content = content + ) } } 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" From 70c6bede971cf5f62210241b2911ce03e3478c8a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 3 Oct 2024 15:25:35 +0000 Subject: [PATCH 11/17] Update screenshots --- ...onents.event_TimelineItemImageViewHideContent_Day_0_en.png | 3 +++ ...ents.event_TimelineItemImageViewHideContent_Night_0_en.png | 3 +++ ...meline.components.event_TimelineItemImageView_Day_3_en.png | 3 +++ ...line.components.event_TimelineItemImageView_Night_3_en.png | 3 +++ ...line.components.event_TimelineItemStickerView_Day_0_en.png | 4 ++-- ...line.components.event_TimelineItemStickerView_Day_1_en.png | 4 ++-- ...line.components.event_TimelineItemStickerView_Day_2_en.png | 4 ++-- ...line.components.event_TimelineItemStickerView_Day_3_en.png | 3 +++ ...ne.components.event_TimelineItemStickerView_Night_0_en.png | 4 ++-- ...ne.components.event_TimelineItemStickerView_Night_1_en.png | 4 ++-- ...ne.components.event_TimelineItemStickerView_Night_2_en.png | 4 ++-- ...ne.components.event_TimelineItemStickerView_Night_3_en.png | 3 +++ ...onents.event_TimelineItemVideoViewHideContent_Day_0_en.png | 3 +++ ...ents.event_TimelineItemVideoViewHideContent_Night_0_en.png | 3 +++ ...meline.components.event_TimelineItemVideoView_Day_3_en.png | 3 +++ ...line.components.event_TimelineItemVideoView_Night_3_en.png | 3 +++ ...ssages.impl.timeline.protection_ProtectedView_Day_0_en.png | 3 +++ ...ages.impl.timeline.protection_ProtectedView_Night_0_en.png | 3 +++ ...ferences.impl.developer_DeveloperSettingsView_Day_0_en.png | 4 ++-- ...ferences.impl.developer_DeveloperSettingsView_Day_1_en.png | 4 ++-- ...ferences.impl.developer_DeveloperSettingsView_Day_2_en.png | 4 ++-- ...rences.impl.developer_DeveloperSettingsView_Night_0_en.png | 4 ++-- ...rences.impl.developer_DeveloperSettingsView_Night_1_en.png | 4 ++-- ...rences.impl.developer_DeveloperSettingsView_Night_2_en.png | 4 ++-- 24 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.protection_ProtectedView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_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_TimelineItemImageViewHideContent_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_TimelineItemImageViewHideContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_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_TimelineItemImageViewHideContent_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_TimelineItemVideoViewHideContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_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_TimelineItemVideoViewHideContent_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_TimelineItemVideoViewHideContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_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_TimelineItemVideoViewHideContent_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 From a3e24578344956a5b1f027532aa4d300effc474d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Oct 2024 18:54:19 +0200 Subject: [PATCH 12/17] SuppressWarnings ModifierClickableOrder --- .../features/messages/impl/timeline/protection/ProtectedView.kt | 1 + 1 file changed, 1 insertion(+) 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 index 0761a3fdfb..f0fbaa8eb1 100644 --- 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 @@ -31,6 +31,7 @@ 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, From fafd5d4871c36fc81512d5f9362c3d54892e6079 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 12:24:50 +0200 Subject: [PATCH 13/17] Let `invokeOnCurrentTimeline` lambda param return Unit so that we can remove `run` block. --- .../impl/messagecomposer/MessageComposerPresenter.kt | 12 +++++------- .../messages/impl/timeline/TimelineController.kt | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) 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 6b80605401..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 @@ -592,13 +592,11 @@ class MessageComposerPresenter @Inject constructor( ) timelineController.invokeOnCurrentTimeline { val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser) - run { - messageComposerContext.composerMode = MessageComposerMode.Reply( - replyToDetails = replyToDetails, - // I guess it's fine to always render the image when restoring a draft - hideImage = false - ) - } + 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/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) } From b889e8681c2ecea4d697809ddfb638887221fd95 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 13:57:59 +0200 Subject: [PATCH 14/17] Rename `hideContent` to `hideMediaContent` --- .../messages/impl/MessagesPresenter.kt | 2 +- .../pinned/list/PinnedMessagesListView.kt | 2 +- .../components/TimelineItemEventRow.kt | 4 ++-- .../TimelineItemGroupedEventsRow.kt | 4 ++-- .../timeline/components/TimelineItemRow.kt | 4 ++-- .../components/TimelineItemStateEventRow.kt | 2 +- .../event/TimelineItemEventContentView.kt | 8 ++++---- .../components/event/TimelineItemImageView.kt | 10 +++++----- .../event/TimelineItemStickerView.kt | 6 +++--- .../components/event/TimelineItemVideoView.kt | 10 +++++----- .../protection/TimelineProtectionPresenter.kt | 6 +++--- .../protection/TimelineProtectionState.kt | 2 +- .../protection/TimelineProtectionStateTest.kt | 20 +++++++++---------- .../tests/konsist/KonsistPreviewTest.kt | 4 ++-- 14 files changed, 42 insertions(+), 42 deletions(-) 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 274e94617d..68383e5829 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 @@ -402,7 +402,7 @@ class MessagesPresenter @AssistedInject constructor( val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser) val composerMode = MessageComposerMode.Reply( replyToDetails = replyToDetails, - hideImage = timelineProtectionState.hideContent(targetEvent.eventId), + 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/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index fc561d57e9..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 @@ -256,7 +256,7 @@ private fun TimelineItemEventContentViewWrapper( } else { TimelineItemEventContentView( content = event.content, - hideContent = timelineProtectionState.hideContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = { }, 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 bc710aeb92..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 @@ -129,7 +129,7 @@ fun TimelineItemEventRow( eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideContent = timelineProtectionState.hideContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, @@ -572,7 +572,7 @@ private fun MessageEventBubbleContent( .clickable(onClick = inReplyToClick) InReplyToView( inReplyTo = inReplyTo, - hideImage = timelineProtectionState.hideContent(inReplyTo.eventId()), + hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()), modifier = inReplyToModifier, ) } 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 eec5adcb4e..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 @@ -56,7 +56,7 @@ fun TimelineItemGroupedEventsRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideContent = timelineProtectionState.hideContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, @@ -120,7 +120,7 @@ private fun TimelineItemGroupedEventsRowContent( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideContent = timelineProtectionState.hideContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, 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 35199837cc..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 @@ -59,7 +59,7 @@ internal fun TimelineItemRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideContent = timelineProtectionState.hideContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, eventSink = eventSink, @@ -118,7 +118,7 @@ internal fun TimelineItemRow( timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = timelineItem.isEvent(focusedEventId), - onClick = if (timelineProtectionState.hideContent(timelineItem.eventId) && timelineItem.mustBeProtected()) { + onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) { {} } else { { onClick(timelineItem) } 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 2e3812a09d..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,7 +71,7 @@ fun TimelineItemStateEventRow( TimelineItemEventContentView( content = event.content, onLinkClick = {}, - hideContent = false, + 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/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index c855714d14..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,7 +35,7 @@ import io.element.android.libraries.architecture.Presenter @Composable fun TimelineItemEventContentView( content: TimelineItemEventContent, - hideContent: Boolean, + hideMediaContent: Boolean, onShowClick: () -> Unit, onLinkClick: (url: String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @@ -71,20 +71,20 @@ fun TimelineItemEventContentView( ) is TimelineItemImageContent -> TimelineItemImageView( content = content, - hideContent = hideContent, + hideMediaContent = hideMediaContent, onShowClick = onShowClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) is TimelineItemStickerContent -> TimelineItemStickerView( content = content, - hideContent = hideContent, + hideMediaContent = hideMediaContent, onShowClick = onShowClick, modifier = modifier, ) is TimelineItemVideoContent -> TimelineItemVideoView( content = content, - hideContent = hideContent, + 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 e7cacede9e..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 @@ -59,7 +59,7 @@ import io.element.android.wysiwyg.compose.EditorStyledText @Composable fun TimelineItemImageView( content: TimelineItemImageContent, - hideContent: Boolean, + hideMediaContent: Boolean, onShowClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, @@ -80,7 +80,7 @@ fun TimelineItemImageView( aspectRatio = content.aspectRatio, ) { ProtectedView( - hideContent = hideContent, + hideContent = hideMediaContent, onShowClick = onShowClick, ) { var isLoaded by remember { mutableStateOf(false) } @@ -133,7 +133,7 @@ fun TimelineItemImageView( internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview { TimelineItemImageView( content = content, - hideContent = false, + hideMediaContent = false, onShowClick = {}, onContentLayoutChange = {}, ) @@ -141,10 +141,10 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon @PreviewsDayNight @Composable -internal fun TimelineItemImageViewHideContentPreview() = ElementPreview { +internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview { TimelineItemImageView( content = aTimelineItemImageContent(), - hideContent = true, + hideMediaContent = true, onShowClick = {}, onContentLayoutChange = {}, ) 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 d41b985d86..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 @@ -39,7 +39,7 @@ private const val STICKER_SIZE_IN_DP = 128 @Composable fun TimelineItemStickerView( content: TimelineItemStickerContent, - hideContent: Boolean, + hideMediaContent: Boolean, onShowClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -54,7 +54,7 @@ fun TimelineItemStickerView( maxHeight = STICKER_SIZE_IN_DP, ) { ProtectedView( - hideContent = hideContent, + hideContent = hideMediaContent, onShowClick = onShowClick, ) { var isLoaded by remember { mutableStateOf(false) } @@ -84,7 +84,7 @@ fun TimelineItemStickerView( internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemStickerContentProvider::class) content: TimelineItemStickerContent) = ElementPreview { TimelineItemStickerView( content = content, - hideContent = false, + 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 9c28172c2e..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 @@ -65,7 +65,7 @@ import io.element.android.wysiwyg.compose.EditorStyledText @Composable fun TimelineItemVideoView( content: TimelineItemVideoContent, - hideContent: Boolean, + hideMediaContent: Boolean, onShowClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, @@ -87,7 +87,7 @@ fun TimelineItemVideoView( contentAlignment = Alignment.Center, ) { ProtectedView( - hideContent = hideContent, + hideContent = hideMediaContent, onShowClick = onShowClick, ) { var isLoaded by remember { mutableStateOf(false) } @@ -151,7 +151,7 @@ fun TimelineItemVideoView( internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview { TimelineItemVideoView( content = content, - hideContent = false, + hideMediaContent = false, onShowClick = {}, onContentLayoutChange = {}, ) @@ -159,10 +159,10 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon @PreviewsDayNight @Composable -internal fun TimelineItemVideoViewHideContentPreview() = ElementPreview { +internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview { TimelineItemVideoView( content = aTimelineItemVideoContent(), - hideContent = true, + hideMediaContent = true, onShowClick = {}, onContentLayoutChange = {}, ) 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 index 46ac375aa4..5d83347e67 100644 --- 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 @@ -25,11 +25,11 @@ class TimelineProtectionPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): TimelineProtectionState { - val hideContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) + val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) var allowedEvents by remember { mutableStateOf>(setOf()) } - val protectionState by remember(hideContent) { + val protectionState by remember(hideMediaContent) { derivedStateOf { - if (hideContent) { + if (hideMediaContent) { ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet()) } else { ProtectionState.RenderAll 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 index 66ebb279f5..af1f26127c 100644 --- 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 @@ -15,7 +15,7 @@ data class TimelineProtectionState( val protectionState: ProtectionState, val eventSink: (TimelineProtectionEvent) -> Unit, ) { - fun hideContent(eventId: EventId?) = when (protectionState) { + fun hideMediaContent(eventId: EventId?) = when (protectionState) { is ProtectionState.RenderAll -> false is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds } 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 index cc42b4fd9b..81187eb6d4 100644 --- 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 @@ -15,30 +15,30 @@ import org.junit.Test class TimelineProtectionStateTest { @Test - fun `when protectionState is RenderAll, hideContent always return null`() { + fun `when protectionState is RenderAll, hideMediaContent always return null`() { val sut = aTimelineProtectionState( protectionState = ProtectionState.RenderAll ) - assertThat(sut.hideContent(null)).isFalse() - assertThat(sut.hideContent(AN_EVENT_ID)).isFalse() + assertThat(sut.hideMediaContent(null)).isFalse() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse() } @Test - fun `when protectionState is RenderOnly with empty set, hideContent always return true`() { + fun `when protectionState is RenderOnly with empty set, hideMediaContent always return true`() { val sut = aTimelineProtectionState( protectionState = ProtectionState.RenderOnly(persistentSetOf()) ) - assertThat(sut.hideContent(null)).isTrue() - assertThat(sut.hideContent(AN_EVENT_ID)).isTrue() + assertThat(sut.hideMediaContent(null)).isTrue() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isTrue() } @Test - fun `when protectionState is RenderOnly with an Event, hideContent can return true or false`() { + 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.hideContent(null)).isTrue() - assertThat(sut.hideContent(AN_EVENT_ID)).isFalse() - assertThat(sut.hideContent(AN_EVENT_ID_2)).isTrue() + assertThat(sut.hideMediaContent(null)).isTrue() + assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse() + assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue() } } 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 1776b070d4..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,8 +118,8 @@ class KonsistPreviewTest { "TimelineItemEventRowWithReplyPreview", "TimelineItemGroupedEventsRowContentCollapsePreview", "TimelineItemGroupedEventsRowContentExpandedPreview", - "TimelineItemImageViewHideContentPreview", - "TimelineItemVideoViewHideContentPreview", + "TimelineItemImageViewHideMediaContentPreview", + "TimelineItemVideoViewHideMediaContentPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", From a72c406c86af4b5d3f08bb9d8f66658a37ef1100 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 14:07:14 +0200 Subject: [PATCH 15/17] Fix preview name. --- .../features/messages/impl/timeline/protection/ProtectedView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index f0fbaa8eb1..6a9db31682 100644 --- 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 @@ -78,7 +78,7 @@ fun ProtectedView( @PreviewsDayNight @Composable -internal fun PreviewProtectedView() = ElementPreview { +internal fun ProtectedViewPreview() = ElementPreview { Box( modifier = Modifier .size(160.dp) From 1fad4d43bec709643da437e61fbdd1fac22c36dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 15:52:41 +0200 Subject: [PATCH 16/17] Do not inject the TimelineProtectionPresenter --- .../android/features/messages/impl/MessagesPresenter.kt | 3 +-- .../android/features/messages/impl/di/MessagesModule.kt | 5 +++++ .../impl/pinned/list/PinnedMessagesListPresenter.kt | 3 +-- .../features/messages/impl/MessagesPresenterTest.kt | 9 ++------- .../impl/pinned/list/PinnedMessagesListPresenterTest.kt | 6 ++---- 5 files changed, 11 insertions(+), 15 deletions(-) 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 68383e5829..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,7 +46,6 @@ 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.TimelineProtectionPresenter 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 @@ -92,7 +91,7 @@ class MessagesPresenter @AssistedInject constructor( private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, - private val timelineProtectionPresenter: TimelineProtectionPresenter, + private val timelineProtectionPresenter: Presenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, 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/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 7452e7b01a..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,7 +30,6 @@ 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.TimelineProtectionPresenter 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 @@ -62,7 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private val room: MatrixRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, private val timelineProvider: PinnedEventsTimelineProvider, - private val timelineProtectionPresenter: TimelineProtectionPresenter, + private val timelineProtectionPresenter: Presenter, private val snackbarDispatcher: SnackbarDispatcher, actionListPresenterFactory: ActionListPresenter.Factory, private val appCoroutineScope: CoroutineScope, 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 60ffcd01d8..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,7 +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.TimelineProtectionPresenter +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 @@ -94,7 +94,6 @@ import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.model.MessageComposerMode @@ -1061,16 +1060,12 @@ class MessagesPresenterTest { val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) - val timelineProtectionPresenter = TimelineProtectionPresenter( - appPreferencesStore = InMemoryAppPreferencesStore(), - ) - return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, - timelineProtectionPresenter = timelineProtectionPresenter, + timelineProtectionPresenter = { aTimelineProtectionState() }, actionListPresenterFactory = FakeActionListPresenter.Factory, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, 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 3ac9995ec7..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,7 +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.TimelineProtectionPresenter +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 @@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.assert @@ -305,14 +304,13 @@ class PinnedMessagesListPresenterTest { initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) ) ) - val timelineProtectionPresenter = TimelineProtectionPresenter(InMemoryAppPreferencesStore()) timelineProvider.launchIn(backgroundScope) return PinnedMessagesListPresenter( navigator = navigator, room = room, timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), timelineProvider = timelineProvider, - timelineProtectionPresenter = timelineProtectionPresenter, + timelineProtectionPresenter = { aTimelineProtectionState() }, snackbarDispatcher = SnackbarDispatcher(), actionListPresenterFactory = FakeActionListPresenter.Factory, analyticsService = analyticsService, From 1f7b05a58d1f0961ffd062ebe8c29ae4bf5f78a2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 4 Oct 2024 15:15:15 +0000 Subject: [PATCH 17/17] Update screenshots --- ...ents.event_TimelineItemImageViewHideMediaContent_Day_0_en.png} | 0 ...ts.event_TimelineItemImageViewHideMediaContent_Night_0_en.png} | 0 ...ents.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png} | 0 ...ts.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Day_0_en.png => features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Night_0_en.png => features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Day_0_en.png => features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Night_0_en.png => features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png} (100%) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideContent_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideContent_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png