Repository: element-hq/element-x-android (language: kotlin)

Issue:
Add user controls for whether to display media (including thumbnails)
### Your use case

Android version of element-hq/element-meta#2518.

Urgent req for T&S.

### Have you considered any alternatives?

_No response_

### Additional context

_No response_

### Are you willing to provide a PR?

No


Discussion hints:
nan

Pull request: Add developer setting to hide images in the timeline
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

<!-- Describe shortly what has been changed -->
Add a switch to the developer settings screen to prevent preview of images, videos and sticker to be rendered automatically in the timeline.
When enabled:
- the blurhash (if available) will be rendered by default, or a color surface will be rendered.
- A button exist to render the image
- The image is not clickable, it must be rendered first
- The thumbnail (for replyied image) is not rendered, expect if the original image has been rendered.
- The image does not render in the notification

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #3386 

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

Waiting for the final design before recording the screenshot.

## Tests

<!-- Explain how you tested your development -->

- Navigate to the developer setting to enable the new option
- Go to a room and observe that the images are not rendered automatically

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR


Test command: ./gradlew :tests:konsist:testDebugUnitTest --tests "io.element.android.tests.konsist.KonsistPreviewTest" :libraries:matrixui:testDebugUnitTest --tests "io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadataKtTest" :features:messages:impl:testDebugUnitTest --tests "io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenterTest" --tests "io.element.android.features.messages.impl.MessagesPresenterTest" --tests "io.element.android.features.messages.impl.pinned.list.PinnedMessagesListPresenterTest" --tests "io.element.android.features.messages.impl.timeline.protection.TimelineProtectionPresenterTest" --tests "io.element.android.features.messages.impl.timeline.TimelineViewTest" --tests "io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenterTest" --tests "io.element.android.features.messages.impl.timeline.protection.ProtectedViewTest" --tests "io.element.android.features.messages.impl.timeline.protection.TimelineProtectionStateTest" :libraries:push:impl:testDebugUnitTest --tests "io.element.android.libraries.push.impl.notifications.DefaultNotifiableEventResolverTest" :features:preferences:impl:testDebugUnitTest --tests "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenterTest" --tests "io.element.android.features.preferences.impl.developer.DeveloperSettingsViewTest"

Fix patch:
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 b0029fe4ee6..388780d384c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
@@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
 import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -90,6 +91,7 @@ class MessagesPresenter @AssistedInject constructor(
     private val composerPresenter: MessageComposerPresenter,
     private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
     timelinePresenterFactory: TimelinePresenter.Factory,
+    private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
     private val actionListPresenterFactory: ActionListPresenter.Factory,
     private val customReactionPresenter: CustomReactionPresenter,
     private val reactionSummaryPresenter: ReactionSummaryPresenter,
@@ -123,6 +125,7 @@ class MessagesPresenter @AssistedInject constructor(
         val composerState = composerPresenter.present()
         val voiceMessageComposerState = voiceMessageComposerPresenter.present()
         val timelineState = timelinePresenter.present()
+        val timelineProtectionState = timelineProtectionPresenter.present()
         val actionListState = actionListPresenter.present()
         val customReactionState = customReactionPresenter.present()
         val reactionSummaryState = reactionSummaryPresenter.present()
@@ -182,6 +185,7 @@ class MessagesPresenter @AssistedInject constructor(
                         composerState = composerState,
                         enableTextFormatting = composerState.showTextFormatting,
                         timelineState = timelineState,
+                        timelineProtectionState = timelineProtectionState,
                     )
                 }
                 is MessagesEvents.ToggleReaction -> {
@@ -213,6 +217,7 @@ class MessagesPresenter @AssistedInject constructor(
             userEventPermissions = userEventPermissions,
             voiceMessageComposerState = voiceMessageComposerState,
             timelineState = timelineState,
+            timelineProtectionState = timelineProtectionState,
             actionListState = actionListState,
             customReactionState = customReactionState,
             reactionSummaryState = reactionSummaryState,
@@ -262,6 +267,7 @@ class MessagesPresenter @AssistedInject constructor(
         action: TimelineItemAction,
         targetEvent: TimelineItem.Event,
         composerState: MessageComposerState,
+        timelineProtectionState: TimelineProtectionState,
         enableTextFormatting: Boolean,
         timelineState: TimelineState,
     ) = launch {
@@ -271,7 +277,7 @@ class MessagesPresenter @AssistedInject constructor(
             TimelineItemAction.Redact -> handleActionRedact(targetEvent)
             TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
             TimelineItemAction.Reply,
-            TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState)
+            TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState)
             TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
             TimelineItemAction.Forward -> handleForwardAction(targetEvent)
             TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
@@ -385,11 +391,18 @@ class MessagesPresenter @AssistedInject constructor(
         }
     }
 
-    private suspend fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
+    private suspend fun handleActionReply(
+        targetEvent: TimelineItem.Event,
+        composerState: MessageComposerState,
+        timelineProtectionState: TimelineProtectionState,
+    ) {
         if (targetEvent.eventId == null) return
         timelineController.invokeOnCurrentTimeline {
             val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser)
-            val composerMode = MessageComposerMode.Reply(replyToDetails = replyToDetails)
+            val composerMode = MessageComposerMode.Reply(
+                replyToDetails = replyToDetails,
+                hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId),
+            )
             composerState.eventSink(
                 MessageComposerEvents.SetMode(composerMode)
             )
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
index 2c5bae6d3b0..2e03cbdb9de 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 2c1a4874407..985471c6416 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 067d0da87b8..36bf0bc5fb8 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
@@ -379,6 +379,7 @@ private fun MessagesViewContent(
                     val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
                     TimelineView(
                         state = state.timelineState,
+                        timelineProtectionState = state.timelineProtectionState,
                         onUserDataClick = onUserDataClick,
                         onLinkClick = onLinkClick,
                         onMessageClick = onMessageClick,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt
index a6cbd68cf6f..1a5aa1c7d1e 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<TypingNotificationState>
+
+    @Binds
+    fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState>
 }
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 aabeaa96a0e..8529732b061 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
@@ -585,10 +585,18 @@ class MessageComposerPresenter @Inject constructor(
                 content = htmlText ?: markdownText
             )
             is ComposerDraftType.Reply -> {
-                messageComposerContext.composerMode = MessageComposerMode.Reply(InReplyToDetails.Loading(draftType.eventId))
+                messageComposerContext.composerMode = MessageComposerMode.Reply(
+                    replyToDetails = InReplyToDetails.Loading(draftType.eventId),
+                    // I guess it's fine to always render the image when restoring a draft
+                    hideImage = false
+                )
                 timelineController.invokeOnCurrentTimeline {
                     val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser)
-                    run { messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails) }
+                    messageComposerContext.composerMode = MessageComposerMode.Reply(
+                        replyToDetails = replyToDetails,
+                        // I guess it's fine to always render the image when restoring a draft
+                        hideImage = false
+                    )
                 }
             }
         }
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt
index 1e557f84bd5..4ccef0f23df 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt
@@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
 import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
 import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
 import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
 import io.element.android.features.messages.impl.typing.TypingNotificationState
 import io.element.android.libraries.architecture.AsyncData
 import io.element.android.libraries.architecture.Presenter
