1378 lines
56 KiB
Swift
1378 lines
56 KiB
Swift
//
|
|
// SBUBaseChannelViewController.swift
|
|
// SendbirdUIKit
|
|
//
|
|
// Created by Tez Park on 2020/11/17.
|
|
// Copyright © 2020 Sendbird, Inc. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import SendbirdChatSDK
|
|
import MobileCoreServices
|
|
import AVKit
|
|
import SafariServices
|
|
import PhotosUI
|
|
|
|
@objcMembers
|
|
open class SBUBaseChannelViewController: SBUBaseViewController, SBUBaseChannelViewModelDelegate, SBUBaseChannelModuleHeaderDelegate, SBUBaseChannelModuleListDelegate, SBUBaseChannelModuleListDataSource, SBUBaseChannelModuleInputDelegate, SBUBaseChannelModuleInputDataSource, SBUBaseChannelViewModelDataSource, UIGestureRecognizerDelegate, UIViewControllerTransitioningDelegate, PHPickerViewControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, SBUSelectablePhotoViewDelegate, SBUFileViewControllerDelegate, SBUCommonViewModelDelegate, SBUAlertViewDelegate {
|
|
|
|
// MARK: - UI Properties (Public)
|
|
public var baseHeaderComponent: SBUBaseChannelModule.Header?
|
|
public var baseListComponent: SBUBaseChannelModule.List?
|
|
public var baseInputComponent: SBUBaseChannelModule.Input?
|
|
|
|
/// To decide whether to use right bar button item or not
|
|
public var useRightBarButtonItem: Bool = true {
|
|
didSet { self.navigationItem.rightBarButtonItem = useRightBarButtonItem ? self.baseHeaderComponent?.rightBarButton : nil }
|
|
}
|
|
|
|
// Theme
|
|
@SBUThemeWrapper(theme: SBUTheme.channelTheme)
|
|
public var theme: SBUChannelTheme
|
|
|
|
// MARK: - Logic Properties (Public)
|
|
public var baseViewModel: SBUBaseChannelViewModel?
|
|
|
|
public var channelName: String?
|
|
|
|
/// The state property to indicate whether it's showing the keyboard or not. It's `false` when ``keyboardWillHide(_:)`` is called and `true` when ``keyboardWillShow(_:)`` is called. The default value is `false`
|
|
/// - Since: 3.2.3
|
|
public var isKeyboardShowing: Bool = false
|
|
/// The `NSLayoutConstraint` value used in ``baseInputComponent``'s bottom anchor constraint. The value is updated when either ``keyboardWillHide(_:)`` or ``keyboardWillShow(_:)`` is called.
|
|
/// - Since: 3.2.3
|
|
public var messageInputViewBottomConstraint: NSLayoutConstraint!
|
|
|
|
// MARK: - Logic Properties (Private)
|
|
var initialMessageInputBottomConstraint: CGFloat = 0
|
|
var initialMessageInputOrigin: CGPoint = .zero
|
|
|
|
var tableViewTopConstraint: NSLayoutConstraint!
|
|
var lastSeenIndexPath: IndexPath?
|
|
|
|
var scrollToInitialPositionHandler: (() -> Void)?
|
|
|
|
var isTransformedList: Bool = true
|
|
|
|
// MARK: - Lifecycle
|
|
@available(*, unavailable)
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("Cannot use `init(coder:)`")
|
|
}
|
|
|
|
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
|
}
|
|
|
|
/// If you have channel object, use this initializer. If you have own message list params, please set it. If not set, it is used as the default value.
|
|
///
|
|
/// See the example below for params generation.
|
|
/// ```
|
|
/// let params = MessageListParams()
|
|
/// params.includeMetaArray = true
|
|
/// params.includeReactions = true
|
|
/// params.includeThreadInfo = true
|
|
/// ...
|
|
/// ```
|
|
/// - note: The `reverse` and the `previousResultSize` properties in the `MessageListParams` are set in the UIKit. Even though you set that property it will be ignored.
|
|
/// - Parameters:
|
|
/// - baseChannel: Channel object
|
|
/// - messageListParams: `MessageListParams` object to be used when loading messages.
|
|
/// - displaysLocalCachedListFirst: (GroupChannel only) If this option is `true`, when a list is received through the local cache during initialization, it is displayed first.
|
|
/// - Since: 1.0.11
|
|
public init(baseChannel: BaseChannel, messageListParams: MessageListParams? = nil, displaysLocalCachedListFirst: Bool = false) {
|
|
super.init(nibName: nil, bundle: nil)
|
|
SBULog.info(#function)
|
|
|
|
if displaysLocalCachedListFirst {
|
|
self.createViewModel(
|
|
channel: baseChannel,
|
|
messageListParams: messageListParams,
|
|
displaysLocalCachedListFirst: displaysLocalCachedListFirst
|
|
)
|
|
} else {
|
|
self.createViewModel(
|
|
channel: baseChannel,
|
|
messageListParams: messageListParams
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Initiates view controller to enter a channel with its URL, message list params and a specific timestamp representing a starting potint.
|
|
///
|
|
/// See the example below for params generation.
|
|
/// ```
|
|
/// let params = MessageListParams()
|
|
/// params.includeMetaArray = true
|
|
/// params.includeReactions = true
|
|
/// params.includeThreadInfo = true
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// - note: The `reverse` and the `previousResultSize` properties in the `MessageListParams` are set in the UIKit. Even though you set that property it will be ignored.
|
|
///
|
|
/// - Parameters:
|
|
/// - channelURL: Channel's URL
|
|
/// - startingPoint: A starting point timestamp to start the message list from.
|
|
/// - messageListParams: `MessageListParams` object to be used when loading messages.
|
|
/// - Since: 2.1.0
|
|
required public init(
|
|
channelURL: String,
|
|
startingPoint: Int64? = nil,
|
|
messageListParams: MessageListParams? = nil
|
|
) {
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
SBULog.info(#function)
|
|
|
|
self.createViewModel(
|
|
channelURL: channelURL,
|
|
messageListParams: messageListParams,
|
|
startingPoint: startingPoint
|
|
)
|
|
}
|
|
|
|
/// Initiates view controller to enter a channel with its URL, message list params and a specific timestamp representing a starting potint.
|
|
///
|
|
/// See the example below for params generation.
|
|
/// ```
|
|
/// let params = MessageListParams()
|
|
/// params.includeMetaArray = true
|
|
/// params.includeReactions = true
|
|
/// params.includeThreadInfo = true
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// - note: The `reverse` and the `previousResultSize` properties in the `MessageListParams` are set in the UIKit. Even though you set that property it will be ignored.
|
|
///
|
|
/// - Parameters:
|
|
/// - channelURL: Channel's URL
|
|
/// - startingPoint: A starting point timestamp to start the message list from.
|
|
/// - messageListParams: `MessageListParams` object to be used when loading messages.
|
|
/// - displaysLocalCachedListFirst: (GroupChannel only) If this option is `true`, when a list is received through the local cache during initialization, it is displayed first.
|
|
/// - Since: 3.3.5
|
|
public init(
|
|
channelURL: String,
|
|
startingPoint: Int64? = nil,
|
|
messageListParams: MessageListParams? = nil,
|
|
displaysLocalCachedListFirst: Bool
|
|
) {
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
SBULog.info(#function)
|
|
|
|
if displaysLocalCachedListFirst {
|
|
self.createViewModel(
|
|
channelURL: channelURL,
|
|
messageListParams: messageListParams,
|
|
startingPoint: startingPoint,
|
|
displaysLocalCachedListFirst: displaysLocalCachedListFirst
|
|
)
|
|
} else {
|
|
self.createViewModel(
|
|
channelURL: channelURL,
|
|
messageListParams: messageListParams,
|
|
startingPoint: startingPoint
|
|
)
|
|
}
|
|
}
|
|
|
|
open override func loadView() {
|
|
super.loadView()
|
|
|
|
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
|
|
|
|
if #available(iOS 13.0, *) {
|
|
self.navigationController?.isModalInPresentation = true
|
|
}
|
|
}
|
|
|
|
open override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
self.registerKeyboardNotifications()
|
|
|
|
self.updateChannelStatus()
|
|
self.updateStyles()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActivity), name: UIApplication.willResignActiveNotification, object: nil)
|
|
}
|
|
|
|
open override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.addGestureHideKeyboard()
|
|
}
|
|
|
|
open override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
if let userProfileView = self.baseListComponent?.userProfileView as? SBUUserProfileView {
|
|
userProfileView.dismiss()
|
|
}
|
|
|
|
self.view.endEditing(true)
|
|
|
|
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
/// Called when the application will resign activity.
|
|
open func applicationWillResignActivity() { }
|
|
|
|
/// Called when the subview will present.
|
|
open func willPresentSubview() {}
|
|
|
|
open override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
self.theme.statusBarStyle
|
|
}
|
|
|
|
deinit {
|
|
self.baseViewModel = nil
|
|
self.baseHeaderComponent = nil
|
|
self.baseListComponent = nil
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - ViewModel
|
|
/// Creates the view model, loading initial messages from given starting point.
|
|
/// - Note: If you want to customize the viewModel, override this function
|
|
/// - Parameters:
|
|
/// - baseChannel: Channel object
|
|
/// - channelURL: Channel's URL
|
|
/// - messageListParams: `MessageListParams` object to be used when loading messages.
|
|
/// - startingPoint: The starting point timestamp of the messages. `nil` to start from the latest.
|
|
/// - showIndicator: Whether to show loading indicator on loading the initial messages.
|
|
/// - Since: 3.0.0
|
|
open func createViewModel(
|
|
channel: BaseChannel? = nil,
|
|
channelURL: String? = nil,
|
|
messageListParams: MessageListParams? = nil,
|
|
startingPoint: Int64? = nil,
|
|
showIndicator: Bool = true
|
|
) { }
|
|
|
|
/// Creates the view model, loading initial messages from given starting point.
|
|
/// - Note: If you want to customize the viewModel, override this function
|
|
/// - Parameters:
|
|
/// - baseChannel: Channel object
|
|
/// - channelURL: Channel's URL
|
|
/// - messageListParams: `MessageListParams` object to be used when loading messages.
|
|
/// - startingPoint: The starting point timestamp of the messages. `nil` to start from the latest.
|
|
/// - showIndicator: Whether to show loading indicator on loading the initial messages.
|
|
/// - displaysLocalCachedListFirst: (GroupChannel only) If this option is `true`, when a list is received through the local cache during initialization, it is displayed first.
|
|
/// - Since: 3.3.5
|
|
open func createViewModel(
|
|
channel: BaseChannel? = nil,
|
|
channelURL: String? = nil,
|
|
messageListParams: MessageListParams? = nil,
|
|
startingPoint: Int64? = nil,
|
|
showIndicator: Bool = true,
|
|
displaysLocalCachedListFirst: Bool
|
|
) { }
|
|
|
|
// MARK: - Sendbird UIKit Life cycle
|
|
|
|
// Please call me after the configuration of the component!
|
|
/// Sets up views including module components.
|
|
///
|
|
/// - Important: When you override this method, please call `super.setupViews` after `configure` method of the component. e.g.
|
|
/// ```swift
|
|
/// override func setupViews() {
|
|
/// self.baseHeaderComponent = SBUModuleSet.groupChannelModule.headerComponent()
|
|
/// self.baseHeaderComponent?.configure(delegate: self, theme: self.theme)
|
|
///
|
|
/// super.setupViews()
|
|
/// }
|
|
/// ```
|
|
open override func setupViews() {
|
|
// Header
|
|
self.navigationItem.titleView = self.baseHeaderComponent?.titleView
|
|
self.navigationItem.leftBarButtonItem = self.baseHeaderComponent?.leftBarButton
|
|
self.navigationItem.rightBarButtonItem = useRightBarButtonItem ? self.baseHeaderComponent?.rightBarButton : nil
|
|
|
|
// List
|
|
if let baseListComponent = baseListComponent {
|
|
self.view.addSubview(baseListComponent)
|
|
}
|
|
|
|
// Input
|
|
if let baseInputComponent = baseInputComponent {
|
|
self.view.addSubview(baseInputComponent)
|
|
}
|
|
|
|
self.scrollToInitialPositionHandler = { [weak self] in
|
|
if Thread.isMainThread {
|
|
self?.baseListComponent?.tableView.layoutIfNeeded()
|
|
self?.baseListComponent?.scrollToInitialPosition()
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
self?.baseListComponent?.tableView.layoutIfNeeded()
|
|
self?.baseListComponent?.scrollToInitialPosition()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
open override func setupStyles() {
|
|
self.setupStyles(theme: self.theme)
|
|
}
|
|
|
|
func setupStyles(theme: SBUChannelTheme) {
|
|
self.setupNavigationBar(
|
|
backgroundColor: self.theme.navigationBarTintColor,
|
|
shadowColor: self.theme.navigationBarShadowColor
|
|
)
|
|
|
|
self.view.backgroundColor = theme.backgroundColor
|
|
}
|
|
|
|
/// Setups scrollBottom view style.
|
|
/// - Parameters:
|
|
/// - scrollBottomView: Scroll bottom view
|
|
/// - theme: theme for setup
|
|
public func setupScrollBottomViewStyle(
|
|
scrollBottomView: UIView,
|
|
theme: SBUComponentTheme = SBUTheme.componentTheme
|
|
) {
|
|
if let baseListComponent = baseListComponent {
|
|
baseListComponent.setupScrollBottomViewStyle(
|
|
scrollBottomView: scrollBottomView,
|
|
theme: theme
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Channel
|
|
|
|
/// Updates channelTitle with channel and channelName
|
|
/// - Since: 3.0.0
|
|
public func updateChannelTitle() {
|
|
if let titleView = self.baseHeaderComponent?.titleView as? SBUChannelTitleView {
|
|
titleView.configure(
|
|
channel: self.baseViewModel?.channel,
|
|
title: self.channelName
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: *
|
|
/// Updates channelStatus with channel
|
|
/// - Since: 3.0.0
|
|
public func updateChannelStatus() {
|
|
guard let channel = self.baseViewModel?.channel else { return }
|
|
if let titleView = self.baseHeaderComponent?.titleView as? SBUChannelTitleView {
|
|
titleView.updateChannelStatus(channel: channel)
|
|
}
|
|
}
|
|
|
|
// MARK: - Message: Menu
|
|
|
|
/// Shows channel settings view controller such as `SBUGroupChannelSettingsViewController`
|
|
open func showChannelSettings() { }
|
|
|
|
/// This function shows message thread view controller.
|
|
/// - Parameters:
|
|
/// - channelURL: Channel url string
|
|
/// - parentMessageId: Parent message's Id
|
|
/// - parentMessageCreatedAt: Parent message's created time
|
|
/// - startingPoint: If not set this value, message thread list will be exposed from the first message. If you want to expose the most recent messages first, use the `.max` value.
|
|
/// - Since: 3.3.0
|
|
open func showMessageThread(channelURL: String, parentMessageId: Int64, parentMessageCreatedAt: Int64? = 0, startingPoint: Int64? = 0) { }
|
|
|
|
/// This function presents `SBUEmojiListViewController`
|
|
/// - Parameter message: `BaseMessage` object
|
|
/// - Since: 1.1.0
|
|
open func showEmojiListModal(message: BaseMessage) {
|
|
let emojiListVC = SBUEmojiListViewController(message: message)
|
|
emojiListVC.modalPresentationStyle = .custom
|
|
emojiListVC.transitioningDelegate = self
|
|
|
|
emojiListVC.emojiTapHandler = { [weak self] emojiKey, setSelect in
|
|
guard let self = self else { return }
|
|
self.baseViewModel?.setReaction(
|
|
message: message,
|
|
emojiKey: emojiKey,
|
|
didSelect: setSelect
|
|
)
|
|
}
|
|
self.present(emojiListVC, animated: true)
|
|
}
|
|
|
|
// MARK: - TableView
|
|
|
|
// MARK: - New message info
|
|
|
|
/// This function updates new message info view's hidden status.
|
|
/// - Parameter hidden: hidden status
|
|
/// - Since: 3.0.0
|
|
public func updateNewMessageInfo(hidden: Bool) {
|
|
guard let newMessageInfoView = self.baseListComponent?.newMessageInfoView else { return }
|
|
guard hidden != newMessageInfoView.isHidden else { return }
|
|
guard let viewModel = self.baseViewModel else { return }
|
|
|
|
newMessageInfoView.isHidden = hidden && !viewModel.hasNext()
|
|
}
|
|
|
|
// MARK: - Common
|
|
|
|
/// This function opens a file according to the file type.
|
|
/// - Parameter fileMessage: fileMessage object
|
|
/// - Since: 3.0.0
|
|
open func openFile(fileMessage: FileMessage) {
|
|
self.willPresentSubview()
|
|
|
|
SBULoading.start()
|
|
let fileType = SBUUtils.getFileType(by: fileMessage)
|
|
switch fileType {
|
|
case .image:
|
|
let viewer = SBUCommonViewControllerSet.FileViewController.init(fileMessage: fileMessage, delegate: self)
|
|
let naviVC = UINavigationController(rootViewController: viewer)
|
|
self.present(naviVC, animated: true)
|
|
|
|
case .etc, .pdf, .video, .audio:
|
|
SBUCacheManager.File.loadFile(
|
|
urlString: fileMessage.url,
|
|
cacheKey: fileMessage.requestId,
|
|
fileName: fileMessage.name
|
|
) { fileURL, _ in
|
|
SBULoading.stop()
|
|
|
|
guard let fileURL = fileURL else {
|
|
SBUToastManager.showToast(parentVC: self, type: .fileOpenFailed)
|
|
return
|
|
}
|
|
|
|
switch fileType {
|
|
case .audio, .video:
|
|
if SBUGlobals.isAVPlayerAlwaysEnabled {
|
|
let vc = AVPlayerViewController()
|
|
vc.player = AVPlayer(url: fileURL)
|
|
self.present(vc, animated: true) { vc.player?.play() }
|
|
} else {
|
|
let dc = UIDocumentInteractionController(url: fileURL)
|
|
dc.name = fileMessage.name
|
|
dc.delegate = self
|
|
dc.presentPreview(animated: true)
|
|
}
|
|
default:
|
|
if let messageInputView = self.baseInputComponent?.messageInputView as? SBUMessageInputView {
|
|
if messageInputView.mode != .quoteReply {
|
|
self.setMessageInputViewMode(.none)
|
|
}
|
|
}
|
|
|
|
if fileURL.scheme == "file" {
|
|
let dc = UIDocumentInteractionController(url: fileURL)
|
|
dc.name = fileMessage.name
|
|
dc.delegate = self
|
|
dc.presentPreview(animated: true)
|
|
} else {
|
|
let safariVC = SFSafariViewController(url: fileURL)
|
|
self.present(safariVC, animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
case .voice:
|
|
SBULoading.stop()
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
/// This function increases the new message count.
|
|
@discardableResult
|
|
public func increaseNewMessageCount() -> Bool {
|
|
guard let tableView = self.baseListComponent?.tableView,
|
|
tableView.contentOffset != .zero,
|
|
self.baseViewModel?.isLoadingNext == false
|
|
else {
|
|
self.lastSeenIndexPath = nil
|
|
return false
|
|
}
|
|
|
|
let firstVisibleIndexPath = tableView.indexPathsForVisibleRows?.first
|
|
?? IndexPath(row: 0, section: 0)
|
|
self.lastSeenIndexPath = IndexPath(row: firstVisibleIndexPath.row + 1, section: 0)
|
|
return true
|
|
}
|
|
|
|
// MARK: - MessageInputView
|
|
|
|
open func setMessageInputViewMode(_ mode: SBUMessageInputMode, message: BaseMessage? = nil) {
|
|
self.baseInputComponent?.updateMessageInputMode(mode, message: message)
|
|
}
|
|
|
|
// MARK: - VoiceMessageInput
|
|
open func showVoiceMessageInput() {}
|
|
|
|
open func dismissVoiceMessageInput() {}
|
|
|
|
open func resetVoiceMessageInput(for resignActivity: Bool = false) {}
|
|
|
|
// MARK: - Error handling
|
|
func errorHandler(_ error: SBError) {
|
|
self.errorHandler(error.localizedDescription, error.code)
|
|
}
|
|
|
|
/// If an error occurs in viewController, a message is sent through here.
|
|
/// If necessary, override to handle errors.
|
|
/// - Parameters:
|
|
/// - message: error message
|
|
/// - code: error code
|
|
open override func errorHandler(_ message: String?, _ code: NSInteger? = nil) {
|
|
SBULog.error("Did receive error: \(message ?? "")")
|
|
}
|
|
|
|
// MARK: - SBUBaseChannelViewModelDelegate
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didChangeChannel channel: BaseChannel?,
|
|
withContext context: MessageContext
|
|
) { }
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didReceiveNewMessage message: BaseMessage,
|
|
forChannel channel: BaseChannel
|
|
) {
|
|
self.increaseNewMessageCount()
|
|
}
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldFinishEditModeForChannel channel: BaseChannel
|
|
) {
|
|
self.setMessageInputViewMode(.none)
|
|
}
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldDismissForChannel channel: BaseChannel?
|
|
) {
|
|
if let navigationController = self.navigationController,
|
|
navigationController.viewControllers.count > 1 {
|
|
navigationController.popToRootViewController(animated: true)
|
|
} else {
|
|
self.dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didChangeMessageList messages: [BaseMessage],
|
|
needsToReload: Bool,
|
|
initialLoad: Bool
|
|
) {
|
|
guard let baseListComponent = baseListComponent else { return }
|
|
let emptyViewType: EmptyViewType = (!initialLoad && viewModel.fullMessageList.isEmpty) ? .noMessages : .none
|
|
baseListComponent.updateEmptyView(type: emptyViewType)
|
|
|
|
defer {
|
|
if !initialLoad && !(self.baseViewModel?.isScrollToInitialPositionFinish ?? false) {
|
|
self.scrollToInitialPositionHandler?()
|
|
self.baseViewModel?.isScrollToInitialPositionFinish = true
|
|
}
|
|
}
|
|
|
|
guard needsToReload else { return }
|
|
|
|
baseListComponent.reloadTableView()
|
|
|
|
guard let lastSeenIndexPath = self.lastSeenIndexPath else {
|
|
let hidden = baseListComponent.isScrollNearByBottom
|
|
baseListComponent.setScrollBottomView(hidden: hidden)
|
|
|
|
if baseListComponent.isScrollNearByBottom,
|
|
let fullMessageList = self.baseViewModel?.fullMessageList,
|
|
!self.isTransformedList {
|
|
self.baseListComponent?.scrollTableView(to: fullMessageList.count - 1)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
baseListComponent.scrollTableView(to: lastSeenIndexPath.row)
|
|
let hidden = baseListComponent.isScrollNearByBottom
|
|
baseListComponent.setScrollBottomView(hidden: hidden)
|
|
}
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
deletedMessages messages: [BaseMessage]
|
|
) { }
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldUpdateScrollInMessageList messages: [BaseMessage],
|
|
forContext context: MessageContext?,
|
|
keepsScroll: Bool
|
|
) {
|
|
SBULog.info("Fetched : \(messages.count), keepScroll : \(keepsScroll)")
|
|
guard let baseListComponent = baseListComponent else { return }
|
|
|
|
guard !messages.isEmpty else {
|
|
SBULog.info("Fetched empty messages.")
|
|
return
|
|
}
|
|
|
|
if context?.source == .eventMessageSent {
|
|
if !keepsScroll {
|
|
self.baseChannelModuleDidTapScrollToButton(baseListComponent, animated: false)
|
|
}
|
|
} else if context?.source != .eventMessageReceived {
|
|
// follow keepScroll flag if context is not `eventMessageReceived`.
|
|
if keepsScroll, !baseChannelViewModel(viewModel, isScrollNearBottomInChannel: viewModel.channel) {
|
|
self.lastSeenIndexPath = baseListComponent.keepCurrentScroll(for: messages)
|
|
}
|
|
} else {
|
|
if !baseChannelViewModel(viewModel, isScrollNearBottomInChannel: viewModel.channel) {
|
|
self.lastSeenIndexPath = baseListComponent.keepCurrentScroll(for: messages)
|
|
}
|
|
}
|
|
}
|
|
|
|
open func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didUpdateReaction reaction: ReactionEvent,
|
|
forMessage message: BaseMessage
|
|
) {
|
|
|
|
}
|
|
|
|
// MARK: - SBUBaseChannelModuleHeaderDelegate
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didUpdateTitleView titleView: UIView?) {
|
|
var titleStackView = UIStackView()
|
|
|
|
if let titleView = titleView {
|
|
titleStackView = UIStackView(arrangedSubviews: [
|
|
titleView,
|
|
headerComponent.titleSpacer
|
|
])
|
|
titleStackView.axis = .horizontal
|
|
}
|
|
|
|
self.navigationItem.titleView = titleStackView
|
|
}
|
|
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didTapTitleView titleView: UIView?) {
|
|
}
|
|
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didUpdateLeftItem leftItem: UIBarButtonItem?) {
|
|
self.navigationItem.leftBarButtonItem = leftItem
|
|
}
|
|
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didUpdateRightItem rightItem: UIBarButtonItem?) {
|
|
if useRightBarButtonItem {
|
|
self.navigationItem.rightBarButtonItem = rightItem
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didTapLeftItem leftItem: UIBarButtonItem) {
|
|
|
|
}
|
|
|
|
open func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didTapRightItem rightItem: UIBarButtonItem) {
|
|
self.willPresentSubview()
|
|
}
|
|
|
|
// MARK: - SBUBaseChannelModuleListDelegate
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
guard let baseViewModel = self.baseViewModel else { return }
|
|
guard baseViewModel.fullMessageList.count > 0 else { return }
|
|
|
|
let sentMessageList = baseViewModel.messageList
|
|
let fullMessageList = baseViewModel.fullMessageList
|
|
let messageListParams = baseViewModel.messageListParams
|
|
|
|
if indexPath.row >= (fullMessageList.count - messageListParams.previousResultSize/2),
|
|
baseViewModel.hasPrevious() {
|
|
if let openChannelViewModel = baseViewModel as? SBUOpenChannelViewModel {
|
|
openChannelViewModel.loadPrevMessages(timestamp: sentMessageList.last?.createdAt)
|
|
} else if let messageThreadViewModel = baseViewModel as? SBUMessageThreadViewModel {
|
|
messageThreadViewModel.loadPrevMessages(timestamp: sentMessageList.last?.createdAt)
|
|
} else {
|
|
baseViewModel.loadPrevMessages()
|
|
}
|
|
} else if indexPath.row < 5,
|
|
baseViewModel.hasNext() {
|
|
baseViewModel.loadNextMessages()
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapMessage message: BaseMessage, forRowAt indexPath: IndexPath) {
|
|
self.dismissKeyboard()
|
|
|
|
switch message {
|
|
|
|
case let userMessage as UserMessage:
|
|
// User message type, only fail case
|
|
guard userMessage.sendingStatus == .failed,
|
|
userMessage.sender?.userId == SBUGlobals.currentUser?.userId else { return }
|
|
self.baseViewModel?.resendMessage(failedMessage: userMessage)
|
|
self.baseChannelModuleDidTapScrollToButton(listComponent, animated: true)
|
|
|
|
case let fileMessage as FileMessage:
|
|
// File message type
|
|
switch fileMessage.sendingStatus {
|
|
case .pending:
|
|
break
|
|
case .failed:
|
|
guard fileMessage.sender?.userId == SBUGlobals.currentUser?.userId else { return }
|
|
self.baseViewModel?.resendMessage(failedMessage: fileMessage)
|
|
self.baseChannelModuleDidTapScrollToButton(listComponent, animated: true)
|
|
case .succeeded:
|
|
self.openFile(fileMessage: fileMessage)
|
|
default:
|
|
break
|
|
}
|
|
|
|
case _ as AdminMessage:
|
|
// Admin message type
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didLongTapMessage message: BaseMessage, forRowAt indexPath: IndexPath) {
|
|
self.view.endEditing(true)
|
|
listComponent.showMessageMenu(on: message, forRowAt: indexPath)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapVoiceMessage fileMessage: FileMessage, cell: UITableViewCell, forRowAt indexPath: IndexPath) {}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapSaveMessage message: BaseMessage) {
|
|
guard let fileMessage = message as? FileMessage else { return }
|
|
SBUDownloadManager.save(fileMessage: fileMessage, parent: self)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapCopyMessage message: BaseMessage) {
|
|
guard let userMessage = message as? UserMessage else { return }
|
|
let pasteboard = UIPasteboard.general
|
|
pasteboard.string = userMessage.message
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapEditMessage message: BaseMessage) {
|
|
guard let channel = self.baseViewModel?.channel else { return }
|
|
guard let userMessage = message as? UserMessage else { return }
|
|
|
|
if channel.isFrozen == false ||
|
|
self.baseViewModel?.isOperator == true {
|
|
self.setMessageInputViewMode(.edit, message: userMessage)
|
|
} else {
|
|
SBULog.info("This channel is frozen")
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapDeleteMessage message: BaseMessage) {
|
|
guard message.threadInfo.replyCount == 0 else { return }
|
|
self.baseViewModel?.deleteMessage(message: message)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapReplyMessage message: BaseMessage) {
|
|
guard message.parentMessage == nil else { return }
|
|
|
|
switch SBUGlobals.reply.replyType {
|
|
case .quoteReply:
|
|
self.setMessageInputViewMode(.quoteReply, message: message)
|
|
case .thread:
|
|
if let channelURL = self.baseViewModel?.channelURL {
|
|
// If it is the parent message itself, use `messageId` rather than `parentMessageId`.
|
|
self.showMessageThread(
|
|
channelURL: channelURL,
|
|
parentMessageId: message.messageId,
|
|
parentMessageCreatedAt: message.createdAt,
|
|
startingPoint: .max
|
|
)
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didDismissMenuForCell cell: UITableViewCell) {
|
|
cell.isSelected = false
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapMoreEmojisOnMessage message: BaseMessage) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.showEmojiListModal(message: message)
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didReactToMessage message: BaseMessage, withEmoji key: String, selected: Bool) {
|
|
self.baseViewModel?.setReaction(
|
|
message: message,
|
|
emojiKey: key,
|
|
didSelect: selected
|
|
)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapRetryFailedMessage failedMessage: BaseMessage) {
|
|
self.baseViewModel?.resendMessage(failedMessage: failedMessage)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapDeleteFailedMessage failedMessage: BaseMessage) {
|
|
self.baseViewModel?.deleteResendableMessage(failedMessage, needReload: true)
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapUserProfile user: SBUUser) {
|
|
self.dismissKeyboard()
|
|
|
|
if let userProfileView = self.baseListComponent?.userProfileView as? SBUUserProfileView,
|
|
let baseView = self.navigationController?.view,
|
|
SBUGlobals.isUserProfileEnabled {
|
|
userProfileView.show(
|
|
baseView: baseView,
|
|
user: user
|
|
)
|
|
}
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didScroll scrollView: UIScrollView) {
|
|
|
|
}
|
|
|
|
open func baseChannelModuleDidTapScrollToButton(_ listComponent: SBUBaseChannelModule.List, animated: Bool) {
|
|
guard self.baseViewModel?.fullMessageList.isEmpty == false else { return }
|
|
self.lastSeenIndexPath = nil
|
|
|
|
DispatchQueue.main.async { [weak self, listComponent, animated] in
|
|
guard let self = self else { return }
|
|
|
|
if let messageInputView = self.baseInputComponent?.messageInputView as? SBUMessageInputView,
|
|
messageInputView.mode != .quoteReply {
|
|
self.setMessageInputViewMode(.none)
|
|
}
|
|
|
|
if self.baseViewModel?.hasNext() ?? false {
|
|
listComponent.tableView.setContentOffset(listComponent.tableView.contentOffset, animated: false)
|
|
self.baseViewModel?.reloadMessageList()
|
|
self.baseListComponent?.scrollTableView(to: 0)
|
|
} else {
|
|
var indexPath = IndexPath(row: 0, section: 0)
|
|
|
|
if !self.isTransformedList,
|
|
let fullMessageCount = self.baseViewModel?.fullMessageList.count {
|
|
indexPath = IndexPath(item: fullMessageCount - 1, section: 0)
|
|
|
|
self.baseViewModel?.reloadMessageList()
|
|
}
|
|
|
|
self.baseListComponent?.scrollTableView(to: indexPath.row, animated: animated)
|
|
self.updateNewMessageInfo(hidden: true)
|
|
self.baseListComponent?.setScrollBottomView(hidden: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
open func baseChannelModuleDidSelectRetry(_ listComponent: SBUBaseChannelModule.List) {
|
|
if let channelURL = self.baseViewModel?.channelURL {
|
|
self.baseViewModel?.loadChannel(channelURL: channelURL)
|
|
}
|
|
}
|
|
|
|
// MARK: SBUBaseChannelModuleListDataSource
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
channelForTableView tableView: UITableView
|
|
) -> BaseChannel? {
|
|
self.baseViewModel?.channel
|
|
}
|
|
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
sentMessagesInTableView tableView: UITableView
|
|
) -> [BaseMessage] {
|
|
self.baseViewModel?.messageList ?? []
|
|
}
|
|
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
fullMessagesInTableView tableView: UITableView
|
|
) -> [BaseMessage] {
|
|
self.baseViewModel?.fullMessageList ?? []
|
|
}
|
|
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
hasNextInTableView tableView: UITableView
|
|
) -> Bool {
|
|
self.baseViewModel?.hasNext() ?? false
|
|
}
|
|
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
lastSeenIndexPathIn tableView: UITableView
|
|
) -> IndexPath? {
|
|
return self.lastSeenIndexPath
|
|
}
|
|
|
|
open func baseChannelModule(
|
|
_ listComponent: SBUBaseChannelModule.List,
|
|
startingPointIn tableView: UITableView
|
|
) -> Int64? {
|
|
return self.baseViewModel?.getStartingPoint()
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, parentViewControllerDisplayMenuItems menuItems: [SBUMenuItem]) -> UIViewController? {
|
|
return self
|
|
}
|
|
|
|
open func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, pendingMessageManagerForCell cell: UITableViewCell) -> (SBUPendingMessageManager?, Bool?) {
|
|
return (self.baseViewModel?.pendingMessageManager, self.baseViewModel?.isThreadMessageMode)
|
|
}
|
|
|
|
// MARK: - SBUBaseChannelModuleInputDelegate
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didUpdateFrozenState isFrozen: Bool) { }
|
|
|
|
open func baseChannelModuleDidStartTyping(_ inputComponent: SBUBaseChannelModule.Input) {
|
|
guard self.baseViewModel?.channel is GroupChannel else { return }
|
|
}
|
|
|
|
open func baseChannelModuleDidEndTyping(_ inputComponent: SBUBaseChannelModule.Input) {
|
|
guard self.baseViewModel?.channel is GroupChannel else { return }
|
|
}
|
|
|
|
open func baseChannelModuleDidTapAdd(_ inputComponent: SBUBaseChannelModule.Input) {
|
|
self.willPresentSubview()
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didTapSend text: String,
|
|
parentMessage: BaseMessage?) {
|
|
self.baseViewModel?.sendUserMessage(text: text, parentMessage: parentMessage)
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didTapResource type: MediaResourceType) {
|
|
switch type {
|
|
case .document: self.showDocumentPicker()
|
|
case .library:
|
|
switch SBUPermissionManager.shared.currentPhotoAccessStatus {
|
|
case .all:
|
|
self.showPhotoLibraryPicker()
|
|
case .limited:
|
|
self.showLimitedPhotoLibraryPicker()
|
|
default:
|
|
self.showPermissionAlert()
|
|
}
|
|
case .camera:
|
|
switch SBUPermissionManager.shared.currentCameraAccessStatus {
|
|
case .authorized:
|
|
self.showCamera()
|
|
default:
|
|
self.showPermissionAlert(forType: .camera)
|
|
}
|
|
default:
|
|
self.showPhotoLibraryPicker()
|
|
}
|
|
}
|
|
|
|
/// Presents `UIDocumentPickerViewController`.
|
|
/// - Since: 2.2.3
|
|
open func showDocumentPicker() {
|
|
let documentPicker = UIDocumentPickerViewController(
|
|
documentTypes: ["public.content"],
|
|
in: UIDocumentPickerMode.import
|
|
)
|
|
documentPicker.delegate = self
|
|
documentPicker.modalPresentationStyle = .formSheet
|
|
self.present(documentPicker, animated: true, completion: nil)
|
|
}
|
|
|
|
open func showLimitedPhotoLibraryPicker() {
|
|
let selectablePhotoVC = SBUSelectablePhotoViewController()
|
|
selectablePhotoVC.delegate = self
|
|
let nav = UINavigationController(rootViewController: selectablePhotoVC)
|
|
self.present(nav, animated: true, completion: nil)
|
|
}
|
|
|
|
/// Presents `UIImagePickerController`. If `SBUGlobals.UsingPHPicker`is `true`, it presents `PHPickerViewController` in iOS 14 or later.
|
|
/// - NOTE: If you want to use customized `PHPickerConfiguration`, please override this method.
|
|
/// - Since: 2.2.3
|
|
open func showPhotoLibraryPicker() {
|
|
if #available(iOS 14, *), SBUGlobals.isPHPickerEnabled {
|
|
var configuration = PHPickerConfiguration()
|
|
configuration.filter = .any(of: [.images, .videos])
|
|
let picker = PHPickerViewController(configuration: configuration)
|
|
picker.delegate = self
|
|
self.present(picker, animated: true, completion: nil)
|
|
return
|
|
}
|
|
|
|
let sourceType: UIImagePickerController.SourceType = .photoLibrary
|
|
let mediaType: [String] = [
|
|
String(kUTTypeImage),
|
|
String(kUTTypeGIF),
|
|
String(kUTTypeMovie)
|
|
]
|
|
|
|
if UIImagePickerController.isSourceTypeAvailable(sourceType) {
|
|
let imagePickerController = UIImagePickerController()
|
|
imagePickerController.delegate = self
|
|
imagePickerController.sourceType = sourceType
|
|
imagePickerController.mediaTypes = mediaType
|
|
self.present(imagePickerController, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
/// Presents `UIImagePickerController` for using camera.
|
|
/// - Since: 2.2.3
|
|
open func showCamera() {
|
|
let sourceType: UIImagePickerController.SourceType = .camera
|
|
let mediaType: [String] = [
|
|
String(kUTTypeImage),
|
|
String(kUTTypeGIF),
|
|
String(kUTTypeMovie)
|
|
]
|
|
|
|
if UIImagePickerController.isSourceTypeAvailable(sourceType) {
|
|
let imagePickerController = UIImagePickerController()
|
|
imagePickerController.delegate = self
|
|
imagePickerController.sourceType = sourceType
|
|
imagePickerController.mediaTypes = mediaType
|
|
self.present(imagePickerController, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
// Shows permission request alert.
|
|
/// - Since: 3.0.0
|
|
open func showPermissionAlert(forType permissionType: SBUPermissionManager.PermissionType = .photoLibrary) {
|
|
self.willPresentSubview()
|
|
|
|
SBUPermissionManager.shared.showPermissionAlert(
|
|
forType: permissionType,
|
|
alertViewDelegate: self
|
|
)
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didTapEdit text: String) {
|
|
guard let message = self.baseViewModel?.inEditingMessage else { return }
|
|
|
|
self.baseViewModel?.updateUserMessage(message: message, text: text)
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didChangeText text: String) {
|
|
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
willChangeMode mode: SBUMessageInputMode, message: BaseMessage?) {
|
|
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
didChangeMode mode: SBUMessageInputMode, message: BaseMessage?) {
|
|
baseViewModel?.inEditingMessage = message as? UserMessage
|
|
}
|
|
|
|
open func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input,
|
|
channelForInputView messageInputView: UIView?) -> BaseChannel? {
|
|
baseViewModel?.channel
|
|
}
|
|
|
|
// MARK: - Input Keyboard events
|
|
/// This function calls ``updateLayoutsWithKeyboard(isHidden:notification:)`` that changes the ``baseInputComponent`` bottom constraint using keyboard height.
|
|
/// - Parameter notification: Notification object with keyboardFrame information
|
|
/// - Important: When override this method, please refer to ``updateLayoutsWithKeyboard(isHidden:notification:)`` to update ``baseInputComponent`` bottom constraint with keyboard height.
|
|
/// - Since: 1.2.5
|
|
open func keyboardWillShow(_ notification: Notification) {
|
|
self.updateLayoutsWithKeyboard(isHidden: false, notification: notification)
|
|
|
|
}
|
|
|
|
/// This function calls ``updateLayoutsWithKeyboard(isHidden:notification:)`` changes the ``baseInputComponent`` bottom constraint using keyboard height.
|
|
/// - Parameter notification: Notification object with keyboardFrame information
|
|
/// - Important: When override this method, please refer to ``updateLayoutsWithKeyboard(isHidden:notification:)`` to update ``baseInputComponent`` bottom constraint with keyboard height.
|
|
/// - Since: 1.2.5
|
|
open func keyboardWillHide(_ notification: Notification) {
|
|
self.updateLayoutsWithKeyboard(isHidden: true, notification: notification)
|
|
}
|
|
|
|
// MARK: - SBUBaseChannelViewModelDataSource
|
|
open func baseChannelViewModel(_ viewModel: SBUBaseChannelViewModel,
|
|
isScrollNearBottomInChannel channel: BaseChannel?) -> Bool {
|
|
return self.baseListComponent?.isScrollNearByBottom ?? true
|
|
}
|
|
|
|
// MARK: - UIGestureRecognizerDelegate
|
|
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
|
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
|
|
-> Bool {
|
|
return true
|
|
}
|
|
|
|
// MARK: - UIViewControllerTransitioningDelegate
|
|
open func presentationController(forPresented presented: UIViewController,
|
|
presenting: UIViewController?,
|
|
source: UIViewController) -> UIPresentationController? {
|
|
return SBUBottomSheetController(
|
|
presentedViewController: presented,
|
|
presenting: presenting
|
|
)
|
|
}
|
|
|
|
// MARK: - UIImagePickerControllerDelegate
|
|
open func imagePickerController(
|
|
_ picker: UIImagePickerController,
|
|
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
|
picker.dismiss(animated: true) { [weak self] in
|
|
guard let self = self else { return }
|
|
guard info[.mediaType] != nil else { return }
|
|
let mediaType = info[.mediaType] as! CFString
|
|
|
|
switch mediaType {
|
|
case kUTTypeImage:
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickImageFile(info: info)
|
|
return
|
|
}
|
|
case kUTTypeMovie:
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickVideoFile(info: info)
|
|
return
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
open func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
picker.dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
// MARK: - PHPickerViewControllerDelegate
|
|
|
|
/// Override this method to handle the `results` from `PHPickerViewController`.
|
|
/// As defaults, it doesn't support multi-selection and live photo.
|
|
/// - Important: To use this method, please assign self as delegate to `PHPickerViewController` object.
|
|
@available(iOS 14, *)
|
|
open func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
|
picker.dismiss(animated: true)
|
|
|
|
results.forEach {
|
|
let itemProvider = $0.itemProvider
|
|
|
|
/// !! Warining !!
|
|
/// Since the image identifier includes the gif identifier, the check of the gif type should take precedence over the image type comparison.
|
|
|
|
// GIF
|
|
if itemProvider.hasItemConformingToTypeIdentifier(UTType.gif.identifier) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickGIFFile(itemProvider: itemProvider)
|
|
return
|
|
}
|
|
}
|
|
|
|
// image
|
|
else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickImageFile(itemProvider: itemProvider)
|
|
return
|
|
}
|
|
}
|
|
|
|
// video
|
|
else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickVideoFile(itemProvider: itemProvider)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UIDocumentPickerDelegate
|
|
open func documentPicker(_ controller: UIDocumentPickerViewController,
|
|
didPickDocumentsAt urls: [URL]) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickDocumentFile(documentURLs: urls)
|
|
return
|
|
}
|
|
}
|
|
|
|
// MARK: - UIDocumentInteractionControllerDelegate
|
|
open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
|
|
return self// or use return self.navigationController for fetching app navigation bar colour
|
|
}
|
|
|
|
// MARK: - SBUSelectablePhotoViewDelegate
|
|
/// Called when the image is picked from `SBUSelectablePhotoViewController`.
|
|
/// - Parameter data: The image data.
|
|
/// - Parameter fileName: The file name.
|
|
open func didTapSendImageData(_ data: Data, fileName: String? = nil, mimeType: String? = nil) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickImageData(data, fileName: fileName, mimeType: mimeType)
|
|
return
|
|
}
|
|
}
|
|
|
|
/// Called when the video is picked from `SBUSelectablePhotoViewController`
|
|
/// - Parameter url: The URL of the video
|
|
open func didTapSendVideoURL(_ url: URL) {
|
|
if let inputComponent = self.baseInputComponent {
|
|
inputComponent.pickVideoURL(url)
|
|
return
|
|
}
|
|
}
|
|
|
|
// MARK: - SBUFileViewControllerDelegate
|
|
open func didSelectDeleteImage(message: FileMessage) {
|
|
SBULog.info("[Request] Delete message: \(message.description)")
|
|
|
|
self.baseViewModel?.deleteMessage(message: message)
|
|
}
|
|
|
|
// MARK: - SBUAlertViewDelegate
|
|
open func didDismissAlertView() { }
|
|
|
|
// MARK: - SBUCommonViewModelDelegate
|
|
open func shouldUpdateLoadingState(_ isLoading: Bool) {
|
|
self.showLoading(isLoading)
|
|
}
|
|
|
|
open func didReceiveError(_ error: SBError?, isBlocker: Bool) {
|
|
self.showLoading(false)
|
|
if self.baseViewModel?.fullMessageList.isEmpty == true, isBlocker {
|
|
self.baseListComponent?.updateEmptyView(type: .error)
|
|
self.baseListComponent?.tableView.reloadData()
|
|
}
|
|
|
|
self.errorHandler(error?.localizedDescription)
|
|
}
|
|
|
|
// MARK: - Deprecated, Unavailable
|
|
|
|
/// This function creates message menu items with cell and message.
|
|
/// - Parameters:
|
|
/// - cell: Sendbird's messageCell object
|
|
/// - message: `BaseMessage` object
|
|
/// - Returns: message menu type array
|
|
@available(*, unavailable, message: "Please use `SBUMenuItem` and `createMessageMenuItems(for:)` in `SBUBaseChannelModule.List` instead.") // 3.1.2
|
|
public func createMessageMenuTypes(_ cell: UITableViewCell,
|
|
message: BaseMessage) -> [MessageMenuItem]? {
|
|
return nil
|
|
}
|
|
|
|
// TODO: UItableViewCell -> SBUBaseMessageCell
|
|
/// This function shows cell's menu.
|
|
///
|
|
/// This is used when the reaction feature is activated.
|
|
/// This function configures the menu items using the `message` object.
|
|
///
|
|
/// - Parameters:
|
|
/// - cell: Message cell
|
|
/// - message: Message object
|
|
/// - Since: 3.0.0
|
|
@available(*, deprecated, message: "Please use `showMessageMenuSheet(for:cell:)` in `SBUBaseChannelModule.List` instead.") // 3.1.2
|
|
public func showMenuViewController(_ cell: UITableViewCell, message: BaseMessage) {
|
|
guard let listComponent = self.baseListComponent else { return }
|
|
listComponent.showMessageMenuSheet(for: message, cell: cell)
|
|
}
|
|
|
|
// TODO: UItableViewCell -> SBUBaseMessageCell
|
|
/// This function shows cell's menu. This is used when the reaction feature is activated.
|
|
/// - Parameters:
|
|
/// - cell: Message cell
|
|
/// - message: Message object
|
|
/// - types: Type array of menu items to use
|
|
/// - Since: 1.2.5
|
|
@available(*, unavailable, message: "Please use `showMessageMenuSheet(for:cell:)` and `createMessageMenuItems(for:)` in `SBUBaseChannelModule.List` instead.") // 3.1.2
|
|
public func showMenuViewController(
|
|
_ cell: UITableViewCell,
|
|
message: BaseMessage,
|
|
types: [MessageMenuItem]?
|
|
) {
|
|
guard let listComponent = self.baseListComponent else { return }
|
|
listComponent.showMessageMenuSheet(for: message, cell: cell)
|
|
}
|
|
|
|
// TODO: UItableViewCell -> SBUBaseMessageCell
|
|
/// This function shows cell's menu.
|
|
///
|
|
/// This is used when the reaction feature is inactivated.
|
|
/// This function configures the menu items using the `message` object.
|
|
/// - Parameters:
|
|
/// - cell: Message cell
|
|
/// - indexPath: IndexPath
|
|
/// - message: Message object
|
|
/// - Since: 3.0.0
|
|
@available(*, deprecated, message: "Please use `showMessageContextMenu(for:cell:forRowAt:)` in `SBUBaseChannelModule.List` instead.") // 3.1.2
|
|
open func showMenuModal(
|
|
_ cell: UITableViewCell,
|
|
indexPath: IndexPath,
|
|
message: BaseMessage
|
|
) {
|
|
self.baseListComponent?.showMessageContextMenu(for: message, cell: cell, forRowAt: indexPath)
|
|
}
|
|
|
|
// TODO: UItableViewCell -> SBUBaseMessageCell
|
|
/// This function shows cell's menu. This is used when the reaction feature is inactivated.
|
|
/// - Parameters:
|
|
/// - cell: Message cell
|
|
/// - indexPath: IndexPath
|
|
/// - message: Message object
|
|
/// - types: Type array of menu items to use
|
|
/// - Since: 1.2.5
|
|
@available(*, deprecated, message: "Please use `showMessageContextMenu(for:cell:forRowAt:)` in `SBUBaseChannelModule.List` instead.") // 3.1.2
|
|
public func showMenuModal(
|
|
_ cell: UITableViewCell,
|
|
indexPath: IndexPath,
|
|
message: BaseMessage,
|
|
types: [MessageMenuItem]?
|
|
) { self.baseListComponent?.showMessageContextMenu(for: message, cell: cell, forRowAt: indexPath) }
|
|
|
|
/// This function shows cell's menu: retry, delete, cancel.
|
|
///
|
|
/// This is used when selected failed message.
|
|
/// - Parameter message: Message object
|
|
/// - Since: 2.1.12
|
|
@available(*, deprecated, message: "Please use `showFailedMessageMenu(on:)` in `SBUBaseChannelModule.List`") // 3.1.2
|
|
public func showFailedMessageMenu(message: BaseMessage) {
|
|
let retryItem = SBUActionSheetItem(
|
|
title: SBUStringSet.Retry,
|
|
color: self.theme.menuItemTintColor
|
|
) { [weak self] in
|
|
self?.baseViewModel?.resendMessage(failedMessage: message)
|
|
if let baseListComponent = self?.baseListComponent {
|
|
self?.baseChannelModuleDidTapScrollToButton(baseListComponent, animated: true)
|
|
}
|
|
}
|
|
let deleteItem = SBUActionSheetItem(
|
|
title: SBUStringSet.Delete,
|
|
color: self.theme.deleteItemColor
|
|
) { [weak self] in
|
|
self?.baseViewModel?.deleteResendableMessage(message, needReload: true)
|
|
}
|
|
let cancelItem = SBUActionSheetItem(
|
|
title: SBUStringSet.Cancel,
|
|
color: self.theme.cancelItemColor,
|
|
completionHandler: nil
|
|
)
|
|
|
|
SBUActionSheet.show(
|
|
items: [retryItem, deleteItem],
|
|
cancelItem: cancelItem
|
|
)
|
|
}
|
|
|
|
/// This function shows delete message alert.
|
|
/// - Parameters:
|
|
/// - message: message object to delete
|
|
/// - oneTimetheme: One-time theme
|
|
/// - Since: 3.0.0
|
|
@available(*, deprecated, message: "Please use `showDeleteMessageAlert(on:oneTimeTheme:)` in `SBUBaseChannelModule.List`") // 3.1.2
|
|
public func showDeleteMessageMenu(message: BaseMessage,
|
|
oneTimetheme: SBUComponentTheme? = nil) {
|
|
self.baseListComponent?.showDeleteMessageAlert(on: message, oneTimeTheme: oneTimetheme)
|
|
}
|
|
}
|