Merge branch 'develop' into gil/SP1_space_creation

# Conflicts:
#	Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift
#	Riot/Modules/Room/RoomCoordinatorParameters.swift
#	Riot/Modules/Room/RoomViewController.m
#	Riot/Modules/TabBar/TabBarCoordinator.swift
This commit is contained in:
Gil Eluard
2022-02-04 14:29:57 +01:00
445 changed files with 17297 additions and 2207 deletions
@@ -0,0 +1,138 @@
//
// 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
class ThreadViewController: RoomViewController {
// MARK: Private
private(set) var threadId: String!
private var permalink: String? {
guard let threadId = threadId else { return nil }
return MXTools.permalink(toEvent: threadId, inRoom: roomDataSource.roomId)
}
class func instantiate(withThreadId threadId: String,
configuration: RoomDisplayConfiguration) -> ThreadViewController {
let threadVC = ThreadViewController.instantiate(with: configuration)
threadVC.threadId = threadId
return threadVC
}
override class func nib() -> UINib! {
// reuse 'RoomViewController.xib' file as the nib
return UINib(nibName: String(describing: RoomViewController.self), bundle: .main)
}
override func finalizeInit() {
super.finalizeInit()
self.saveProgressTextInput = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let threadId = threadId else { return }
mainSession?.threadingService.markThreadAsRead(threadId)
}
override func setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) {
super.setRoomTitleViewClass(ThreadRoomTitleView.self)
guard let threadTitleView = self.titleView as? ThreadRoomTitleView else {
return
}
threadTitleView.mode = .specificThread(threadId: threadId)
}
override func onButtonPressed(_ sender: Any) {
if let sender = sender as? UIBarButtonItem, sender == navigationItem.rightBarButtonItem {
showThreadActions()
return
}
super.onButtonPressed(sender)
}
override func handleTypingNotification(_ typing: Bool) {
// no-op
}
private func showThreadActions() {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let viewInRoomAction = UIAlertAction(title: VectorL10n.roomEventActionViewInRoom,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.delegate?.roomViewController(self,
showRoomWithId: self.roomDataSource.roomId,
eventId: self.threadId)
})
alertController.addAction(viewInRoomAction)
let copyLinkAction = UIAlertAction(title: VectorL10n.threadCopyLinkToThread,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.copyPermalink()
})
alertController.addAction(copyLinkAction)
let shareAction = UIAlertAction(title: VectorL10n.roomEventActionShare,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.sharePermalink()
})
alertController.addAction(shareAction)
alertController.addAction(UIAlertAction(title: VectorL10n.cancel,
style: .cancel,
handler: nil))
alertController.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
self.present(alertController, animated: true, completion: nil)
}
private func copyPermalink() {
guard let permalink = permalink, let url = URL(string: permalink) else {
return
}
MXKPasteboardManager.shared.pasteboard.url = url
view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo,
image: Asset.Images.linkIcon.image,
additionalMargin: self.roomInputToolbarContainerHeightConstraint.constant)
}
private func sharePermalink() {
guard let permalink = permalink else {
return
}
let activityVC = UIActivityViewController(activityItems: [permalink],
applicationActivities: nil)
activityVC.modalTransitionStyle = .coverVertical
activityVC.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
present(activityVC, animated: true, completion: nil)
}
}
@@ -0,0 +1,79 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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 ThreadListCoordinator: ThreadListCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let parameters: ThreadListCoordinatorParameters
private var threadListViewModel: ThreadListViewModelProtocol
private let threadListViewController: ThreadListViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: ThreadListCoordinatorDelegate?
// MARK: - Setup
init(parameters: ThreadListCoordinatorParameters) {
self.parameters = parameters
let threadListViewModel = ThreadListViewModel(session: self.parameters.session,
roomId: self.parameters.roomId)
let threadListViewController = ThreadListViewController.instantiate(with: threadListViewModel)
self.threadListViewModel = threadListViewModel
self.threadListViewController = threadListViewController
}
// MARK: - Public
func start() {
self.threadListViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.threadListViewController
}
}
// MARK: - ThreadListViewModelCoordinatorDelegate
extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate {
func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) {
self.delegate?.threadListCoordinatorDidLoadThreads(self)
}
func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) {
self.delegate?.threadListCoordinatorDidSelectThread(self, thread: thread)
}
func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) {
self.delegate?.threadListCoordinatorDidSelectRoom(self, roomId: thread.roomId, eventId: thread.id)
}
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) {
self.delegate?.threadListCoordinatorDidCancel(self)
}
}
@@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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
/// ThreadListCoordinator input parameters
struct ThreadListCoordinatorParameters {
/// The Matrix session
let session: MXSession
/// Room identifier
let roomId: String
}
@@ -0,0 +1,31 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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 ThreadListCoordinatorDelegate: AnyObject {
func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol)
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread)
func threadListCoordinatorDidSelectRoom(_ coordinator: ThreadListCoordinatorProtocol, roomId: String, eventId: String)
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol)
}
/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle thread list navigation flow.
protocol ThreadListCoordinatorProtocol: Coordinator, Presentable {
var delegate: ThreadListCoordinatorDelegate? { get }
}
@@ -0,0 +1,33 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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
/// ThreadListViewController view actions exposed to view model
enum ThreadListViewAction {
case loadData
case complete
case showFilterTypes
case selectFilterType(_ type: ThreadListFilterType)
case selectThread(_ index: Int)
case longPressThread(_ index: Int)
case actionViewInRoom
case actionCopyLinkToThread
case actionShare
case cancel
}
@@ -0,0 +1,71 @@
<?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>
<!--Thread List View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="ThreadListViewController" 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="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="X8K-NO-SQ3">
<rect key="frame" x="0.0" y="44" width="414" height="852"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<gestureRecognizers/>
<connections>
<outlet property="dataSource" destination="V8j-Lb-PgC" id="FCQ-5E-AuZ"/>
<outlet property="delegate" destination="V8j-Lb-PgC" id="Kxs-vj-1RW"/>
<outletCollection property="gestureRecognizers" destination="OxP-Mp-c6Z" appends="YES" id="371-L6-Skg"/>
</connections>
</tableView>
<view contentMode="scaleToFill" placeholderIntrinsicWidth="414" placeholderIntrinsicHeight="818" translatesAutoresizingMaskIntoConstraints="NO" id="7VY-m9-wCS" customClass="ThreadListEmptyView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="delegate" destination="V8j-Lb-PgC" id="SQT-2N-wnf"/>
</connections>
</view>
</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 firstAttribute="trailing" secondItem="7VY-m9-wCS" secondAttribute="trailing" id="JYi-uG-Yqc"/>
<constraint firstItem="X8K-NO-SQ3" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="Mfw-In-peq"/>
<constraint firstAttribute="bottom" secondItem="7VY-m9-wCS" secondAttribute="bottom" id="PkX-MO-5aO"/>
<constraint firstAttribute="bottom" secondItem="X8K-NO-SQ3" secondAttribute="bottom" id="SIG-7P-2CK"/>
<constraint firstItem="7VY-m9-wCS" firstAttribute="top" secondItem="EL9-GA-lwo" secondAttribute="top" id="aFs-kb-rJp"/>
<constraint firstItem="7VY-m9-wCS" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="acw-H6-LKX"/>
<constraint firstItem="X8K-NO-SQ3" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="ehI-Nc-6di"/>
<constraint firstAttribute="trailing" secondItem="X8K-NO-SQ3" secondAttribute="trailing" id="hbZ-R8-8kH"/>
</constraints>
</view>
<connections>
<outlet property="emptyView" destination="7VY-m9-wCS" id="Yny-Rn-edF"/>
<outlet property="threadsTableView" destination="X8K-NO-SQ3" id="owA-Uh-r9B"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="OxP-Mp-c6Z">
<connections>
<action selector="longPressed:" destination="V8j-Lb-PgC" id="rvv-ml-CSq"/>
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="-3198.5507246376815" y="-647.54464285714278"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,370 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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 ThreadListViewController: UIViewController {
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var threadsTableView: UITableView!
@IBOutlet private weak var emptyView: ThreadListEmptyView!
// MARK: Private
private var viewModel: ThreadListViewModelProtocol!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var titleView: ThreadRoomTitleView!
// MARK: - Setup
class func instantiate(with viewModel: ThreadListViewModelProtocol) -> ThreadListViewController {
let viewController = StoryboardScene.ThreadListViewController.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.
self.setupViews()
self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.threadsTableView)
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.keyboardAvoider?.startAvoiding()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.keyboardAvoider?.stopAvoiding()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
guard let titleView = self.titleView else { return }
if UIApplication.shared.statusBarOrientation.isPortrait {
titleView.updateLayout(for: .landscapeLeft)
} else {
titleView.updateLayout(for: .portrait)
}
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.headerBackgroundColor
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
emptyView.update(theme: theme)
emptyView.backgroundColor = theme.colors.background
self.threadsTableView.backgroundColor = theme.backgroundColor
self.threadsTableView.separatorColor = theme.colors.separator
self.threadsTableView.reloadData()
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
let titleView = ThreadRoomTitleView.loadFromNib()
titleView.mode = .allThreads
titleView.configure(withModel: viewModel.titleModel)
titleView.updateLayout(for: UIApplication.shared.statusBarOrientation)
self.titleView = titleView
navigationItem.leftItemsSupplementBackButton = true
vc_removeBackTitle()
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleView)
navigationItem.rightBarButtonItem = UIBarButtonItem(image: Asset.Images.threadsFilter.image,
style: .plain,
target: self,
action: #selector(filterButtonTapped(_:)))
self.threadsTableView.tableFooterView = UIView()
self.threadsTableView.register(cellType: ThreadTableViewCell.self)
self.threadsTableView.keyboardDismissMode = .interactive
}
private func render(viewState: ThreadListViewState) {
switch viewState {
case .idle:
break
case .loading:
renderLoading()
case .loaded:
renderLoaded()
case .empty(let model):
renderEmptyView(withModel: model)
case .showingFilterTypes:
renderShowingFilterTypes()
case .showingLongPressActions(let index):
renderShowingLongPressActions(index)
case .share(let url, let index):
renderShare(url, index: index)
case .toastForCopyLink:
toastForCopyLink()
case .error(let error):
render(error: error)
}
}
private func renderLoading() {
emptyView.isHidden = true
threadsTableView.isHidden = true
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded() {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
threadsTableView.isHidden = false
self.threadsTableView.reloadData()
navigationItem.rightBarButtonItem?.isEnabled = true
switch viewModel.selectedFilterType {
case .all:
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilter.image
case .myThreads:
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image
}
}
private func renderEmptyView(withModel model: ThreadListEmptyModel) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
emptyView.configure(withModel: model)
threadsTableView.isHidden = true
emptyView.isHidden = false
navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads
switch viewModel.selectedFilterType {
case .all:
navigationItem.rightBarButtonItem = nil
case .myThreads:
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image
}
}
private func renderShowingFilterTypes() {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let allThreadsAction = UIAlertAction(title: ThreadListFilterType.all.title,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.process(viewAction: .selectFilterType(.all))
})
if self.viewModel.selectedFilterType == .all {
allThreadsAction.setValue(true, forKey: "checked")
}
alertController.addAction(allThreadsAction)
let myThreadsAction = UIAlertAction(title: ThreadListFilterType.myThreads.title,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.process(viewAction: .selectFilterType(.myThreads))
})
if self.viewModel.selectedFilterType == .myThreads {
myThreadsAction.setValue(true, forKey: "checked")
}
alertController.addAction(myThreadsAction)
alertController.addAction(UIAlertAction(title: VectorL10n.cancel,
style: .cancel,
handler: nil))
alertController.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
self.present(alertController, animated: true, completion: nil)
}
private func renderShowingLongPressActions(_ index: Int) {
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionViewInRoom,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.process(viewAction: .actionViewInRoom)
}))
controller.addAction(UIAlertAction(title: VectorL10n.threadCopyLinkToThread,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.process(viewAction: .actionCopyLinkToThread)
}))
controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionShare,
style: .default,
handler: { [weak self] action in
guard let self = self else { return }
self.viewModel.process(viewAction: .actionShare)
}))
controller.addAction(UIAlertAction(title: VectorL10n.cancel,
style: .cancel,
handler: nil))
if let cell = threadsTableView.cellForRow(at: IndexPath(row: index, section: 0)) {
controller.popoverPresentationController?.sourceView = cell
} else {
controller.popoverPresentationController?.sourceView = view
}
self.present(controller, animated: true, completion: nil)
}
private func renderShare(_ url: URL, index: Int) {
let activityVC = UIActivityViewController(activityItems: [url],
applicationActivities: nil)
activityVC.modalTransitionStyle = .coverVertical
if let cell = threadsTableView.cellForRow(at: IndexPath(row: index, section: 0)) {
activityVC.popoverPresentationController?.sourceView = cell
} else {
activityVC.popoverPresentationController?.sourceView = view
}
present(activityVC, animated: true, completion: nil)
}
private func toastForCopyLink() {
view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo,
image: Asset.Images.linkIcon.image)
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
// MARK: - Actions
@objc
private func filterButtonTapped(_ sender: UIBarButtonItem) {
self.viewModel.process(viewAction: .showFilterTypes)
}
@IBAction private func longPressed(_ sender: UILongPressGestureRecognizer) {
guard sender.state == .began else {
return
}
let point = sender.location(in: threadsTableView)
guard let indexPath = threadsTableView.indexPathForRow(at: point) else {
return
}
guard let cell = threadsTableView.cellForRow(at: indexPath) else {
return
}
if cell.isHighlighted {
viewModel.process(viewAction: .longPressThread(indexPath.row))
}
}
}
// MARK: - ThreadListViewModelViewDelegate
extension ThreadListViewController: ThreadListViewModelViewDelegate {
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol,
didUpdateViewState viewSate: ThreadListViewState) {
self.render(viewState: viewSate)
}
}
// MARK: - UITableViewDataSource
extension ThreadListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfThreads
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.update(theme: theme)
if let threadModel = viewModel.threadModel(at: indexPath.row) {
cell.configure(withModel: threadModel)
}
return cell
}
}
// MARK: - UITableViewDelegate
extension ThreadListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.backgroundColor = theme.backgroundColor
cell.selectedBackgroundView = UIView()
cell.selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
viewModel.process(viewAction: .selectThread(indexPath.row))
}
}
// MARK: - ThreadListEmptyViewDelegate
extension ThreadListViewController: ThreadListEmptyViewDelegate {
func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) {
viewModel.process(viewAction: .selectFilterType(.all))
}
}
@@ -0,0 +1,347 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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 ThreadListViewModel: ThreadListViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private var threads: [MXThread] = []
private var eventFormatter: MXKEventFormatter?
private var roomState: MXRoomState?
private var currentOperation: MXHTTPOperation?
private var longPressedThread: MXThread?
// MARK: Public
weak var viewDelegate: ThreadListViewModelViewDelegate?
weak var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate?
var selectedFilterType: ThreadListFilterType = .all
private(set) var viewState: ThreadListViewState = .idle {
didSet {
self.viewDelegate?.threadListViewModel(self, didUpdateViewState: viewState)
}
}
// MARK: - Setup
init(session: MXSession,
roomId: String) {
self.session = session
self.roomId = roomId
session.threadingService.addDelegate(self)
}
deinit {
session.threadingService.removeDelegate(self)
self.cancelOperations()
}
// MARK: - Public
func process(viewAction: ThreadListViewAction) {
switch viewAction {
case .loadData:
loadData()
case .complete:
coordinatorDelegate?.threadListViewModelDidLoadThreads(self)
case .showFilterTypes:
viewState = .showingFilterTypes
case .selectFilterType(let type):
selectedFilterType = type
loadData()
case .selectThread(let index):
selectThread(index)
case .longPressThread(let index):
longPressThread(index)
case .actionViewInRoom:
actionViewInRoom()
case .actionCopyLinkToThread:
actionCopyLinkToThread()
case .actionShare:
actionShare()
case .cancel:
cancelOperations()
coordinatorDelegate?.threadListViewModelDidCancel(self)
}
}
var numberOfThreads: Int {
return threads.count
}
func threadModel(at index: Int) -> ThreadModel? {
guard index < threads.count else {
return nil
}
return model(forThread: threads[index])
}
var titleModel: ThreadRoomTitleModel {
guard let room = session.room(withRoomId: roomId) else {
return .empty
}
let avatarViewData = AvatarViewData(matrixItemId: room.matrixItemId,
displayName: room.displayName,
avatarUrl: room.mxContentUri,
mediaManager: room.mxSession.mediaManager,
fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId,
room.displayName))
let encrpytionBadge: UIImage?
if let summary = room.summary, summary.isEncrypted, session.crypto != nil {
encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel())
} else {
encrpytionBadge = nil
}
return ThreadRoomTitleModel(roomAvatar: avatarViewData,
roomEncryptionBadge: encrpytionBadge,
roomDisplayName: room.displayName)
}
private var emptyViewModel: ThreadListEmptyModel {
switch selectedFilterType {
case .all:
return ThreadListEmptyModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoAll,
tip: VectorL10n.threadsEmptyTip,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: true)
case .myThreads:
return ThreadListEmptyModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoMy,
tip: nil,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: false)
}
}
// MARK: - Private
private func model(forThread thread: MXThread) -> ThreadModel {
let rootAvatarViewData: AvatarViewData?
let rootMessageSender: MXUser?
let lastAvatarViewData: AvatarViewData?
let lastMessageSender: MXUser?
let rootMessageText = rootMessageText(forThread: thread)
let (lastMessageText, lastMessageTime) = lastMessageTextAndTime(forThread: thread)
let notificationStatus = ThreadNotificationStatus(withThread: thread)
// root message
if let rootMessage = thread.rootMessage, let senderId = rootMessage.sender {
rootMessageSender = session.user(withUserId: rootMessage.sender)
let fallbackImage = AvatarFallbackImage.matrixItem(senderId,
rootMessageSender?.displayname)
rootAvatarViewData = AvatarViewData(matrixItemId: senderId,
displayName: rootMessageSender?.displayname,
avatarUrl: rootMessageSender?.avatarUrl,
mediaManager: session.mediaManager,
fallbackImage: fallbackImage)
} else {
rootAvatarViewData = nil
rootMessageSender = nil
}
// last message
if let lastMessage = thread.lastMessage, let senderId = lastMessage.sender {
lastMessageSender = session.user(withUserId: lastMessage.sender)
let fallbackImage = AvatarFallbackImage.matrixItem(senderId,
lastMessageSender?.displayname)
lastAvatarViewData = AvatarViewData(matrixItemId: senderId,
displayName: lastMessageSender?.displayname,
avatarUrl: lastMessageSender?.avatarUrl,
mediaManager: session.mediaManager,
fallbackImage: fallbackImage)
} else {
lastAvatarViewData = nil
lastMessageSender = nil
}
let summaryModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies,
lastMessageSenderAvatar: lastAvatarViewData,
lastMessageText: lastMessageText)
return ThreadModel(rootMessageSenderUserId: rootMessageSender?.userId,
rootMessageSenderAvatar: rootAvatarViewData,
rootMessageSenderDisplayName: rootMessageSender?.displayname,
rootMessageText: rootMessageText,
rootMessageRedacted: thread.rootMessage?.isRedactedEvent() ?? false,
lastMessageTime: lastMessageTime,
summaryModel: summaryModel,
notificationStatus: notificationStatus)
}
private func rootMessageText(forThread thread: MXThread) -> NSAttributedString? {
guard let eventFormatter = eventFormatter else {
return nil
}
guard let message = thread.rootMessage else {
return nil
}
if message.isReply(), let newMessage = message.copy() as? MXEvent {
var jsonDict = newMessage.isEncrypted ? newMessage.clear?.jsonDictionary() : newMessage.jsonDictionary()
if var content = jsonDict?["content"] as? [String: Any] {
content.removeValue(forKey: "format")
content.removeValue(forKey: "formatted_body")
content.removeValue(forKey: kMXEventRelationRelatesToKey)
if let replyText = MXReplyEventParser().parse(newMessage)?.bodyParts.replyText {
content["body"] = replyText
}
jsonDict?["content"] = content
}
let trimmedMessage = MXEvent(fromJSON: jsonDict)
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
return eventFormatter.attributedString(from: trimmedMessage,
with: roomState,
error: formatterError)
}
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
return eventFormatter.attributedString(from: message,
with: roomState,
error: formatterError)
}
private func lastMessageTextAndTime(forThread thread: MXThread) -> (NSAttributedString?, String?) {
guard let eventFormatter = eventFormatter else {
return (nil, nil)
}
guard let message = thread.lastMessage else {
return (nil, nil)
}
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
return (
eventFormatter.attributedString(from: message,
with: roomState,
error: formatterError),
eventFormatter.dateString(from: message, withTime: true)
)
}
private func loadData(showLoading: Bool = true) {
if showLoading {
viewState = .loading
}
switch selectedFilterType {
case .all:
threads = session.threadingService.threads(inRoom: roomId)
case .myThreads:
threads = session.threadingService.participatedThreads(inRoom: roomId)
}
if threads.isEmpty {
viewState = .empty(emptyViewModel)
return
}
threadsLoaded()
}
private func threadsLoaded() {
guard let eventFormatter = session.roomSummaryUpdateDelegate as? MXKEventFormatter,
let room = session.room(withRoomId: roomId) else {
// go into loaded state
self.viewState = .loaded
return
}
room.state { [weak self] roomState in
guard let self = self else { return }
self.eventFormatter = eventFormatter
self.roomState = roomState
// go into loaded state
self.viewState = .loaded
}
}
private func selectThread(_ index: Int) {
guard index < threads.count else {
return
}
let thread = threads[index]
coordinatorDelegate?.threadListViewModelDidSelectThread(self, thread: thread)
}
private func longPressThread(_ index: Int) {
guard index < threads.count else {
return
}
longPressedThread = threads[index]
viewState = .showingLongPressActions(index)
}
private func actionViewInRoom() {
guard let thread = longPressedThread else {
return
}
coordinatorDelegate?.threadListViewModelDidSelectThreadViewInRoom(self, thread: thread)
longPressedThread = nil
}
private func actionCopyLinkToThread() {
guard let thread = longPressedThread else {
return
}
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId),
let url = URL(string: permalink) {
MXKPasteboardManager.shared.pasteboard.url = url
viewState = .toastForCopyLink
}
longPressedThread = nil
}
private func actionShare() {
guard let thread = longPressedThread,
let index = threads.firstIndex(of: thread) else {
return
}
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId),
let url = URL(string: permalink) {
viewState = .share(url, index)
}
longPressedThread = nil
}
private func cancelOperations() {
self.currentOperation?.cancel()
}
}
extension ThreadListViewModel: MXThreadingServiceDelegate {
func threadingServiceDidUpdateThreads(_ service: MXThreadingService) {
loadData(showLoading: false)
}
}
@@ -0,0 +1,60 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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 ThreadListViewModelViewDelegate: AnyObject {
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didUpdateViewState viewSate: ThreadListViewState)
}
protocol ThreadListViewModelCoordinatorDelegate: AnyObject {
func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol)
func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread)
func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread)
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol)
}
/// Protocol describing the view model used by `ThreadListViewController`
protocol ThreadListViewModelProtocol {
var viewDelegate: ThreadListViewModelViewDelegate? { get set }
var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate? { get set }
func process(viewAction: ThreadListViewAction)
var viewState: ThreadListViewState { get }
var titleModel: ThreadRoomTitleModel { get }
var selectedFilterType: ThreadListFilterType { get }
var numberOfThreads: Int { get }
func threadModel(at index: Int) -> ThreadModel?
}
enum ThreadListFilterType {
case all
case myThreads
var title: String {
switch self {
case .all:
return VectorL10n.threadsActionAllThreads
case .myThreads:
return VectorL10n.threadsActionMyThreads
}
}
}
@@ -0,0 +1,32 @@
// File created from ScreenTemplate
// $ createScreen.sh Threads/ThreadList ThreadList
/*
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
/// ThreadListViewController view state
enum ThreadListViewState {
case idle
case loading
case loaded
case empty(_ viewModel: ThreadListEmptyModel)
case showingFilterTypes
case showingLongPressActions(_ index: Int)
case share(_ url: URL, _ index: Int)
case toastForCopyLink
case error(Error)
}
@@ -0,0 +1,44 @@
//
// 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 ThreadModel {
let rootMessageSenderUserId: String?
let rootMessageSenderAvatar: AvatarViewDataProtocol?
let rootMessageSenderDisplayName: String?
let rootMessageText: NSAttributedString?
let rootMessageRedacted: Bool
let lastMessageTime: String?
let summaryModel: ThreadSummaryModel?
let notificationStatus: ThreadNotificationStatus
}
enum ThreadNotificationStatus {
case none
case notified
case highlighted
init(withThread thread: MXThread) {
if thread.highlightCount > 0 {
self = .highlighted
} else if thread.isParticipated && thread.notificationCount > 0 {
self = .notified
} else {
self = .none
}
}
}
@@ -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 UIKit
/// Dot view for a thread notification status
class ThreadNotificationStatusView: UIView {
private var theme: Theme
init(withTheme theme: Theme) {
self.theme = theme
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
theme = ThemeService.shared().theme
super.init(coder: coder)
}
/// Current status. Update this property to change background color accordingly.
var status: ThreadNotificationStatus = .none {
didSet {
updateBgColor()
}
}
private func updateBgColor() {
switch status {
case .none:
backgroundColor = .clear
case .notified:
backgroundColor = theme.colors.secondaryContent
case .highlighted:
backgroundColor = theme.colors.alert
}
}
}
extension ThreadNotificationStatusView: Themable {
func update(theme: Theme) {
self.theme = theme
updateBgColor()
}
}
@@ -0,0 +1,110 @@
//
// 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 ThreadTableViewCell: UITableViewCell {
private enum Constants {
static let separatorInset: UIEdgeInsets = UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0)
}
private var theme: Theme = ThemeService.shared().theme
private var configuredSenderId: String?
private var configuredRootMessageRedacted: Bool = false
private var rootMessageColor: UIColor {
return configuredRootMessageRedacted ?
theme.colors.secondaryContent :
theme.colors.primaryContent
}
@IBOutlet private weak var rootMessageAvatarView: UserAvatarView!
@IBOutlet private weak var rootMessageSenderLabel: UILabel!
@IBOutlet private weak var rootMessageContentLabel: UILabel!
@IBOutlet private weak var lastMessageTimeLabel: UILabel!
@IBOutlet private weak var summaryView: ThreadSummaryView!
@IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView!
private static var usernameColorGenerator = UserNameColorGenerator()
override func awakeFromNib() {
super.awakeFromNib()
separatorInset = Constants.separatorInset
}
func configure(withModel model: ThreadModel) {
if let rootAvatar = model.rootMessageSenderAvatar {
rootMessageAvatarView.fill(with: rootAvatar)
} else {
rootMessageAvatarView.avatarImageView.image = nil
}
configuredSenderId = model.rootMessageSenderUserId
configuredRootMessageRedacted = model.rootMessageRedacted
updateRootMessageSenderColor()
rootMessageSenderLabel.text = model.rootMessageSenderDisplayName
if let rootMessageText = model.rootMessageText {
updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor)
} else {
rootMessageContentLabel.attributedText = nil
}
lastMessageTimeLabel.text = model.lastMessageTime
if let summaryModel = model.summaryModel {
summaryView.configure(withModel: summaryModel)
}
notificationStatusView.status = model.notificationStatus
}
private func updateRootMessageSenderColor() {
if let senderUserId = configuredSenderId {
rootMessageSenderLabel.textColor = Self.usernameColorGenerator.color(from: senderUserId)
} else {
rootMessageSenderLabel.textColor = Self.usernameColorGenerator.defaultColor
}
}
private func updateRootMessageContentAttributes(_ string: NSAttributedString, color: UIColor) {
let mutable = NSMutableAttributedString(attributedString: string)
mutable.addAttributes([
.foregroundColor: color
], range: NSRange(location: 0, length: mutable.length))
rootMessageContentLabel.attributedText = mutable
}
}
extension ThreadTableViewCell: NibReusable {}
extension ThreadTableViewCell: Themable {
func update(theme: Theme) {
self.theme = theme
Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent
Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars
updateRootMessageSenderColor()
rootMessageAvatarView.backgroundColor = .clear
if let attributedText = rootMessageContentLabel.attributedText {
updateRootMessageContentAttributes(attributedText, color: rootMessageColor)
}
lastMessageTimeLabel.textColor = theme.colors.secondaryContent
summaryView.update(theme: theme)
summaryView.backgroundColor = .clear
notificationStatusView.update(theme: theme)
}
}
@@ -0,0 +1,104 @@
<?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="System colors in document resources" minToolsVersion="11.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"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I32-A5-WWw" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="12" y="12" width="32" height="32"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="f2R-6E-jRr"/>
<constraint firstAttribute="height" constant="32" id="uWM-eP-XnP"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
<rect key="frame" x="56" y="12" width="201" height="17"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="Time" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C2U-Ih-4Oh">
<rect key="frame" x="265" y="14" width="28" height="15"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aUq-D2-1KM" customClass="ThreadNotificationStatusView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="302" y="17" width="8" height="8"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" constant="8" id="2Fi-Ug-ZZa"/>
<constraint firstAttribute="height" constant="8" id="GQ5-OL-z1s"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="4"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Message" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzR-f9-3qV">
<rect key="frame" x="56" y="33" width="236" height="15"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="44" y="60" width="264" height="32"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="32" id="Pnm-yi-36O"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="I32-A5-WWw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="28p-b3-xMJ"/>
<constraint firstItem="108-Xh-aZf" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="2Dt-BH-xjF"/>
<constraint firstItem="Md3-uq-cSB" firstAttribute="top" secondItem="xzR-f9-3qV" secondAttribute="bottom" constant="8" id="6mB-Yd-Pyg"/>
<constraint firstAttribute="trailing" secondItem="aUq-D2-1KM" secondAttribute="trailing" constant="10" id="Du2-UR-wBe"/>
<constraint firstAttribute="bottom" secondItem="Md3-uq-cSB" secondAttribute="bottom" constant="12" id="Ppd-HN-Ehg"/>
<constraint firstItem="I32-A5-WWw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="Trt-CK-Tly"/>
<constraint firstItem="Md3-uq-cSB" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="44" id="Vpf-02-TgV"/>
<constraint firstAttribute="trailing" secondItem="xzR-f9-3qV" secondAttribute="trailing" constant="28" id="Zz9-PK-l9b"/>
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="leading" secondItem="108-Xh-aZf" secondAttribute="trailing" constant="8" id="bE8-Yy-3B9"/>
<constraint firstItem="xzR-f9-3qV" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="g8i-lt-K8f"/>
<constraint firstItem="aUq-D2-1KM" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="17" id="rvj-qg-S3J"/>
<constraint firstItem="108-Xh-aZf" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="sXf-FI-gD3"/>
<constraint firstItem="xzR-f9-3qV" firstAttribute="top" secondItem="108-Xh-aZf" secondAttribute="bottom" constant="4" id="tQN-Rr-MIS"/>
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="14" id="u3s-nr-avO"/>
<constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="12" id="vxt-vD-jy8"/>
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="27" id="wNc-xV-uIR"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="lastMessageTimeLabel" destination="C2U-Ih-4Oh" id="pf3-df-T65"/>
<outlet property="notificationStatusView" destination="aUq-D2-1KM" id="IDB-Yf-weu"/>
<outlet property="rootMessageAvatarView" destination="I32-A5-WWw" id="zJW-QQ-jsG"/>
<outlet property="rootMessageContentLabel" destination="xzR-f9-3qV" id="97u-na-8XW"/>
<outlet property="rootMessageSenderLabel" destination="108-Xh-aZf" id="nUc-qK-UCD"/>
<outlet property="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/>
</connections>
<point key="canvasLocation" x="2.8985507246376816" y="127.23214285714285"/>
</tableViewCell>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,26 @@
//
// 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 ThreadListEmptyModel {
let icon: UIImage?
let title: String?
let info: String?
let tip: String?
let showAllThreadsButtonTitle: String?
let showAllThreadsButtonHidden: Bool
}
@@ -0,0 +1,75 @@
//
// 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
@objc
protocol ThreadListEmptyViewDelegate: AnyObject {
func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView)
}
/// View to be shown on the thread list screen when no thread is available. Use a `ThreadListEmptyModel` instance to configure.
class ThreadListEmptyView: UIView {
@IBOutlet weak var delegate: ThreadListEmptyViewDelegate?
@IBOutlet private weak var iconBackgroundView: UIView!
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var infoLabel: UILabel!
@IBOutlet private weak var tipLabel: UILabel!
@IBOutlet private weak var showAllThreadsButton: UIButton!
required init?(coder: NSCoder) {
super.init(coder: coder)
loadNibContent()
}
func configure(withModel model: ThreadListEmptyModel) {
iconView.image = model.icon
titleLabel.text = model.title
infoLabel.text = model.info
tipLabel.text = model.tip
showAllThreadsButton.setTitle(model.showAllThreadsButtonTitle,
for: .normal)
showAllThreadsButton.isHidden = model.showAllThreadsButtonHidden
titleLabel.isHidden = titleLabel.text?.isEmpty ?? true
infoLabel.isHidden = infoLabel.text?.isEmpty ?? true
tipLabel.isHidden = tipLabel.text?.isEmpty ?? true
}
@IBAction private func showAllThreadsButtonTapped(_ sender: UIButton) {
delegate?.threadListEmptyViewTappedShowAllThreads(self)
}
}
extension ThreadListEmptyView: NibOwnerLoadable {}
extension ThreadListEmptyView: Themable {
func update(theme: Theme) {
iconBackgroundView.backgroundColor = theme.colors.system
iconView.tintColor = theme.colors.secondaryContent
titleLabel.textColor = theme.colors.primaryContent
infoLabel.textColor = theme.colors.secondaryContent
tipLabel.textColor = theme.colors.secondaryContent
showAllThreadsButton.tintColor = theme.colors.accent
}
}
@@ -0,0 +1,110 @@
<?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="retina3_5" 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" customClass="ThreadListEmptyView" customModule="Riot" customModuleProvider="target">
<connections>
<outlet property="iconBackgroundView" destination="TO4-Bz-2iH" id="J0d-n5-T7m"/>
<outlet property="iconView" destination="96m-sr-xQJ" id="iKm-fe-wZ5"/>
<outlet property="infoLabel" destination="OE7-gq-abZ" id="9QP-LH-Wvh"/>
<outlet property="showAllThreadsButton" destination="uTW-xb-Z9y" id="phx-FN-Dn2"/>
<outlet property="tipLabel" destination="RyB-Ah-jey" id="Rh7-W2-EDU"/>
<outlet property="titleLabel" destination="0q6-zY-VZH" id="RCC-ZR-q4F"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="313" height="540"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="f50-47-PNo">
<rect key="frame" x="20" y="108" width="273" height="324"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lm2-HJ-sTN">
<rect key="frame" x="16.5" y="0.0" width="240" height="1"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="SCQ-dJ-7RE"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TO4-Bz-2iH">
<rect key="frame" x="104.5" y="21" width="64" height="64"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="threads_icon" translatesAutoresizingMaskIntoConstraints="NO" id="96m-sr-xQJ">
<rect key="frame" x="16" y="16" width="32" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="K7U-U0-prN"/>
<constraint firstAttribute="width" secondItem="96m-sr-xQJ" secondAttribute="height" multiplier="1:1" id="Pgd-Qw-0Ju"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="64" id="3dO-ZF-YeI"/>
<constraint firstItem="96m-sr-xQJ" firstAttribute="centerY" secondItem="TO4-Bz-2iH" secondAttribute="centerY" id="EEK-EC-o8J"/>
<constraint firstAttribute="height" constant="64" id="iUz-gL-c7h"/>
<constraint firstItem="96m-sr-xQJ" firstAttribute="centerX" secondItem="TO4-Bz-2iH" secondAttribute="centerX" id="yqs-Ua-JWD"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="32"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Keep discussions organised with threads" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0q6-zY-VZH">
<rect key="frame" x="17.5" y="105" width="238" height="43"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Threads help keep your conversations on-topic and easy to track." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OE7-gq-abZ">
<rect key="frame" x="4.5" y="168" width="264.5" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: Use “Thread” option when selecting a message." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RyB-Ah-jey">
<rect key="frame" x="20.5" y="224" width="232.5" height="29"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uTW-xb-Z9y">
<rect key="frame" x="80" y="273" width="113" height="30"/>
<state key="normal" title="Show all threads"/>
<connections>
<action selector="showAllThreadsButtonTapped:" destination="-1" eventType="touchUpInside" id="cX4-am-oWF"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TKE-Zn-n2Q">
<rect key="frame" x="16.5" y="323" width="240" height="1"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="oXf-fF-mRt"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="f50-47-PNo" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="GG5-ib-fd0"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="f50-47-PNo" secondAttribute="trailing" constant="20" id="VsT-ay-7ZE"/>
<constraint firstItem="f50-47-PNo" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="20" id="gnS-Sc-jsF"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="-218.4375" y="-10"/>
</view>
</objects>
<resources>
<image name="threads_icon" width="32" height="32"/>
</resources>
</document>
@@ -0,0 +1,193 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Threads Threads ThreadList
/*
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
@objcMembers
final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let parameters: ThreadsCoordinatorParameters
private var selectedThreadCoordinator: RoomCoordinator?
private var navigationRouter: NavigationRouterType {
return self.parameters.navigationRouter
}
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: ThreadsCoordinatorDelegate?
// MARK: - Setup
init(parameters: ThreadsCoordinatorParameters) {
self.parameters = parameters
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(didPopModule(_:)),
name: NavigationRouter.didPopModule,
object: nil)
}
// MARK: - Public
func start() {
let rootCoordinator: Coordinator & Presentable
if let threadId = parameters.threadId {
rootCoordinator = createThreadCoordinator(forThreadId: threadId)
} else {
rootCoordinator = createThreadListCoordinator()
}
rootCoordinator.start()
self.add(childCoordinator: rootCoordinator)
// Detect when view controller has been dismissed by gesture when presented modally (not in full screen).
self.navigationRouter.toPresentable().presentationController?.delegate = self
if self.navigationRouter.modules.isEmpty == false {
self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
})
} else {
self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
}
}
}
func stop() {
if selectedThreadCoordinator != nil {
let modules = self.navigationRouter.modules
// if a thread is selected from the thread list coordinator, then navigation stack will look like:
// ... -> Screen A -> Thread List Screen -> Thread Screen
// we'll try to pop to Screen A here
// sanity check: navigation stack contains at least 3 items
guard modules.count >= 3 else {
return
}
let moduleToGoBack = modules[modules.count - 3]
self.navigationRouter.popToModule(moduleToGoBack, animated: true)
} else {
self.navigationRouter.popModule(animated: true)
}
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private
@objc
private func didPopModule(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let module = userInfo[NavigationRouter.NotificationUserInfoKey.module] as? Presentable,
let selectedThreadCoordinator = selectedThreadCoordinator else {
return
}
if module.toPresentable() == selectedThreadCoordinator.toPresentable() {
selectedThreadCoordinator.delegate = nil
remove(childCoordinator: selectedThreadCoordinator)
self.selectedThreadCoordinator = nil
}
}
private func createThreadListCoordinator() -> ThreadListCoordinator {
let coordinatorParameters = ThreadListCoordinatorParameters(session: self.parameters.session,
roomId: self.parameters.roomId)
let coordinator = ThreadListCoordinator(parameters: coordinatorParameters)
coordinator.delegate = self
return coordinator
}
private func createThreadCoordinator(forThreadId threadId: String) -> RoomCoordinator {
let parameters = RoomCoordinatorParameters(navigationRouter: navigationRouter,
navigationRouterStore: nil,
session: parameters.session,
parentSpaceId: nil,
roomId: parameters.roomId,
eventId: nil,
threadId: threadId,
displayConfiguration: .forThreads)
let coordinator = RoomCoordinator(parameters: parameters)
coordinator.delegate = self
return coordinator
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ThreadsCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.delegate?.threadsCoordinatorDidDismissInteractively(self)
}
}
// MARK: - ThreadListCoordinatorDelegate
extension ThreadsCoordinator: ThreadListCoordinatorDelegate {
func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) {
}
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) {
let roomCoordinator = createThreadCoordinator(forThreadId: thread.id)
selectedThreadCoordinator = roomCoordinator
roomCoordinator.start()
self.add(childCoordinator: roomCoordinator)
}
func threadListCoordinatorDidSelectRoom(_ coordinator: ThreadListCoordinatorProtocol, roomId: String, eventId: String) {
self.delegate?.threadsCoordinatorDidSelect(self, roomId: roomId, eventId: eventId)
}
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) {
self.delegate?.threadsCoordinatorDidComplete(self)
}
}
// MARK: - RoomCoordinatorDelegate
extension ThreadsCoordinator: RoomCoordinatorDelegate {
func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) {
}
func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) {
}
func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) {
self.delegate?.threadsCoordinatorDidSelect(self, roomId: roomId, eventId: eventId)
}
func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) {
}
}
@@ -0,0 +1,149 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Threads Threads ThreadList
/*
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 ThreadsCoordinatorBridgePresenterDelegate {
func threadsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter)
func threadsCoordinatorBridgePresenterDelegateDidSelect(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter,
roomId: String,
eventId: String?)
func threadsCoordinatorBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter)
}
/// ThreadsCoordinatorBridgePresenter enables to start ThreadsCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (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 ThreadsCoordinatorBridgePresenter: NSObject {
// MARK: - Constants
private enum NavigationType {
case present
case push
}
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let threadId: String?
private var navigationType: NavigationType = .present
private var coordinator: ThreadsCoordinator?
// MARK: Public
weak var delegate: ThreadsCoordinatorBridgePresenterDelegate?
// MARK: - Setup
/// Initializer
/// - Parameters:
/// - session: Session instance
/// - roomId: Room identifier
/// - threadId: Thread identifier. Specified thread will be opened if provided, the thread list otherwise
init(session: MXSession,
roomId: String,
threadId: String?) {
self.session = session
self.roomId = roomId
self.threadId = threadId
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 threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session,
roomId: self.roomId,
threadId: self.threadId)
let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters)
threadsCoordinator.delegate = self
let presentable = threadsCoordinator.toPresentable()
viewController.present(presentable, animated: animated, completion: nil)
threadsCoordinator.start()
self.coordinator = threadsCoordinator
self.navigationType = .present
}
func push(from navigationController: UINavigationController, animated: Bool) {
let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session,
roomId: self.roomId,
threadId: self.threadId,
navigationRouter: navigationRouter)
let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters)
threadsCoordinator.delegate = self
threadsCoordinator.start() // Will trigger the view controller push
self.coordinator = threadsCoordinator
self.navigationType = .push
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
switch navigationType {
case .present:
// Dismiss modal
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
completion?()
}
case .push:
// stop coordinator to pop modules as needed
coordinator.stop()
self.coordinator = nil
completion?()
}
}
}
// MARK: - ThreadsCoordinatorDelegate
extension ThreadsCoordinatorBridgePresenter: ThreadsCoordinatorDelegate {
func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol) {
self.delegate?.threadsCoordinatorBridgePresenterDelegateDidComplete(self)
}
func threadsCoordinatorDidSelect(_ coordinator: ThreadsCoordinatorProtocol, roomId: String, eventId: String?) {
self.delegate?.threadsCoordinatorBridgePresenterDelegateDidSelect(self, roomId: roomId, eventId: eventId)
}
func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol) {
self.delegate?.threadsCoordinatorBridgePresenterDidDismissInteractively(self)
}
}
@@ -0,0 +1,45 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Threads Threads ThreadList
/*
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
/// ThreadsCoordinator input parameters
struct ThreadsCoordinatorParameters {
/// The Matrix session
let session: MXSession
/// Room identifier
let roomId: String
/// Thread identifier. Specified thread will be opened if provided, the thread list otherwise
let threadId: String?
/// The navigation router that manage physical navigation
let navigationRouter: NavigationRouterType
init(session: MXSession,
roomId: String,
threadId: String?,
navigationRouter: NavigationRouterType? = nil) {
self.session = session
self.roomId = roomId
self.threadId = threadId
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}
@@ -0,0 +1,33 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Threads Threads ThreadList
/*
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 ThreadsCoordinatorDelegate: AnyObject {
func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol)
func threadsCoordinatorDidSelect(_ coordinator: ThreadsCoordinatorProtocol, roomId: String, eventId: String?)
/// Called when the view has been dismissed by gesture when presented modally (not in full screen).
func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol)
}
/// `ThreadsCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow.
protocol ThreadsCoordinatorProtocol: Coordinator, Presentable {
var delegate: ThreadsCoordinatorDelegate? { get }
}