Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : room settings - security and privacy #4212

Merged
merged 30 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
002325c
feat(security&privacy) : first implementation of ui
ganfra Jan 8, 2025
d354144
change(room details) : update room details with new sections organisa…
ganfra Jan 17, 2025
a781cc0
feat(security & privacy) : introduce EditRoomAddress screen
ganfra Jan 17, 2025
b549400
feat(security&privacy) : expose new sdk methods
ganfra Jan 21, 2025
c391b5b
feat(security&privacy) : get data from sdk
ganfra Jan 21, 2025
129eb45
feat(security&privacy) : introduce navigator
ganfra Jan 21, 2025
9faa305
feat(room address) : extract some reusable code
ganfra Jan 21, 2025
f8cd8b3
feat(security&privacy) : start handling edition of room address
ganfra Jan 21, 2025
b7831f4
feat(security&privacy) : expose methods from sdk to update alias and …
ganfra Jan 22, 2025
65e5447
feat(security&privacy) : manage save action for edit room address
ganfra Jan 22, 2025
0e6c86f
feat(privacy&security) : extract some code for address management
ganfra Jan 22, 2025
392299d
feat(security&privacy) : update the save address algorithm
ganfra Jan 22, 2025
7eda945
feat(security&privacy) : manage encryption settings
ganfra Jan 22, 2025
19d49a3
feat(security&privacy) : expose more methods from sdk
ganfra Jan 23, 2025
edee18a
feat(security&privacy) : manage save action and some edge cases.
ganfra Jan 23, 2025
75fef6b
feat(security&privacy) : introduce permissions and use in RoomDetails
ganfra Jan 23, 2025
88fce64
feat(security&privacy) : use permissions and improve save
ganfra Jan 23, 2025
ba0a857
feat(security&privacy) : update strings
ganfra Jan 24, 2025
c07a7d9
feat(security&privacy) : make the whole RoomDirectoryVisibility item …
ganfra Jan 24, 2025
fdc4f1b
feat(security&privacy) : start writing tests
ganfra Jan 27, 2025
c0c7d5b
feat(security&privacy) : write View tests
ganfra Jan 27, 2025
876d06f
feat(security&privacy) : add all tests for EditRoomAddress classes
ganfra Jan 27, 2025
d992f38
feat(security&privacy) : clean code
ganfra Jan 28, 2025
58918b1
feat(security&privacy) : update canShowSecurityAndPrivacy check
ganfra Jan 28, 2025
d586bdc
feat(security&privacy) : some more clean up
ganfra Jan 28, 2025
706b298
feat(security&privacy) : fix tests after some rework landed
ganfra Jan 28, 2025
82f5926
Update screenshots
ElementBot Jan 28, 2025
8880eed
feat(security&privacy) : improve and document code after PR review.
ganfra Jan 29, 2025
15f2d0b
feat(security&privacy) : add more previews
ganfra Jan 29, 2025
c10da01
Update screenshots
ElementBot Jan 29, 2025
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 @@ -12,5 +12,5 @@ sealed interface SecurityAndPrivacyEvents {
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents
data object EnableEncryption: SecurityAndPrivacyEvents
data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents
data class ChangeVisibleInRoomDirectory(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents
data class ChangeRoomVisibility(val isVisibleInRoomDirectory: Boolean) : SecurityAndPrivacyEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,129 @@
package io.element.android.features.roomdetails.impl.securityandprivacy

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.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import java.util.Optional
import javax.inject.Inject

class SecurityAndPrivacyPresenter @Inject constructor() : Presenter<SecurityAndPrivacyState> {
private val matrixClient: MatrixClient,
private val room: MatrixRoom,
) : Presenter<SecurityAndPrivacyState> {

@Composable
override fun present(): SecurityAndPrivacyState {
val homeserverName = remember { matrixClient.userIdServerName() }
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)

val isVisibleInRoomDirectory = remember {
mutableStateOf<AsyncData<Boolean>>(AsyncData.Uninitialized)
}

val savedSettings by remember {
mutableStateOf(
derivedStateOf {
SecurityAndPrivacySettings(
roomAccess = SecurityAndPrivacyRoomAccess.InviteOnly,
isEncrypted = true,
isVisibleInRoomDirectory = Optional.empty(),
historyVisibility = Optional.empty(),
formattedAddress = Optional.empty(),
roomAccess = roomInfo?.joinRule.map(),
isEncrypted = room.isEncrypted,
isVisibleInRoomDirectory = Optional.ofNullable(isVisibleInRoomDirectory.value),
historyVisibility = Optional.ofNullable(roomInfo?.historyVisibility?.map()),
formattedAddress = Optional.ofNullable(roomInfo?.canonicalAlias?.value),
)
)
}
}

var currentRoomAccess by remember(savedSettings.roomAccess) {
mutableStateOf(savedSettings.roomAccess)
}
var currentHistoryVisibility by remember(savedSettings.historyVisibility) {
mutableStateOf(savedSettings.historyVisibility)
}
var currentVisibleInRoomDirectory by remember(savedSettings.isVisibleInRoomDirectory) {
mutableStateOf(savedSettings.isVisibleInRoomDirectory)
}
var currentIsEncrypted by remember(savedSettings.isEncrypted) {
mutableStateOf(savedSettings.isEncrypted)
}
val currentSettings = SecurityAndPrivacySettings(
roomAccess = currentRoomAccess,
isEncrypted = currentIsEncrypted,
isVisibleInRoomDirectory = currentVisibleInRoomDirectory,
historyVisibility = currentHistoryVisibility,
formattedAddress = savedSettings.formattedAddress,
)

fun handleEvents(event: SecurityAndPrivacyEvents) {
when (event) {
SecurityAndPrivacyEvents.Save -> {}
is SecurityAndPrivacyEvents.ChangeRoomAccess -> {}
is SecurityAndPrivacyEvents.EnableEncryption -> {}
is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> {}
is SecurityAndPrivacyEvents.ChangeVisibleInRoomDirectory -> {}
SecurityAndPrivacyEvents.Save -> {
}
is SecurityAndPrivacyEvents.ChangeRoomAccess -> {
currentRoomAccess = event.roomAccess
}
is SecurityAndPrivacyEvents.EnableEncryption -> {
currentIsEncrypted = true
}
is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> {
currentHistoryVisibility = Optional.of(event.historyVisibility)
}
is SecurityAndPrivacyEvents.ChangeRoomVisibility -> {
currentVisibleInRoomDirectory = Optional.of(AsyncData.Success(event.isVisibleInRoomDirectory))
}
}
}

return SecurityAndPrivacyState(
savedSettings = savedSettings,
currentSettings = savedSettings,
homeserverName = "",
canBeSaved = true,
currentSettings = currentSettings,
homeserverName = homeserverName,
eventSink = ::handleEvents
)
}
}

private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess {
return when (this) {
JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone
JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin
is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember
is JoinRule.Custom,
JoinRule.Invite,
JoinRule.Private,
null -> SecurityAndPrivacyRoomAccess.InviteOnly
Copy link
Member

Choose a reason for hiding this comment

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

Maybe explain why all these are mapped to InviteOnly?

}
}

private fun SecurityAndPrivacyRoomAccess.map(): JoinRule {
return when (this) {
SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public
SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock
SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private
SecurityAndPrivacyRoomAccess.SpaceMember -> error("Unsupported")
Copy link
Member

Choose a reason for hiding this comment

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

I think this is used in a safe context at the moment (a runCatching wrapped block), but maybe we should be more careful about it? If we add some other usage in the future we might be crashing the app.

Also, documenting why this is not supported would be nice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to nullable

}
}

private fun RoomHistoryVisibility.map(): SecurityAndPrivacyHistoryVisibility {
return when (this) {
RoomHistoryVisibility.Joined,
RoomHistoryVisibility.Invited -> SecurityAndPrivacyHistoryVisibility.SinceInvite
RoomHistoryVisibility.Shared,
is RoomHistoryVisibility.Custom -> SecurityAndPrivacyHistoryVisibility.SinceSelection
RoomHistoryVisibility.WorldReadable -> SecurityAndPrivacyHistoryVisibility.Anyone
}
}

private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility {
return when (this) {
SecurityAndPrivacyHistoryVisibility.SinceSelection -> RoomHistoryVisibility.Shared
SecurityAndPrivacyHistoryVisibility.SinceInvite -> RoomHistoryVisibility.Invited
SecurityAndPrivacyHistoryVisibility.Anyone -> RoomHistoryVisibility.WorldReadable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ data class SecurityAndPrivacyState(
val savedSettings: SecurityAndPrivacySettings,
val currentSettings: SecurityAndPrivacySettings,
val homeserverName: String,
val canBeSaved: Boolean,
val eventSink: (SecurityAndPrivacyEvents) -> Unit
) {

val canBeSaved = savedSettings != currentSettings

val showRoomVisibilitySections = currentSettings.roomAccess != SecurityAndPrivacyRoomAccess.InviteOnly && currentSettings.historyVisibility.isPresent

val availableHistoryVisibilities = buildSet {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
isVisibleInRoomDirectory = Optional.of(AsyncData.Success(true))
)
),
aSecurityAndPrivacyState(canBeSaved = false)
)
}

Expand All @@ -62,13 +61,11 @@ fun aSecurityAndPrivacySettings(
fun aSecurityAndPrivacyState(
currentSettings: SecurityAndPrivacySettings = aSecurityAndPrivacySettings(),
savedSettings: SecurityAndPrivacySettings = currentSettings,
canBeSaved: Boolean = true,
homeserverName: String = "myserver.xyz",
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
) = SecurityAndPrivacyState(
currentSettings = currentSettings,
savedSettings = savedSettings,
homeserverName = homeserverName,
canBeSaved = canBeSaved,
eventSink = eventSink
)
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ fun SecurityAndPrivacyView(
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding),
.padding(padding)
.imePadding()
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(32.dp),
) {
RoomAccessSection(
Expand All @@ -87,11 +87,14 @@ fun SecurityAndPrivacyView(
homeserverName = state.homeserverName,
onRoomAddressClick = { },
isVisibleInPublicDirectory = state.currentSettings.isVisibleInRoomDirectory,
onVisibilityChange = { },
onVisibilityChange = { isVisible ->
state.eventSink(SecurityAndPrivacyEvents.ChangeRoomVisibility(isVisible))
},
)
}
EncryptionSection(
isEncryptionEnabled = state.currentSettings.isEncrypted,
isSectionEnabled = !state.savedSettings.isEncrypted,
onEnableEncryption = { state.eventSink(SecurityAndPrivacyEvents.EnableEncryption) },
)
if (state.showRoomHistoryVisibilitySection) {
Expand Down Expand Up @@ -243,8 +246,8 @@ private fun RoomAddressSection(
ListItemContent.Custom {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
Expand All @@ -270,6 +273,7 @@ private fun RoomAddressSection(
@Composable
private fun EncryptionSection(
isEncryptionEnabled: Boolean,
isSectionEnabled: Boolean,
onEnableEncryption: () -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -282,7 +286,7 @@ private fun EncryptionSection(
supportingContent = { Text(text = stringResource(CommonStrings.screen_security_and_privacy_encryption_section_footer)) },
trailingContent = ListItemContent.Switch(
checked = isEncryptionEnabled,
enabled = !isEncryptionEnabled,
enabled = isSectionEnabled,
onChange = { onEnableEncryption() }
),
)
Expand Down