Add alert to encrypted rooms with visible history (Android). (#5709)

* 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: https://github.com/element-hq/element-meta/issues/2875

* chore: Fix linting issues.

* feat: Move alert showing logic into state presenter.

* chore: Fix linting issues.

* tests: Fixup tests.

* feat: Use real link.

* chore: Update license header.

* chore: Add (c) to license headers.

* chore: Add `.` to license header.

* feat: Lock alert behind history sharing developer setting.

* ci: Trigger record screenshots

* feat: Create repo key using session ID for multi-account support.

* feat: Use session ID hash for constructing data store.

* tests: Correct and update tests.

* tests: Update snapshots.

* feat: Prevent identity alert from displaying with history visibility alert.

* feat: Tidy up HistoryVisibleStatePresenter logic, update previews.

* chore: Remove unused import.

* chore: Update screenshots.

* feat: Add translation string.

* chore: Remove redundant temporary translation file.
This commit is contained in:
Skye Elliot
2025-12-08 20:19:15 +00:00
committed by GitHub
parent 374c43794a
commit d0ba02dd7e
32 changed files with 541 additions and 4 deletions

View File

@@ -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)

View File

@@ -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
@@ -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>,
@@ -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()
@@ -278,6 +281,7 @@ class MessagesPresenter(
timelineState = timelineState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
actionListState = actionListState,
customReactionState = customReactionState,

View File

@@ -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
@@ -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,

View File

@@ -14,7 +14,10 @@ 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.aRoomMemberIdentityStateChange
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.link.aLinkState
@@ -48,6 +51,7 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -83,6 +87,19 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
)
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
historyVisibleState = aHistoryVisibleState(showAlert = true)
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange())),
historyVisibleState = aHistoryVisibleState(showAlert = true)
)
)
}
@@ -103,6 +120,7 @@ fun aMessagesState(
),
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
identityChangeState: IdentityChangeState = anIdentityChangeState(),
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
linkState: LinkState = aLinkState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
@@ -125,6 +143,7 @@ fun aMessagesState(
voiceMessageComposerState = voiceMessageComposerState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
timelineState = timelineState,
readReceiptBottomSheetState = readReceiptBottomSheetState,

View File

@@ -53,6 +53,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
@@ -486,10 +487,17 @@ private fun MessagesViewComposerBottomSheetContents(
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
if (state.composerState.suggestions.isEmpty() &&
state.composerState.textEditorState is TextEditorState.Markdown) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
if (state.identityChangeState.roomMemberIdentityStateChanges.isNotEmpty()) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
} else {
HistoryVisibleStateView(
state = state.historyVisibleState,
onLinkClick = onLinkClick,
)
}
}
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
it.identityState == IdentityState.VerificationViolation

View File

