mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-26 11:30:50 +02:00
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:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user