Add thread list empty view

This commit is contained in:
ismailgulek
2021-11-22 16:16:15 +03:00
parent 1b4d172b80
commit 8e303788c1
12 changed files with 305 additions and 5 deletions
@@ -25,23 +25,35 @@
<outlet property="delegate" destination="V8j-Lb-PgC" id="Kxs-vj-1RW"/>
</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="44" width="414" height="852"/>
<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="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"/>
<constraint firstItem="7VY-m9-wCS" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="obE-c9-v49"/>
</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"/>
</objects>
<point key="canvasLocation" x="-3198" y="-647"/>
<point key="canvasLocation" x="-3198.5507246376815" y="-647.54464285714278"/>
</scene>
</scenes>
<resources>
@@ -31,6 +31,7 @@ final class ThreadListViewController: UIViewController {
// MARK: Outlets
@IBOutlet private weak var threadsTableView: UITableView!
@IBOutlet private weak var emptyView: ThreadListEmptyView!
// MARK: Private
@@ -102,6 +103,8 @@ final class ThreadListViewController: UIViewController {
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()
@@ -142,6 +145,8 @@ final class ThreadListViewController: UIViewController {
self.renderLoading()
case .loaded:
self.renderLoaded()
case .empty(let viewModel):
self.renderEmptyView(withViewModel: viewModel)
case .showingFilterTypes:
self.renderShowingFilterTypes()
case .error(let error):
@@ -150,12 +155,24 @@ final class ThreadListViewController: UIViewController {
}
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
}
private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
emptyView.configure(withViewModel: emptyViewModel)
threadsTableView.isHidden = true
emptyView.isHidden = false
navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads
}
private func renderShowingFilterTypes() {
@@ -254,3 +271,13 @@ extension ThreadListViewController: UITableViewDelegate {
}
}
// MARK: - ThreadListEmptyViewDelegate
extension ThreadListViewController: ThreadListEmptyViewDelegate {
func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) {
viewModel.process(viewAction: .selectFilterType(.all))
}
}
@@ -54,6 +54,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
}
deinit {
session.threadingService.removeDelegate(self)
self.cancelOperations()
}
@@ -113,6 +114,25 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
roomDisplayName: room.displayName)
}
private var emptyViewModel: ThreadListEmptyViewModel {
switch selectedFilterType {
case .all:
return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoAll,
tip: VectorL10n.threadsEmptyTip,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: true)
case .myThreads:
return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoMy,
tip: VectorL10n.threadsEmptyTip,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: false)
}
}
// MARK: - Private
private func viewModel(forThread thread: MXThread) -> ThreadViewModel {
@@ -195,9 +215,11 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
)
}
private func loadData() {
private func loadData(showLoading: Bool = true) {
viewState = .loading
if showLoading {
viewState = .loading
}
switch selectedFilterType {
case .all:
@@ -206,6 +228,11 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
threads = session.threadingService.participatedThreads(inRoom: roomId)
}
if threads.isEmpty {
viewState = .empty(emptyViewModel)
return
}
threadsLoaded()
}
@@ -244,8 +271,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
extension ThreadListViewModel: MXThreadingServiceDelegate {
func threadingServiceDidUpdateThreads(_ service: MXThreadingService) {
threads = service.threads(inRoom: roomId)
viewState = .loaded
loadData(showLoading: false)
}
}
@@ -23,6 +23,7 @@ enum ThreadListViewState {
case idle
case loading
case loaded
case empty(_ viewModel: ThreadListEmptyViewModel)
case showingFilterTypes
case error(Error)
}
@@ -0,0 +1,73 @@
//
// 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)
}
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(withViewModel viewModel: ThreadListEmptyViewModel) {
iconView.image = viewModel.icon
titleLabel.text = viewModel.title
infoLabel.text = viewModel.info
tipLabel.text = viewModel.tip
showAllThreadsButton.setTitle(viewModel.showAllThreadsButtonTitle,
for: .normal)
showAllThreadsButton.isHidden = viewModel.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
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="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="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="437" 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="126" width="397" height="288"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lm2-HJ-sTN">
<rect key="frame" x="78.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="166.5" y="21" width="64" height="64"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="room_context_menu_reply_in_thread" 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="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0q6-zY-VZH">
<rect key="frame" x="27.5" y="105" width="342.5" height="21.5"/>
<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="3" y="146.5" width="391.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="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RyB-Ah-jey">
<rect key="frame" x="50.5" y="202.5" width="296.5" height="14.5"/>
<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="142" y="237" 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="78.5" y="287" 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="-100.72463768115942" y="-9.375"/>
</view>
</objects>
<resources>
<image name="room_context_menu_reply_in_thread" width="18" height="18"/>
</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 ThreadListEmptyViewModel {
let icon: UIImage?
let title: String?
let info: String?
let tip: String?
let showAllThreadsButtonTitle: String?
let showAllThreadsButtonHidden: Bool
}