Merge pull request #7396 from vector-im/nimau/6612-dm-email

Direct Message: manage encrypted DM in case of invite by email
This commit is contained in:
Nicolas Mauri 2023-03-06 12:04:53 +01:00 committed by GitHub
commit c568b5ed1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 362 additions and 19 deletions

View file

@ -365,6 +365,7 @@
"room_creation_invite_another_user" = "User ID, name or email";
"room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email.";
"room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again.";
"room_creation_only_one_email_invite" = "You can only invite one email at a time";
// Room recents
"room_recents_directory_section" = "ROOM DIRECTORY";
@ -2295,6 +2296,9 @@ Tap the + to start adding people.";
"room_invites_empty_view_title" = "Nothing new.";
"room_invites_empty_view_information" = "This is where your invites appear.";
"room_waiting_other_participants_title" = "Waiting for users to join %@";
"room_waiting_other_participants_message" = "Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted";
// MARK: - Space Selector
"space_selector_title" = "My spaces";

View file

@ -5267,6 +5267,10 @@ public class VectorL10n: NSObject {
public static var roomCreationNameTitle: String {
return VectorL10n.tr("Vector", "room_creation_name_title")
}
/// You can only invite one email at a time
public static var roomCreationOnlyOneEmailInvite: String {
return VectorL10n.tr("Vector", "room_creation_only_one_email_invite")
}
/// (e.g. @bob:homeserver1; @john:homeserver2...)
public static var roomCreationParticipantsPlaceholder: String {
return VectorL10n.tr("Vector", "room_creation_participants_placeholder")
@ -6623,6 +6627,14 @@ public class VectorL10n: NSObject {
public static var roomUnsentMessagesUnknownDevicesNotification: String {
return VectorL10n.tr("Vector", "room_unsent_messages_unknown_devices_notification")
}
/// Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted
public static func roomWaitingOtherParticipantsMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_waiting_other_participants_message", p1)
}
/// Waiting for users to join %@
public static func roomWaitingOtherParticipantsTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_waiting_other_participants_title", p1)
}
/// End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption.
public static var roomWarningAboutEncryption: String {
return VectorL10n.tr("Vector", "room_warning_about_encryption")

View file

@ -31,6 +31,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
private let parentSpaceId: String?
private let initialSection: RoomInfoSection
private let dismissOnCancel: Bool
private let canAddParticipants: Bool
private weak var roomSettingsViewController: RoomSettingsViewController?
private lazy var segmentedViewController: SegmentedViewController = {
@ -43,6 +44,8 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
participants.parentSpaceId = self.parentSpaceId
participants.delegate = self
participants.screenTracker = AnalyticsScreenTracker(screen: .roomMembers)
participants.showInviteUserFab = self.canAddParticipants
let files = RoomFilesViewController()
files.finalizeInit()
@ -105,6 +108,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.room = parameters.room
self.parentSpaceId = parameters.parentSpaceId
self.initialSection = parameters.initialSection
self.canAddParticipants = parameters.canAddParticipants
self.dismissOnCancel = parameters.dismissOnCancel
}

View file

@ -33,12 +33,14 @@ class RoomInfoCoordinatorParameters: NSObject {
let parentSpaceId: String?
let initialSection: RoomInfoSection
let dismissOnCancel: Bool
let canAddParticipants: Bool
init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, dismissOnCancel: Bool) {
init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool = true, dismissOnCancel: Bool) {
self.session = session
self.room = room
self.parentSpaceId = parentSpaceId
self.initialSection = initialSection
self.canAddParticipants = canAddParticipants
self.dismissOnCancel = dismissOnCancel
super.init()
}
@ -50,4 +52,8 @@ class RoomInfoCoordinatorParameters: NSObject {
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) {
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, dismissOnCancel: false)
}
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool) {
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, canAddParticipants: canAddParticipants, dismissOnCancel: false)
}
}

View file

