Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ object LearnMoreConfig {
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
const val HISTORY_VISIBLE_URL: String = "https://element.io/en/help#e2ee-history-sharing"
}
1 change: 1 addition & 0 deletions features/messages/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.sigpwned.emoji4j)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvent
Expand Down Expand Up @@ -107,6 +108,7 @@ class MessagesPresenter(
@Assisted private val timelinePresenter: Presenter<TimelineState>,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
private val historyVisibleStatePresenter: Presenter<HistoryVisibleState>,
private val linkPresenter: Presenter<LinkState>,
@Assisted private val actionListPresenter: Presenter<ActionListState>,
private val customReactionPresenter: Presenter<CustomReactionState>,
Expand Down Expand Up @@ -158,6 +160,7 @@ class MessagesPresenter(
val timelineState = timelinePresenter.present()
val timelineProtectionState = timelineProtectionPresenter.present()
val identityChangeState = identityChangeStatePresenter.present()
val historyVisibleState = historyVisibleStatePresenter.present()
val actionListState = actionListPresenter.present()
val linkState = linkPresenter.present()
val customReactionState = customReactionPresenter.present()
Expand Down Expand Up @@ -278,6 +281,7 @@ class MessagesPresenter(
timelineState = timelineState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
actionListState = actionListState,
customReactionState = customReactionState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl

import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
Expand Down Expand Up @@ -40,6 +41,7 @@ data class MessagesState(
val timelineState: TimelineState,
val timelineProtectionState: TimelineProtectionState,
val identityChangeState: IdentityChangeState,
val historyVisibleState: HistoryVisibleState,
val linkState: LinkState,
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
Expand Down Expand Up @@ -103,6 +105,7 @@ fun aMessagesState(
),
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
identityChangeState: IdentityChangeState = anIdentityChangeState(),
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
linkState: LinkState = aLinkState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
Expand All @@ -125,6 +128,7 @@ fun aMessagesState(
voiceMessageComposerState = voiceMessageComposerState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
timelineState = timelineState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleStateView
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
import io.element.android.features.messages.impl.link.LinkEvents
import io.element.android.features.messages.impl.link.LinkView
Expand Down Expand Up @@ -476,6 +477,10 @@ private fun MessagesViewComposerBottomSheetContents(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
HistoryVisibleStateView(
state = state.historyVisibleState,
onLinkClick = onLinkClick,
)
}
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
it.identityState == IdentityState.VerificationViolation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

interface HistoryVisibleAcknowledgementRepository {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the correct naming scheme to use?

fun hasAcknowledged(roomId: RoomId): Flow<Boolean>
suspend fun setAcknowledged(roomId: RoomId, value: Boolean)
}

@ContributesBinding(SessionScope::class)
class DefaultHistoryVisibleAcknowledgementRepository(
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : HistoryVisibleAcknowledgementRepository {
val store = preferenceDataStoreFactory.create("elementx_historyvisible")

override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
return store.data.map { prefs ->
val acknowledged = prefs[booleanPreferencesKey(roomId.value)] ?: false
acknowledged
}
}

override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
store.edit { prefs ->
prefs[booleanPreferencesKey(roomId.value)] = value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

sealed interface HistoryVisibleEvent {
data object Acknowledge : HistoryVisibleEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

data class HistoryVisibleState(
val showAlert: Boolean,
val eventSink: (HistoryVisibleEvent) -> Unit,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import kotlinx.coroutines.launch

@Inject
class HistoryVisibleStatePresenter(
private val repository: HistoryVisibleAcknowledgementRepository,
private val room: JoinedRoom,
) : Presenter<HistoryVisibleState> {
@Composable
override fun present(): HistoryVisibleState {
val roomInfo by room.roomInfoFlow.collectAsState()
// Implicitly acknowledge the initial event to avoid flashes in UI.
val acknowledged by repository.hasAcknowledged(room.roomId).collectAsState(initial = true)

val coroutineScope = rememberCoroutineScope()

LaunchedEffect(roomInfo.historyVisibility) {
if (roomInfo.historyVisibility == RoomHistoryVisibility.Joined && acknowledged) {
repository.setAcknowledged(room.roomId, false)
}
}

return HistoryVisibleState(
showAlert = roomInfo.historyVisibility != RoomHistoryVisibility.Joined && roomInfo.isEncrypted == true && !acknowledged,
eventSink = { event ->
when (event) {
is HistoryVisibleEvent.Acknowledge ->
coroutineScope.launch {
repository.setAcknowledged(room.roomId, true)
}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

import androidx.compose.ui.tooling.preview.PreviewParameterProvider

class HistoryVisibleStateProvider : PreviewParameterProvider<HistoryVisibleState> {
override val values: Sequence<HistoryVisibleState>
get() = sequenceOf(
aHistoryVisibleState(showAlert = false),
aHistoryVisibleState(showAlert = true),
)
}

internal fun aHistoryVisibleState(
showAlert: Boolean = false,
eventSink: (HistoryVisibleEvent) -> Unit = {},
) = HistoryVisibleState(
showAlert,
eventSink = eventSink,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertLevel
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings

@Composable
fun HistoryVisibleStateView(
state: HistoryVisibleState,
onLinkClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
if (!state.showAlert) {
return
}

ComposerAlertMolecule(
modifier = modifier,
avatar = null,
showIcon = true,
level = ComposerAlertLevel.Info,
content = buildAnnotatedString {
val learnMoreStr = stringResource(CommonStrings.action_learn_more)
val fullText = stringResource(R.string.crypto_history_visible, learnMoreStr)
append(fullText)
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Bold,
color = ElementTheme.colors.textPrimary
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
addLink(
url = LinkAnnotation.Url(
url = LearnMoreConfig.HISTORY_VISIBLE_URL,
linkInteractionListener = {
onLinkClick(LearnMoreConfig.HISTORY_VISIBLE_URL, true)
}
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
},
submitText = stringResource(CommonStrings.action_dismiss),
onSubmitClick = { state.eventSink(HistoryVisibleEvent.Acknowledge) },
)
}

@PreviewsDayNight
@Composable
internal fun HistoryVisibleStateViewPreview(
@PreviewParameter(HistoryVisibleStateProvider::class) state: HistoryVisibleState,
) = ElementPreview {
HistoryVisibleStateView(
state = state,
onLinkClick = { _, _ -> },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.messages.impl.crypto.historyvisible

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.MessagesView
import io.element.android.features.messages.impl.aMessagesState
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown

@PreviewsDayNight
@Composable
internal fun MessagesViewWithHistoryVisiblePreview(
@PreviewParameter(HistoryVisibleStateProvider::class) historyVisibleState: HistoryVisibleState
) = ElementPreview {
MessagesView(
state = aMessagesState(
composerState = aMessageComposerState(
textEditorState = aTextEditorStateMarkdown(
initialText = "",
initialFocus = false,
)
),
historyVisibleState = historyVisibleState,
),
onBackClick = {},
onRoomDetailsClick = {},
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onLinkClick = { _, _ -> },
onSendLocationClick = {},
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {}
)
}
Loading
Loading