@@ -0,0 +1,48 @@
/*
* 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.androidutils.hash.hash
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface HistoryVisibleAcknowledgementRepository {
fun hasAcknowledged(roomId: RoomId): Flow<Boolean>
suspend fun setAcknowledged(roomId: RoomId, value: Boolean)
}
@ContributesBinding(SessionScope::class)
class DefaultHistoryVisibleAcknowledgementRepository(
sessionId: SessionId,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : HistoryVisibleAcknowledgementRepository {
val store =
sessionId.value.hash().take(16).let { hash ->
preferenceDataStoreFactory.create("elementx_historyvisible_$hash")
}
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
}
}
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -0,0 +1,62 @@
/*
* 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.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@Inject
class HistoryVisibleStatePresenter(
private val featureFlagService: FeatureFlagService,
private val repository: HistoryVisibleAcknowledgementRepository,
private val room: JoinedRoom,
) : Presenter<HistoryVisibleState> {
@Composable
override fun present(): HistoryVisibleState {
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
val roomInfo by room.roomInfoFlow.collectAsState()
// Implicitly assume the alert is initially acknowledged to avoid flashes in UI.
val acknowledged by repository.hasAcknowledged(room.roomId).collectAsState(initial = true)
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(roomInfo.historyVisibility, acknowledged) {
if (roomInfo.historyVisibility == RoomHistoryVisibility.Joined && acknowledged) {
repository.setAcknowledged(room.roomId, false)
}
}
fun handleEvent(event: HistoryVisibleEvent) {
when (event) {
is HistoryVisibleEvent.Acknowledge -> coroutineScope.setAcknowledged(room.roomId, true)
}
}
return HistoryVisibleState(
showAlert = isFeatureEnabled && roomInfo.historyVisibility != RoomHistoryVisibility.Joined && roomInfo.isEncrypted == true && !acknowledged,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.setAcknowledged(roomId: RoomId, value: Boolean) = launch {
repository.setAcknowledged(roomId, value)
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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 = true),
)
}
internal fun aHistoryVisibleState(
showAlert: Boolean = false,
eventSink: (HistoryVisibleEvent) -> Unit = {},
) = HistoryVisibleState(
showAlert,
eventSink = eventSink,
)

View File

@@ -0,0 +1,81 @@
/*
* 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.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(CommonStrings.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 = { _, _ -> },
)
}

View File

@@ -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.compose.runtime.Composable
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() = ElementPreview {
MessagesView(
state = aMessagesState(
composerState = aMessageComposerState(
textEditorState = aTextEditorStateMarkdown(
initialText = "",
initialFocus = false,
)
),
historyVisibleState = aHistoryVisibleState(showAlert = true),
),
onBackClick = {},
onRoomDetailsClick = {},
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onLinkClick = { _, _ -> },
onSendLocationClick = {},
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {}
)
}

View File

@@ -11,6 +11,8 @@ package io.element.android.features.messages.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleStatePresenter
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter
@@ -61,4 +63,7 @@ interface MessagesBindsModule {
@Binds
fun bindIdentityChangeStatePresenter(presenter: IdentityChangeStatePresenter): Presenter<IdentityChangeState>
@Binds
fun bindHistoryVisibleStatePresenter(presenter: HistoryVisibleStatePresenter): Presenter<HistoryVisibleState>
}

View File

@@ -17,6 +17,7 @@ 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.anActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.link.aLinkState
@@ -1355,6 +1356,7 @@ class MessagesPresenterTest {
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
identityChangeStatePresenter = { anIdentityChangeState() },
historyVisibleStatePresenter = { aHistoryVisibleState() },
linkPresenter = { aLinkState() },
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
customReactionPresenter = { aCustomReactionState() },

View File

@@ -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 io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeHistoryVisibleAcknowledgementRepository(
private val acknowledgements: MutableMap<RoomId, MutableStateFlow<Boolean>> = mutableMapOf()
) : HistoryVisibleAcknowledgementRepository {
override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
return acknowledgements.getOrPut(roomId) {
MutableStateFlow(false)
}
}
override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
val flow = acknowledgements.getOrPut(roomId) {
MutableStateFlow(value)
}
flow.emit(value)
}
companion object {
/**
* Create the repository with a pre-existing entry.
*/
fun withRoom(roomId: RoomId, acknowledged: Boolean = false): FakeHistoryVisibleAcknowledgementRepository {
return FakeHistoryVisibleAcknowledgementRepository(
mutableMapOf(
roomId to MutableStateFlow(acknowledged)
)
)
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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 com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class HistoryVisibleStatePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - not visible if feature disabled`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, enabled = false, acknowledged = false)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room shared, unencrypted`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = false))
val presenter = createHistoryVisibleStatePresenter(room)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room joined, encrypted`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = false))
val presenter = createHistoryVisibleStatePresenter(room)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room shared, encrypted, unacknowledged`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, acknowledged = false)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.showAlert).isFalse()
val nextState = awaitItem()
assertThat(nextState.showAlert).isTrue()
}
}
@Test
fun `present - initial with room shared, encrypted, acknowledged`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, acknowledged = true)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - transition from joined + unencrypted, to shared + encrypted`() = runTest {
val room = FakeJoinedRoom()
val featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true))
val repository = FakeHistoryVisibleAcknowledgementRepository()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = false))
val presenter = HistoryVisibleStatePresenter(
featureFlagService,
repository,
room,
)
presenter.test {
// emitted by the feature flag service(?)
assertThat(awaitItem().showAlert).isFalse()
// emitted state from room info assignment
assertThat(awaitItem().showAlert).isFalse()
// room is marked as encrypted
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = true))
assertThat(awaitItem().showAlert).isFalse()
// room history visibility is changed to shared
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
assertThat(awaitItem().showAlert).isTrue()
// alert is acknowledged
repository.setAcknowledged(room.roomId, true)
assertThat(awaitItem().showAlert).isFalse()
}
}
private fun createHistoryVisibleStatePresenter(
room: JoinedRoom = FakeJoinedRoom(),
enabled: Boolean = true,
acknowledged: Boolean = false
): HistoryVisibleStatePresenter {
return HistoryVisibleStatePresenter(
room = room,
featureFlagService = FakeFeatureFlagService(mapOf("feature.enableKeyShareOnInvite" to enabled)),
repository = FakeHistoryVisibleAcknowledgementRepository.withRoom(room.roomId, acknowledged)
)
}
}