@ -125,6 +125,8 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
@property (nonatomic, strong, nullable) ComposerLinkActionBridgePresenter *composerLinkActionBridgePresenter;
@property (weak, nonatomic, nullable) UIViewController *waitingOtherParticipantViewController;
@property (nonatomic) BOOL isWaitingForOtherParticipants;
/**
Retrieve the live data source in cases where the timeline is not live.

View file

@ -185,6 +185,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// Time to display notification content in the timeline
MXTaskProfile *notificationTaskProfile;
// Observe kMXEventTypeStringRoomMember events
__weak id roomMemberEventListener;
}
@property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView;
@ -233,6 +236,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// scroll state just before the layout change, and restore it after the layout.
@property (nonatomic) BOOL wasScrollAtBottomBeforeLayout;
// Check if we should wait for other participants
@property (nonatomic, readonly) BOOL shouldWaitForOtherParticipants;
@end
@implementation RoomViewController
@ -329,6 +335,7 @@ static CGSize kThreadListBarButtonItemImageSize;
_showMissedDiscussionsBadge = YES;
_scrollToBottomHidden = YES;
_isWaitingForOtherParticipants = NO;
// Listen to the event sent state changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeSentState:) name:kMXEventDidChangeSentStateNotification object:nil];
@ -372,7 +379,10 @@ static CGSize kThreadListBarButtonItemImageSize;
// Prepare missed dicussion badge (if any)
self.showMissedDiscussionsBadge = _showMissedDiscussionsBadge;
// Refresh the waiting for other participants state
[self refreshWaitForOtherParticipantsState];
// Set up the room title view according to the data source (if any)
[self refreshRoomTitle];
@ -1194,9 +1204,9 @@ static CGSize kThreadListBarButtonItemImageSize;
BOOL canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsMessage:kMXEventTypeStringRoomMessage]);
BOOL isRoomObsolete = self.roomDataSource.roomState.isObsolete;
BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded];
BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded];
if (isRoomObsolete || isResourceLimitExceeded)
if (isRoomObsolete || isResourceLimitExceeded || _isWaitingForOtherParticipants)
{
roomInputToolbarViewClass = nil;
shouldDismissContextualMenu = YES;
@ -1532,6 +1542,8 @@ static CGSize kThreadListBarButtonItemImageSize;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil];
[self waitForOtherParticipant:NO];
[super destroy];
}
@ -1638,6 +1650,57 @@ static CGSize kThreadListBarButtonItemImageSize;
return self.customizedRoomDataSource.isCurrentUserSharingActiveLocation;
}
#pragma mark - Wait for 3rd party invitee
- (void)setIsWaitingForOtherParticipants:(BOOL)isWaitingForOtherParticipants
{
if (_isWaitingForOtherParticipants == isWaitingForOtherParticipants)
{
return;
}
_isWaitingForOtherParticipants = isWaitingForOtherParticipants;
[self updateRoomInputToolbarViewClassIfNeeded];
if (_isWaitingForOtherParticipants)
{
if (self->roomMemberEventListener == nil)
{
MXWeakify(self);
self->roomMemberEventListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
if (direction != MXTimelineDirectionForwards)
{
return;
}
[self refreshWaitForOtherParticipantsState];
}];
}
}
else
{
if (self->roomMemberEventListener != nil)
{
[self.roomDataSource.room removeListener:self->roomMemberEventListener];
self->roomMemberEventListener = nil;
}
}
}
- (BOOL)shouldWaitForOtherParticipants
{
MXRoomState *roomState = self.roomDataSource.roomState;
BOOL isDirect = self.roomDataSource.room.isDirect;
// Wait for the other participant only if it is a direct encrypted room with only one member waiting for a third party guest.
return (isDirect && roomState.isEncrypted && roomState.membersCount.members == 1 && roomState.thirdPartyInvites.count > 0);
}
- (void)refreshWaitForOtherParticipantsState
{
[self waitForOtherParticipant:self.shouldWaitForOtherParticipants];
}
#pragma mark - Internals
- (UIBarButtonItem *)videoCallBarButtonItem
@ -1948,7 +2011,7 @@ static CGSize kThreadListBarButtonItemImageSize;
[self refreshMissedDiscussionsCount:YES];
if (RiotSettings.shared.enableThreads)
if (RiotSettings.shared.enableThreads && !_isWaitingForOtherParticipants)
{
if (self.roomDataSource.threadId)
{
@ -2260,8 +2323,8 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated
{
RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection];
RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection canAddParticipants: !self.isWaitingForOtherParticipants];
self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters];
self.roomInfoCoordinatorBridgePresenter.delegate = self;
@ -7450,7 +7513,7 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service
{
if (!service)
if (!service || _isWaitingForOtherParticipants)
{
return;
}

View file

@ -251,6 +251,50 @@ extension RoomViewController {
composerLinkActionBridgePresenter = presenter
presenter.present(from: self, animated: true)
}
@objc func showWaitingOtherParticipantHeader() {
let controller = VectorHostingController(rootView: RoomWaitingForMembers())
guard let headerView = controller.view else {
return
}
self.waitingOtherParticipantViewController = controller
self.addChild(controller)
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
headerView.translatesAutoresizingMaskIntoConstraints = false
containerView.vc_addSubViewMatchingParent(headerView, withInsets: UIEdgeInsets(top: 9, left: 9, bottom: -9, right: -9))
self.bubblesTableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
containerView.centerXAnchor.constraint(equalTo: self.bubblesTableView.centerXAnchor),
containerView.widthAnchor.constraint(equalTo: self.bubblesTableView.widthAnchor),
containerView.topAnchor.constraint(equalTo: self.bubblesTableView.topAnchor)
])
controller.didMove(toParent: self)
self.bubblesTableView.tableHeaderView?.layoutIfNeeded()
}
@objc func hideWaitingOtherParticipantHeader() {
guard let waitingOtherParticipantViewController else {
return
}
waitingOtherParticipantViewController.removeFromParent()
self.bubblesTableView.tableHeaderView = nil
waitingOtherParticipantViewController.didMove(toParent: nil)
self.waitingOtherParticipantViewController = nil
}
@objc func waitForOtherParticipant(_ wait: Bool) {
self.isWaitingForOtherParticipants = wait
if wait {
showWaitingOtherParticipantHeader()
} else {
hideWaitingOtherParticipantHeader()
}
}
}
// MARK: - Private Helpers

View file

@ -35,6 +35,9 @@
UIBarButtonItem *cancelBarButtonItem;
UIBarButtonItem *createBarButtonItem;
// SearchBar text
NSString *currentSearch;
// HTTP Request
MXHTTPOperation *roomCreationRequest;
@ -45,10 +48,13 @@
@property (weak, nonatomic) IBOutlet UIView *searchBarHeader;
@property (weak, nonatomic) IBOutlet UISearchBar *searchBarView;
@property (weak, nonatomic) IBOutlet UIView *searchBarHeaderBorder;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchBarHeaderHeightConstraint;
@property (nonatomic, strong) InviteFriendsPresenter *inviteFriendsPresenter;
@property (nonatomic, weak) InviteFriendsHeaderView *inviteFriendsHeaderView;
@property (nonatomic, weak) UIView *onlyOneEmailInvitationView;
@end
@implementation StartChatViewController
@ -130,6 +136,11 @@
- (void)setupInviteFriendsHeaderView
{
if (self.inviteFriendsHeaderView)
{
return;
}
if (!RiotSettings.shared.allowInviteExernalUsers)
{
self.contactsTableView.tableHeaderView = nil;
@ -152,7 +163,7 @@
[self setupInviteFriendsHeaderView];
}
}
else
else if (self.inviteFriendsHeaderView != nil)
{
self.contactsTableView.tableHeaderView = nil;
}
@ -304,6 +315,104 @@
contactsDataSource.ignoredContactsByMatrixId[self.mainSession.myUser.userId] = userContact;
}
}
// hide the search bar if a participant is already invited by email
BOOL hideSearchBar = [self participantsAlreadyContainAnEmail];
self.searchBarHeader.alpha = hideSearchBar ? 0.0f : 1.0f;
self.searchBarHeaderHeightConstraint.constant = hideSearchBar ? 0.0f : 50.0f;
[UIView animateWithDuration:0.2f animations:^{
[self.view layoutIfNeeded];
}];
}
- (BOOL)participantsAlreadyContainAnEmail
{
for (MXKContact* participant in participants)
{
// if it is not a matrix contact or a local contact with a MatrixID
if (participant.matrixIdentifiers.count == 0 && ![MXTools isMatrixUserIdentifier:participant.displayName])
{
return YES;
}
}
return NO;
}
- (BOOL)canAddParticipant: (MXKContact*) contact
{
if (!contact)
{
return YES;
}
// The following rules will be applied only if the resulting room is going to be encrypted
if (![self.mainSession vc_homeserverConfiguration].encryption.isE2EEByDefaultEnabled)
{
return YES;
}
// If we have already invited an email, we cannot add another participant
if ([self participantsAlreadyContainAnEmail])
{
return NO;
}
// if it is not a matrix contact, nor a local contact with a MatrixID, and if there is already at least one participant, another participant cannot be added.
if ((contact.matrixIdentifiers.count == 0 && ![MXTools isMatrixUserIdentifier:contact.displayName]) && participants.count > 0)
{
return NO;
}
// Otherwise, we should be able to add this participant
return YES;
}
- (void)showAllowOnlyOneInvitByEmailAllowedHeaderView:(BOOL)visible
{
if (visible)
{
if (!self.onlyOneEmailInvitationView)
{
UIView *headerView = [[UIView alloc] initWithFrame: CGRectZero];
headerView.translatesAutoresizingMaskIntoConstraints = NO;
UILabel *label = [[UILabel alloc] initWithFrame: CGRectZero];
label.numberOfLines = 0;
label.textColor = ThemeService.shared.theme.textSecondaryColor;
label.font = [UIFont systemFontOfSize:14 weight:UIFontWeightLight];
label.adjustsFontSizeToFitWidth = YES;
label.text = VectorL10n.roomCreationOnlyOneEmailInvite;
label.translatesAutoresizingMaskIntoConstraints = NO;
[headerView addSubview:label];
[NSLayoutConstraint activateConstraints:@[
[label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:16],
[label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-16],
[label.topAnchor constraintEqualToAnchor:headerView.topAnchor constant:8],
[label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-8],
]];
[label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[headerView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
self.onlyOneEmailInvitationView = headerView;
self.contactsTableView.tableHeaderView = self.onlyOneEmailInvitationView;
[NSLayoutConstraint activateConstraints:@[
[headerView.leadingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.leadingAnchor],
[headerView.trailingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.trailingAnchor]
]];
[self.contactsTableView.tableHeaderView layoutIfNeeded];
}
}
else if (self.onlyOneEmailInvitationView != nil)
{
if (self.contactsTableView.tableHeaderView == self.onlyOneEmailInvitationView)
{
self.contactsTableView.tableHeaderView = nil;
}
self.onlyOneEmailInvitationView = nil;
}
}
- (void)showInviteFriendsFromSourceView:(UIView*)sourceView
@ -367,6 +476,14 @@
if (_isAddParticipantSearchBarEditing)
{
cell = [contactsDataSource tableView:tableView cellForRowAtIndexPath:indexPath];
MXKContact* contact = [contactsDataSource contactAtIndexPath:indexPath];
if (![self canAddParticipant:contact])
{
// Prevent to add it
cell.contentView.alpha = 0.5;
cell.userInteractionEnabled = NO;
cell.accessoryView = nil;
}
}
else if (indexPath.section == participantsSection)
{
@ -533,7 +650,7 @@
// Prepare the invited participant data
NSMutableArray *inviteArray = [NSMutableArray array];
NSMutableArray *invite3PIDArray = [NSMutableArray array];
NSMutableArray<MXInvite3PID *> *invite3PIDArray = [NSMutableArray array];
// Check whether some users must be invited
for (MXKContact *contact in participants)
@ -594,7 +711,7 @@
// Is it a direct chat?
BOOL isDirect = ((inviteArray.count + invite3PIDArray.count == 1) ? YES : NO);
// In case of a direct chat with only one user id, we open the first available direct chat
// or creates a new one (if it doesn't exist).
if (isDirect && inviteArray.count)
@ -606,6 +723,19 @@
}
else
{
// We don't want to create a new direct room for a 3rd party invite if we already have one
NSString *first3rdPartyInvitee = invite3PIDArray.firstObject.address;
if (isDirect && first3rdPartyInvitee)
{
MXRoom *existingRoom = [self.mainSession directJoinedRoomWithUserId:first3rdPartyInvitee];
if (existingRoom)
{
[self stopActivityIndicator];
[[AppDelegate theDelegate] showRoom:existingRoom.roomId andEventId:nil withMatrixSession:self.mainSession];
return;
}
}
// Ensure direct chat are created with equal ops on both sides (the trusted_private_chat preset)
MXRoomPreset preset = (isDirect ? kMXRoomPresetTrustedPrivateChat : nil);
@ -635,7 +765,7 @@
roomCreationParameters.isDirect = isDirect;
roomCreationParameters.preset = preset;
if (canEnableE2E && roomCreationParameters.invite3PIDArray == nil)
if (canEnableE2E)
{
roomCreationParameters.initialStateEvents = @[
[MXRoomCreationParameters initialStateEventForEncryptionWithAlgorithm:kMXCryptoMegolmAlgorithm
@ -644,6 +774,9 @@
self->roomCreationRequest = [self.mainSession createRoomWithParameters:roomCreationParameters success:^(MXRoom *room) {
// Update the room summary
[room.summary resetRoomStateData];
self->roomCreationRequest = nil;
[self stopActivityIndicator];
@ -702,8 +835,28 @@
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[contactsDataSource searchWithPattern:searchText forceReset:NO];
self->currentSearch = searchText;
if (searchText != nil && searchText.length > 0)
{
MXKContact *contact = nil;
if ([MXTools isMatrixUserIdentifier:searchText])
{
contact = [[MXKContact alloc] initMatrixContactWithDisplayName:searchText andMatrixID:searchText];
}
else if ([MXTools isEmailAddress:searchText])
{
contact = [[MXKContact alloc] initContactWithDisplayName:searchText emails:nil phoneNumbers:nil andThumbnail:nil];
}
[self showAllowOnlyOneInvitByEmailAllowedHeaderView: ![self canAddParticipant:contact]];
}
else
{
[self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO];
}
[contactsDataSource searchWithPattern:searchText forceReset:NO];
self.contactsAreFilteredWithSearch = searchText.length ? YES : NO;
}
@ -718,6 +871,7 @@
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
searchBar.text = nil;
self->currentSearch = nil;
self.isAddParticipantSearchBarEditing = NO;
// Reset filtering
@ -725,6 +879,8 @@
// Leave search
[searchBar resignFirstResponder];
[self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO];
}
#pragma mark - ContactsTableViewControllerDelegate
@ -763,14 +919,14 @@
}
}
if (contact)
if ([self canAddParticipant:contact])
{
// Update here the mutable list of participants
[participants addObject:contact];
// Refresh display by leaving search session
[self searchBarCancelButtonClicked:_searchBarView];
}
// Refresh display by leaving search session
[self searchBarCancelButtonClicked:_searchBarView];
}
#pragma mark - InviteFriendsHeaderViewDelegate

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -13,6 +13,7 @@
<outlet property="contactsTableView" destination="kNf-Ll-jvH" id="PDi-bW-2CG"/>
<outlet property="searchBarHeader" destination="Zm7-AB-ZtE" id="6ee-P0-twi"/>
<outlet property="searchBarHeaderBorder" destination="gcy-W7-89G" id="tsy-SP-KaJ"/>
<outlet property="searchBarHeaderHeightConstraint" destination="kSM-fg-IHB" id="nrd-MK-yrq"/>
<outlet property="searchBarView" destination="bsq-3U-VjV" id="x3M-wX-RW8"/>
<outlet property="view" destination="iN0-l3-epB" id="csR-cn-S4h"/>
</connections>

View file

@ -0,0 +1,50 @@
//
// Copyright 2023 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.
//
import SwiftUI
struct RoomWaitingForMembers: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
var body: some View {
ZStack {
HStack(alignment: .top) {
Image(uiImage: Asset.Images.membersListIcon.image)
VStack(alignment: .leading, spacing: 6) {
Text(VectorL10n.roomWaitingOtherParticipantsTitle(AppInfo.current.displayName))
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
.frame(maxWidth: .infinity, alignment: .leading)
Text(VectorL10n.roomWaitingOtherParticipantsMessage(AppInfo.current.displayName))
.font(theme.fonts.caption1)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(9)
.background(theme.colors.system)
.cornerRadius(4)
}
}
}
struct RoomWaitingForMembers_Previews: PreviewProvider {
static var previews: some View {
RoomWaitingForMembers()
.padding(16)
}
}

1
changelog.d/6612.change Normal file
View file

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