Merge branch 'develop' into ismail/5096_thread_notifications

This commit is contained in:
ismailgulek
2022-01-27 03:20:25 +03:00
210 changed files with 3878 additions and 1073 deletions
@@ -113,11 +113,11 @@ class ThreadViewController: RoomViewController {
}
private func copyPermalink() {
guard let permalink = permalink else {
guard let permalink = permalink, let url = URL(string: permalink) else {
return
}
MXKPasteboardManager.shared.pasteboard.string = permalink
MXKPasteboardManager.shared.pasteboard.url = url
view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo,
image: Asset.Images.linkIcon.image,
additionalMargin: self.roomInputToolbarContainerHeightConstraint.constant)
@@ -25,7 +25,7 @@ protocol ThreadListCoordinatorDelegate: AnyObject {
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol)
}
/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow.
/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle thread list navigation flow.
protocol ThreadListCoordinatorProtocol: Coordinator, Presentable {
var delegate: ThreadListCoordinatorDelegate? { get }
}
@@ -20,12 +20,6 @@ import UIKit
final class ThreadListViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let aConstant: Int = 666
}
// MARK: - Properties
// MARK: Outlets
@@ -125,7 +119,7 @@ final class ThreadListViewController: UIViewController {
private func setupViews() {
let titleView = ThreadRoomTitleView.loadFromNib()
titleView.mode = .allThreads
titleView.configure(withViewModel: viewModel.titleViewModel)
titleView.configure(withModel: viewModel.titleModel)
titleView.updateLayout(for: UIApplication.shared.statusBarOrientation)
self.titleView = titleView
navigationItem.leftItemsSupplementBackButton = true
@@ -150,14 +144,14 @@ final class ThreadListViewController: UIViewController {
renderLoading()
case .loaded:
renderLoaded()
case .empty(let viewModel):
renderEmptyView(withViewModel: viewModel)
case .empty(let model):
renderEmptyView(withModel: model)
case .showingFilterTypes:
renderShowingFilterTypes()
case .showingLongPressActions:
renderShowingLongPressActions()
case .share(let string):
renderShare(string)
case .showingLongPressActions(let index):
renderShowingLongPressActions(index)
case .share(let url, let index):
renderShare(url, index: index)
case .toastForCopyLink:
toastForCopyLink()
case .error(let error):
@@ -184,9 +178,9 @@ final class ThreadListViewController: UIViewController {
}
}
private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) {
private func renderEmptyView(withModel model: ThreadListEmptyModel) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
emptyView.configure(withViewModel: emptyViewModel)
emptyView.configure(withModel: model)
threadsTableView.isHidden = true
emptyView.isHidden = false
navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads
@@ -232,7 +226,7 @@ final class ThreadListViewController: UIViewController {
self.present(alertController, animated: true, completion: nil)
}
private func renderShowingLongPressActions() {
private func renderShowingLongPressActions(_ index: Int) {
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionViewInRoom,
@@ -259,14 +253,25 @@ final class ThreadListViewController: UIViewController {
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(_ string: String) {
let activityVC = UIActivityViewController(activityItems: [string],
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)
}
@@ -327,8 +332,8 @@ extension ThreadListViewController: UITableViewDataSource {
let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.update(theme: theme)
if let threadVM = viewModel.threadViewModel(at: indexPath.row) {
cell.configure(withViewModel: threadVM)
if let threadModel = viewModel.threadModel(at: indexPath.row) {
cell.configure(withModel: threadModel)
}
return cell
@@ -92,14 +92,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
return threads.count
}
func threadViewModel(at index: Int) -> ThreadViewModel? {
func threadModel(at index: Int) -> ThreadModel? {
guard index < threads.count else {
return nil
}
return viewModel(forThread: threads[index])
return model(forThread: threads[index])
}
var titleViewModel: ThreadRoomTitleViewModel {
var titleModel: ThreadRoomTitleModel {
guard let room = session.room(withRoomId: roomId) else {
return .empty
}
@@ -118,33 +118,33 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
encrpytionBadge = nil
}
return ThreadRoomTitleViewModel(roomAvatar: avatarViewData,
roomEncryptionBadge: encrpytionBadge,
roomDisplayName: room.displayName)
return ThreadRoomTitleModel(roomAvatar: avatarViewData,
roomEncryptionBadge: encrpytionBadge,
roomDisplayName: room.displayName)
}
private var emptyViewModel: ThreadListEmptyViewModel {
private var emptyViewModel: ThreadListEmptyModel {
switch selectedFilterType {
case .all:
return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoAll,
tip: VectorL10n.threadsEmptyTip,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: true)
return ThreadListEmptyModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoAll,
tip: VectorL10n.threadsEmptyTip,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: true)
case .myThreads:
return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoMy,
tip: nil,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: false)
return ThreadListEmptyModel(icon: Asset.Images.threadsIcon.image,
title: VectorL10n.threadsEmptyTitle,
info: VectorL10n.threadsEmptyInfoMy,
tip: nil,
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
showAllThreadsButtonHidden: false)
}
}
// MARK: - Private
private func viewModel(forThread thread: MXThread) -> ThreadViewModel {
private func model(forThread thread: MXThread) -> ThreadModel {
let rootAvatarViewData: AvatarViewData?
let rootMessageSender: MXUser?
let lastAvatarViewData: AvatarViewData?
@@ -184,19 +184,19 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
lastAvatarViewData = nil
lastMessageSender = nil
}
let summaryViewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies,
lastMessageSenderAvatar: lastAvatarViewData,
lastMessageText: lastMessageText)
return ThreadViewModel(rootMessageSenderUserId: rootMessageSender?.userId,
rootMessageSenderAvatar: rootAvatarViewData,
rootMessageSenderDisplayName: rootMessageSender?.displayname,
rootMessageText: rootMessageText,
rootMessageRedacted: thread.rootMessage?.isRedactedEvent() ?? false,
lastMessageTime: lastMessageTime,
summaryViewModel: summaryViewModel,
notificationStatus: notificationStatus)
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? {
@@ -298,7 +298,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
return
}
longPressedThread = threads[index]
viewState = .showingLongPressActions
viewState = .showingLongPressActions(index)
}
private func actionViewInRoom() {
@@ -313,19 +313,22 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
guard let thread = longPressedThread else {
return
}
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) {
MXKPasteboardManager.shared.pasteboard.string = permalink
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 else {
guard let thread = longPressedThread,
let index = threads.firstIndex(of: thread) else {
return
}
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) {
viewState = .share(permalink)
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId),
let url = URL(string: permalink) {
viewState = .share(url, index)
}
longPressedThread = nil
}
@@ -39,10 +39,10 @@ protocol ThreadListViewModelProtocol {
var viewState: ThreadListViewState { get }
var titleViewModel: ThreadRoomTitleViewModel { get }
var titleModel: ThreadRoomTitleModel { get }
var selectedFilterType: ThreadListFilterType { get }
var numberOfThreads: Int { get }
func threadViewModel(at index: Int) -> ThreadViewModel?
func threadModel(at index: Int) -> ThreadModel?
}
enum ThreadListFilterType {
@@ -23,10 +23,10 @@ enum ThreadListViewState {
case idle
case loading
case loaded
case empty(_ viewModel: ThreadListEmptyViewModel)
case empty(_ viewModel: ThreadListEmptyModel)
case showingFilterTypes
case showingLongPressActions
case share(_ string: String)
case showingLongPressActions(_ index: Int)
case share(_ url: URL, _ index: Int)
case toastForCopyLink
case error(Error)
}
@@ -16,22 +16,22 @@
import Foundation
struct ThreadViewModel {
var rootMessageSenderUserId: String?
var rootMessageSenderAvatar: AvatarViewDataProtocol?
var rootMessageSenderDisplayName: String?
var rootMessageText: NSAttributedString?
var rootMessageRedacted: Bool
var lastMessageTime: String?
var summaryViewModel: ThreadSummaryViewModel?
var notificationStatus: ThreadNotificationStatus
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
@@ -40,10 +40,7 @@ class ThreadTableViewCell: UITableViewCell {
@IBOutlet private weak var summaryView: ThreadSummaryView!
@IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView!
private static var usernameColorGenerator: UserNameColorGenerator = {
let generator = UserNameColorGenerator()
return generator
}()
private static var usernameColorGenerator = UserNameColorGenerator()
override func awakeFromNib() {
super.awakeFromNib()
@@ -51,26 +48,26 @@ class ThreadTableViewCell: UITableViewCell {
separatorInset = Constants.separatorInset
}
func configure(withViewModel viewModel: ThreadViewModel) {
if let rootAvatar = viewModel.rootMessageSenderAvatar {
func configure(withModel model: ThreadModel) {
if let rootAvatar = model.rootMessageSenderAvatar {
rootMessageAvatarView.fill(with: rootAvatar)
} else {
rootMessageAvatarView.avatarImageView.image = nil
}
configuredSenderId = viewModel.rootMessageSenderUserId
configuredRootMessageRedacted = viewModel.rootMessageRedacted
configuredSenderId = model.rootMessageSenderUserId
configuredRootMessageRedacted = model.rootMessageRedacted
updateRootMessageSenderColor()
rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName
if let rootMessageText = viewModel.rootMessageText {
rootMessageSenderLabel.text = model.rootMessageSenderDisplayName
if let rootMessageText = model.rootMessageText {
updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor)
} else {
rootMessageContentLabel.attributedText = nil
}
lastMessageTimeLabel.text = viewModel.lastMessageTime
if let summaryViewModel = viewModel.summaryViewModel {
summaryView.configure(withViewModel: summaryViewModel)
lastMessageTimeLabel.text = model.lastMessageTime
if let summaryModel = model.summaryModel {
summaryView.configure(withModel: summaryModel)
}
notificationStatusView.status = viewModel.notificationStatus
notificationStatusView.status = model.notificationStatus
}
private func updateRootMessageSenderColor() {
@@ -16,7 +16,7 @@
import Foundation
struct ThreadListEmptyViewModel {
struct ThreadListEmptyModel {
let icon: UIImage?
let title: String?
let info: String?
@@ -22,6 +22,7 @@ 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?
@@ -38,14 +39,14 @@ class ThreadListEmptyView: UIView {
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,
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 = viewModel.showAllThreadsButtonHidden
showAllThreadsButton.isHidden = model.showAllThreadsButtonHidden
titleLabel.isHidden = titleLabel.text?.isEmpty ?? true
infoLabel.isHidden = infoLabel.text?.isEmpty ?? true
+13 -4
View File
@@ -54,7 +54,12 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol {
func start() {
let rootCoordinator = self.createThreadListCoordinator()
let rootCoordinator: Coordinator & Presentable
if let threadId = parameters.threadId {
rootCoordinator = createThreadCoordinator(forThreadId: threadId)
} else {
rootCoordinator = createThreadListCoordinator()
}
rootCoordinator.start()
@@ -77,6 +82,10 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol {
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
}
@@ -116,13 +125,13 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol {
return coordinator
}
private func createThreadCoordinator(forThread thread: MXThread) -> RoomCoordinator {
private func createThreadCoordinator(forThreadId threadId: String) -> RoomCoordinator {
let parameters = RoomCoordinatorParameters(navigationRouter: navigationRouter,
navigationRouterStore: nil,
session: parameters.session,
roomId: parameters.roomId,
eventId: nil,
threadId: thread.id,
threadId: threadId,
displayConfiguration: .forThreads)
let coordinator = RoomCoordinator(parameters: parameters)
coordinator.delegate = self
@@ -145,7 +154,7 @@ extension ThreadsCoordinator: ThreadListCoordinatorDelegate {
}
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) {
let roomCoordinator = createThreadCoordinator(forThread: thread)
let roomCoordinator = createThreadCoordinator(forThreadId: thread.id)
selectedThreadCoordinator = roomCoordinator
roomCoordinator.start()
self.add(childCoordinator: roomCoordinator)
@@ -45,6 +45,7 @@ final class ThreadsCoordinatorBridgePresenter: NSObject {
private let session: MXSession
private let roomId: String
private let threadId: String?
private var navigationType: NavigationType = .present
private var coordinator: ThreadsCoordinator?
@@ -53,11 +54,18 @@ final class ThreadsCoordinatorBridgePresenter: NSObject {
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) {
roomId: String,
threadId: String?) {
self.session = session
self.roomId = roomId
self.threadId = threadId
super.init()
}
@@ -71,7 +79,8 @@ final class ThreadsCoordinatorBridgePresenter: NSObject {
func present(from viewController: UIViewController, animated: Bool) {
let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session,
roomId: self.roomId)
roomId: self.roomId,
threadId: self.threadId)
let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters)
threadsCoordinator.delegate = self
@@ -89,6 +98,7 @@ final class ThreadsCoordinatorBridgePresenter: NSObject {
let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session,
roomId: self.roomId,
threadId: self.threadId,
navigationRouter: navigationRouter)
let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters)
@@ -26,15 +26,20 @@ struct ThreadsCoordinatorParameters {
/// 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())
}
}