create encrypted DM for user invite by email (#8172)

Co-authored-by: jonnyandrew <jonny.andrew@protonmail.com>
This commit is contained in:
Yoan Pintas 2023-03-06 23:05:43 +01:00 committed by GitHub
parent 29f2bf25fc
commit 94675b9f85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 113 additions and 149 deletions

1
changelog.d/6912.misc Normal file
View file

@ -0,0 +1 @@
Direct Message: Manage encrypted DM in case of invite by email

View file

@ -1817,6 +1817,7 @@
<string name="add_by_qr_code">Add by QR code</string>
<string name="qr_code">QR code</string>
<string name="creating_direct_room">"Creating room…"</string>
<string name="direct_room_user_list_only_invite_one_email">You can only invite one email at a time</string>
<string name="direct_room_user_list_known_title">Known Users</string>
<string name="direct_room_user_list_suggestions_title">Suggestions</string>
@ -2561,6 +2562,8 @@
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
<string name="direct_room_encryption_enabled_tile_description">Messages in this chat are end-to-end encrypted.</string>
<string name="direct_room_encryption_enabled_tile_description_future">Messages in this chat will be end-to-end encrypted.</string>
<string name="direct_room_encryption_enabled_waiting_users">Waiting for users to join ${app_name}</string>
<string name="direct_room_encryption_enabled_waiting_users_tile_description">Once invited users have joined ${app_name}, you will be able to chat and the room will be end-to-end encrypted</string>
<string name="encryption_not_enabled">Encryption not enabled</string>
<string name="encryption_misconfigured">Encryption is misconfigured</string>
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>

View file

@ -190,10 +190,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
return params.enableEncryptionIfInvitedUsersSupportIt &&
// Parity with web, enable if users have encryption ready devices
// for now remove checks on cross signing and 3pid invites
// for now remove checks on cross signing
// && crossSigningService.isCrossSigningVerified()
params.invite3pids.isEmpty() &&
params.invitedUserIds.isNotEmpty() &&
params.invitedUserIds.let { userIds ->
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)

View file

@ -115,7 +115,12 @@ internal class RoomDisplayNameResolver @Inject constructor(
val leftMembersNames = roomMembers.queryLeftRoomMembersEvent()
.findAll()
.map { displayNameResolver.getBestName(it.toMatrixItem()) }
roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames)
val directUserId = roomSummary?.directUserId
if (!directUserId.isNullOrBlank() && leftMembersNames.isEmpty()) {
directUserId
} else {
roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames)
}
}
1 -> {
roomDisplayNameFallbackProvider.getNameFor1member(

View file

@ -57,7 +57,6 @@ import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.home.room.list.home.invites.InvitesViewModel
import im.vector.app.features.home.room.list.home.release.ReleaseNotesViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.location.live.map.LiveLocationMapViewModel
@ -500,11 +499,6 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(StartAppViewModel::class)
fun startAppViewModelFactory(factory: StartAppViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)
fun homeServerCapabilitiesViewModelFactory(factory: HomeServerCapabilitiesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(InviteUsersToRoomViewModel::class)

View file

@ -93,6 +93,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
submitMenuItemId = R.id.action_create_direct_room,
single3pidSelection = true,
)
)
}

View file

