mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-24 10:32:46 +02:00
Implement thread list
This commit is contained in:
@@ -40,7 +40,8 @@ final class ThreadListCoordinator: ThreadListCoordinatorProtocol {
|
||||
|
||||
init(parameters: ThreadListCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let threadListViewModel = ThreadListViewModel(session: self.parameters.session)
|
||||
let threadListViewModel = ThreadListViewModel(session: self.parameters.session,
|
||||
roomId: self.parameters.roomId)
|
||||
let threadListViewController = ThreadListViewController.instantiate(with: threadListViewModel)
|
||||
self.threadListViewModel = threadListViewModel
|
||||
self.threadListViewController = threadListViewController
|
||||
@@ -60,8 +61,8 @@ final class ThreadListCoordinator: ThreadListCoordinatorProtocol {
|
||||
// MARK: - ThreadListViewModelCoordinatorDelegate
|
||||
extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate {
|
||||
|
||||
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) {
|
||||
self.delegate?.threadListCoordinator(self, didCompleteWithUserDisplayName: userDisplayName)
|
||||
func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) {
|
||||
self.delegate?.threadListCoordinatorDidLoadThreads(self)
|
||||
}
|
||||
|
||||
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) {
|
||||
|
||||
@@ -24,5 +24,6 @@ struct ThreadListCoordinatorParameters {
|
||||
/// The Matrix session
|
||||
let session: MXSession
|
||||
|
||||
|
||||
/// Room identifier
|
||||
let roomId: String
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol ThreadListCoordinatorDelegate: AnyObject {
|
||||
func threadListCoordinator(_ coordinator: ThreadListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?)
|
||||
func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
|
||||
<device id="retina6_1" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<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="14490.49"/>
|
||||
<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>
|
||||
<!--Template Screen View Controller-->
|
||||
<!--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">
|
||||
@@ -18,74 +17,26 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9U2-KL-ZVA">
|
||||
<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"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="e7g-um-WO4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="208"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="voD-3Q-ryt">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="208"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="A message" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bxI-mu-qng">
|
||||
<rect key="frame" x="20" y="40" width="374" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DOt-5E-FjF">
|
||||
<rect key="frame" x="20" y="158" width="374" height="30"/>
|
||||
<state key="normal" title="OK"/>
|
||||
<connections>
|
||||
<action selector="doneButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="uvI-tt-Nfj"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="bxI-mu-qng" secondAttribute="trailing" constant="20" id="2v5-vH-NEd"/>
|
||||
<constraint firstItem="DOt-5E-FjF" firstAttribute="top" secondItem="bxI-mu-qng" secondAttribute="bottom" constant="100" id="C4r-0w-VXj"/>
|
||||
<constraint firstItem="bxI-mu-qng" firstAttribute="leading" secondItem="voD-3Q-ryt" secondAttribute="leading" constant="20" id="I1A-QW-IJG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="DOt-5E-FjF" secondAttribute="trailing" constant="20" id="NKP-2G-Czj"/>
|
||||
<constraint firstItem="bxI-mu-qng" firstAttribute="top" secondItem="voD-3Q-ryt" secondAttribute="top" constant="40" id="bU7-a2-LIj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="DOt-5E-FjF" secondAttribute="bottom" constant="20" id="dmC-vE-FeB"/>
|
||||
<constraint firstItem="DOt-5E-FjF" firstAttribute="leading" secondItem="voD-3Q-ryt" secondAttribute="leading" constant="20" id="flU-tM-8hK"/>
|
||||
<constraint firstAttribute="width" priority="750" constant="500" id="glD-Sz-73O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="voD-3Q-ryt" secondAttribute="bottom" id="63a-5e-ptU"/>
|
||||
<constraint firstItem="voD-3Q-ryt" firstAttribute="centerX" secondItem="e7g-um-WO4" secondAttribute="centerX" id="P2G-mq-gQW"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="voD-3Q-ryt" secondAttribute="trailing" id="QgV-SO-5yf"/>
|
||||
<constraint firstItem="voD-3Q-ryt" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="e7g-um-WO4" secondAttribute="leading" id="YPo-u1-PtT"/>
|
||||
<constraint firstItem="voD-3Q-ryt" firstAttribute="top" secondItem="e7g-um-WO4" secondAttribute="top" id="rhQ-96-szL"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="e7g-um-WO4" secondAttribute="trailing" id="GyG-Fh-PME"/>
|
||||
<constraint firstItem="e7g-um-WO4" firstAttribute="width" secondItem="9U2-KL-ZVA" secondAttribute="width" id="Ok2-WQ-Zgc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="e7g-um-WO4" secondAttribute="bottom" id="Y46-NP-zAc"/>
|
||||
<constraint firstItem="e7g-um-WO4" firstAttribute="leading" secondItem="9U2-KL-ZVA" secondAttribute="leading" id="aoV-Yh-AcD"/>
|
||||
<constraint firstItem="e7g-um-WO4" firstAttribute="top" secondItem="9U2-KL-ZVA" secondAttribute="top" id="pFN-bA-SHw"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="V8j-Lb-PgC" id="FCQ-5E-AuZ"/>
|
||||
<outlet property="delegate" destination="V8j-Lb-PgC" id="Kxs-vj-1RW"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
|
||||
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="9U2-KL-ZVA" secondAttribute="bottom" id="7Cb-nY-CsO"/>
|
||||
<constraint firstItem="9U2-KL-ZVA" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="GdQ-hK-muG"/>
|
||||
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="9U2-KL-ZVA" secondAttribute="trailing" id="sbD-ek-vGJ"/>
|
||||
<constraint firstItem="bFg-jh-JZB" firstAttribute="top" secondItem="9U2-KL-ZVA" secondAttribute="top" id="wTB-V6-IHV"/>
|
||||
<constraint firstItem="X8K-NO-SQ3" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="BnL-lB-eXd"/>
|
||||
<constraint firstItem="X8K-NO-SQ3" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="Mfw-In-peq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="X8K-NO-SQ3" secondAttribute="bottom" id="SIG-7P-2CK"/>
|
||||
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="X8K-NO-SQ3" secondAttribute="trailing" id="WvE-5E-UNz"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="doneButton" destination="DOt-5E-FjF" id="ktw-U4-efQ"/>
|
||||
<outlet property="informationLabel" destination="bxI-mu-qng" id="pbX-aZ-inC"/>
|
||||
<outlet property="scrollView" destination="9U2-KL-ZVA" id="ojG-2y-X7b"/>
|
||||
<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"/>
|
||||
@@ -93,4 +44,9 @@
|
||||
<point key="canvasLocation" x="-3198" y="-647"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -30,10 +30,7 @@ final class ThreadListViewController: UIViewController {
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
@IBOutlet private weak var scrollView: UIScrollView!
|
||||
|
||||
@IBOutlet private weak var informationLabel: UILabel!
|
||||
@IBOutlet private weak var doneButton: UIButton!
|
||||
@IBOutlet private weak var threadsTableView: UITableView!
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@@ -60,7 +57,7 @@ final class ThreadListViewController: UIViewController {
|
||||
// Do any additional setup after loading the view.
|
||||
|
||||
self.setupViews()
|
||||
self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView)
|
||||
self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.threadsTableView)
|
||||
self.activityPresenter = ActivityIndicatorPresenter()
|
||||
self.errorPresenter = MXKErrorAlertPresentation()
|
||||
|
||||
@@ -99,12 +96,9 @@ final class ThreadListViewController: UIViewController {
|
||||
theme.applyStyle(onNavigationBar: navigationBar)
|
||||
}
|
||||
|
||||
|
||||
// TODO: Set view colors here
|
||||
self.informationLabel.textColor = theme.textPrimaryColor
|
||||
|
||||
self.doneButton.backgroundColor = theme.backgroundColor
|
||||
theme.applyStyle(onButton: self.doneButton)
|
||||
self.threadsTableView.backgroundColor = theme.backgroundColor
|
||||
self.threadsTableView.separatorColor = theme.colors.separator
|
||||
self.threadsTableView.reloadData()
|
||||
}
|
||||
|
||||
private func registerThemeServiceDidChangeThemeNotification() {
|
||||
@@ -116,17 +110,18 @@ final class ThreadListViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func setupViews() {
|
||||
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
|
||||
self?.cancelButtonAction()
|
||||
}
|
||||
let filterBarButtonItem = UIBarButtonItem(image: Asset.Images.roomContextMenuMore.image,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(filterButtonTapped(_:)))
|
||||
|
||||
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||
self.navigationItem.rightBarButtonItem = filterBarButtonItem
|
||||
|
||||
self.title = "Template"
|
||||
self.title = "Threads"
|
||||
|
||||
self.scrollView.keyboardDismissMode = .interactive
|
||||
|
||||
self.informationLabel.text = "VectorL10n.threadListTitle"
|
||||
self.threadsTableView.tableFooterView = UIView()
|
||||
self.threadsTableView.register(cellType: ThreadTableViewCell.self)
|
||||
self.threadsTableView.keyboardDismissMode = .interactive
|
||||
}
|
||||
|
||||
private func render(viewState: ThreadListViewState) {
|
||||
@@ -135,8 +130,8 @@ final class ThreadListViewController: UIViewController {
|
||||
break
|
||||
case .loading:
|
||||
self.renderLoading()
|
||||
case .loaded(let displayName):
|
||||
self.renderLoaded(displayName: displayName)
|
||||
case .loaded:
|
||||
self.renderLoaded()
|
||||
case .error(let error):
|
||||
self.render(error: error)
|
||||
}
|
||||
@@ -144,13 +139,11 @@ final class ThreadListViewController: UIViewController {
|
||||
|
||||
private func renderLoading() {
|
||||
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
|
||||
self.informationLabel.text = "Fetch display name"
|
||||
}
|
||||
|
||||
private func renderLoaded(displayName: String) {
|
||||
private func renderLoaded() {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
|
||||
self.informationLabel.text = "You display name: \(displayName)"
|
||||
self.threadsTableView.reloadData()
|
||||
}
|
||||
|
||||
private func render(error: Error) {
|
||||
@@ -158,23 +151,54 @@ final class ThreadListViewController: UIViewController {
|
||||
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction private func doneButtonAction(_ sender: Any) {
|
||||
@objc
|
||||
private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
||||
self.viewModel.process(viewAction: .complete)
|
||||
}
|
||||
|
||||
private func cancelButtonAction() {
|
||||
self.viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - ThreadListViewModelViewDelegate
|
||||
|
||||
extension ThreadListViewController: ThreadListViewModelViewDelegate {
|
||||
|
||||
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didUpdateViewState viewSate: ThreadListViewState) {
|
||||
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol,
|
||||
didUpdateViewState viewSate: ThreadListViewState) {
|
||||
self.render(viewState: viewSate)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if let threadVM = viewModel.threadViewModel(at: indexPath.row) {
|
||||
cell.configure(withViewModel: threadVM)
|
||||
}
|
||||
cell.update(theme: theme)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,9 +25,12 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
// 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 userDisplayName: String?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -42,8 +45,10 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession) {
|
||||
init(session: MXSession,
|
||||
roomId: String) {
|
||||
self.session = session
|
||||
self.roomId = roomId
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -57,37 +62,131 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
case .loadData:
|
||||
self.loadData()
|
||||
case .complete:
|
||||
self.coordinatorDelegate?.threadListViewModel(self, didCompleteWithUserDisplayName: self.userDisplayName)
|
||||
self.coordinatorDelegate?.threadListViewModelDidLoadThreads(self)
|
||||
case .cancel:
|
||||
self.cancelOperations()
|
||||
self.coordinatorDelegate?.threadListViewModelDidCancel(self)
|
||||
}
|
||||
}
|
||||
|
||||
var numberOfThreads: Int {
|
||||
return threads.count
|
||||
}
|
||||
|
||||
func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard index < threads.count else {
|
||||
return nil
|
||||
}
|
||||
return viewModel(forThread: threads[index])
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func viewModel(forThread thread: MXThread) -> ThreadViewModel {
|
||||
let rootAvatarViewData: AvatarViewData?
|
||||
let rootMessageSender: MXUser?
|
||||
let lastAvatarViewData: AvatarViewData?
|
||||
let lastMessageSender: MXUser?
|
||||
let rootMessageText: String?
|
||||
let lastMessageText: String?
|
||||
let lastMessageTime: String?
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if let eventFormatter = eventFormatter {
|
||||
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
|
||||
rootMessageText = eventFormatter.string(from: thread.rootMessage,
|
||||
with: roomState,
|
||||
error: formatterError)
|
||||
lastMessageText = eventFormatter.string(from: thread.lastMessage,
|
||||
with: roomState,
|
||||
error: formatterError)
|
||||
lastMessageTime = eventFormatter.dateString(from: thread.lastMessage, withTime: true)
|
||||
} else {
|
||||
rootMessageText = nil
|
||||
lastMessageText = nil
|
||||
lastMessageTime = nil
|
||||
}
|
||||
|
||||
let summaryViewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies,
|
||||
lastMessageSenderAvatar: lastAvatarViewData,
|
||||
lastMessageText: lastMessageText)
|
||||
|
||||
return ThreadViewModel(rootMessageSenderAvatar: rootAvatarViewData,
|
||||
rootMessageSenderDisplayName: rootMessageSender?.displayname,
|
||||
rootMessageText: rootMessageText,
|
||||
lastMessageTime: lastMessageTime,
|
||||
summaryViewModel: summaryViewModel)
|
||||
}
|
||||
|
||||
private func loadData() {
|
||||
|
||||
viewState = .loading
|
||||
|
||||
// Check first that the user homeserver is federated with the Riot-bot homeserver
|
||||
self.currentOperation = self.session.matrixRestClient.displayName(forUser: self.session.myUser.userId) { [weak self] (response) in
|
||||
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
threads = session.threadingService.threads(inRoom: roomId)
|
||||
session.threadingService.addDelegate(self)
|
||||
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
|
||||
|
||||
switch response {
|
||||
case .success(let userDisplayName):
|
||||
self.viewState = .loaded(userDisplayName)
|
||||
self.userDisplayName = userDisplayName
|
||||
case .failure(let error):
|
||||
self.viewState = .error(error)
|
||||
}
|
||||
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 cancelOperations() {
|
||||
self.currentOperation?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension ThreadListViewModel: MXThreadingServiceDelegate {
|
||||
|
||||
func threadingServiceDidUpdateThreads(_ service: MXThreadingService) {
|
||||
threads = service.threads(inRoom: roomId)
|
||||
viewState = .loaded
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ protocol ThreadListViewModelViewDelegate: AnyObject {
|
||||
}
|
||||
|
||||
protocol ThreadListViewModelCoordinatorDelegate: AnyObject {
|
||||
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?)
|
||||
func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol)
|
||||
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol)
|
||||
}
|
||||
|
||||
@@ -36,4 +36,7 @@ protocol ThreadListViewModelProtocol {
|
||||
func process(viewAction: ThreadListViewAction)
|
||||
|
||||
var viewState: ThreadListViewState { get }
|
||||
|
||||
var numberOfThreads: Int { get }
|
||||
func threadViewModel(at index: Int) -> ThreadViewModel?
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ import Foundation
|
||||
enum ThreadListViewState {
|
||||
case idle
|
||||
case loading
|
||||
case loaded(_ displayName: String)
|
||||
case loaded
|
||||
case error(Error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
@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!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
separatorInset = Constants.separatorInset
|
||||
}
|
||||
|
||||
func configure(withViewModel viewModel: ThreadViewModel) {
|
||||
if let rootAvatar = viewModel.rootMessageSenderAvatar {
|
||||
rootMessageAvatarView.fill(with: rootAvatar)
|
||||
} else {
|
||||
rootMessageAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName
|
||||
rootMessageContentLabel.text = viewModel.rootMessageText
|
||||
lastMessageTimeLabel.text = viewModel.lastMessageTime
|
||||
if let summaryViewModel = viewModel.summaryViewModel {
|
||||
summaryView.configure(withViewModel: summaryViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ThreadTableViewCell: NibReusable {}
|
||||
|
||||
extension ThreadTableViewCell: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
contentView.backgroundColor = theme.colors.background
|
||||
rootMessageAvatarView.backgroundColor = .clear
|
||||
rootMessageContentLabel.textColor = theme.colors.primaryContent
|
||||
lastMessageTimeLabel.textColor = theme.colors.secondaryContent
|
||||
summaryView.update(theme: theme)
|
||||
summaryView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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="104" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="104"/>
|
||||
<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="104"/>
|
||||
<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" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
|
||||
<rect key="frame" x="56" y="12" width="200" 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="264" y="12" width="28" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<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="17"/>
|
||||
<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="47" y="58" width="245" height="34"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</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="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="47" 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="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="12" id="u3s-nr-avO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="28" id="vxt-vD-jy8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="28" 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="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="129.91071428571428"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// 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 ThreadViewModel {
|
||||
var rootMessageSenderAvatar: AvatarViewDataProtocol?
|
||||
var rootMessageSenderDisplayName: String?
|
||||
var rootMessageText: String?
|
||||
var lastMessageTime: String?
|
||||
var summaryViewModel: ThreadSummaryViewModel?
|
||||
}
|
||||
Reference in New Issue
Block a user