Merge branch 'develop' into gil/SP1_space_creation

This commit is contained in:
Gil Eluard
2022-03-01 10:15:13 +01:00
164 changed files with 3643 additions and 1650 deletions
@@ -1,47 +0,0 @@
//
// 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
import MatrixSDK
import CommonKit
/// Presenter which displays activity / loading indicators using app-wide `AppNavigator`, thus displaying them in a unified way,
/// and `ActivityCenter`/`Activity`, which ensures that only one activity is shown at a given time.
///
/// Note: clients can skip using `AppActivityIndicatorPresenter` and instead coordiinate with `AppNavigatorProtocol` directly.
/// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers
/// written in objective-c.
@objc final class AppActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType {
private let appNavigator: AppNavigatorProtocol
private var activity: Activity?
init(appNavigator: AppNavigatorProtocol) {
self.appNavigator = appNavigator
}
@objc func presentActivityIndicator() {
activity = appNavigator.addLoadingActivity()
}
@objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) {
activity = nil
}
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) {
MXLog.error("[AppActivityIndicatorPresenter] Shared activity indicator does not support presenting from custom views")
}
}
@@ -0,0 +1,62 @@
//
// 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
import MatrixSDK
import CommonKit
/// Presenter which displays loading spinners using app-wide `AppNavigator`, thus displaying them in a unified way,
/// and `UserIndicatorCenter`/`UserIndicator`, which ensures that only one indicator is shown at a given time.
///
/// Note: clients can skip using `AppUserIndicatorPresenter` and instead coordinate with `AppNavigatorProtocol` directly.
/// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers
/// written in objective-c.
@objc final class AppUserIndicatorPresenter: NSObject, ActivityIndicatorPresenterType {
private let appNavigator: AppNavigatorProtocol
private var loadingIndicator: UserIndicator?
private var otherIndicators = [UserIndicator]()
init(appNavigator: AppNavigatorProtocol) {
self.appNavigator = appNavigator
}
@objc func presentActivityIndicator() {
presentActivityIndicator(label: VectorL10n.homeSyncing)
}
@objc func presentActivityIndicator(label: String) {
guard loadingIndicator == nil || loadingIndicator?.state == .completed else {
// The app is very liberal with calling `presentActivityIndicator` (often not matched by corresponding `removeCurrentActivityIndicator`),
// so there is no reason to keep adding new indiciators if there is one already showing.
return
}
loadingIndicator = appNavigator.addUserIndicator(.loading(label))
}
@objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) {
loadingIndicator = nil
}
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) {
MXLog.error("[AppUserIndicatorPresenter] Shared indicator presenter does not support presenting from custom views")
}
@objc func presentSuccess(label: String) {
appNavigator.addUserIndicator(.success(label)).store(in: &otherIndicators)
}
}
@@ -0,0 +1,53 @@
//
// 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 CommonKit
import UIKit
/// Presenter which displays fullscreen loading spinners, and conforming to legacy `ActivityIndicatorPresenterType`,
/// but interally wrapping an `UserIndicatorPresenter` which is used in conjuction with `UserIndicator` and `UserIndicatorQueue`.
///
/// Note: clients can skip using `FullscreenActivityIndicatorPresenter` and instead coordinate with `AppNavigatorProtocol` directly.
/// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers
/// written in objective-c.
@objc final class FullscreenActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType {
private let label: String
private weak var viewController: UIViewController?
private var indicator: UserIndicator?
init(label: String, viewController: UIViewController) {
self.label = label
self.viewController = viewController
}
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) {
guard let viewController = viewController else {
return
}
let request = UserIndicatorRequest(
presenter: FullscreenLoadingIndicatorPresenter(label: label, viewController: viewController),
dismissal: .manual
)
indicator = UserIndicatorQueue.shared.add(request)
}
@objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) {
indicator?.cancel()
}
}
@@ -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
import UIKit
import MatrixSDK
final class LabelledActivityIndicatorView: UIView, Themable {
private enum Constants {
static let padding = UIEdgeInsets(top: 20, left: 40, bottom: 15, right: 40)
static let activityIndicatorScale = CGFloat(1.5)
static let cornerRadius: CGFloat = 12.0
static let stackBackgroundOpacity: CGFloat = 0.9
static let stackSpacing: CGFloat = 15
static let backgroundOpacity: CGFloat = 0.5
}
private let stackBackgroundView: UIView = {
let view = UIView()
view.layer.cornerRadius = Constants.cornerRadius
view.alpha = Constants.stackBackgroundOpacity
return view
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.distribution = .fill
stack.alignment = .center
stack.spacing = Constants.stackSpacing
return stack
}()
private let activityIndicator: UIActivityIndicatorView = {
let view = UIActivityIndicatorView()
view.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale)
view.startAnimating()
return view
}()
private let label: UILabel = {
return UILabel()
}()
init(text: String) {
super.init(frame: .zero)
setup(text: text)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(text: String) {
setupStackView()
label.text = text
}
private func setupStackView() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: centerYAnchor)
])
stackView.addArrangedSubview(activityIndicator)
stackView.addArrangedSubview(label)
insertSubview(stackBackgroundView, belowSubview: stackView)
stackBackgroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackBackgroundView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Constants.padding.top),
stackBackgroundView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.padding.bottom),
stackBackgroundView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Constants.padding.left),
stackBackgroundView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Constants.padding.right)
])
}
func update(theme: Theme) {
backgroundColor = theme.colors.primaryContent.withAlphaComponent(Constants.backgroundOpacity)
stackBackgroundView.backgroundColor = theme.colors.system
activityIndicator.color = theme.colors.secondaryContent
label.font = theme.fonts.calloutSB
label.textColor = theme.colors.secondaryContent
}
}
@@ -0,0 +1,79 @@
//
// 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 CommonKit
import UIKit
/// A `UserIndicatorPresentable` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls.
/// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable {
private let label: String
private weak var viewController: UIViewController?
private weak var view: UIView?
init(label: String, viewController: UIViewController) {
self.label = label
self.viewController = viewController
}
func present() {
// Find the current top navigation controller
var presentingController: UIViewController? = viewController
while presentingController?.navigationController != nil {
presentingController = presentingController?.navigationController
}
guard let presentingController = presentingController else {
return
}
let view = LabelledActivityIndicatorView(text: label)
view.update(theme: ThemeService.shared().theme)
self.view = view
view.translatesAutoresizingMaskIntoConstraints = false
presentingController.view.addSubview(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: presentingController.view.topAnchor),
view.bottomAnchor.constraint(equalTo: presentingController.view.bottomAnchor),
view.leadingAnchor.constraint(equalTo: presentingController.view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: presentingController.view.trailingAnchor)
])
view.alpha = 0
UIView.animate(withDuration: 0.2) {
view.alpha = 1
}
}
func dismiss() {
guard let view = view, view.superview != nil else {
return
}
// If `present` and `dismiss` are called right after each other without delay,
// the view does not correctly pick up `currentState` of alpha. Dispatching onto
// the main queue skips a few run loops, giving the system time to render
// current state.
DispatchQueue.main.async {
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) {
view.alpha = 0
} completion: { _ in
view.removeFromSuperview()
}
}
}
}
@@ -17,16 +17,17 @@
import Foundation
import UIKit
import CommonKit
import MatrixSDK
/// An `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators, and managed by an `Activity`,
/// meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes.
class ActivityIndicatorToastPresenter: ActivityPresentable {
private let text: String
/// A `UserIndicatorPresentable` responsible for showing / hiding a toast view for loading spinners or success messages.
/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
class ToastUserIndicatorPresenter: UserIndicatorPresentable {
private let viewState: ToastViewState
private weak var navigationController: UINavigationController?
private weak var view: UIView?
init(text: String, navigationController: UINavigationController) {
self.text = text
init(viewState: ToastViewState, navigationController: UINavigationController) {
self.viewState = viewState
self.navigationController = navigationController
}
@@ -35,15 +36,15 @@ class ActivityIndicatorToastPresenter: ActivityPresentable {
return
}
let view = ActivityIndicatorToastView(text: text)
let view = RoundedToastView(viewState: viewState)
view.update(theme: ThemeService.shared().theme)
self.view = view
view.translatesAutoresizingMaskIntoConstraints = false
navigationController.view.addSubview(view)
NSLayoutConstraint.activate([
view.centerXAnchor.constraint(equalTo: navigationController.navigationBar.centerXAnchor),
view.topAnchor.constraint(equalTo: navigationController.navigationBar.bottomAnchor)
view.centerXAnchor.constraint(equalTo: navigationController.view.centerXAnchor),
view.topAnchor.constraint(equalTo: navigationController.navigationBar.safeAreaLayoutGuide.bottomAnchor)
])
view.alpha = 0
@@ -69,7 +70,6 @@ class ActivityIndicatorToastPresenter: ActivityPresentable {
view.transform = .init(translationX: 0, y: -5)
} completion: { _ in
view.removeFromSuperview()
self.view = nil
}
}
}
@@ -133,10 +133,15 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange;
- (void)forceRefresh;
/**
Tell whether the sections are shrinkable. NO by default.
Tell whether the sections are shrinkable. YES by default.
*/
@property (nonatomic) BOOL areSectionsShrinkable;
/**
Return true if the given section is currently shrinked.
*/
- (BOOL)isSectionShrinkedAt:(NSInteger)section;
/**
Get the sticky header view for the specified section.
@@ -78,7 +78,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[self resetSectionIndexes];
_areSectionsShrinkable = NO;
_areSectionsShrinkable = YES;
shrinkedSectionsBitMask = 0;
roomTagsListenerByUserId = [[NSMutableDictionary alloc] init];
@@ -661,7 +661,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
title = [VectorL10n roomRecentsSuggestedRoomsSection];
}
if (count)
if (count && !(section == invitesSection))
{
NSString *roomCount = [NSString stringWithFormat:@" %tu", count];
@@ -684,12 +684,16 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
return sectionTitle;
}
- (UIView *)badgeViewForHeaderTitleInHomeSection:(NSInteger)section
- (UIView *)badgeViewForHeaderTitleInSection:(NSInteger)section
{
// Prepare a badge to display the total of missed notifications in this section.
id<MXRoomListDataCounts> counts = nil;
UIView *missedNotifAndUnreadBadgeBgView = nil;
if (section == invitesSection)
{
counts = self.recentsListService.invitedRoomListData.counts;
}
if (section == favoritesSection)
{
counts = self.recentsListService.favoritedRoomListData.counts;
@@ -715,8 +719,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
counts = self.recentsListService.suggestedRoomListData.counts;
}
NSUInteger numberOfNotifications = counts.total.numberOfNotifications;
NSUInteger numberOfHighlights = counts.total.numberOfHighlights;
// Invites are counted as highlights for the badge view display.
NSUInteger numberOfNotifications = counts.total.numberOfNotifications + counts.total.numberOfInvitedRooms;
NSUInteger numberOfHighlights = counts.total.numberOfHighlights + counts.total.numberOfInvitedRooms;
if (numberOfNotifications)
{
@@ -833,14 +838,16 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
chevronView.contentMode = UIViewContentModeCenter;
sectionHeader.accessoryView = chevronView;
}
else if (_recentsDataSourceMode == RecentsDataSourceModeHome)
if (_recentsDataSourceMode == RecentsDataSourceModeHome
|| _recentsDataSourceMode == RecentsDataSourceModePeople
|| _recentsDataSourceMode == RecentsDataSourceModeRooms)
{
// Add a badge to display the total of missed notifications by section.
UIView *badgeView = [self badgeViewForHeaderTitleInHomeSection:section];
UIView *badgeView = [self badgeViewForHeaderTitleInSection:section];
if (badgeView)
{
sectionHeader.accessoryView = badgeView;
sectionHeader.rightAccessoryView = badgeView;
}
}
@@ -1510,4 +1517,55 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[self.delegate dataSource:self didCellChange:update];
}
#pragma mark - Shrinkable
- (BOOL)isSectionShrinkedAt:(NSInteger)section
{
if (_areSectionsShrinkable == NO)
{
return NO;
}
if (section == favoritesSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_FAVORITES))
{
return YES;
}
if (section == peopleSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_PEOPLE))
{
return YES;
}
if (section == conversationSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_CONVERSATIONS))
{
return YES;
}
if (section == directorySection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY))
{
return YES;
}
if (section == lowPrioritySection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_LOWPRIORITY))
{
return YES;
}
if (section == serverNoticeSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_SERVERNOTICE))
{
return YES;
}
if (section == invitesSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_INVITES))
{
return YES;
}
if (section == suggestedRoomsSection && (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_SUGGESTED))
{
return YES;
}
return NO;
}
@end
@@ -51,10 +51,10 @@ public class DiscussionsCount: NSObject {
super.init()
}
public init(withRoomListDataCounts counts: MXRoomListDataCounts) {
self.numberOfNotified = counts.numberOfNotifiedRooms
self.numberOfHighlighted = counts.numberOfHighlightedRooms + counts.numberOfInvitedRooms
self.numberOfUnsent = counts.numberOfUnsentRooms
public init(withRoomListDataCounts counts: [MXRoomListDataCounts]) {
self.numberOfNotified = counts.reduce(0, { $0 + $1.numberOfNotifiedRooms })
self.numberOfHighlighted = counts.reduce(0, { $0 + $1.numberOfHighlightedRooms + $1.numberOfInvitedRooms })
self.numberOfUnsent = counts.reduce(0, { $0 + $1.numberOfUnsentRooms })
super.init()
}
}
@@ -19,7 +19,7 @@
@class RootTabEmptyView;
@class AnalyticsScreenTimer;
@class AppActivityIndicatorPresenter;
@class AppUserIndicatorPresenter;
/**
Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance.
@@ -98,9 +98,9 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
/**
Presenter for displaying app-wide activity / loading indicators. If not set, the view controller will use legacy activity indicators
Presenter for displaying app-wide user indicators. If not set, the view controller will use legacy activity indicators
*/
@property (nonatomic, strong) AppActivityIndicatorPresenter *activityPresenter;
@property (nonatomic, strong) AppUserIndicatorPresenter *userIndicatorPresenter;
/**
Return the sticky header for the specified section of the table view
@@ -314,6 +314,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
[[NSNotificationCenter defaultCenter] removeObserver:kMXNotificationCenterDidUpdateRulesObserver];
kMXNotificationCenterDidUpdateRulesObserver = nil;
}
[self stopActivityIndicator];
}
- (void)viewDidAppear:(BOOL)animated
@@ -1040,6 +1042,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
SectionHeaderView *updatedSectionHeaderView = (SectionHeaderView *)updatedHeaderView;
sectionHeaderView.headerLabel = updatedSectionHeaderView.headerLabel;
sectionHeaderView.accessoryView = updatedSectionHeaderView.accessoryView;
sectionHeaderView.rightAccessoryView = updatedSectionHeaderView.rightAccessoryView;
}
}
}
@@ -1280,8 +1283,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
MXRoom *room = [self.mainSession roomWithRoomId:currentRoomId];
if (room)
{
[self startActivityIndicator];
[self startActivityIndicatorWithLabel:[VectorL10n roomParticipantsLeaveProcessing]];
// cancel pending uploads/downloads
// they are useless by now
[MXMediaManager cancelDownloadsInCacheFolder:room.roomId];
@@ -1296,6 +1298,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
[self.userIndicatorPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]];
// Force table refresh
[self cancelEditionMode:YES];
}
@@ -2421,20 +2424,28 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
#pragma mark - Activity Indicator
- (BOOL)providesCustomActivityIndicator {
return self.activityPresenter != nil;
return self.userIndicatorPresenter != nil;
}
- (void)startActivityIndicatorWithLabel:(NSString *)label {
if (self.userIndicatorPresenter) {
[self.userIndicatorPresenter presentActivityIndicatorWithLabel:label];
} else {
[super startActivityIndicator];
}
}
- (void)startActivityIndicator {
if (self.activityPresenter) {
[self.activityPresenter presentActivityIndicator];
if (self.userIndicatorPresenter) {
[self.userIndicatorPresenter presentActivityIndicator];
} else {
[super startActivityIndicator];
}
}
- (void)stopActivityIndicator {
if (self.activityPresenter) {
[self.activityPresenter removeCurrentActivityIndicatorWithAnimated:YES completion:nil];
if (self.userIndicatorPresenter) {
[self.userIndicatorPresenter removeCurrentActivityIndicatorWithAnimated:YES completion:nil];
} else {
[super stopActivityIndicator];
}
@@ -32,7 +32,18 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
// MARK: - Fetchers
private var invitedRoomListDataFetcher: MXRoomListDataFetcher?
private var invitedRoomListDataFetcher: MXRoomListDataFetcher? {
switch mode {
case .home:
return invitedRoomListDataFetcherForHome
case .people:
return invitedRoomListDataFetcherForPeople
case .rooms:
return invitedRoomListDataFetcherForRooms
default:
return nil
}
}
private var favoritedRoomListDataFetcher: MXRoomListDataFetcher?
private var directRoomListDataFetcher: MXRoomListDataFetcher? {
switch mode {
@@ -62,19 +73,24 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private var conversationRoomListDataFetcherForRooms: MXRoomListDataFetcher?
private var directRoomListDataFetcherForHome: MXRoomListDataFetcher?
private var directRoomListDataFetcherForPeople: MXRoomListDataFetcher?
private var invitedRoomListDataFetcherForHome: MXRoomListDataFetcher?
private var invitedRoomListDataFetcherForPeople: MXRoomListDataFetcher?
private var invitedRoomListDataFetcherForRooms: MXRoomListDataFetcher?
// MARK: - Private
private var fetcherTypesForMode: [RecentsDataSourceMode: FetcherTypes] = [
.home: [.invited, .favorited, .directHome, .conversationHome, .lowPriority, .serverNotice, .suggested],
.favourites: [.favorited],
.people: [.directPeople],
.rooms: [.conversationRooms, .suggested]
.people: [.invited, .directPeople],
.rooms: [.invited, .conversationRooms, .suggested]
]
private var allFetchers: [MXRoomListDataFetcher] {
return [
invitedRoomListDataFetcher,
invitedRoomListDataFetcherForHome,
invitedRoomListDataFetcherForPeople,
invitedRoomListDataFetcherForRooms,
favoritedRoomListDataFetcher,
directRoomListDataFetcherForHome,
directRoomListDataFetcherForPeople,
@@ -120,7 +136,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
return result
}
private var hideInvitedSection: Bool {
return MXSDKOptions.sharedInstance().autoAcceptRoomInvites
}
@@ -204,20 +220,20 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
guard let totalCounts = favoritedRoomListDataFetcher?.data?.counts.total else {
return .zero
}
return DiscussionsCount(withRoomListDataCounts: totalCounts)
return DiscussionsCount(withRoomListDataCounts: [totalCounts])
}
public var peopleMissedDiscussionsCount: DiscussionsCount {
guard let totalCounts = directRoomListDataFetcherForPeople?.data?.counts.total else {
return .zero
}
let invitesCount = invitedRoomListDataFetcherForPeople?.data?.counts.total
let directCount = directRoomListDataFetcherForPeople?.data?.counts.total
let totalCounts = [invitesCount, directCount].compactMap { $0 }
return DiscussionsCount(withRoomListDataCounts: totalCounts)
}
public var conversationMissedDiscussionsCount: DiscussionsCount {
guard let totalCounts = conversationRoomListDataFetcherForRooms?.data?.counts.total else {
return .zero
}
let invitesCount = invitedRoomListDataFetcherForRooms?.data?.counts.total
let conversationCount = conversationRoomListDataFetcherForRooms?.data?.counts.total
let totalCounts = [invitesCount, conversationCount].compactMap { $0 }
return DiscussionsCount(withRoomListDataCounts: totalCounts)
}
@@ -269,7 +285,9 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
removeAllDelegates()
allFetchers.forEach({ $0.stop() })
invitedRoomListDataFetcher = nil
invitedRoomListDataFetcherForHome = nil
invitedRoomListDataFetcherForPeople = nil
invitedRoomListDataFetcherForRooms = nil
favoritedRoomListDataFetcher = nil
directRoomListDataFetcherForHome = nil
directRoomListDataFetcherForPeople = nil
@@ -435,7 +453,8 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [],
onlySuggested: Bool = false,
paginate: Bool = true) -> MXRoomListDataFetcher {
paginate: Bool = true,
strictMatches: Bool = false) -> MXRoomListDataFetcher {
guard let session = session else {
fatalError("Session deallocated")
}
@@ -443,7 +462,8 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
onlySuggested: onlySuggested,
query: query,
space: space,
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace)
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace,
strictMatches: strictMatches)
let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions,
sortOptions: sortOptions,
@@ -455,6 +475,22 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
return fetcher
}
private func createInvitedRoomListDataFetcherForPeople() -> MXRoomListDataFetcher {
let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.invited, .direct], paginate: false, strictMatches: true)
updateInvitedFetcher(fetcher, for: .people)
fetcher.addDelegate(self)
fetcher.paginate()
return fetcher
}
private func createInvitedRoomListDataFetcherForRooms() -> MXRoomListDataFetcher {
let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.invited], paginate: false)
updateInvitedFetcher(fetcher, for: .rooms)
fetcher.addDelegate(self)
fetcher.paginate()
return fetcher
}
private func createDirectRoomListDataFetcherForHome() -> MXRoomListDataFetcher {
let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.direct], paginate: false)
@@ -500,7 +536,9 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
return
}
if !hideInvitedSection {
invitedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.invited])
invitedRoomListDataFetcherForHome = createCommonRoomListDataFetcher(withDataTypes: [.invited])
invitedRoomListDataFetcherForPeople = createInvitedRoomListDataFetcherForPeople()
invitedRoomListDataFetcherForRooms = createInvitedRoomListDataFetcherForRooms()
}
favoritedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.favorited])
directRoomListDataFetcherForHome = createDirectRoomListDataFetcherForHome()
@@ -516,18 +554,30 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
private func updateDirectFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) {
var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .space]
var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .space, .invited, .lowPriority]
switch mode {
case .home:
notDataTypes.insert([.invited, .favorited, .lowPriority])
notDataTypes.insert(.favorited)
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
case .people:
notDataTypes.insert([.lowPriority])
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
default:
break
}
}
private func updateInvitedFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) {
var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .lowPriority, .serverNotice, .space]
switch mode {
case .people:
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
case .rooms:
notDataTypes.insert([.direct])
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
default:
break
}
}
private func updateFavoritedFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) {
switch mode {
@@ -543,15 +593,12 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
private func updateConversationFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) {
var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .lowPriority, .serverNotice, .space]
var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .invited, .lowPriority, .serverNotice, .space]
switch mode {
case .home:
notDataTypes.insert([.invited, .favorited])
notDataTypes.insert(.favorited)
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
case .rooms:
if hideInvitedSection {
notDataTypes.insert(.invited)
}
fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes
default:
break
@@ -603,7 +650,7 @@ private struct FetcherTypes: OptionSet {
static let lowPriority = FetcherTypes(rawValue: 1 << 6)
static let serverNotice = FetcherTypes(rawValue: 1 << 7)
static let suggested = FetcherTypes(rawValue: 1 << 8)
static let none: FetcherTypes = []
static let all: FetcherTypes = [
.invited, .favorited, .directHome, .directPeople, .conversationHome, .conversationRooms, .lowPriority, .serverNotice, .suggested]
@@ -52,6 +52,11 @@
*/
@property (nonatomic, strong) UIView *accessoryView;
/**
Right accessory view for header. Both width and height will be used.
*/
@property (nonatomic, strong) UIView *rightAccessoryView;
/**
A view which spans the bottom view. No frame value will be used. Height will be remaining of the view at below topViewHeight.
*/
@@ -85,6 +85,19 @@ static const CGFloat kInterItemsSpaceHorizontal = 8.0;
[self setNeedsLayout];
}
- (void)setRightAccessoryView:(UIView *)rightAccessoryView
{
// remove old one
[_rightAccessoryView removeFromSuperview];
_rightAccessoryView = rightAccessoryView;
if (_rightAccessoryView)
{
// add new one
[self.contentView addSubview:_rightAccessoryView];
}
[self setNeedsLayout];
}
- (void)setBottomView:(UIView *)bottomView
{
// remove old one
@@ -169,6 +182,10 @@ static const CGFloat kInterItemsSpaceHorizontal = 8.0;
{
rightMargin += _accessoryView.frame.size.width + kInterItemsSpaceHorizontal;
}
if (_rightAccessoryView)
{
rightMargin += _rightAccessoryView.frame.size.width + kInterItemsSpaceHorizontal;
}
if (_bottomView)
{
// set header label top
@@ -179,7 +196,8 @@ static const CGFloat kInterItemsSpaceHorizontal = 8.0;
// center header label vertically
frame.origin.y = MAX(0, (self.contentView.bounds.size.height - frame.size.height)/2);
}
frame.size.width = self.contentView.bounds.size.width - leftMargin - rightMargin;
frame.size.width = MIN(self.contentView.bounds.size.width - leftMargin - rightMargin,
[_headerLabel sizeThatFits:self.frame.size].width);
_headerLabel.frame = frame;
}
@@ -190,11 +208,30 @@ static const CGFloat kInterItemsSpaceHorizontal = 8.0;
rightMargin = MAX(_rightInset, 20);
CGRect frame = _accessoryView.frame;
frame.origin.x = self.contentView.bounds.size.width - frame.size.width - rightMargin;
if(_headerLabel)
{
frame.origin.x = leftMargin + _headerLabel.frame.size.width + kInterItemsSpaceHorizontal;
}
else
{
frame.origin.x = leftMargin;
}
frame.origin.y = MAX(0, (_topViewHeight - frame.size.height)/2);
_accessoryView.frame = frame;
}
if (_rightAccessoryView)
{
// reset margins
leftMargin = MAX(_leftInset, 20);
rightMargin = MAX(_rightInset, 20);
CGRect frame = _rightAccessoryView.frame;
frame.origin.x = self.contentView.bounds.size.width - frame.size.width - rightMargin;
frame.origin.y = MAX(0, (_topViewHeight - frame.size.height)/2);
_rightAccessoryView.frame = frame;
}
if (_bottomView)
{
// reset margins
@@ -1,91 +0,0 @@
//
// 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
import DesignKit
class ActivityIndicatorToastView: UIView, Themable {
private struct Constants {
static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
static let shadowOffset = CGSize(width: 0, height: 4)
static let shadowRadius = CGFloat(12)
static let shadowOpacity = Float(0.1)
}
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 5
return stack
}()
private let activityIndicator: UIActivityIndicatorView = {
let view = UIActivityIndicatorView()
view.transform = .init(scaleX: 0.75, y: 0.75)
view.startAnimating()
return view
}()
private let label: UILabel = {
return UILabel()
}()
init(text: String) {
super.init(frame: .zero)
setup(text: text)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(text: String) {
setupLayer()
setupStackView()
stackView.addArrangedSubview(activityIndicator)
stackView.addArrangedSubview(label)
label.text = text
}
private func setupStackView() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right)
])
}
private func setupLayer() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = Constants.shadowOffset
layer.shadowRadius = Constants.shadowRadius
layer.shadowOpacity = Constants.shadowOpacity
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = layer.frame.height / 2
}
func update(theme: Theme) {
backgroundColor = UIColor.white
label.font = theme.fonts.subheadline
}
}
@@ -17,7 +17,7 @@
import Foundation
import UIKit
class BasicToastView: UIView, Themable {
class RectangleToastView: UIView, Themable {
private enum Constants {
static let padding: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
@@ -0,0 +1,120 @@
//
// 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
import DesignKit
class RoundedToastView: UIView, Themable {
private struct ShadowStyle {
let offset: CGSize
let radius: CGFloat
let opacity: Float
}
private struct Constants {
static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
static let activityIndicatorScale = CGFloat(0.75)
static let imageViewSize = CGFloat(15)
static let lightShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 12, opacity: 0.1)
static let darkShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 4, opacity: 0.2)
}
private lazy var activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView()
indicator.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale)
indicator.startAnimating()
return indicator
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewSize),
imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize)
])
return imageView
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.spacing = 5
return stack
}()
private let label: UILabel = {
return UILabel()
}()
init(viewState: ToastViewState) {
super.init(frame: .zero)
setup(viewState: viewState)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(viewState: ToastViewState) {
setupStackView()
stackView.addArrangedSubview(toastView(for: viewState.style))
stackView.addArrangedSubview(label)
label.text = viewState.label
}
private func setupStackView() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right)
])
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = layer.frame.height / 2
}
func update(theme: Theme) {
backgroundColor = theme.colors.background
stackView.arrangedSubviews.first?.tintColor = theme.colors.primaryContent
label.font = theme.fonts.subheadline
label.textColor = theme.colors.primaryContent
let shadowStyle = theme.identifier == ThemeIdentifier.dark.rawValue ? Constants.darkShadow : Constants.lightShadow
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = shadowStyle.offset
layer.shadowRadius = shadowStyle.radius
layer.shadowOpacity = shadowStyle.opacity
}
private func toastView(for style: ToastViewState.Style) -> UIView {
switch style {
case .loading:
return activityIndicator
case .success:
imageView.image = Asset.Images.checkmark.image
return imageView
}
}
}
@@ -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
struct ToastViewState {
enum Style {
case loading
case success
}
let style: Style
let label: String
}