@ -124,7 +124,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
}
val result = runCatchingToAsync {
if (vectorPreferences.isDeferredDmEnabled()) {
if (vectorPreferences.isDeferredDmEnabled() && roomParams.invite3pids.isEmpty()) {
session.roomService().createLocalRoom(roomParams)
} else {
analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse()))

View file

@ -1176,6 +1176,10 @@ class TimelineFragment :
views.hideComposerViews()
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent))
}
if (summary.isDirect && summary.isEncrypted && summary.joinedMembersCount == 1 && summary.invitedMembersCount == 0) {
views.hideComposerViews()
}
} else if (summary?.membership == Membership.INVITE && inviter != null) {
views.hideComposerViews()
lazyLoadedViews.inviteView(true)?.apply {

View file

@ -56,22 +56,37 @@ class EncryptionItemFactory @Inject constructor(
val description: String
val shield: StatusTileTimelineItem.ShieldUIState
if (isSafeAlgorithm) {
val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse()
title = stringProvider.getString(R.string.encryption_enabled)
description = stringProvider.getString(
val roomSummary = session.getRoomSummary(event.root.roomId.orEmpty())
val isDirect = roomSummary?.isDirect.orFalse()
val (resTitle, resDescription, resShield) = when {
isDirect -> {
val isWaitingUser = roomSummary?.isEncrypted.orFalse() && roomSummary?.joinedMembersCount == 1 && roomSummary.invitedMembersCount == 0
when {
isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> {
R.string.direct_room_encryption_enabled_tile_description_future
}
isDirect -> {
R.string.direct_room_encryption_enabled_tile_description
}
else -> {
R.string.encryption_enabled_tile_description
}
RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> Triple(
R.string.encryption_enabled,
R.string.direct_room_encryption_enabled_tile_description_future,
StatusTileTimelineItem.ShieldUIState.BLACK
)
isWaitingUser -> Triple(
R.string.direct_room_encryption_enabled_waiting_users,
R.string.direct_room_encryption_enabled_waiting_users_tile_description,
StatusTileTimelineItem.ShieldUIState.WAITING
)
else -> Triple(
R.string.encryption_enabled,
R.string.direct_room_encryption_enabled_tile_description,
StatusTileTimelineItem.ShieldUIState.BLACK
)
}
)
shield = StatusTileTimelineItem.ShieldUIState.BLACK
}
else -> {
Triple(R.string.encryption_enabled, R.string.encryption_enabled_tile_description, StatusTileTimelineItem.ShieldUIState.BLACK)
}
}
title = stringProvider.getString(resTitle)
description = stringProvider.getString(resDescription)
shield = resShield
} else {
title = stringProvider.getString(R.string.encryption_misconfigured)
description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description)

View file

@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.themes.ThemeUtils
import me.gujun.android.span.span
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.util.toMatrixItem
@ -127,26 +128,38 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
}
private fun renderE2ESecureTile(holder: Holder) {
val resources = holder.expandView.resources
val description = when {
val (title, description, drawable) = when {
isDirectRoom -> {
if (attributes.isLocalRoom) {
resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
} else {
resources.getString(R.string.direct_room_encryption_enabled_tile_description)
val isWaitingUser = roomSummary?.isEncrypted.orFalse() && roomSummary?.joinedMembersCount == 1 && roomSummary?.invitedMembersCount == 0
when {
attributes.isLocalRoom -> Triple(
R.string.encryption_enabled,
R.string.direct_room_encryption_enabled_tile_description_future,
R.drawable.ic_shield_black
)
isWaitingUser -> Triple(
R.string.direct_room_encryption_enabled_waiting_users,
R.string.direct_room_encryption_enabled_waiting_users_tile_description,
R.drawable.ic_room_profile_member_list
)
else -> Triple(
R.string.encryption_enabled,
R.string.direct_room_encryption_enabled_tile_description,
R.drawable.ic_shield_black
)
}
}
else -> {
resources.getString(R.string.encryption_enabled_tile_description)
Triple(R.string.encryption_enabled, R.string.encryption_enabled_tile_description, R.drawable.ic_shield_black)
}
}
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleTextView.text = holder.expandView.resources.getString(title)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
ContextCompat.getDrawable(holder.view.context, drawable),
null, null, null
)
holder.e2eTitleDescriptionView.text = description
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(description)
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
}

View file

@ -57,6 +57,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
ShieldUIState.GREEN -> R.drawable.ic_shield_trusted
ShieldUIState.BLACK -> R.drawable.ic_shield_black
ShieldUIState.RED -> R.drawable.ic_shield_warning
ShieldUIState.WAITING -> R.drawable.ic_room_profile_member_list
ShieldUIState.ERROR -> R.drawable.ic_warning_badge
}
@ -101,6 +102,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
BLACK,
RED,
GREEN,
WAITING,
ERROR
}
}

View file

