mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-04 15:07:43 +02:00
Merge branch 'develop' into gil/SP1_space_creation
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user