@@ -60,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
     private val room: MatrixRoom,
     timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
     private val timelineProvider: PinnedEventsTimelineProvider,
+    private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
     private val snackbarDispatcher: SnackbarDispatcher,
     actionListPresenterFactory: ActionListPresenter.Factory,
     private val appCoroutineScope: CoroutineScope,
@@ -97,14 +99,13 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
                 )
             )
         }
-
+        val timelineProtectionState = timelineProtectionPresenter.present()
         val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
         val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
 
         var pinnedMessageItems by remember {
             mutableStateOf<AsyncData<ImmutableList<TimelineItem>>>(AsyncData.Uninitialized)
         }
-
         PinnedMessagesListEffect(
             onItemsChange = { newItems ->
                 pinnedMessageItems = newItems
@@ -119,6 +120,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
 
         return pinnedMessagesListState(
             timelineRoomInfo = timelineRoomInfo,
+            timelineProtectionState = timelineProtectionState,
             userEventPermissions = userEventPermissions,
             timelineItems = pinnedMessageItems,
             eventSink = ::handleEvents
@@ -214,6 +216,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
     @Composable
     private fun pinnedMessagesListState(
         timelineRoomInfo: TimelineRoomInfo,
+        timelineProtectionState: TimelineProtectionState,
         userEventPermissions: UserEventPermissions,
         timelineItems: AsyncData<ImmutableList<TimelineItem>>,
         eventSink: (PinnedMessagesListEvents) -> Unit
@@ -228,6 +231,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
                     val actionListState = actionListPresenter.present()
                     PinnedMessagesListState.Filled(
                         timelineRoomInfo = timelineRoomInfo,
+                        timelineProtectionState = timelineProtectionState,
                         userEventPermissions = userEventPermissions,
                         timelineItems = timelineItems.data,
                         actionListState = actionListState,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt
index 82105a2e35b..3a15c9f8bcb 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<TimelineItem>,
         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 d394b6efad7..bd51652cb4c 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<TimelineItem> = 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 ad62ad616af..3c7f76e5074 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
@@ -32,6 +32,8 @@ import io.element.android.features.messages.impl.timeline.components.event.Timel
 import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
 import io.element.android.features.messages.impl.timeline.model.TimelineItem
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
 import io.element.android.features.poll.api.pollcontent.PollTitleView
 import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
 import io.element.android.libraries.designsystem.components.button.BackButton
@@ -77,8 +79,8 @@ fun PinnedMessagesListView(
                 onLinkClick = onLinkClick,
                 onErrorDismiss = onBackClick,
                 modifier = Modifier
-                        .padding(padding)
-                        .consumeWindowInsets(padding),
+                    .padding(padding)
+                    .consumeWindowInsets(padding),
             )
         }
     )
@@ -208,6 +210,7 @@ private fun PinnedMessagesListLoaded(
                 timelineItem = timelineItem,
                 timelineRoomInfo = state.timelineRoomInfo,
                 renderReadReceipts = false,
+                timelineProtectionState = state.timelineProtectionState,
                 isLastOutgoingMessage = false,
                 focusedEventId = null,
                 onUserDataClick = onUserDataClick,
@@ -225,6 +228,7 @@ private fun PinnedMessagesListLoaded(
                 eventContentView = { event, contentModifier, onContentLayoutChange ->
                     TimelineItemEventContentViewWrapper(
                         event = event,
+                        timelineProtectionState = state.timelineProtectionState,
                         onLinkClick = onLinkClick,
                         modifier = contentModifier,
                         onContentLayoutChange = onContentLayoutChange
@@ -238,6 +242,7 @@ private fun PinnedMessagesListLoaded(
 @Composable
 private fun TimelineItemEventContentViewWrapper(
     event: TimelineItem.Event,
+    timelineProtectionState: TimelineProtectionState,
     onLinkClick: (String) -> Unit,
     onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
     modifier: Modifier = Modifier,
@@ -251,6 +256,8 @@ private fun TimelineItemEventContentViewWrapper(
     } else {
         TimelineItemEventContentView(
             content = event.content,
+            hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+            onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
             onLinkClick = onLinkClick,
             eventSink = { },
             modifier = modifier,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
index d83e5266945..c55750dac5b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
@@ -57,7 +57,7 @@ class TimelineController @Inject constructor(
         return detachedTimeline.map { !it.isPresent }
     }
 
-    suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
+    suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Unit)) {
         currentTimelineFlow.value.run {
             block(this)
         }
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 819f7a1f3a8..b40e24b88a9 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 0781cc3ecec..f0e976e3685 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
@@ -57,6 +57,8 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
 import io.element.android.features.messages.impl.timeline.model.TimelineItem
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
+import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
 import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -65,13 +67,13 @@ import io.element.android.libraries.designsystem.theme.components.Icon
 import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter
 import io.element.android.libraries.matrix.api.core.EventId
 import io.element.android.libraries.matrix.api.core.UserId
-import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
 import io.element.android.libraries.ui.strings.CommonStrings
 import kotlinx.coroutines.launch
 
 @Composable
 fun TimelineView(
     state: TimelineState,
+    timelineProtectionState: TimelineProtectionState,
     onUserDataClick: (UserId) -> Unit,
     onLinkClick: (String) -> Unit,
     onMessageClick: (TimelineItem.Event) -> Unit,
@@ -114,10 +116,6 @@ fun TimelineView(
         state.eventSink(TimelineEvents.FocusOnEvent(eventId))
     }
 
-    fun onShieldClick(shield: MessageShield) {
-        state.eventSink(TimelineEvents.ShowShieldDialog(shield))
-    }
-
     // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
     AnimatedVisibility(visible = true, enter = fadeIn()) {
         Box(modifier) {
@@ -137,6 +135,7 @@ fun TimelineView(
                     TimelineItemRow(
                         timelineItem = timelineItem,
                         timelineRoomInfo = state.timelineRoomInfo,
+                        timelineProtectionState = timelineProtectionState,
                         renderReadReceipts = state.renderReadReceipts,
                         isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()),
                         focusedEventId = state.focusedEventId,
@@ -320,6 +319,7 @@ internal fun TimelineViewPreview(
                 ),
                 focusedEventIndex = 0,
             ),
+            timelineProtectionState = aTimelineProtectionState(),
             onUserDataClick = {},
             onLinkClick = {},
             onMessageClick = {},
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt
index 4566bf88bc4..806a81b7fe7 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 0db7a8ac525..1fa5e7f9a1b 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 5a5fd6d470b..67342203bdc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
@@ -70,6 +70,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
 import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
 import io.element.android.libraries.designsystem.components.EqualWidthColumn
 import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -108,6 +110,7 @@ private val BUBBLE_INCOMING_OFFSET = 16.dp
 fun TimelineItemEventRow(
     event: TimelineItem.Event,
     timelineRoomInfo: TimelineRoomInfo,
+    timelineProtectionState: TimelineProtectionState,
     renderReadReceipts: Boolean,
     isLastOutgoingMessage: Boolean,
     isHighlighted: Boolean,
@@ -126,6 +129,8 @@ fun TimelineItemEventRow(
     eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { contentModifier, onContentLayoutChange ->
         TimelineItemEventContentView(
             content = event.content,
+            hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+            onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
             onLinkClick = onLinkClick,
             eventSink = eventSink,
             modifier = contentModifier,
@@ -164,6 +169,7 @@ fun TimelineItemEventRow(
                     }
                     TimelineItemEventRowContent(
                         event = event,
+                        timelineProtectionState = timelineProtectionState,
                         isHighlighted = isHighlighted,
                         timelineRoomInfo = timelineRoomInfo,
                         interactionSource = interactionSource,
@@ -197,6 +203,7 @@ fun TimelineItemEventRow(
         } else {
             TimelineItemEventRowContent(
                 event = event,
+                timelineProtectionState = timelineProtectionState,
                 isHighlighted = isHighlighted,
                 timelineRoomInfo = timelineRoomInfo,
                 interactionSource = interactionSource,
@@ -252,6 +259,7 @@ private fun SwipeSensitivity(
 @Composable
 private fun TimelineItemEventRowContent(
     event: TimelineItem.Event,
+    timelineProtectionState: TimelineProtectionState,
     isHighlighted: Boolean,
     timelineRoomInfo: TimelineRoomInfo,
     interactionSource: MutableInteractionSource,
@@ -330,6 +338,7 @@ private fun TimelineItemEventRowContent(
         ) {
             MessageEventBubbleContent(
                 event = event,
+                timelineProtectionState = timelineProtectionState,
                 onMessageLongClick = onLongClick,
                 inReplyToClick = inReplyToClick,
                 eventSink = eventSink,
@@ -411,6 +420,7 @@ private fun MessageSenderInformation(
 @Composable
 private fun MessageEventBubbleContent(
     event: TimelineItem.Event,
+    timelineProtectionState: TimelineProtectionState,
     onMessageLongClick: () -> Unit,
     inReplyToClick: () -> Unit,
     eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@@ -560,7 +570,11 @@ private fun MessageEventBubbleContent(
                 .clip(RoundedCornerShape(6.dp))
                 // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent
                 .clickable(onClick = inReplyToClick)
-            InReplyToView(inReplyTo, modifier = inReplyToModifier)
+            InReplyToView(
+                inReplyTo = inReplyTo,
+                hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
+                modifier = inReplyToModifier,
+            )
         }
         if (inReplyToDetails != null) {
             // Use SubComposeLayout only if necessary as it can have consequences on the performance.
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
index 75f1ccc02cc..d280efa62c8 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
@@ -25,6 +25,9 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
 import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
 import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
 import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
+import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
 import io.element.android.libraries.matrix.api.core.EventId
@@ -34,6 +37,7 @@ import io.element.android.libraries.matrix.api.core.UserId
 fun TimelineItemGroupedEventsRow(
     timelineItem: TimelineItem.GroupedEvents,
     timelineRoomInfo: TimelineRoomInfo,
+    timelineProtectionState: TimelineProtectionState,
     renderReadReceipts: Boolean,
     isLastOutgoingMessage: Boolean,
     focusedEventId: EventId?,
@@ -52,6 +56,8 @@ fun TimelineItemGroupedEventsRow(
         { event, contentModifier, onContentLayoutChange ->
             TimelineItemEventContentView(
                 content = event.content,
+                hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+                onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
                 onLinkClick = onLinkClick,
                 eventSink = eventSink,
                 modifier = contentModifier,
@@ -70,6 +76,7 @@ fun TimelineItemGroupedEventsRow(
         onExpandGroupClick = ::onExpandGroupClick,
         timelineItem = timelineItem,
         timelineRoomInfo = timelineRoomInfo,
+        timelineProtectionState = timelineProtectionState,
         focusedEventId = focusedEventId,
         renderReadReceipts = renderReadReceipts,
         isLastOutgoingMessage = isLastOutgoingMessage,
@@ -94,6 +101,7 @@ private fun TimelineItemGroupedEventsRowContent(
     onExpandGroupClick: () -> Unit,
     timelineItem: TimelineItem.GroupedEvents,
     timelineRoomInfo: TimelineRoomInfo,
+    timelineProtectionState: TimelineProtectionState,
     focusedEventId: EventId?,
     renderReadReceipts: Boolean,
     isLastOutgoingMessage: Boolean,
@@ -112,6 +120,8 @@ private fun TimelineItemGroupedEventsRowContent(
         { event, contentModifier, onContentLayoutChange ->
             TimelineItemEventContentView(
                 content = event.content,
+                hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+                onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
                 onLinkClick = onLinkClick,
                 eventSink = eventSink,
                 modifier = contentModifier,
@@ -136,6 +146,7 @@ private fun TimelineItemGroupedEventsRowContent(
                     TimelineItemRow(
                         timelineItem = subGroupEvent,
                         timelineRoomInfo = timelineRoomInfo,
+                        timelineProtectionState = timelineProtectionState,
                         renderReadReceipts = renderReadReceipts,
                         isLastOutgoingMessage = isLastOutgoingMessage,
                         focusedEventId = focusedEventId,
@@ -178,6 +189,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
         onExpandGroupClick = {},
         timelineItem = events,
         timelineRoomInfo = aTimelineRoomInfo(),
+        timelineProtectionState = aTimelineProtectionState(),
         focusedEventId = events.events.first().eventId,
         renderReadReceipts = true,
         isLastOutgoingMessage = false,
@@ -202,6 +214,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
         onExpandGroupClick = {},
         timelineItem = aGroupedEvents(withReadReceipts = true),
         timelineRoomInfo = aTimelineRoomInfo(),
+        timelineProtectionState = aTimelineProtectionState(),
         focusedEventId = null,
         renderReadReceipts = true,
         isLastOutgoingMessage = false,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
index 56f509f9fb2..c7c1cb5350b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
@@ -26,6 +26,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
+import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
+import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
 import io.element.android.libraries.designsystem.text.toPx
 import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
 import io.element.android.libraries.matrix.api.core.EventId
@@ -37,6 +40,7 @@ internal fun TimelineItemRow(
     timelineRoomInfo: TimelineRoomInfo,
     renderReadReceipts: Boolean,
     isLastOutgoingMessage: Boolean,
+    timelineProtectionState: TimelineProtectionState,
     focusedEventId: EventId?,
     onUserDataClick: (UserId) -> Unit,
     onLinkClick: (String) -> Unit,
@@ -55,6 +59,8 @@ internal fun TimelineItemRow(
         { event, contentModifier, onContentLayoutChange ->
             TimelineItemEventContentView(
                 content = event.content,
+                hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+                onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
                 onLinkClick = onLinkClick,
                 eventSink = eventSink,
                 modifier = contentModifier,
@@ -109,9 +115,14 @@ internal fun TimelineItemRow(
                             event = timelineItem,
                             timelineRoomInfo = timelineRoomInfo,
                             renderReadReceipts = renderReadReceipts,
+                            timelineProtectionState = timelineProtectionState,
                             isLastOutgoingMessage = isLastOutgoingMessage,
                             isHighlighted = timelineItem.isEvent(focusedEventId),
-                            onClick = { onClick(timelineItem) },
+                            onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) {
+                                {}
+                            } else {
+                                { onClick(timelineItem) }
+                            },
                             onLongClick = { onLongClick(timelineItem) },
                             onLinkClick = onLinkClick,
                             onUserDataClick = onUserDataClick,
@@ -133,6 +144,7 @@ internal fun TimelineItemRow(
                 TimelineItemGroupedEventsRow(
                     timelineItem = timelineItem,
                     timelineRoomInfo = timelineRoomInfo,
+                    timelineProtectionState = timelineProtectionState,
                     renderReadReceipts = renderReadReceipts,
                     isLastOutgoingMessage = isLastOutgoingMessage,
                     focusedEventId = focusedEventId,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
index ffb398a0d18..f023a63c359 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
@@ -71,6 +71,8 @@ fun TimelineItemStateEventRow(
                 TimelineItemEventContentView(
                     content = event.content,
                     onLinkClick = {},
+                    hideMediaContent = false,
+                    onShowClick = {},
                     eventSink = eventSink,
                     modifier = Modifier.defaultTimelineContentPadding()
                 )
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt
index 48072ce5bf3..76777f4ae1b 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 49b8731ae0e..b24a5ca0be5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
@@ -35,6 +35,8 @@ import io.element.android.libraries.architecture.Presenter
 @Composable
 fun TimelineItemEventContentView(
     content: TimelineItemEventContent,
+    hideMediaContent: Boolean,
+    onShowClick: () -> Unit,
     onLinkClick: (url: String) -> Unit,
     eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
     modifier: Modifier = Modifier,
@@ -69,15 +71,21 @@ fun TimelineItemEventContentView(
         )
         is TimelineItemImageContent -> TimelineItemImageView(
             content = content,
+            hideMediaContent = hideMediaContent,
+            onShowClick = onShowClick,
             onContentLayoutChange = onContentLayoutChange,
             modifier = modifier,
         )
         is TimelineItemStickerContent -> TimelineItemStickerView(
             content = content,
+            hideMediaContent = hideMediaContent,
+            onShowClick = onShowClick,
             modifier = modifier,
         )
         is TimelineItemVideoContent -> TimelineItemVideoView(
             content = content,
+            hideMediaContent = hideMediaContent,
+            onShowClick = onShowClick,
             onContentLayoutChange = onContentLayoutChange,
             modifier = modifier
         )
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
index 9f9222f4586..85b2b7f6788 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
@@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
+import io.element.android.features.messages.impl.timeline.protection.ProtectedView
 import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -58,6 +59,8 @@ import io.element.android.wysiwyg.compose.EditorStyledText
 @Composable
 fun TimelineItemImageView(
     content: TimelineItemImageContent,
+    hideMediaContent: Boolean,
+    onShowClick: () -> Unit,
     onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
     modifier: Modifier = Modifier,
 ) {
@@ -76,23 +79,28 @@ fun TimelineItemImageView(
             modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
             aspectRatio = content.aspectRatio,
         ) {
-            var isLoaded by remember { mutableStateOf(false) }
-            AsyncImage(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
-                model = MediaRequestData(
-                    source = content.preferredMediaSource,
-                    kind = MediaRequestData.Kind.File(
-                        body = content.filename ?: content.body,
-                        mimeType = content.mimeType,
+            ProtectedView(
+                hideContent = hideMediaContent,
+                onShowClick = onShowClick,
+            ) {
+                var isLoaded by remember { mutableStateOf(false) }
+                AsyncImage(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
+                    model = MediaRequestData(
+                        source = content.preferredMediaSource,
+                        kind = MediaRequestData.Kind.File(
+                            body = content.filename ?: content.body,
+                            mimeType = content.mimeType,
+                        ),
                     ),
-                ),
-                contentScale = ContentScale.Fit,
-                alignment = Alignment.Center,
-                contentDescription = description,
-                onState = { isLoaded = it is AsyncImagePainter.State.Success },
-            )
+                    contentScale = ContentScale.Fit,
+                    alignment = Alignment.Center,
+                    contentDescription = description,
+                    onState = { isLoaded = it is AsyncImagePainter.State.Success },
+                )
+            }
         }
 
         if (content.showCaption) {
@@ -123,7 +131,23 @@ fun TimelineItemImageView(
 @PreviewsDayNight
 @Composable
 internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview {
-    TimelineItemImageView(content, {})
+    TimelineItemImageView(
+        content = content,
+        hideMediaContent = false,
+        onShowClick = {},
+        onContentLayoutChange = {},
+    )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
+    TimelineItemImageView(
+        content = aTimelineItemImageContent(),
+        hideMediaContent = true,
+        onShowClick = {},
+        onContentLayoutChange = {},
+    )
 }
 
 @PreviewsDayNight
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
index f895eddc85a..cef5acd1dd4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
@@ -7,44 +7,84 @@
 
 package io.element.android.features.messages.impl.timeline.components.event
 
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.compose.AsyncImagePainter
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
-import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
+import io.element.android.features.messages.impl.timeline.protection.ProtectedView
+import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
 import io.element.android.libraries.designsystem.preview.ElementPreview
 import io.element.android.libraries.designsystem.preview.PreviewsDayNight
 import io.element.android.libraries.matrix.ui.media.MediaRequestData
+import io.element.android.libraries.ui.strings.CommonStrings
 
 private const val STICKER_SIZE_IN_DP = 128
 
 @Composable
 fun TimelineItemStickerView(
     content: TimelineItemStickerContent,
+    hideMediaContent: Boolean,
+    onShowClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
-    Box(
-        modifier = modifier
-            .heightIn(min = STICKER_SIZE_IN_DP.dp, max = STICKER_SIZE_IN_DP.dp)
-            .aspectRatio(aspectRatio, false),
-        contentAlignment = Alignment.TopStart,
+    val description = content.body.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
+    Column(
+        modifier = modifier.semantics { contentDescription = description },
     ) {
-        BlurHashAsyncImage(
-            model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
-            blurHash = content.blurhash,
-        )
+        TimelineItemAspectRatioBox(
+            modifier = Modifier.blurHashBackground(content.blurhash, alpha = 0.9f),
+            aspectRatio = content.aspectRatio,
+            minHeight = STICKER_SIZE_IN_DP,
+            maxHeight = STICKER_SIZE_IN_DP,
+        ) {
+            ProtectedView(
+                hideContent = hideMediaContent,
+                onShowClick = onShowClick,
+            ) {
+                var isLoaded by remember { mutableStateOf(false) }
+                AsyncImage(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
+                    model = MediaRequestData(
+                        source = content.preferredMediaSource,
+                        kind = MediaRequestData.Kind.File(
+                            body = content.body,
+                            mimeType = content.mimeType,
+                        ),
+                    ),
+                    contentScale = ContentScale.Fit,
+                    alignment = Alignment.Center,
+                    contentDescription = description,
+                    onState = { isLoaded = it is AsyncImagePainter.State.Success },
+                )
+            }
+        }
     }
 }
 
 @PreviewsDayNight
 @Composable
 internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemStickerContentProvider::class) content: TimelineItemStickerContent) = ElementPreview {
-    TimelineItemStickerView(content)
+    TimelineItemStickerView(
+        content = content,
+        hideMediaContent = false,
+        onShowClick = {},
+    )
 }
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
index 7324d3368bc..7815e02610d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
@@ -51,6 +51,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
+import io.element.android.features.messages.impl.timeline.protection.ProtectedView
 import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
 import io.element.android.libraries.designsystem.modifiers.roundedBackground
 import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -64,6 +65,8 @@ import io.element.android.wysiwyg.compose.EditorStyledText
 @Composable
 fun TimelineItemVideoView(
     content: TimelineItemVideoContent,
+    hideMediaContent: Boolean,
+    onShowClick: () -> Unit,
     onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
     modifier: Modifier = Modifier,
 ) {
@@ -83,33 +86,38 @@ fun TimelineItemVideoView(
             aspectRatio = content.aspectRatio,
             contentAlignment = Alignment.Center,
         ) {
-            var isLoaded by remember { mutableStateOf(false) }
-            AsyncImage(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
-                model = MediaRequestData(
-                    source = content.thumbnailSource,
-                    kind = MediaRequestData.Kind.File(
-                        body = content.filename ?: content.body,
-                        mimeType = content.mimeType
-                    )
-                ),
-                contentScale = ContentScale.Fit,
-                alignment = Alignment.Center,
-                contentDescription = description,
-                onState = { isLoaded = it is AsyncImagePainter.State.Success },
-            )
-
-            Box(
-                modifier = Modifier.roundedBackground(),
-                contentAlignment = Alignment.Center,
+            ProtectedView(
+                hideContent = hideMediaContent,
+                onShowClick = onShowClick,
             ) {
-                Image(
-                    Icons.Default.PlayArrow,
-                    contentDescription = stringResource(id = CommonStrings.a11y_play),
-                    colorFilter = ColorFilter.tint(Color.White),
+                var isLoaded by remember { mutableStateOf(false) }
+                AsyncImage(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
+                    model = MediaRequestData(
+                        source = content.thumbnailSource,
+                        kind = MediaRequestData.Kind.File(
+                            body = content.filename ?: content.body,
+                            mimeType = content.mimeType
+                        )
+                    ),
+                    contentScale = ContentScale.Fit,
+                    alignment = Alignment.Center,
+                    contentDescription = description,
+                    onState = { isLoaded = it is AsyncImagePainter.State.Success },
                 )
+
+                Box(
+                    modifier = Modifier.roundedBackground(),
+                    contentAlignment = Alignment.Center,
+                ) {
+                    Image(
+                        Icons.Default.PlayArrow,
+                        contentDescription = stringResource(id = CommonStrings.a11y_play),
+                        colorFilter = ColorFilter.tint(Color.White),
+                    )
+                }
             }
         }
 
@@ -141,7 +149,23 @@ fun TimelineItemVideoView(
 @PreviewsDayNight
 @Composable
 internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview {
-    TimelineItemVideoView(content, {})
+    TimelineItemVideoView(
+        content = content,
+        hideMediaContent = false,
+        onShowClick = {},
+        onContentLayoutChange = {},
+    )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
+    TimelineItemVideoView(
+        content = aTimelineItemVideoContent(),
+        hideMediaContent = true,
+        onShowClick = {},
+        onContentLayoutChange = {},
+    )
 }
 
 @PreviewsDayNight
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt
index 0d90ec9d090..268cda18297 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<TimelineI
     override val values: Sequence<TimelineItemImageContent>
         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 adff977e32c..bf231bd7fab 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<Timelin
     override val values: Sequence<TimelineItemStickerContent>
         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 9d494e95e9d..510de5a1008 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<TimelineI
     override val values: Sequence<TimelineItemVideoContent>
         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 00000000000..6a9db31682d
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.protection
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@SuppressWarnings("ModifierClickableOrder")
+@Composable
+fun ProtectedView(
+    hideContent: Boolean,
+    onShowClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit,
+) {
+    if (hideContent) {
+        Box(
+            modifier = modifier
+                .fillMaxSize()
+                .background(Color(0x99000000)),
+            contentAlignment = Alignment.Center,
+        ) {
+            ElementTheme(darkTheme = false) {
+                // Not using a button to be able to have correct size
+                Text(
+                    modifier = Modifier
+                        .clip(RoundedCornerShape(percent = 50))
+                        .clickable(
+                            onClick = onShowClick,
+                            role = Role.Button,
+                        )
+                        .padding(4.dp)
+                        .border(
+                            width = 1.dp,
+                            color = ElementTheme.colors.borderInteractiveSecondary,
+                            shape = RoundedCornerShape(percent = 50),
+                        )
+                        .padding(
+                            horizontal = 16.dp,
+                            vertical = 4.dp,
+                        ),
+                    text = stringResource(CommonStrings.action_show),
+                    color = ElementTheme.colors.textOnSolidPrimary,
+                    style = ElementTheme.typography.fontBodyLgMedium,
+                )
+            }
+        }
+    } else {
+        content()
+    }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun ProtectedViewPreview() = ElementPreview {
+    Box(
+        modifier = Modifier
+            .size(160.dp)
+            .blurHashBackground(A_BLUR_HASH)
+    ) {
+        ProtectedView(
+            hideContent = true,
+            onShowClick = {},
+            content = {},
+        )
+    }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt
new file mode 100644
index 00000000000..824f3843ea7
--- /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 00000000000..78c878c3a49
--- /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 00000000000..5d83347e679
--- /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<TimelineProtectionState> {
+    @Composable
+    override fun present(): TimelineProtectionState {
+        val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false)
+        var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
+        val protectionState by remember(hideMediaContent) {
+            derivedStateOf {
+                if (hideMediaContent) {
+                    ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
+                } else {
+                    ProtectionState.RenderAll
+                }
+            }
+        }
+
+        fun handleEvent(event: TimelineProtectionEvent) {
+            when (event) {
+                is TimelineProtectionEvent.ShowContent -> {
+                    allowedEvents = allowedEvents + setOfNotNull(event.eventId)
+                }
+            }
+        }
+
+        return TimelineProtectionState(
+            protectionState = protectionState,
+            eventSink = { event -> handleEvent(event) }
+        )
+    }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt
new file mode 100644
index 00000000000..af1f26127c6
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.protection
+
+import androidx.compose.runtime.Immutable
+import io.element.android.libraries.matrix.api.core.EventId
+import kotlinx.collections.immutable.ImmutableSet
+
+data class TimelineProtectionState(
+    val protectionState: ProtectionState,
+    val eventSink: (TimelineProtectionEvent) -> Unit,
+) {
+    fun hideMediaContent(eventId: EventId?) = when (protectionState) {
+        is ProtectionState.RenderAll -> false
+        is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds
+    }
+}
+
+@Immutable
+sealed interface ProtectionState {
+    data object RenderAll : ProtectionState
+    data class RenderOnly(val eventIds: ImmutableSet<EventId>) : 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 00000000000..f0c2acbaa7d
--- /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 77602c4e127..6d8572a5168 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 af7c05f592a..a113a80a1f1 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 f4b599a504e..e4c86411978 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<Unit>,
     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 34918c6ea4d..601ed2ee7a3 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<Unit> = 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 f2e427e3be8..dc06036c433 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/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 bea09115440..0225a447500 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 b7ba762ce6f..1da14d38394 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 6fe11ec254e..70a30c809a8 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/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 f05b56614ac..ecdcca780bc 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<Boolean>
 
+    suspend fun setHideImagesAndVideos(value: Boolean)
+    fun doesHideImagesAndVideosFlow(): Flow<Boolean>
+
     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 ab985e798d4..d9cbc6cc033 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<Boolean> {
+        return store.data.map { prefs ->
+            prefs[hideImagesAndVideosKey] ?: false
+        }
+    }
+
     override suspend fun reset() {
         store.edit { it.clear() }
     }
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 283b28ab310..08e95fcfcf5 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
@@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
 import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
 import io.element.android.libraries.matrix.ui.messages.toPlainText
+import io.element.android.libraries.preferences.api.store.AppPreferencesStore
 import io.element.android.libraries.push.impl.R
 import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
 import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@@ -45,6 +46,7 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
 import io.element.android.libraries.ui.strings.CommonStrings
 import io.element.android.services.toolbox.api.strings.StringProvider
 import io.element.android.services.toolbox.api.systemclock.SystemClock
+import kotlinx.coroutines.flow.first
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -69,6 +71,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
     @ApplicationContext private val context: Context,
     private val permalinkParser: PermalinkParser,
     private val callNotificationEventResolver: CallNotificationEventResolver,
+    private val appPreferencesStore: AppPreferencesStore,
 ) : NotifiableEventResolver {
     override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
         // Restore session
@@ -103,7 +106,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
                     timestamp = this.timestamp,
                     senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
                     body = messageBody,
-                    imageUriString = fetchImageIfPresent(client)?.toString(),
+                    imageUriString = content.fetchImageIfPresent(client)?.toString(),
                     roomName = roomDisplayName,
                     roomIsDm = isDm,
                     roomAvatarPath = roomAvatarUrl,
@@ -148,7 +151,6 @@ class DefaultNotifiableEventResolver @Inject constructor(
                     timestamp = this.timestamp,
                     senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
                     body = stringProvider.getString(CommonStrings.common_call_invite),
-                    imageUriString = fetchImageIfPresent(client)?.toString(),
                     roomName = roomDisplayName,
                     roomIsDm = isDm,
                     roomAvatarPath = roomAvatarUrl,
@@ -288,22 +290,21 @@ class DefaultNotifiableEventResolver @Inject constructor(
         }
     }
 
-    private suspend fun NotificationData.fetchImageIfPresent(client: MatrixClient): Uri? {
-        val fileResult = when (val content = this.content) {
-            is NotificationContent.MessageLike.RoomMessage -> {
-                when (val messageType = content.messageType) {
-                    is ImageMessageType -> notificationMediaRepoFactory.create(client)
-                        .getMediaFile(
-                            mediaSource = messageType.source,
-                            mimeType = messageType.info?.mimetype,
-                            body = messageType.body,
-                        )
-                    is VideoMessageType -> null // Use the thumbnail here?
-                    else -> null
-                }
-            }
+    private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? {
+        if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) {
+            return null
+        }
+        val fileResult = when (val messageType = messageType) {
+            is ImageMessageType -> notificationMediaRepoFactory.create(client)
+                .getMediaFile(
+                    mediaSource = messageType.source,
+                    mimeType = messageType.info?.mimetype,
+                    body = messageType.body,
+                )
+            is VideoMessageType -> null // Use the thumbnail here?
             else -> null
-        } ?: return null
+        }
+            ?: return null
 
         return fileResult
             .onFailure {
diff --git a/libraries/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 a8d22330d30..f9988adfba2 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 a71461e3380..1f2e041ac5e 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 69ff2eeb48d..fafa65e64fe 100644
--- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt
+++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt
@@ -27,7 +27,8 @@ sealed interface MessageComposerMode {
     ) : Special
 
     data class Reply(
-        val replyToDetails: InReplyToDetails
+        val replyToDetails: InReplyToDetails,
+        val hideImage: Boolean,
     ) : Special {
         val eventId: EventId = replyToDetails.eventId()
     }
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 11e1ab922ae..3fb2adc584f 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 @@
   <string name="action_send_message">"Send message"</string>
   <string name="action_share">"Share"</string>
   <string name="action_share_link">"Share link"</string>
+  <string name="action_show">"Show"</string>
   <string name="action_sign_in_again">"Sign in again"</string>
   <string name="action_signout">"Sign out"</string>
   <string name="action_signout_anyway">"Sign out anyway"</string>


Test patch:
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 79792f4db1c..209d719b82a 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
@@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
 import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
+import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
 import io.element.android.features.messages.impl.typing.aTypingNotificationState
 import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
 import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
@@ -1059,12 +1060,12 @@ class MessagesPresenterTest {
         val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
         val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
         val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
-
         return MessagesPresenter(
             room = matrixRoom,
             composerPresenter = messageComposerPresenter,
             voiceMessageComposerPresenter = voiceMessageComposerPresenter,
             timelinePresenterFactory = timelinePresenterFactory,
+            timelineProtectionPresenter = { aTimelineProtectionState() },
             actionListPresenterFactory = FakeActionListPresenter.Factory,
             customReactionPresenter = customReactionPresenter,
             reactionSummaryPresenter = reactionSummaryPresenter,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
index 6ec4cdab6a1..df0cc1bdc27 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 b7c3a09c5cd..b8715568d1f 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt
@@ -14,6 +14,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
 import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
 import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
 import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
 import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -309,6 +310,7 @@ class PinnedMessagesListPresenterTest {
             room = room,
             timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
             timelineProvider = timelineProvider,
+            timelineProtectionPresenter = { aTimelineProtectionState() },
             snackbarDispatcher = SnackbarDispatcher(),
             actionListPresenterFactory = FakeActionListPresenter.Factory,
             analyticsService = analyticsService,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
index 88f4edf0039..315abd83020 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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
     setSafeContent {
         TimelineView(
             state = state,
+            timelineProtectionState = timelineProtectionState,
             onUserDataClick = onUserDataClick,
             onLinkClick = onLinkClick,
             onMessageClick = onMessageClick,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt
new file mode 100644
index 00000000000..5d82b292bf7
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.protection
+
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.lambda.lambdaError
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ProtectedViewTest {
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+    @Test
+    fun `when hideContent is false, the content is rendered`() {
+        rule.setProtectedView(
+            hideContent = false,
+            content = {
+                Text("Hello")
+            }
+        )
+        rule.onNodeWithText("Hello").assertExists()
+    }
+
+    @Test
+    fun `when hideContent is true, the content is not rendered, and user can reveal it`() {
+        ensureCalledOnce {
+            rule.setProtectedView(
+                hideContent = true,
+                onShowClick = it,
+                content = {
+                    Text("Hello")
+                }
+            )
+            rule.onNodeWithText("Hello").assertDoesNotExist()
+            rule.clickOn(CommonStrings.action_show)
+        }
+    }
+}
+
+private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setProtectedView(
+    hideContent: Boolean = false,
+    onShowClick: () -> Unit = { lambdaError() },
+    content: @Composable () -> Unit = {},
+) {
+    setContent {
+        ProtectedView(
+            hideContent = hideContent,
+            onShowClick = onShowClick,
+            content = content
+        )
+    }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt
new file mode 100644
index 00000000000..97b97ac3d53
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.protection
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.preferences.api.store.AppPreferencesStore
+import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.test
+import kotlinx.collections.immutable.persistentSetOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+class TimelineProtectionPresenterTest {
+    @get:Rule
+    val warmUpRule = WarmUpRule()
+
+    @Test
+    fun `present - initial state`() = runTest {
+        val presenter = createPresenter()
+        presenter.test {
+            val initialState = awaitItem()
+            assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll)
+        }
+    }
+
+    @Test
+    fun `present - protected`() = runTest {
+        val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true)
+        val presenter = createPresenter(appPreferencesStore)
+        presenter.test {
+            skipItems(1)
+            val initialState = awaitItem()
+            assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
+            // ShowContent with null should have no effect.
+            initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
+            initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
+            val finalState = awaitItem()
+            assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
+        }
+    }
+
+    private fun createPresenter(
+        appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
+    ) = TimelineProtectionPresenter(
+        appPreferencesStore = appPreferencesStore,
+    )
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt
new file mode 100644
index 00000000000..81187eb6d4b
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.protection
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
+import kotlinx.collections.immutable.persistentSetOf
+import org.junit.Test
+
+class TimelineProtectionStateTest {
+    @Test
+    fun `when protectionState is RenderAll, hideMediaContent always return null`() {
+        val sut = aTimelineProtectionState(
+            protectionState = ProtectionState.RenderAll
+        )
+        assertThat(sut.hideMediaContent(null)).isFalse()
+        assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse()
+    }
+
+    @Test
+    fun `when protectionState is RenderOnly with empty set, hideMediaContent always return true`() {
+        val sut = aTimelineProtectionState(
+            protectionState = ProtectionState.RenderOnly(persistentSetOf())
+        )
+        assertThat(sut.hideMediaContent(null)).isTrue()
+        assertThat(sut.hideMediaContent(AN_EVENT_ID)).isTrue()
+    }
+
+    @Test
+    fun `when protectionState is RenderOnly with an Event, hideMediaContent can return true or false`() {
+        val sut = aTimelineProtectionState(
+            protectionState = ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID))
+        )
+        assertThat(sut.hideMediaContent(null)).isTrue()
+        assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse()
+        assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue()
+    }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
index 70117c6162e..b13e2a4fb10 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
@@ -17,13 +17,12 @@ import app.cash.turbine.TurbineTestContext
 import app.cash.turbine.test
 import com.google.common.truth.Truth.assertThat
 import im.vector.app.features.analytics.plan.Composer
+import io.element.android.features.messages.impl.messagecomposer.aReplyMode
 import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
 import io.element.android.features.messages.test.FakeMessageComposerContext
 import io.element.android.libraries.matrix.api.core.ProgressCallback
-import io.element.android.libraries.matrix.test.AN_EVENT_ID
 import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
 import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
 import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
 import io.element.android.libraries.mediaupload.api.MediaSender
 import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
@@ -708,8 +707,6 @@ class VoiceMessageComposerPresenterTest {
         )
 }
 
-private fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID))
-
 private fun aVoiceMessageComposerEvent(
     isReply: Boolean = false
 ) = Composer(
diff --git a/features/preferences/impl/src/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 0b1ce2cfdba..128f4aa7059 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 82e8df69930..f6319007d41 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<DeveloperSettingsEvents>()
@@ -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<DeveloperSettingsEvents>()
+        rule.setDeveloperSettingsView(
+            state = aDeveloperSettingsState(
+                eventSink = eventsRecorder
+            ),
+        )
+        rule.onNodeWithText("Hide image & video previews").performClick()
+        eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
+    }
 }
 
 private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(
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 26e7df96311..9e4318b3be0 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/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 c4591c93551..36ad0f9363e 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<Boolean> {
+        return hideImagesAndVideos
+    }
+
     override suspend fun reset() {
         // No op
     }
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 de74d1d7a90..aae4d84d382 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<NotificationData?> = Result.success(null),
+        appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
     ): DefaultNotifiableEventResolver {
         val context = RuntimeEnvironment.getApplication() as Context
         notificationService?.givenGetNotificationResult(notificationResult)
@@ -821,6 +824,7 @@ class DefaultNotifiableEventResolverTest {
             callNotificationEventResolver = DefaultCallNotificationEventResolver(
                 stringProvider = AndroidStringProvider(context.resources)
             ),
+            appPreferencesStore = appPreferencesStore,
         )
     }
 }
diff --git a/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 4f5c2d9c2a2..ac3474e7f98 100644
--- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt
+++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt
@@ -118,6 +118,8 @@ class KonsistPreviewTest {
                 "TimelineItemEventRowWithReplyPreview",
                 "TimelineItemGroupedEventsRowContentCollapsePreview",
                 "TimelineItemGroupedEventsRowContentExpandedPreview",
+                "TimelineItemImageViewHideMediaContentPreview",
+                "TimelineItemVideoViewHideMediaContentPreview",
                 "TimelineItemVoiceViewUnifiedPreview",
                 "TimelineVideoWithCaptionRowPreview",
                 "TimelineViewMessageShieldPreview",
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png
new file mode 100644
index 00000000000..0103ed25484
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78ea6cbfcf12e405eca8b953b3d847e73f80121ad47beb6346563a2e9b5d567b
+size 56342
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png
new file mode 100644
index 00000000000..9e571c9b940
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:047da66e7f7e78478b1d8442028224073e3d31493b5facf926472fc526532be1
+size 56473
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png
new file mode 100644
index 00000000000..1b6fb4bab8e
--- /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 00000000000..d6fd8eeb701
--- /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 78579bbd4a5..40ad608500e 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 f79503d7806..f1d9752b9eb 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 d9ee17d120c..4238d4bab88 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 00000000000..1b6fb4bab8e
--- /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 d96328629f2..1db3339236d 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 593fb030c1c..c6c6efe376a 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 0e4582b9ed8..94bac0a1610 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 00000000000..d6fd8eeb701
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
+size 3659
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png
new file mode 100644
index 00000000000..0103ed25484
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78ea6cbfcf12e405eca8b953b3d847e73f80121ad47beb6346563a2e9b5d567b
+size 56342
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png
new file mode 100644
index 00000000000..9e571c9b940
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:047da66e7f7e78478b1d8442028224073e3d31493b5facf926472fc526532be1
+size 56473
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png
new file mode 100644
index 00000000000..a1719dd0a54
--- /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 00000000000..03bc461c8cc
--- /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 00000000000..6dd8676eee5
--- /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 00000000000..3c4052880c4
--- /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 d46d57762de..c02cae3bafb 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 d46d57762de..c02cae3bafb 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 5119090213e..15296e31b68 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 2294fd4c8ac..78295963cb0 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 2294fd4c8ac..78295963cb0 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 1e6522216aa..d32653f9fbd 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