Merge branch 'develop' into doug/fix_warnings

# Conflicts:
#	Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift
This commit is contained in:
Doug 2021-07-12 17:54:34 +01:00
commit 989f19696d
72 changed files with 1920 additions and 116 deletions

View file

@ -5,10 +5,17 @@ Changes to be released in next version
*
🙌 Improvements
*
* Room Notification Settings: Ability to change between "All Messages", "Mentions and Keywords" and "None". Not yet exposed in Element UI. (#4458).
* Add support for sending slow motion videos (#4483).
🐛 Bugfix
*
* VoIP: Do not present ended calls.
* More fixes to Main.storyboard layout on iPhone 12 Pro Max (#4527)
* Fix crash on Apple Silicon Macs.
* Media Picker: Generate video thumbnails with the correct orientation (#4515).
* Directory List (pop-up one): Fix duplicate rooms being shown (#4537).
* Use different title for scan button for self verification (#4525).
* it's easy for the back button to trigger a leftpanel reveal (#4438).
⚠️ API Changes
*

View file

@ -295,6 +295,7 @@ final class BuildSettings: NSObject {
static let roomSettingsScreenShowFlairSettings: Bool = true
static let roomSettingsScreenShowAdvancedSettings: Bool = true
static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption: Bool = true
static let roomSettingsScreenShowNotificationsV2: Bool = false
// MARK: - Room Member Screen

View file

@ -151,7 +151,7 @@
<!--People View Controller-->
<scene sceneID="Qba-PP-lco">
<objects>
<viewController storyboardIdentifier="PeopleViewController" id="IGB-jr-yFz" customClass="PeopleViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="PeopleViewController" extendedLayoutIncludesOpaqueBars="YES" id="IGB-jr-yFz" customClass="PeopleViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Mhy-d3-Jh6"/>
<viewControllerLayoutGuide type="bottom" id="Hkk-qB-8tq"/>
@ -177,7 +177,7 @@
<!--Favourites View Controller-->
<scene sceneID="z6B-k5-ano">
<objects>
<viewController storyboardIdentifier="FavouritesViewController" id="HnD-LA-psC" customClass="FavouritesViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="FavouritesViewController" extendedLayoutIncludesOpaqueBars="YES" id="HnD-LA-psC" customClass="FavouritesViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="pOc-AC-QkD"/>
<viewControllerLayoutGuide type="bottom" id="W6L-Au-CaZ"/>
@ -463,7 +463,7 @@
<!--Rooms View Controller-->
<scene sceneID="SDg-Pp-8Uj">
<objects>
<viewController storyboardIdentifier="RoomsViewController" id="HPQ-zg-lZR" customClass="RoomsViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="RoomsViewController" extendedLayoutIncludesOpaqueBars="YES" id="HPQ-zg-lZR" customClass="RoomsViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Hkg-kw-ioH"/>
<viewControllerLayoutGuide type="bottom" id="UI8-oQ-9M9"/>
@ -581,7 +581,7 @@
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="mhb-l9-pM3"/>
<segue reference="Tfl-tq-LQp"/>
<segue reference="f5u-Y1-7nt"/>
</inferredMetricsTieBreakers>
<resources>

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "room_action_notification_muted.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_action_notification_muted@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_action_notification_muted@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "notifications.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "notifications@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "notifications@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 B

View file

@ -710,6 +710,7 @@ Tap the + to start adding people.";
"room_details_topic" = "Topic";
"room_details_favourite_tag" = "Favourite";
"room_details_low_priority_tag" = "Low priority";
"room_details_notifs" = "Notifications";
"room_details_mute_notifs" = "Mute notifications";
"room_details_direct_chat" = "Direct Chat";
"room_details_access_section"="Who can access this room?";
@ -772,6 +773,17 @@ Tap the + to start adding people.";
"room_details_copy_room_address" = "Copy Room Address";
"room_details_copy_room_url" = "Copy Room URL";
// Room Notification Settings
"room_notifs_settings_notify_me_for" = "Notify me for";
"room_notifs_settings_all_messages" = "All Messages";
"room_notifs_settings_mentions_and_keywords" = "Mentions and Keywords only";
"room_notifs_settings_none" = "None";
"room_notifs_settings_done_action" = "Done";
"room_notifs_settings_cancel_action" = "Cancel";
"room_notifs_settings_manage_notifications" = "You can manage notifications in %@";
"room_notifs_settings_account_settings" = "Account settings";
"room_notifs_settings_encrypted_room_notice" = "Please note that mentions & keyword notifications are not available in encrypted rooms on mobile.";
// Group Details
"group_details_title" = "Community Details";
"group_details_home" = "Home";
@ -1375,6 +1387,7 @@ Tap the + to start adding people.";
"key_verification_verify_qr_code_information_other_device" = "Scan the code below to verify:";
"key_verification_verify_qr_code_emoji_information" = "Verify by comparing unique emoji.";
"key_verification_verify_qr_code_scan_code_action" = "Scan their code";
"key_verification_verify_qr_code_scan_code_other_device_action" = "Scan with this device";
"key_verification_verify_qr_code_cannot_scan_action" = "Can't scan?";
"key_verification_verify_qr_code_start_emoji_action" = "Verify by emoji";

View file

@ -85,6 +85,7 @@ internal enum Asset {
internal static let roomActionFavourite = ImageAsset(name: "room_action_favourite")
internal static let roomActionLeave = ImageAsset(name: "room_action_leave")
internal static let roomActionNotification = ImageAsset(name: "room_action_notification")
internal static let roomActionNotificationMuted = ImageAsset(name: "room_action_notification_muted")
internal static let roomActionPriorityHigh = ImageAsset(name: "room_action_priority_high")
internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low")
internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork")
@ -144,6 +145,7 @@ internal enum Asset {
internal static let membersListIcon = ImageAsset(name: "members_list_icon")
internal static let modIcon = ImageAsset(name: "mod_icon")
internal static let moreReactions = ImageAsset(name: "more_reactions")
internal static let notifications = ImageAsset(name: "notifications")
internal static let scrollup = ImageAsset(name: "scrollup")
internal static let roomsEmptyScreenArtwork = ImageAsset(name: "rooms_empty_screen_artwork")
internal static let roomsEmptyScreenArtworkDark = ImageAsset(name: "rooms_empty_screen_artwork_dark")

View file

@ -172,6 +172,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<Riot.RoomInfoListViewController>(storyboard: RoomInfoListViewController.self)
}
internal enum RoomNotificationSettingsViewController: StoryboardType {
internal static let storyboardName = "RoomNotificationSettingsViewController"
internal static let initialScene = InitialSceneType<Riot.RoomNotificationSettingsViewController>(storyboard: RoomNotificationSettingsViewController.self)
}
internal enum SecretsRecoveryWithKeyViewController: StoryboardType {
internal static let storyboardName = "SecretsRecoveryWithKeyViewController"

View file

@ -2014,6 +2014,10 @@ internal enum VectorL10n {
internal static var keyVerificationVerifyQrCodeScanCodeAction: String {
return VectorL10n.tr("Vector", "key_verification_verify_qr_code_scan_code_action")
}
/// Scan with this device
internal static var keyVerificationVerifyQrCodeScanCodeOtherDeviceAction: String {
return VectorL10n.tr("Vector", "key_verification_verify_qr_code_scan_code_other_device_action")
}
/// QR code has been successfully validated.
internal static var keyVerificationVerifyQrCodeScanOtherCodeSuccessMessage: String {
return VectorL10n.tr("Vector", "key_verification_verify_qr_code_scan_other_code_success_message")
@ -2710,6 +2714,10 @@ internal enum VectorL10n {
internal static var roomDetailsNoLocalAddressesForDm: String {
return VectorL10n.tr("Vector", "room_details_no_local_addresses_for_dm")
}
/// Notifications
internal static var roomDetailsNotifs: String {
return VectorL10n.tr("Vector", "room_details_notifs")
}
/// Members
internal static var roomDetailsPeople: String {
return VectorL10n.tr("Vector", "room_details_people")
@ -3018,6 +3026,42 @@ internal enum VectorL10n {
internal static var roomNoPrivilegesToCreateGroupCall: String {
return VectorL10n.tr("Vector", "room_no_privileges_to_create_group_call")
}
/// Account settings
internal static var roomNotifsSettingsAccountSettings: String {
return VectorL10n.tr("Vector", "room_notifs_settings_account_settings")
}
/// All Messages
internal static var roomNotifsSettingsAllMessages: String {
return VectorL10n.tr("Vector", "room_notifs_settings_all_messages")
}
/// Cancel
internal static var roomNotifsSettingsCancelAction: String {
return VectorL10n.tr("Vector", "room_notifs_settings_cancel_action")
}
/// Done
internal static var roomNotifsSettingsDoneAction: String {
return VectorL10n.tr("Vector", "room_notifs_settings_done_action")
}
/// Please note that mentions & keyword notifications are not available in encrypted rooms on mobile.
internal static var roomNotifsSettingsEncryptedRoomNotice: String {
return VectorL10n.tr("Vector", "room_notifs_settings_encrypted_room_notice")
}
/// You can manage notifications in %@
internal static func roomNotifsSettingsManageNotifications(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_notifs_settings_manage_notifications", p1)
}
/// Mentions and Keywords only
internal static var roomNotifsSettingsMentionsAndKeywords: String {
return VectorL10n.tr("Vector", "room_notifs_settings_mentions_and_keywords")
}
/// None
internal static var roomNotifsSettingsNone: String {
return VectorL10n.tr("Vector", "room_notifs_settings_none")
}
/// Notify me for
internal static var roomNotifsSettingsNotifyMeFor: String {
return VectorL10n.tr("Vector", "room_notifs_settings_notify_me_for")
}
/// Connectivity to the server has been lost.
internal static var roomOfflineNotification: String {
return VectorL10n.tr("Vector", "room_offline_notification")

View file

@ -393,7 +393,9 @@ class CallPresenter: NSObject {
if let oldCallVC = self.callVCs.values.first,
self.presentedCallVC == nil,
!self.uiOperationQueue.containsPresentCallVCOperation,
!self.uiOperationQueue.containsEnterPiPOperation {
!self.uiOperationQueue.containsEnterPiPOperation,
let oldCall = oldCallVC.mxCall,
oldCall.state != .ended {
// present the call screen after dismissing this one
self.presentCallVC(oldCallVC)
}

View file

@ -351,6 +351,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
NSURL *messageSoundURL = [[NSBundle mainBundle] URLForResource:@"message" withExtension:@"caf"];
AudioServicesCreateSystemSoundID((__bridge CFURLRef)messageSoundURL, &_messageSound);
// Set app info now as Mac (Designed for iPad) accesses it before didFinishLaunching is called
self.appInfo = AppInfo.current;
MXLogDebug(@"[AppDelegate] willFinishLaunchingWithOptions: Done");
return YES;
@ -371,8 +374,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
_configuration = [AppConfiguration new];
self.appInfo = AppInfo.current;
// Log app information
NSString *appDisplayName = self.appInfo.displayName;
NSString* appVersion = self.appVersion;

View file

@ -161,10 +161,15 @@
- (void)makeDirectEditedRoom:(BOOL)isDirect;
/**
Enable/disable the notifications for the selected room.
*/
Enable/disable the notifications for the selected room.
*/
- (void)muteEditedRoomNotifications:(BOOL)mute;
/**
Edit notification settings for the selected room.
*/
- (void)changeEditedRoomNotificationSettings;
/**
Show room directory.
*/

View file

@ -34,7 +34,7 @@
#import "Riot-Swift.h"
@interface RecentsViewController () <CreateRoomCoordinatorBridgePresenterDelegate, RoomsDirectoryCoordinatorBridgePresenterDelegate>
@interface RecentsViewController () <CreateRoomCoordinatorBridgePresenterDelegate, RoomsDirectoryCoordinatorBridgePresenterDelegate, RoomNotificationSettingsCoordinatorBridgePresenterDelegate>
{
// Tell whether a recents refresh is pending (suspended during editing mode).
BOOL isRefreshPending;
@ -74,6 +74,8 @@
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
@property (nonatomic, strong) RoomNotificationSettingsCoordinatorBridgePresenter *roomNotificationSettingsCoordinatorBridgePresenter;
@end
@implementation RecentsViewController
@ -1031,12 +1033,31 @@
UIContextualAction *muteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
[self muteEditedRoomNotifications:!isMuted];
if ([BuildSettings roomSettingsScreenShowNotificationsV2])
{
[self changeEditedRoomNotificationSettings];
}
else
{
[self muteEditedRoomNotifications:!isMuted];
}
completionHandler(YES);
}];
muteAction.backgroundColor = actionBackgroundColor;
UIImage *notificationImage = [UIImage imageNamed:@"room_action_notification"];
UIImage *notificationImage;
if([BuildSettings roomSettingsScreenShowNotificationsV2])
{
notificationImage = isMuted ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"];
}
else
{
notificationImage = [UIImage imageNamed:@"room_action_notification"];
}
notificationImage = [notificationImage vc_tintedImageUsingColor:isMuted ? unselectedColor : selectedColor];
muteAction.image = [notificationImage vc_notRenderedImage];
@ -1298,6 +1319,23 @@
}
}
- (void)changeEditedRoomNotificationSettings
{
if (editedRoomId)
{
// Check whether the user didn't leave the room
MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId];
if (room)
{
// navigate
self.roomNotificationSettingsCoordinatorBridgePresenter = [[RoomNotificationSettingsCoordinatorBridgePresenter alloc] initWithRoom:room];
self.roomNotificationSettingsCoordinatorBridgePresenter.delegate = self;
[self.roomNotificationSettingsCoordinatorBridgePresenter presentFrom:self animated:YES];
}
[self cancelEditionMode:isRefreshPending];
}
}
- (void)muteEditedRoomNotifications:(BOOL)mute
{
if (editedRoomId)
@ -1307,27 +1345,27 @@
if (room)
{
[self startActivityIndicator];
if (mute)
{
[room mentionsOnly:^{
[self stopActivityIndicator];
// Leave editing mode
[self cancelEditionMode:isRefreshPending];
[self cancelEditionMode:self->isRefreshPending];
}];
}
else
{
[room allMessages:^{
[self stopActivityIndicator];
// Leave editing mode
[self cancelEditionMode:isRefreshPending];
[self cancelEditionMode:self->isRefreshPending];
}];
}
}
@ -2219,4 +2257,11 @@
}
}
#pragma mark - RoomNotificationSettingsCoordinatorBridgePresenterDelegate
-(void)roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(RoomNotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.roomNotificationSettingsCoordinatorBridgePresenter = nil;
}
@end

View file

@ -0,0 +1,39 @@
//
// Copyright 2021 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 Foundation
import Reusable
class TitleHeaderView: UITableViewHeaderFooterView {
@IBOutlet weak var label: UILabel!
func update(title: String) {
label.text = title.uppercased()
}
}
extension TitleHeaderView: NibReusable {}
extension TitleHeaderView: Themable {
func update(theme: Theme) {
contentView.backgroundColor = theme.headerBackgroundColor
label.textColor = theme.headerTextSecondaryColor
label.font = theme.fonts.body
}
}

View file

@ -0,0 +1,39 @@
<?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">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="Yhn-wn-PmC" customClass="TitleHeaderView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Yhn-wn-PmC" id="o2E-Jb-B0E">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Gq6-Mz-QTu">
<rect key="frame" x="20" y="11" width="374" height="22"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="Gq6-Mz-QTu" firstAttribute="bottom" secondItem="o2E-Jb-B0E" secondAttribute="bottomMargin" id="3FQ-Oa-GSW"/>
<constraint firstAttribute="trailingMargin" secondItem="Gq6-Mz-QTu" secondAttribute="trailing" id="NOy-36-cTp"/>
<constraint firstItem="Gq6-Mz-QTu" firstAttribute="top" secondItem="o2E-Jb-B0E" secondAttribute="topMargin" id="iyg-1J-QRk"/>
<constraint firstItem="Gq6-Mz-QTu" firstAttribute="leading" secondItem="o2E-Jb-B0E" secondAttribute="leadingMargin" id="rPi-h4-Odq"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="label" destination="Gq6-Mz-QTu" id="dWm-UN-U4w"/>
</connections>
<point key="canvasLocation" x="-86" y="-242"/>
</tableViewCell>
</objects>
</document>

View file

@ -54,20 +54,20 @@
self.titleLabel.text = NSLocalizedStringFromTable(@"directory_search_results_title", @"Vector", nil);
// Do we need to display like ">20 results found" or "18 results found"?
NSString *descriptionLabel = (publicRoomsDirectoryDataSource.moreThanRoomsCount && publicRoomsDirectoryDataSource.roomsCount > 0) ? NSLocalizedStringFromTable(@"directory_search_results_more_than", @"Vector", nil) : NSLocalizedStringFromTable(@"directory_search_results", @"Vector", nil);
NSString *descriptionLabel = (publicRoomsDirectoryDataSource.searchResultsCountIsLimited && publicRoomsDirectoryDataSource.searchResultsCount > 0) ? NSLocalizedStringFromTable(@"directory_search_results_more_than", @"Vector", nil) : NSLocalizedStringFromTable(@"directory_search_results", @"Vector", nil);
self.descriptionLabel.text = [NSString stringWithFormat:descriptionLabel,
publicRoomsDirectoryDataSource.roomsCount,
publicRoomsDirectoryDataSource.searchResultsCount,
publicRoomsDirectoryDataSource.searchPattern];
}
else
{
self.titleLabel.text = NSLocalizedStringFromTable(@"directory_cell_title", @"Vector", nil);
self.descriptionLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"directory_cell_description", @"Vector", nil),
publicRoomsDirectoryDataSource.roomsCount];
publicRoomsDirectoryDataSource.searchResultsCount];
}
if (publicRoomsDirectoryDataSource.roomsCount)
if (publicRoomsDirectoryDataSource.searchResultsCount)
{
self.userInteractionEnabled = YES;
self.chevronImageView.hidden = NO;

View file

@ -347,7 +347,16 @@
tableViewCell.notificationsButton.tag = room.isMute || room.isMentionsOnly;
[tableViewCell.notificationsButton addTarget:self action:@selector(onNotificationsButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
tableViewCell.notificationsImageView.image = [UIImage imageNamed:@"room_action_notification"];
if ([BuildSettings roomSettingsScreenShowNotificationsV2])
{
tableViewCell.notificationsImageView.image = tableViewCell.notificationsButton.tag ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"];
}
else
{
tableViewCell.notificationsImageView.image = [UIImage imageNamed:@"room_action_notification"];
}
tableViewCell.notificationsImageView.tintColor = tableViewCell.notificationsButton.tag ? unselectedColor : selectedColor;
// Get the room tag (use only the first one).
@ -663,8 +672,15 @@
MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId];
if (room)
{
UIButton *button = (UIButton*)sender;
[self muteEditedRoomNotifications:!button.tag];
if ([BuildSettings roomSettingsScreenShowNotificationsV2])
{
[self changeEditedRoomNotificationSettings];
}
else
{
UIButton *button = (UIButton*)sender;
[self muteEditedRoomNotifications:!button.tag];
}
}
}
}

View file

@ -143,7 +143,9 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController {
self.titleLabel.text = VectorL10n.keyVerificationVerifyQrCodeTitle
self.informationLabel.text = VectorL10n.keyVerificationVerifyQrCodeInformation
self.scanCodeButton.setTitle(VectorL10n.keyVerificationVerifyQrCodeScanCodeAction, for: .normal)
// Hide until we have the type of the verification request
self.scanCodeButton.isHidden = true
self.cannotScanButton.setTitle(VectorL10n.keyVerificationVerifyQrCodeCannotScanAction, for: .normal)
}
@ -157,8 +159,8 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController {
self.render(error: error)
case .scannedCodeValidated(let isValid):
self.renderScannedCode(valid: isValid)
case .cancelled(let reason):
self.renderCancelled(reason: reason)
case .cancelled(let reason, let verificationKind):
self.renderCancelled(reason: reason, verificationKind: verificationKind)
case .cancelledByMe(let reason):
self.renderCancelledByMe(reason: reason)
}
@ -195,10 +197,13 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController {
switch viewData.verificationKind {
case .user:
informationText = VectorL10n.keyVerificationVerifyQrCodeInformation
self.scanCodeButton.setTitle(VectorL10n.keyVerificationVerifyQrCodeScanCodeAction, for: .normal)
default:
informationText = VectorL10n.keyVerificationVerifyQrCodeInformationOtherDevice
self.scanCodeButton.setTitle(VectorL10n.keyVerificationVerifyQrCodeScanCodeOtherDeviceAction, for: .normal)
}
self.scanCodeButton.isHidden = false
self.informationLabel.text = informationText
}
}
@ -231,12 +236,21 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController {
}
}
private func renderCancelled(reason: MXTransactionCancelCode) {
private func renderCancelled(reason: MXTransactionCancelCode,
verificationKind: KeyVerificationKind) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.stopQRCodeScanningIfPresented()
self.errorPresenter.presentError(from: self.alertPresentingViewController, title: "", message: VectorL10n.deviceVerificationCancelled, animated: true) {
// if we're verifying with someone else, let the user know they cancelled.
// if we're verifying our own device, assume the user probably knows since it was them who
// cancelled on their other device
if verificationKind == .user {
self.errorPresenter.presentError(from: self.alertPresentingViewController, title: "", message: VectorL10n.deviceVerificationCancelled, animated: true) {
self.dismissQRCodeScanningIfPresented(animated: false)
self.viewModel.process(viewAction: .cancel)
}
} else {
self.dismissQRCodeScanningIfPresented(animated: false)
self.viewModel.process(viewAction: .cancel)
}

View file

@ -225,7 +225,7 @@ final class KeyVerificationVerifyByScanningViewModel: KeyVerificationVerifyBySca
return
}
self.unregisterTransactionDidStateChangeNotification()
self.update(viewState: .cancelled(reason))
self.update(viewState: .cancelled(cancelCode: reason, verificationKind: verificationKind))
case MXSASTransactionStateCancelledByMe:
guard let reason = transaction.reasonCancelCode else {
return
@ -251,7 +251,7 @@ final class KeyVerificationVerifyByScanningViewModel: KeyVerificationVerifyBySca
return
}
self.unregisterTransactionDidStateChangeNotification()
self.update(viewState: .cancelled(reason))
self.update(viewState: .cancelled(cancelCode: reason, verificationKind: verificationKind))
case .cancelledByMe:
guard let reason = transaction.reasonCancelCode else {
return

View file

@ -29,7 +29,7 @@ enum KeyVerificationVerifyByScanningViewState {
case loading
case loaded(viewData: KeyVerificationVerifyByScanningViewData)
case scannedCodeValidated(isValid: Bool)
case cancelled(MXTransactionCancelCode)
case cancelled(cancelCode: MXTransactionCancelCode, verificationKind: KeyVerificationKind)
case cancelledByMe(MXTransactionCancelCode)
case error(Error)
}

View file

@ -83,8 +83,8 @@ extension MediaPickerCoordinator: MediaPickerViewControllerDelegate {
self.delegate?.mediaPickerCoordinator(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelectVideo videoURL: URL!) {
self.delegate?.mediaPickerCoordinator(self, didSelectVideoAt: videoURL)
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelectVideo videoAsset: AVAsset!) {
self.delegate?.mediaPickerCoordinator(self, didSelectVideo: videoAsset)
}
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelect assets: [PHAsset]!) {

View file

@ -20,7 +20,7 @@ import Foundation
@objc protocol MediaPickerCoordinatorBridgePresenterDelegate {
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideoAt url: URL)
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideo videoAsset: AVAsset)
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectAssets assets: [PHAsset])
func mediaPickerCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter)
}
@ -110,8 +110,8 @@ extension MediaPickerCoordinatorBridgePresenter: MediaPickerCoordinatorDelegate
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideoAt url: URL) {
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectVideoAt: url)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideo videoAsset: AVAsset) {
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectVideo: videoAsset)
}
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectAssets assets: [PHAsset]) {

View file

@ -20,7 +20,7 @@ import Foundation
protocol MediaPickerCoordinatorDelegate: AnyObject {
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideoAt url: URL)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideo videoAsset: AVAsset)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectAssets assets: [PHAsset])
func mediaPickerCoordinatorDidCancel(_ coordinator: MediaPickerCoordinatorType)
}

