Skip to content

Commit b0189d2

Browse files
committed
feat: Add visible history alert to encrypted rooms.
- Adds a dismissable alert that is displayed whenever the user opens a room with `history_visibility` != `joined`. When cleared, this is recorded in the app's data store. - When opening a room with `history_visibility` = `joined`, this flag is cleared.` Issue: element-hq/element-meta#2875
1 parent cbbd0ff commit b0189d2

File tree

18 files changed

+405
-0
lines changed

18 files changed

+405
-0
lines changed

appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ object LearnMoreConfig {
1212
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
1313
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
1414
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
15+
const val HISTORY_VISIBLE_URL: String = "https://example.com"
1516
}

features/messages/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ dependencies {
6666
implementation(libs.jsoup)
6767
implementation(libs.androidx.constraintlayout)
6868
implementation(libs.androidx.constraintlayout.compose)
69+
implementation(libs.androidx.datastore.preferences)
6970
implementation(libs.androidx.media3.exoplayer)
7071
implementation(libs.androidx.media3.ui)
7172
implementation(libs.sigpwned.emoji4j)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.element.android.features.messages.api.timeline.HtmlConverterProvider
3131
import io.element.android.features.messages.impl.actionlist.ActionListEvents
3232
import io.element.android.features.messages.impl.actionlist.ActionListState
3333
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
34+
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
3435
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
3536
import io.element.android.features.messages.impl.link.LinkState
3637
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
@@ -106,6 +107,7 @@ class MessagesPresenter(
106107
@Assisted private val timelinePresenter: Presenter<TimelineState>,
107108
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
108109
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
110+
private val historyVisibleStatePresenter: Presenter<HistoryVisibleState>,
109111
private val linkPresenter: Presenter<LinkState>,
110112
@Assisted private val actionListPresenter: Presenter<ActionListState>,
111113
private val customReactionPresenter: Presenter<CustomReactionState>,
@@ -157,6 +159,7 @@ class MessagesPresenter(
157159
val timelineState = timelinePresenter.present()
158160
val timelineProtectionState = timelineProtectionPresenter.present()
159161
val identityChangeState = identityChangeStatePresenter.present()
162+
val historyVisibleState = historyVisibleStatePresenter.present()
160163
val actionListState = actionListPresenter.present()
161164
val linkState = linkPresenter.present()
162165
val customReactionState = customReactionPresenter.present()
@@ -277,6 +280,7 @@ class MessagesPresenter(
277280
timelineState = timelineState,
278281
timelineProtectionState = timelineProtectionState,
279282
identityChangeState = identityChangeState,
283+
historyVisibleState = historyVisibleState,
280284
linkState = linkState,
281285
actionListState = actionListState,
282286
customReactionState = customReactionState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl
1010
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
1111
import io.element.android.features.messages.impl.actionlist.ActionListState
1212
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
13+
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
1314
import io.element.android.features.messages.impl.link.LinkState
1415
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
1516
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
@@ -39,6 +40,7 @@ data class MessagesState(
3940
val timelineState: TimelineState,
4041
val timelineProtectionState: TimelineProtectionState,
4142
val identityChangeState: IdentityChangeState,
43+
val historyVisibleState: HistoryVisibleState,
4244
val linkState: LinkState,
4345
val actionListState: ActionListState,
4446
val customReactionState: CustomReactionState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
1313
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
1414
import io.element.android.features.messages.impl.actionlist.ActionListState
1515
import io.element.android.features.messages.impl.actionlist.anActionListState
16+
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
17+
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
1618
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
1719
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
1820
import io.element.android.features.messages.impl.link.LinkState
@@ -102,6 +104,7 @@ fun aMessagesState(
102104
),
103105
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
104106
identityChangeState: IdentityChangeState = anIdentityChangeState(),
107+
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
105108
linkState: LinkState = aLinkState(),
106109
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
107110
actionListState: ActionListState = anActionListState(),
@@ -124,6 +127,7 @@ fun aMessagesState(
124127
voiceMessageComposerState = voiceMessageComposerState,
125128
timelineProtectionState = timelineProtectionState,
126129
identityChangeState = identityChangeState,
130+
historyVisibleState = historyVisibleState,
127131
linkState = linkState,
128132
timelineState = timelineState,
129133
readReceiptBottomSheetState = readReceiptBottomSheetState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
4646
import io.element.android.features.messages.impl.actionlist.ActionListEvents
4747
import io.element.android.features.messages.impl.actionlist.ActionListView
4848
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
49+
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleStateView
4950
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
5051
import io.element.android.features.messages.impl.link.LinkEvents
5152
import io.element.android.features.messages.impl.link.LinkView
@@ -470,6 +471,10 @@ private fun MessagesViewComposerBottomSheetContents(
470471
state = state.identityChangeState,
471472
onLinkClick = onLinkClick,
472473
)
474+
HistoryVisibleStateView(
475+
state = state.historyVisibleState,
476+
onLinkClick = onLinkClick,
477+
)
473478
}
474479
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
475480
it.identityState == IdentityState.VerificationViolation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.historyvisible
9+
10+
import androidx.datastore.preferences.core.booleanPreferencesKey
11+
import androidx.datastore.preferences.core.edit
12+
import dev.zacsweers.metro.ContributesBinding
13+
import io.element.android.libraries.di.SessionScope
14+
import io.element.android.libraries.matrix.api.core.RoomId
15+
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
16+
import kotlinx.coroutines.flow.Flow
17+
import kotlinx.coroutines.flow.map
18+
19+
interface HistoryVisibleAcknowledgementRepository {
20+
fun hasAcknowledged(roomId: RoomId): Flow<Boolean>
21+
suspend fun setAcknowledged(roomId: RoomId, value: Boolean)
22+
}
23+
24+
@ContributesBinding(SessionScope::class)
25+
class DefaultHistoryVisibleAcknowledgementRepository(
26+
preferenceDataStoreFactory: PreferenceDataStoreFactory,
27+
) : HistoryVisibleAcknowledgementRepository {
28+
val store = preferenceDataStoreFactory.create("elementx_historyvisible")
29+
30+
override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
31+
return store.data.map { prefs ->
32+
val acknowledged = prefs[booleanPreferencesKey(roomId.value)] ?: false
33+
acknowledged
34+
}
35+
}
36+
37+
override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
38+
store.edit { prefs ->
39+
prefs[booleanPreferencesKey(roomId.value)] = value
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.historyvisible
9+
10+
sealed interface HistoryVisibleEvent {
11+
data object Acknowledge : HistoryVisibleEvent
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.historyvisible
9+
10+
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
11+
12+
data class HistoryVisibleState(
13+
val roomHistoryVisibility: RoomHistoryVisibility,
14+
val roomIsEncrypted: Boolean,
15+
val acknowledged: Boolean,
16+
val eventSink: (HistoryVisibleEvent) -> Unit,
17+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.crypto.historyvisible
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.LaunchedEffect
12+
import androidx.compose.runtime.collectAsState
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.rememberCoroutineScope
15+
import dev.zacsweers.metro.Inject
16+
import io.element.android.libraries.architecture.Presenter
17+
import io.element.android.libraries.matrix.api.room.JoinedRoom
18+
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
19+
import kotlinx.coroutines.launch
20+
21+
@Inject
22+
class HistoryVisibleStatePresenter(
23+
private val repository: HistoryVisibleAcknowledgementRepository,
24+
private val room: JoinedRoom,
25+
) : Presenter<HistoryVisibleState> {
26+
@Composable
27+
override fun present(): HistoryVisibleState {
28+
val roomInfo by room.roomInfoFlow.collectAsState()
29+
// Implicitly acknowledge the initial event to avoid flashes in UI.
30+
val acknowledged by repository.hasAcknowledged(room.roomId).collectAsState(initial = true)
31+
32+
val coroutineScope = rememberCoroutineScope()
33+
34+
LaunchedEffect(roomInfo.historyVisibility) {
35+
if (roomInfo.historyVisibility == RoomHistoryVisibility.Joined && acknowledged) {
36+
repository.setAcknowledged(room.roomId, false)
37+
}
38+
}
39+
40+
return HistoryVisibleState(
41+
roomHistoryVisibility = roomInfo.historyVisibility,
42+
roomIsEncrypted = roomInfo.isEncrypted == true,
43+
acknowledged = acknowledged,
44+
eventSink = { event ->
45+
when (event) {
46+
is HistoryVisibleEvent.Acknowledge ->
47+
coroutineScope.launch {
48+
repository.setAcknowledged(room.roomId, true)
49+
}
50+
}
51+
}
52+
)
53+
}
54+
}

0 commit comments

Comments
 (0)