@ -1,83 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.homeserver
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.EntryPoints
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.SingletonEntryPoint
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
class HomeServerCapabilitiesViewModel @AssistedInject constructor(
@Assisted initialState: HomeServerCapabilitiesViewState,
private val session: Session,
private val rawService: RawService
) : VectorViewModel<HomeServerCapabilitiesViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<HomeServerCapabilitiesViewModel, HomeServerCapabilitiesViewState> {
override fun create(initialState: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel
}
companion object : MavericksViewModelFactory<HomeServerCapabilitiesViewModel, HomeServerCapabilitiesViewState> by hiltMavericksViewModelFactory() {
override fun initialState(viewModelContext: ViewModelContext): HomeServerCapabilitiesViewState {
val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getSafeActiveSession()
return HomeServerCapabilitiesViewState(
capabilities = session?.homeServerCapabilitiesService()?.getHomeServerCapabilities() ?: HomeServerCapabilities()
)
}
}
init {
initAdminE2eByDefault()
}
private fun initAdminE2eByDefault() {
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = tryOrNull {
rawService.getElementWellknown(session.sessionParams)
?.isE2EByDefault()
?: true
} ?: true
setState {
copy(
isE2EByDefault = adminE2EByDefault
)
}
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -1,25 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.homeserver
import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
data class HomeServerCapabilitiesViewState(
val capabilities: HomeServerCapabilities = HomeServerCapabilities(),
val isE2EByDefault: Boolean = true
) : MavericksState

View file

@ -25,6 +25,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.epoxy.profiles.notifications.textHeaderItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
@ -61,6 +62,13 @@ class UserListController @Inject constructor(
val currentState = state ?: return
val host = this
if (currentState.isE2EByDefault && currentState.single3pidSelection && currentState.pendingSelections.isNotEmpty()) {
textHeaderItem {
id("userListNotificationHeader")
textRes(R.string.direct_room_user_list_only_invite_one_email)
}
}
// Build generic items
if (currentState.searchTerm.isBlank()) {
if (currentState.showInviteActions()) {

View file

@ -27,7 +27,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
@ -42,7 +41,6 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.showIdentityServerConsentDialog
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentUserListBinding
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.settings.VectorSettingsActivity
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -63,7 +61,6 @@ class UserListFragment :
private val args: UserListFragmentArgs by args()
private val viewModel: UserListViewModel by activityViewModel()
private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentUserListBinding {
@ -86,7 +83,7 @@ class UserListFragment :
setupRecyclerView()
setupSearchView()
homeServerCapabilitiesViewModel.onEach {
viewModel.onEach {
views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
}

View file

@ -26,6 +26,7 @@ data class UserListFragmentArgs(
val submitMenuItemId: Int,
val excludedUserIds: Set<String>? = null,
val singleSelection: Boolean = false,
val single3pidSelection: Boolean = false,
val showInviteActions: Boolean = true,
val showContactBookAction: Boolean = true,
val showToolbar: Boolean = true

View file

@ -31,6 +31,9 @@ import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.fetchIdentityServerWithTerms
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
@ -41,6 +44,7 @@ import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
@ -57,6 +61,7 @@ data class ThreePidUser(
class UserListViewModel @AssistedInject constructor(
@Assisted initialState: UserListViewState,
private val stringProvider: StringProvider,
private val rawService: RawService,
private val session: Session
) : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
@ -84,6 +89,7 @@ class UserListViewModel @AssistedInject constructor(
}
init {
initAdminE2eByDefault()
observeUsers()
setState {
copy(
@ -93,6 +99,22 @@ class UserListViewModel @AssistedInject constructor(
session.identityService().addListener(identityServerListener)
}
private fun initAdminE2eByDefault() {
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = tryOrNull {
rawService.getElementWellknown(session.sessionParams)
?.isE2EByDefault()
?: true
} ?: true
setState {
copy(
isE2EByDefault = adminE2EByDefault
)
}
}
}
private fun cleanISURL(url: String?): String? {
return url?.removePrefix("https://")
}
@ -258,8 +280,13 @@ class UserListViewModel @AssistedInject constructor(
}
private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state ->
val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection)
setState { copy(pendingSelections = selections) }
val canSelectUser = !state.isE2EByDefault || state.pendingSelections.isEmpty() || !state.single3pidSelection ||
(action.pendingSelection is PendingSelection.UserPendingSelection &&
state.pendingSelections.last() is PendingSelection.UserPendingSelection)
if (canSelectUser) {
val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection)
setState { copy(pendingSelections = selections) }
}
}
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state ->

View file

@ -32,6 +32,8 @@ data class UserListViewState(
val pendingSelections: Set<PendingSelection> = emptySet(),
val searchTerm: String = "",
val singleSelection: Boolean,
val single3pidSelection: Boolean,
val isE2EByDefault: Boolean = false,
val configuredIdentityServer: String? = null,
private val showInviteActions: Boolean,
val showContactBookAction: Boolean
@ -40,6 +42,7 @@ data class UserListViewState(
constructor(args: UserListFragmentArgs) : this(
excludedUserIds = args.excludedUserIds,
singleSelection = args.singleSelection,
single3pidSelection = args.single3pidSelection,
showInviteActions = args.showInviteActions,
showContactBookAction = args.showContactBookAction
)