View file

@ -39,9 +39,9 @@
Tells the delegate that the user select a video.
@param mediaPickerController the `MediaPickerViewController` instance.
@param videoURL the local url of the video to send.
@param videoAsset an `AVAsset` that represents the video to send.
*/
- (void)mediaPickerController:(MediaPickerViewController *)mediaPickerController didSelectVideo:(NSURL*)videoURL;
- (void)mediaPickerController:(MediaPickerViewController *)mediaPickerController didSelectVideo:(AVAsset*)videoAsset;
/**
Tells the delegate that the user wants to cancel media picking.

View file

@ -608,28 +608,19 @@
if (asset)
{
if ([asset isKindOfClass:[AVURLAsset class]])
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Got AVAsset for video");
AVURLAsset *avURLAsset = (AVURLAsset*)asset;
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Got AVAsset for video");
// Validate first the selected video
[self validateSelectedVideo:asset responseHandler:^(BOOL isValidated) {
if (isValidated)
{
[self.delegate mediaPickerController:self didSelectVideo:asset];
}
// Validate first the selected video
[self validateSelectedVideo:[avURLAsset URL] responseHandler:^(BOOL isValidated) {
if (isValidated)
{
[self.delegate mediaPickerController:self didSelectVideo:[avURLAsset URL]];
}
self->isValidationInProgress = NO;
}];
}
else
{
MXLogDebug(@"[MediaPickerVC] Selected video asset is not initialized from an URL!");
self->isValidationInProgress = NO;
}
}];
}
else
{
@ -693,7 +684,7 @@
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)validateSelectedVideo:(NSURL*)selectedVideoURL responseHandler:(void (^)(BOOL isValidated))handler
- (void)validateSelectedVideo:(AVAsset*)selectedVideo responseHandler:(void (^)(BOOL isValidated))handler
{
[self dismissImageValidationView];
@ -727,15 +718,16 @@
videoPlayer = [[AVPlayerViewController alloc] init];
if (videoPlayer)
{
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:selectedVideo];
videoPlayer.allowsPictureInPicturePlayback = NO;
videoPlayer.updatesNowPlayingInfoCenter = NO;
videoPlayer.player = [AVPlayer playerWithURL:selectedVideoURL];
videoPlayer.player = [AVPlayer playerWithPlayerItem:item];
videoPlayer.videoGravity = AVLayerVideoGravityResizeAspect;
videoPlayer.showsPlaybackControls = NO;
// create a thumbnail for the first frame
AVAsset *asset = [AVAsset assetWithURL:selectedVideoURL];
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:selectedVideo];
generator.appliesPreferredTrackTransform = YES;
CGImageRef thumbnailRef = [generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:nil];
// set thumbnail on validationView

View file

@ -135,7 +135,7 @@ extension SingleImagePickerPresenter: MediaPickerCoordinatorBridgePresenterDeleg
self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideoAt url: URL) {
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideo videoAsset: AVAsset) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}

View file

@ -62,11 +62,16 @@
@property (nonatomic, readonly) NSString *directoryServerDisplayname;
/**
The number of public rooms matching `searchPattern`.
It is accurate only if 'moreThanRoomsCount' is NO.
The number of public rooms that have been fetched so far.
*/
@property (nonatomic, readonly) NSUInteger roomsCount;
/**
The total number of public rooms matching `searchPattern`.
It is accurate only if 'searchResultsCountIsLimited' is NO.
*/
@property (nonatomic, readonly) NSUInteger searchResultsCount;
/**
In case of search with a lot of matching public rooms, we cannot return an accurate
value except by paginating the full list of rooms, which is not expected.
@ -74,7 +79,7 @@
This flag indicates that we know that there is more matching rooms than we got
so far.
*/
@property (nonatomic, readonly) BOOL moreThanRoomsCount;
@property (nonatomic, readonly) BOOL searchResultsCountIsLimited;
/**
The maximum number of public rooms to retrieve during a pagination.

View file

@ -165,6 +165,11 @@ static NSString *const kNSFWKeyword = @"nsfw";
}
}
- (NSUInteger)roomsCount
{
return rooms.count;
}
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession
{
NSIndexPath *indexPath = nil;
@ -217,8 +222,8 @@ static NSString *const kNSFWKeyword = @"nsfw";
// Reset all pagination vars
[rooms removeAllObjects];
nextBatch = nil;
_roomsCount = 0;
_moreThanRoomsCount = NO;
_searchResultsCount = 0;
_searchResultsCountIsLimited = NO;
_hasReachedPaginationEnd = NO;
}
@ -264,14 +269,14 @@ static NSString *const kNSFWKeyword = @"nsfw";
if (!self->_searchPattern)
{
// When there is no search, we can use totalRoomCountEstimate returned by the server
self->_roomsCount = publicRoomsResponse.totalRoomCountEstimate;
self->_moreThanRoomsCount = NO;
self->_searchResultsCount = publicRoomsResponse.totalRoomCountEstimate;
self->_searchResultsCountIsLimited = NO;
}
else
{
// Else we can only display something like ">20 matching rooms"
self->_roomsCount = self->rooms.count;
self->_moreThanRoomsCount = publicRoomsResponse.nextBatch ? YES : NO;
self->_searchResultsCount = self->rooms.count;
self->_searchResultsCountIsLimited = publicRoomsResponse.nextBatch ? YES : NO;
}
// Detect pagination end

View file

@ -887,8 +887,10 @@ const CGFloat kTypingCellHeight = 24;
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure
{
AVURLAsset *videoAsset = [AVURLAsset assetWithURL:videoLocalURL];
UIImage *videoThumbnail = [MXKVideoThumbnailGenerator.shared generateThumbnailFrom:videoLocalURL];
[self sendVideo:videoLocalURL withThumbnail:videoThumbnail success:success failure:failure];
[self sendVideoAsset:videoAsset withThumbnail:videoThumbnail success:success failure:failure];
}
- (void)acceptVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure

View file

@ -0,0 +1,51 @@
//
// Copyright 2021 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 Foundation
import Reusable
class RoomNotificationSettingsAvatarView: UIView {
@IBOutlet weak var avatarView: MXKImageView!
@IBOutlet weak var nameLabel: UILabel!
func configure(viewData: AvatarViewDataProtocol) {
let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: viewData.matrixItemId, withDisplayName: viewData.displayName)
if let avatarUrl = viewData.avatarUrl {
avatarView.enableInMemoryCache = true
avatarView.setImageURI(avatarUrl,
withType: nil,
andImageOrientation: .up,
toFitViewSize: avatarView.frame.size,
with: MXThumbnailingMethodCrop,
previewImage: avatarImage,
mediaManager: viewData.mediaManager)
} else {
avatarView.image = avatarImage
}
nameLabel.text = viewData.displayName
}
}
extension RoomNotificationSettingsAvatarView: NibLoadable { }
extension RoomNotificationSettingsAvatarView: Themable {
func update(theme: Theme) {
nameLabel?.font = theme.fonts.title3SB
nameLabel?.textColor = theme.textPrimaryColor
}
}

View file

@ -0,0 +1,57 @@
<?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">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="RoomNotificationSettingsAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="192"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<subviews>
<view autoresizesSubviews="NO" clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q3Z-S1-Py9" customClass="MXKImageView">
<rect key="frame" x="167" y="20" width="80" height="80"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="80" id="tFe-kl-KGy"/>
<constraint firstAttribute="width" constant="80" id="tOu-Mt-atQ"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="40"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pBK-NN-5cI">
<rect key="frame" x="182" y="108" width="50" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="yzb-2V-G1M"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="q3Z-S1-Py9" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="20" id="1EV-ZD-9aG"/>
<constraint firstItem="pBK-NN-5cI" firstAttribute="top" secondItem="q3Z-S1-Py9" secondAttribute="bottom" constant="8" id="Rcf-3z-sEY"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="pBK-NN-5cI" secondAttribute="bottom" constant="60" id="V3E-vX-joC"/>
<constraint firstItem="pBK-NN-5cI" firstAttribute="centerX" secondItem="q3Z-S1-Py9" secondAttribute="centerX" id="vNG-KB-1Vd"/>
<constraint firstItem="q3Z-S1-Py9" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="wKJ-BP-2RA"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="avatarView" destination="q3Z-S1-Py9" id="cLO-Y3-6If"/>
<outlet property="nameLabel" destination="pBK-NN-5cI" id="Wt8-Oe-9mK"/>
</connections>
<point key="canvasLocation" x="86.956521739130437" y="99.776785714285708"/>
</view>
</objects>
</document>

View file

@ -0,0 +1,100 @@
//
// Copyright 2021 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 Foundation
@objc protocol RoomNotificationSettingsCoordinatorBridgePresenterDelegate {
func roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomNotificationSettingsCoordinatorBridgePresenter)
}
/// RoomNotificationSettingsCoordinatorBridgePresenter enables to start RoomNotificationSettingsCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers).
/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
@objcMembers
final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let room: MXRoom
private var coordinator: RoomNotificationSettingsCoordinator?
// MARK: Public
weak var delegate: RoomNotificationSettingsCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(room: MXRoom) {
self.room = room
super.init()
}
// MARK: - Public
// NOTE: Default value feature is not compatible with Objective-C.
// func present(from viewController: UIViewController, animated: Bool) {
// self.present(from: viewController, animated: animated)
// }
func present(from viewController: UIViewController, animated: Bool) {
let roomNotificationSettingsCoordinator = RoomNotificationSettingsCoordinator(room: room)
roomNotificationSettingsCoordinator.delegate = self
let presentable = roomNotificationSettingsCoordinator.toPresentable()
let navigationController = RiotNavigationController(rootViewController: presentable)
navigationController.modalPresentationStyle = .formSheet
presentable.presentationController?.delegate = self
viewController.present(navigationController, animated: animated, completion: nil)
roomNotificationSettingsCoordinator.start()
self.coordinator = roomNotificationSettingsCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - RoomNotificationSettingsCoordinatorDelegate
extension RoomNotificationSettingsCoordinatorBridgePresenter: RoomNotificationSettingsCoordinatorDelegate {
func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension RoomNotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}

View file

@ -0,0 +1,22 @@
//
// Copyright 2021 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 Foundation
struct RoomNotificationSettingsCellViewData {
let notificicationState: RoomNotificationState
let selected: Bool
}

View file

@ -0,0 +1,75 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
import UIKit
final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordinatorType {
// MARK: - Properties
// MARK: Private
private var roomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType
private let roomNotificationSettingsViewController: RoomNotificationSettingsViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: RoomNotificationSettingsCoordinatorDelegate?
// MARK: - Setup
init(room: MXRoom, showAvatar: Bool = true) {
let repository = RoomNotificationSettingsService(room: room)
let avatarData = showAvatar ? RoomAvatarViewData(
roomId: room.roomId,
displayName: room.summary.displayname,
avatarUrl: room.summary.avatar,
mediaManager: room.mxSession.mediaManager
) : nil
let roomNotificationSettingsViewModel = RoomNotificationSettingsViewModel(roomNotificationService: repository, roomEncrypted: room.summary.isEncrypted, avatarViewData: avatarData)
let roomNotificationSettingsViewController = RoomNotificationSettingsViewController.instantiate(with: roomNotificationSettingsViewModel)
self.roomNotificationSettingsViewModel = roomNotificationSettingsViewModel
self.roomNotificationSettingsViewController = roomNotificationSettingsViewController
}
// MARK: - Public methods
func start() {
self.roomNotificationSettingsViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.roomNotificationSettingsViewController
}
}
// MARK: - RoomNotificationSettingsViewModelCoordinatorDelegate
extension RoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate {
func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) {
self.delegate?.roomNotificationSettingsCoordinatorDidComplete(self)
}
func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) {
self.delegate?.roomNotificationSettingsCoordinatorDidCancel(self)
}
}

View file

@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
protocol RoomNotificationSettingsCoordinatorDelegate: AnyObject {
func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType)
func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType)
}
/// `RoomNotificationSettingsCoordinatorType` is a protocol describing a Coordinator that handles changes to the room navigation settings navigation flow.
protocol RoomNotificationSettingsCoordinatorType: Coordinator, Presentable {
var delegate: RoomNotificationSettingsCoordinatorDelegate? { get }
}

View file

@ -0,0 +1,60 @@
//
// Copyright 2021 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 UIKit
import Reusable
class RoomNotificationSettingsFooter: UITableViewHeaderFooterView {
struct State {
let showEncryptedNotice: Bool
let showAccountLink: Bool
}
@IBOutlet weak var label: UILabel!
func update(footerState: State) {
// Don't include link until global settings in place
// let paragraphStyle = NSMutableParagraphStyle()
// paragraphStyle.lineHeightMultiple = 1.16
// let paragraphAttributes: [NSAttributedString.Key: Any] = [
// NSAttributedString.Key.kern: -0.08,
// NSAttributedString.Key.paragraphStyle: paragraphStyle,
// NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13.0)
// ]
// let linkStr = VectorL10n.roomNotifsSettingsAccountSettings
// let formatStr = VectorL10n.roomNotifsSettingsManageNotifications(linkStr)
//
// let formattedStr = String(format: formatStr, arguments: [linkStr])
// let footer0 = NSMutableAttributedString(string: formattedStr, attributes: paragraphAttributes)
// let linkRange = (footer0.string as NSString).range(of: linkStr)
// footer0.addAttribute(NSAttributedString.Key.link, value: Constants.linkToAccountSettings, range: linkRange)
label.text = footerState.showEncryptedNotice ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : nil
}
}
extension RoomNotificationSettingsFooter: NibReusable {}
extension RoomNotificationSettingsFooter: Themable {
func update(theme: Theme) {
contentView.backgroundColor = theme.headerBackgroundColor
label.textColor = theme.headerTextSecondaryColor
}
}

View file

@ -0,0 +1,39 @@
<?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">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="Ecs-Zf-2tR" customClass="RoomNotificationSettingsFooter" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="76"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Ecs-Zf-2tR" id="f2j-NO-hdS">
<rect key="frame" x="0.0" y="0.0" width="414" height="76"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xlF-dY-Ud8">
<rect key="frame" x="20" y="16" width="374" height="44"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="xlF-dY-Ud8" firstAttribute="bottom" secondItem="f2j-NO-hdS" secondAttribute="bottom" constant="-16" id="BWu-8S-Qnj"/>
<constraint firstItem="xlF-dY-Ud8" firstAttribute="top" secondItem="f2j-NO-hdS" secondAttribute="top" constant="16" id="Lpw-g0-S41"/>
<constraint firstItem="xlF-dY-Ud8" firstAttribute="leading" secondItem="f2j-NO-hdS" secondAttribute="leadingMargin" id="Zf5-c0-agj"/>
<constraint firstAttribute="trailingMargin" secondItem="xlF-dY-Ud8" secondAttribute="trailing" id="wIa-z6-N5E"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="label" destination="xlF-dY-Ud8" id="qhR-4x-7Yt"/>
</connections>
<point key="canvasLocation" x="37" y="-210"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,27 @@
//
// Copyright 2021 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 Foundation
typealias UpdateRoomNotificationStateCompletion = () -> Void
typealias RoomNotificationStateCallback = (RoomNotificationState) -> Void
protocol RoomNotificationSettingsServiceType {
func observeNotificationState(listener: @escaping RoomNotificationStateCallback)
func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion)
var notificationState: RoomNotificationState { get }
}

View file

@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
/// RoomNotificationSettingsViewController view actions exposed to view model
enum RoomNotificationSettingsViewAction {
case load
case selectNotificationState(RoomNotificationState)
case save
case cancel
}

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Room Notification Settings View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="RoomNotificationSettingsViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="1Jo-Pf-c9m">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<connections>
<outlet property="dataSource" destination="V8j-Lb-PgC" id="pQ7-Q4-4cn"/>
<outlet property="delegate" destination="V8j-Lb-PgC" id="snv-x4-IWg"/>
</connections>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="bFg-jh-JZB" firstAttribute="bottom" secondItem="1Jo-Pf-c9m" secondAttribute="bottom" id="TYv-T2-NmY"/>
<constraint firstItem="1Jo-Pf-c9m" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="f6H-cf-mjJ"/>
<constraint firstItem="1Jo-Pf-c9m" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="gcX-5S-aMb"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="1Jo-Pf-c9m" secondAttribute="trailing" id="hJ7-5d-23W"/>
</constraints>
</view>
<connections>
<outlet property="mainTableView" destination="1Jo-Pf-c9m" id="Edg-Ng-fo9"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-3198" y="-647"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -0,0 +1,233 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 UIKit
final class RoomNotificationSettingsViewController: UIViewController {
// MARK: - Properties
private enum Constants {
static let linkToAccountSettings = "linkToAccountSettings"
}
// MARK: Outlets
@IBOutlet private weak var mainTableView: UITableView!
// MARK: Private
private var viewModel: RoomNotificationSettingsViewModelType!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private lazy var avatarView: RoomNotificationSettingsAvatarView = {
RoomNotificationSettingsAvatarView.loadFromNib()
}()
private struct Row {
var cellViewData: RoomNotificationSettingsCellViewData
var action: (() -> Void)?
}
private struct Section {
var title: String
var rows: [Row]
var footerState: RoomNotificationSettingsFooter.State
}
private var sections: [Section] = [] {
didSet {
mainTableView.reloadData()
}
}
private var viewState: RoomNotificationSettingsViewStateType!
// MARK: - Setup
class func instantiate(with viewModel: RoomNotificationSettingsViewModelType) -> RoomNotificationSettingsViewController {
let viewController = StoryboardScene.RoomNotificationSettingsViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupViews()
activityPresenter = ActivityIndicatorPresenter()
errorPresenter = MXKErrorAlertPresentation()
registerThemeServiceDidChangeThemeNotification()
update(theme: theme)
viewModel.viewDelegate = self
viewModel.process(viewAction: .load)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return theme.statusBarStyle
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
view.backgroundColor = theme.headerBackgroundColor
mainTableView.backgroundColor = theme.headerBackgroundColor
if let navigationBar = navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
mainTableView.reloadData()
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
update(theme: ThemeService.shared().theme)
}
private func setupViews() {
self.title = VectorL10n.roomDetailsNotifs
let doneBarButtonItem = MXKBarButtonItem(title: VectorL10n.roomNotifsSettingsDoneAction, style: .plain) { [weak self] in
self?.viewModel.process(viewAction: .save)
}
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.roomNotifsSettingsCancelAction, style: .plain) { [weak self] in
self?.viewModel.process(viewAction: .cancel)
}
if navigationController?.navigationBar.backItem == nil {
navigationItem.leftBarButtonItem = cancelBarButtonItem
}
navigationItem.rightBarButtonItem = doneBarButtonItem
mainTableView.register(cellType: RoomNotificationSettingsCell.self)
mainTableView.register(headerFooterViewType: RoomNotificationSettingsFooter.self)
mainTableView.register(headerFooterViewType: TitleHeaderView.self)
mainTableView.sectionFooterHeight = UITableView.automaticDimension
mainTableView.sectionHeaderHeight = UITableView.automaticDimension
mainTableView.estimatedSectionFooterHeight = 50
mainTableView.estimatedSectionHeaderHeight = 30
}
private func render(viewState: RoomNotificationSettingsViewStateType) {
if viewState.saving {
activityPresenter.presentActivityIndicator(on: view, animated: true)
} else {
activityPresenter.removeCurrentActivityIndicator(animated: true)
}
self.viewState = viewState
if let avatarData = viewState.avatarData {
mainTableView.tableHeaderView = avatarView
avatarView.configure(viewData: avatarData)
avatarView.update(theme: theme)
}
updateSections()
}
private func updateSections() {
let rows = viewState.notificationOptions.map({ (setting) -> Row in
let cellViewData = RoomNotificationSettingsCellViewData(notificicationState: setting, selected: viewState.notificationState == setting)
return Row(cellViewData: cellViewData,
action: {
self.viewModel.process(viewAction: .selectNotificationState(setting))
})
})
let footerState = RoomNotificationSettingsFooter.State(showEncryptedNotice: viewState.roomEncrypted, showAccountLink: false)
let section0 = Section(title: VectorL10n.roomNotifsSettingsNotifyMeFor, rows: rows, footerState: footerState)
sections = [
section0
]
}
}
// MARK: - UITableViewDataSource
extension RoomNotificationSettingsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = sections[indexPath.section].rows[indexPath.row]
let cell: RoomNotificationSettingsCell = tableView.dequeueReusableCell(for: indexPath)
cell.update(state: row.cellViewData)
cell.update(theme: theme)
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
}
// MARK: - UITableViewDelegate
extension RoomNotificationSettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let headerView: TitleHeaderView = tableView.dequeueReusableHeaderFooterView() else { return nil }
headerView.update(title: sections[section].title)
headerView.update(theme: theme)
return headerView
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let footerView: RoomNotificationSettingsFooter = tableView.dequeueReusableHeaderFooterView() else { return nil }
let footerState = sections[section].footerState
footerView.update(footerState: footerState)
footerView.update(theme: theme)
return footerView
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let row = sections[indexPath.section].rows[indexPath.row]
row.action?()
}
}
// MARK: - RoomNotificationSettingsViewModelViewDelegate
extension RoomNotificationSettingsViewController: RoomNotificationSettingsViewModelViewDelegate {
func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewSate: RoomNotificationSettingsViewStateType) {
render(viewState: viewSate)
}
}

View file

@ -0,0 +1,89 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
final class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType {
// MARK: - Properties
// MARK: Private
private let roomNotificationService: RoomNotificationSettingsServiceType
private var state: RoomNotificationSettingsViewState {
willSet {
update(viewState: newValue)
}
}
// MARK: Public
weak var viewDelegate: RoomNotificationSettingsViewModelViewDelegate?
weak var coordinatorDelegate: RoomNotificationSettingsViewModelCoordinatorDelegate?
// MARK: - Setup
init(roomNotificationService: RoomNotificationSettingsServiceType, roomEncrypted: Bool, avatarViewData: AvatarViewDataProtocol?) {
self.roomNotificationService = roomNotificationService
let notificationState = Self.mapNotificationStateOnRead(encrypted: roomEncrypted, state: roomNotificationService.notificationState)
self.state = RoomNotificationSettingsViewState(roomEncrypted: roomEncrypted, saving: false, notificationState: notificationState, avatarData: avatarViewData)
self.roomNotificationService.observeNotificationState { [weak self] state in
guard let self = self else { return }
self.state.notificationState = Self.mapNotificationStateOnRead(encrypted: roomEncrypted, state: state)
}
}
// MARK: - Public
func process(viewAction: RoomNotificationSettingsViewAction) {
switch viewAction {
case .load:
update(viewState: self.state)
case .selectNotificationState(let state):
self.state.notificationState = state
case .save:
self.state.saving = true
roomNotificationService.update(state: state.notificationState) { [weak self] in
guard let self = self else { return }
self.state.saving = false
self.coordinatorDelegate?.roomNotificationSettingsViewModelDidComplete(self)
}
case .cancel:
coordinatorDelegate?.roomNotificationSettingsViewModelDidCancel(self)
}
}
// MARK: - Private
private static func mapNotificationStateOnRead(encrypted: Bool, state: RoomNotificationState) -> RoomNotificationState {
if encrypted, case .mentionsAndKeywordsOnly = state {
// Notifications not supported on encrypted rooms, map mentionsOnly to mute on read
return .mute
} else {
return state
}
}
private func update(viewState: RoomNotificationSettingsViewStateType) {
self.viewDelegate?.roomNotificationSettingsViewModel(self, didUpdateViewState: viewState)
}
}

View file

@ -0,0 +1,37 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
protocol RoomNotificationSettingsViewModelViewDelegate: AnyObject {
func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewState: RoomNotificationSettingsViewStateType)
}
protocol RoomNotificationSettingsViewModelCoordinatorDelegate: AnyObject {
func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType)
func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType)
}
/// Protocol describing the view model used by `RoomNotificationSettingsViewController`
protocol RoomNotificationSettingsViewModelType {
var viewDelegate: RoomNotificationSettingsViewModelViewDelegate? { get set }
var coordinatorDelegate: RoomNotificationSettingsViewModelCoordinatorDelegate? { get set }
func process(viewAction: RoomNotificationSettingsViewAction)
}

View file

@ -0,0 +1,42 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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 Foundation
/// RoomNotificationSettingsViewController view state
struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType {
let roomEncrypted: Bool
var saving: Bool
var notificationState: RoomNotificationState
var notificationOptions: [RoomNotificationState] {
if roomEncrypted {
return [.all, .mute]
} else {
return RoomNotificationState.allCases
}
}
let avatarData: AvatarViewDataProtocol?
}
protocol RoomNotificationSettingsViewStateType {
var saving: Bool { get }
var roomEncrypted: Bool { get }
var notificationOptions: [RoomNotificationState] { get }
var notificationState: RoomNotificationState { get }
var avatarData: AvatarViewDataProtocol? { get }
}

View file

@ -0,0 +1,23 @@
//
// Copyright 2021 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 Foundation
enum RoomNotificationState: CaseIterable {
case all
case mentionsAndKeywordsOnly
case mute
}

View file

@ -0,0 +1,56 @@
//
// Copyright 2021 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 UIKit
import Reusable
class RoomNotificationSettingsCell: UITableViewCell {
func update(state: RoomNotificationSettingsCellViewData) {
textLabel?.text = state.notificicationState.title
if state.selected {
accessoryView = UIImageView(image: Asset.Images.checkmark.image)
} else {
accessoryView = nil
}
}
}
extension RoomNotificationSettingsCell: Reusable {}
extension RoomNotificationSettingsCell: Themable {
func update(theme: Theme) {
textLabel?.font = theme.fonts.body
textLabel?.textColor = theme.textPrimaryColor
backgroundColor = theme.backgroundColor
contentView.backgroundColor = .clear
tintColor = theme.tintColor
selectedBackgroundView = UIView()
selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor
}
}
fileprivate extension RoomNotificationState {
var title: String {
switch self {
case .all:
return VectorL10n.roomNotifsSettingsAllMessages
case .mentionsAndKeywordsOnly:
return VectorL10n.roomNotifsSettingsMentionsAndKeywords
case .mute:
return VectorL10n.roomNotifsSettingsNone
}
}
}

View file

@ -0,0 +1,325 @@
//
// Copyright 2021 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 Foundation
final class RoomNotificationSettingsService: RoomNotificationSettingsServiceType {
typealias Completion = () -> Void
// MARK: - Properties
// MARK: Private
private let room: MXRoom
private var notificationCenterDidUpdateObserver: NSObjectProtocol?
private var notificationCenterDidFailObserver: NSObjectProtocol?
private var observers: [ObjectIdentifier] = []
// MARK: Public
var notificationState: RoomNotificationState {
room.notificationState
}
// MARK: - Setup
init(room: MXRoom) {
self.room = room
}
deinit {
observers.forEach(NotificationCenter.default.removeObserver)
}
// MARK: - Public
func observeNotificationState(listener: @escaping RoomNotificationStateCallback) {
let observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
listener(self.room.notificationState)
}
observers += [ObjectIdentifier(observer)]
}
func update(state: RoomNotificationState, completion: @escaping Completion) {
switch state {
case .all:
allMessages(completion: completion)
case .mentionsAndKeywordsOnly:
mentionsOnly(completion: completion)
case .mute:
mute(completion: completion)
}
}
// MARK: - Private
private func mute(completion: @escaping Completion) {
guard !room.isMuted else {
completion()
return
}
if let rule = room.roomPushRule {
removePushRule(rule: rule) {
self.mute(completion: completion)
}
return
}
guard let rule = room.overridePushRule else {
self.addPushRuleToMute(completion: completion)
return
}
guard notificationCenterDidUpdateObserver == nil else {
MXLog.debug("[RoomNotificationSettingsService] Request in progress: ignore push rule update")
completion()
return
}
// if the user defined one, use it
if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) {
enablePushRule(rule: rule, completion: completion)
} else {
removePushRule(rule: rule) {
self.addPushRuleToMute(completion: completion)
}
}
}
private func mentionsOnly(completion: @escaping Completion) {
guard !room.isMentionsOnly else {
completion()
return
}
if let rule = room.overridePushRule, room.isMuted {
removePushRule(rule: rule) {
self.mentionsOnly(completion: completion)
}
return
}
guard let rule = room.roomPushRule else {
addPushRuleToMentionOnly(completion: completion)
return
}
guard notificationCenterDidUpdateObserver == nil else {
MXLog.debug("[MXRoom+Riot] Request in progress: ignore push rule update")
completion()
return
}
// if the user defined one, use it
if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) {
enablePushRule(rule: rule, completion: completion)
} else {
removePushRule(rule: rule) {
self.addPushRuleToMentionOnly(completion: completion)
}
}
}
private func allMessages(completion: @escaping Completion) {
if !room.isMentionsOnly && !room.isMuted {
completion()
return
}
if let rule = room.overridePushRule, room.isMuted {
removePushRule(rule: rule) {
self.allMessages(completion: completion)
}
return
}
if let rule = room.roomPushRule, room.isMentionsOnly {
removePushRule(rule: rule, completion: completion)
}
}
private func addPushRuleToMentionOnly(completion: @escaping Completion) {
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.roomPushRule != nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.addRoomRule(
room.roomId,
notify: false,
sound: false,
highlight: false)
}
private func addPushRuleToMute(completion: @escaping Completion) {
guard let roomId = room.roomId else {
return
}
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.overridePushRule != nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.addOverrideRule(
withId: roomId,
conditions: [["kind": "event_match", "key": "room_id", "pattern": roomId]],
notify: false,
sound: false,
highlight: false
)
}
private func removePushRule(rule: MXPushRule, completion: @escaping Completion) {
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.mxSession.notificationCenter.rule(byId: rule.ruleId) == nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.removeRule(rule)
}
private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) {
handleUpdateCallback(completion) {
// No way to check whether this notification concerns the push rule. Consider the change is applied.
return true
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.enableRule(rule, isEnabled: true)
}
private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) {
notificationCenterDidUpdateObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
if releaseCheck() {
self.removeObservers()
completion()
}
}
}
private func handleFailureCallback(_ completion: @escaping Completion) {
notificationCenterDidFailObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidFailRulesUpdate),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
self.removeObservers()
completion()
}
}
func removeObservers() {
if let observer = self.notificationCenterDidUpdateObserver {
NotificationCenter.default.removeObserver(observer)
self.notificationCenterDidUpdateObserver = nil
}
if let observer = self.notificationCenterDidFailObserver {
NotificationCenter.default.removeObserver(observer)
self.notificationCenterDidFailObserver = nil
}
}
}
// We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment
fileprivate extension MXRoom {
typealias Completion = () -> Void
func getRoomRule(from rules: [Any]) -> MXPushRule? {
guard let pushRules = rules as? [MXPushRule] else {
return nil
}
return pushRules.first(where: { self.roomId == $0.ruleId })
}
var overridePushRule: MXPushRule? {
getRoomRule(from: mxSession.notificationCenter.rules.global.override)
}
var roomPushRule: MXPushRule? {
getRoomRule(from: mxSession.notificationCenter.rules.global.room)
}
var notificationState: RoomNotificationState {
if isMuted {
return .mute
}
if isMentionsOnly {
return .mentionsAndKeywordsOnly
}
return .all
}
var isMuted: Bool {
// Check whether an override rule has been defined with the roomm id as rule id.
// This kind of rule is created to mute the room
guard let rule = self.overridePushRule,
rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify),
rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else {
return false
}
return rule.enabled
}
var isMentionsOnly: Bool {
// Check push rules at room level
guard let rule = roomPushRule else { return false }
return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify)
}
}
fileprivate extension MXPushRule {
func actionsContains(actionType: MXPushRuleActionType) -> Bool {
guard let actions = actions as? [MXPushRuleAction] else {
return false
}
return actions.contains(where: { $0.actionType == actionType })
}
func conditionIsEnabled(kind: MXPushRuleConditionType, for roomId: String) -> Bool {
guard let conditions = conditions as? [MXPushRuleCondition] else {
return false
}
let ruleContainsCondition = conditions.contains { condition in
guard case kind = MXPushRuleConditionType(identifier: condition.kind),
let key = condition.parameters["key"] as? String,
let pattern = condition.parameters["pattern"] as? String
else { return false }
return key == "room_id" && pattern == roomId
}
return ruleContainsCondition && enabled
}
}

View file

@ -137,6 +137,12 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
return coordinator
}
private func createRoomNotificationSettingsCoordinator() -> RoomNotificationSettingsCoordinator {
let coordinator = RoomNotificationSettingsCoordinator(room: room, showAvatar: false)
coordinator.delegate = self
return coordinator
}
private func showRoomDetails(with target: RoomInfoListTarget, animated: Bool) {
switch target {
case .integrations:
@ -152,8 +158,16 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.navigationRouter.push(search, animated: animated, popCompletion: nil)
}
})
case .notifications:
let coordinator = createRoomNotificationSettingsCoordinator()
coordinator.start()
self.add(childCoordinator: coordinator)
self.navigationRouter.push(coordinator, animated: true, popCompletion: nil)
default:
segmentedViewController.selectedIndex = target.tabIndex
guard let tabIndex = target.tabIndex else {
fatalError("No settings tab index for this target.")
}
segmentedViewController.selectedIndex = tabIndex
if case .settings(let roomSettingsField) = target {
roomSettingsViewController?.selectedRoomSettingsField = roomSettingsField
@ -184,3 +198,14 @@ extension RoomInfoCoordinator: RoomParticipantsViewControllerDelegate {
}
}
extension RoomInfoCoordinator: RoomNotificationSettingsCoordinatorDelegate {
func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) {
self.navigationRouter.popModule(animated: true)
}
func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) {
}
}

View file

@ -24,25 +24,21 @@ enum RoomInfoListTarget: Equatable {
case uploads
case integrations
case search
var tabIndex: UInt {
let tabIndex: UInt
case notifications
var tabIndex: UInt? {
switch self {
case .members:
tabIndex = 0
return 0
case .uploads:
tabIndex = 1
return 1
case .settings:
tabIndex = 2
case .integrations:
tabIndex = 3
case .search:
tabIndex = 4
return 2
default:
return nil
}
return tabIndex
}
}
/// RoomInfoListViewController view actions exposed to view model

View file

@ -150,6 +150,9 @@ final class RoomInfoListViewController: UIViewController {
let rowSettings = Row(type: .default, icon: Asset.Images.settingsIcon.image, text: VectorL10n.roomDetailsSettings, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .navigate(target: .settings()))
}
let roomNotifications = Row(type: .default, icon: Asset.Images.notifications.image, text: VectorL10n.roomDetailsNotifs, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .navigate(target: .notifications))
}
let text = viewData.numberOfMembers == 1 ? VectorL10n.roomInfoListOneMember : VectorL10n.roomInfoListSeveralMembers(String(viewData.numberOfMembers))
let rowMembers = Row(type: .default, icon: Asset.Images.userIcon.image, text: text, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .navigate(target: .members))
@ -165,6 +168,10 @@ final class RoomInfoListViewController: UIViewController {
}
var rows = [rowSettings]
if BuildSettings.roomSettingsScreenShowNotificationsV2 {
rows.append(roomNotifications)
}
if RiotSettings.shared.roomInfoScreenShowIntegrations {
rows.append(rowIntegrations)
}

View file

@ -6056,7 +6056,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedVideo:url isPhotoLibraryAsset:NO];
AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url];
[roomInputToolbarView sendSelectedVideoAsset:selectedVideo isPhotoLibraryAsset:NO];
}
}
@ -6080,7 +6081,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
}
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectVideoAt:(NSURL *)url
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectVideo:(AVAsset *)videoAsset
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
@ -6088,7 +6089,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedVideo:url isPhotoLibraryAsset:YES];
[roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:YES];
}
}

View file

@ -528,7 +528,10 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
{
[sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_DIRECT_CHAT];
}
[sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS];
if (!BuildSettings.roomSettingsScreenShowNotificationsV2)
{
[sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS];
}
[sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE];
[tmpSections addObject:sectionMain];

View file

@ -245,9 +245,9 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
}
private func setupSideMenuGestures() {
self.parameters.appNavigator.sideMenu.addScreenEdgePanGesturesToPresent(to: self.masterNavigationController.view)
self.parameters.appNavigator.sideMenu.addPanGestureToPresent(to: self.masterNavigationController.navigationBar)
if let rootViewController = self.masterNavigationController.viewControllers.first {
self.parameters.appNavigator.sideMenu.addScreenEdgePanGesturesToPresent(to: rootViewController.view)
}
}
// MARK: Navigation

View file

@ -1165,8 +1165,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
}
// Retrieve the video frame at 1 sec to define the video thumbnail
AVURLAsset *urlAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil];
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset];
AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil];
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset];
assetImageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMake(1, 1);
CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil];
@ -1174,7 +1174,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease(imageRef);
[room sendVideo:videoLocalUrl withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) {
[room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) {
if (successBlock)
{
successBlock();

View file

@ -0,0 +1,146 @@
//
// Copyright 2021 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 XCTest
@testable import Riot
class MockRoomNotificationSettingsService: RoomNotificationSettingsServiceType {
var listener: RoomNotificationStateCallback?
var notificationState: RoomNotificationState
init(initialState: RoomNotificationState) {
notificationState = initialState
}
func observeNotificationState(listener: @escaping RoomNotificationStateCallback) {
self.listener = listener
}
func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion) {
self.notificationState = state
completion()
listener?(state)
}
}
class MockRoomNotificationSettingsView: RoomNotificationSettingsViewModelViewDelegate {
var viewState: RoomNotificationSettingsViewStateType?
func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewState: RoomNotificationSettingsViewStateType) {
self.viewState = viewState
}
}
class MockRoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate {
var didComplete = false
var didCancel = false
func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) {
didComplete = true
}
func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) {
didCancel = true
}
}
class RoomNotificationSettingsViewModelTests: XCTestCase {
enum Constants{
static let roomDisplayName: String = "Test Room Name"
static let roomId: String = "1"
static let avatarUrl: String = "http://test.url.com"
static let avatarData = RoomAvatarViewData(roomId: "1", displayName: roomDisplayName, avatarUrl: avatarUrl, mediaManager: MXMediaManager())
}
var coordinator: MockRoomNotificationSettingsCoordinator!
var service: MockRoomNotificationSettingsService!
var view: MockRoomNotificationSettingsView!
var viewModel: RoomNotificationSettingsViewModel!
override func setUpWithError() throws {
service = MockRoomNotificationSettingsService(initialState: .all)
view = MockRoomNotificationSettingsView()
coordinator = MockRoomNotificationSettingsCoordinator()
}
func setupViewModel(roomEncrypted: Bool, showAvatar: Bool) {
let avatarData = showAvatar ? Constants.avatarData : nil
let viewModel = RoomNotificationSettingsViewModel(roomNotificationService: service, roomEncrypted: roomEncrypted, avatarViewData: avatarData)
viewModel.viewDelegate = view
viewModel.coordinatorDelegate = coordinator
self.viewModel = viewModel
}
func testUnloaded() throws {
setupViewModel(roomEncrypted: true, showAvatar: false)
XCTAssertNil(view.viewState)
}
func testUnencryptedOptions() throws {
setupViewModel(roomEncrypted: false, showAvatar: false)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState)
XCTAssertTrue(view.viewState!.notificationOptions.count == 3)
}
func testEncryptedOptions() throws {
setupViewModel(roomEncrypted: true, showAvatar: false)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState)
XCTAssertTrue(view.viewState!.notificationOptions.count == 2)
}
func testAvatar() throws {
setupViewModel(roomEncrypted: true, showAvatar: true)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState?.avatarData)
XCTAssertEqual(view.viewState!.avatarData!.avatarUrl, Constants.avatarUrl)
}
func testSelectionUpdateAndSave() throws {
setupViewModel(roomEncrypted: false, showAvatar: false)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState)
XCTAssertTrue(view.viewState!.notificationState == .all)
viewModel.process(viewAction: .selectNotificationState(.mentionsAndKeywordsOnly))
XCTAssertTrue(view.viewState!.notificationState == .mentionsAndKeywordsOnly)
viewModel.process(viewAction: .save)
XCTAssertTrue(service.notificationState == .mentionsAndKeywordsOnly)
XCTAssertTrue(coordinator.didComplete)
}
func testCancel() throws {
setupViewModel(roomEncrypted: false, showAvatar: false)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState)
viewModel.process(viewAction: .cancel)
XCTAssertTrue(coordinator.didCancel)
}
func testMentionsOnlyNotAvaileOnEncryptedRoom() throws {
service = MockRoomNotificationSettingsService(initialState: .mentionsAndKeywordsOnly)
setupViewModel(roomEncrypted: true, showAvatar: false)
viewModel.process(viewAction: .load)
XCTAssertNotNil(view.viewState)
XCTAssertTrue(view.viewState!.notificationState == .mute)
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 New Vector Ltd
Copyright 2021 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.