sendbird-uikit-ios/Sources/View/Channel/SBUOpenChannelViewControlle...

859 lines
37 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SBUOpenChannelViewController.swift
// SendbirdUIKit
//
// Created by Tez Park on 03/02/2020.
// Copyright © 2020 Sendbird, Inc. All rights reserved.
//
import UIKit
import SendbirdChatSDK
import Photos
@objcMembers
open class SBUOpenChannelViewController: SBUBaseChannelViewController, SBUOpenChannelViewModelDelegate, SBUOpenChannelModuleHeaderDelegate, SBUOpenChannelModuleListDelegate, SBUOpenChannelModuleInputDelegate, SBUOpenChannelModuleMediaDelegate, SBUOpenChannelModuleListDataSource, SBUOpenChannelModuleInputDataSource, SBUOpenChannelViewModelDataSource {
// MARK: - UI properties (Public)
public var headerComponent: SBUOpenChannelModule.Header? {
get { self.baseHeaderComponent as? SBUOpenChannelModule.Header }
set { self.baseHeaderComponent = newValue }
}
public var listComponent: SBUOpenChannelModule.List? {
get { self.baseListComponent as? SBUOpenChannelModule.List }
set { self.baseListComponent = newValue }
}
public var inputComponent: SBUOpenChannelModule.Input? {
get { self.baseInputComponent as? SBUOpenChannelModule.Input }
set { self.baseInputComponent = newValue }
}
public var mediaComponent: SBUOpenChannelModule.Media?
// for channel info
@SBUThemeWrapper(theme: SBUTheme.overlayTheme.channelTheme, setToDefault: true)
public var overlayTheme: SBUChannelTheme
public var prevOrientation: UIDeviceOrientation = .unknown
public var currentOrientation: UIDeviceOrientation = .unknown
public var weakHeaderComponentBottomConstraint: NSLayoutConstraint = .init()
// MARK: - UI properties (Private)
// for constraint
private var mediaComponentConstraint: [NSLayoutConstraint] = []
private var headerComponentConstraint: NSLayoutConstraint!
private var headerComponentConstraints: [NSLayoutConstraint] = []
private var headerHeightConstraint: NSLayoutConstraint!
// for top content area in portrait mode
private var listTopMarginView = SBUMarginView()
private var listTopMarginConstraints: [NSLayoutConstraint] = []
// for right content area in landscape mode
private var listLeftMarginView = SBUMarginView()
private var listLeftMarginConstraints: [NSLayoutConstraint] = []
// constant
private let headerHeight: CGFloat = 56
private var currentWidth: CGFloat = 0
// MARK: - Logic properties (Public)
public var viewModel: SBUOpenChannelViewModel? {
get { self.baseViewModel as? SBUOpenChannelViewModel }
set { self.baseViewModel = newValue }
}
public override var channel: OpenChannel? { self.viewModel?.channel as? OpenChannel }
// Component
/// If it's `true`, the navigation bar will be hidden.
public var hideNavigationBar: Bool = false
/// If it's `true`, the channel info view will be hidden.
public var hideChannelInfoView: Bool = true
/// Sets text in `channelInfoView.descriptionLabel`
public var channelDescription: String?
// Media
/// A boolean value whether the media view is enabled or not. The default value is `false`.
/// - Note: Use `enableMediaView(_:)` to set value.
/// ```
/// self.enableMediaView(true)
/// self.print(isMediaViewEnabled) // true
/// ```
public private(set) var isMediaViewEnabled: Bool = false
/// A relative ratio value of `mediaView`to entire screen. The default value is `0`.
/// - Note: Use `updateMessageListRatio(to ratio:)` to set value.
public private(set) var mediaViewRatio: CGFloat = 0.0
/// A relative ratio value of messaging view to entire screen. The default value is `1`
/// - Note: Use `updateMessageListRatio(to ratio:)` to set value.
public private(set) var messageListRatio: CGFloat = 1.0
/// A boolean value whether `mediaView` is overlay or not. The default value is `false`.
/// - Note: Use `overlayMediaView(_:messageListRatio:)` to set value.
public private(set) var isMediaViewOverlaying: Bool = false
/// If the media view area extends outside the screens safe areas, it's `true`. The default value is `true`.
/// - Note: Use `mediaViewIgnoringSafeArea(_:)` to set value.
public private(set) var isMediaViewIgnoringSafeArea: Bool = true
// MARK: - Logic properties (Private)
// MARK: - Lifecycle
/// If you have channel object, use this initialize function. And, 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
/// ...
/// ```
/// - 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.
/// - Parameter channel: Channel object
required public init(channel: OpenChannel, messageListParams: MessageListParams? = nil) {
super.init(baseChannel: channel, messageListParams: messageListParams)
self.headerComponent = SBUModuleSet.openChannelModule.headerComponent
self.listComponent = SBUModuleSet.openChannelModule.listComponent
self.inputComponent = SBUModuleSet.openChannelModule.inputComponent
self.mediaComponent = SBUModuleSet.openChannelModule.mediaComponent
}
required public init(
channelURL: String,
startingPoint: Int64? = nil,
messageListParams: MessageListParams? = nil
) {
super.init(
channelURL: channelURL,
startingPoint: startingPoint,
messageListParams: messageListParams
)
self.headerComponent = SBUModuleSet.openChannelModule.headerComponent
self.listComponent = SBUModuleSet.openChannelModule.listComponent
self.inputComponent = SBUModuleSet.openChannelModule.inputComponent
self.mediaComponent = SBUModuleSet.openChannelModule.mediaComponent
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.registerOrientationChangeNotification()
}
open override func viewDidLoad() {
super.viewDidLoad()
}
open override func viewWillTransition(to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator) {
/// - NOTE: Not called when the orientation was changed to `.portraitUpsideDown`
self.currentWidth = size.width
self.updateLayouts()
self.updateStyles()
}
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.configureOffset()
}
deinit {
SBULog.info("")
}
// MARK: - ViewModel
open override func createViewModel(
channel: BaseChannel? = nil,
channelURL: String? = nil,
messageListParams: MessageListParams? = nil,
startingPoint: Int64? = nil,
showIndicator: Bool = true
) {
guard channel != nil || channelURL != nil else {
SBULog.error("Either the channel or the channelURL parameter must be set.")
return
}
self.baseViewModel = SBUOpenChannelViewModel(
channel: channel,
channelURL: channelURL,
messageListParams: messageListParams,
startingPoint: startingPoint,
delegate: self,
dataSource: self
)
}
// MARK: - Sendbird UIKit Life cycle
open override func setupViews() {
let theme = self.isMediaViewOverlaying ? self.overlayTheme : self.theme
// media view (OpenChannel)
self.navigationController?.isNavigationBarHidden = self.hideNavigationBar
if let mediaComponent = self.mediaComponent {
self.view.addSubview(mediaComponent)
if let listComponent = listComponent {
self.view.insertSubview(mediaComponent, belowSubview: listComponent)
}
}
self.mediaComponent?.isHidden = !self.isMediaViewEnabled
super.setupViews()
headerComponent?
.configure(delegate: self, theme: theme)
listComponent?
.configure(delegate: self, dataSource: self, theme: theme)
inputComponent?
.configure(delegate: self, dataSource: self, theme: theme)
mediaComponent?
.configure(delegate: self, theme: theme)
// This view is above `mediaView`
self.view.addSubview(self.listTopMarginView)
self.view.addSubview(self.listLeftMarginView)
// channel info view
if let headerComponent = headerComponent {
self.view.addSubview(headerComponent)
headerComponent.hidesChannelInfoView = self.hideChannelInfoView
headerComponent.overlaysChannelInfoView = self.isMediaViewOverlaying
}
if let inputComponent = inputComponent {
(inputComponent.messageInputView as? SBUMessageInputView)?.isOverlay = self.isMediaViewOverlaying
}
// Orientation
self.currentWidth = self.view.frame.width
self.prevOrientation = UIDevice.current.orientation
self.currentOrientation = UIDevice.current.orientation
}
open override func setupLayouts() {
super.setupLayouts()
// Media component layout
if let mediaComponent = self.mediaComponent {
mediaComponent.translatesAutoresizingMaskIntoConstraints = false
switch self.currentOrientation {
case .landscapeLeft, .landscapeRight:
self.mediaComponentConstraint = [
mediaComponent.leadingAnchor.constraint(
equalTo: self.isMediaViewIgnoringSafeArea
? self.view.leadingAnchor
: self.view.layoutMarginsGuide.leadingAnchor
),
mediaComponent.topAnchor.constraint(equalTo: self.view.topAnchor),
mediaComponent.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
self.isMediaViewEnabled
? mediaComponent.widthAnchor.constraint(
equalTo: self.view.widthAnchor,
multiplier: self.mediaViewRatio
)
: mediaComponent.widthAnchor.constraint(equalToConstant: 0)
]
default:
self.mediaComponentConstraint = [
mediaComponent.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
mediaComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
mediaComponent.topAnchor.constraint(
equalTo: self.isMediaViewIgnoringSafeArea
? self.view.topAnchor
: self.view.layoutMarginsGuide.topAnchor
),
self.isMediaViewEnabled
? mediaComponent.heightAnchor.constraint(
equalTo: self.view.heightAnchor,
multiplier: self.mediaViewRatio
)
: mediaComponent.heightAnchor.constraint(equalToConstant: 0)
]
}
self.mediaComponentConstraint.forEach { $0.isActive = true }
}
switch self.currentOrientation {
case .landscapeRight, .landscapeLeft:
self.listLeftMarginView.translatesAutoresizingMaskIntoConstraints = false
self.listLeftMarginConstraints = [
self.listLeftMarginView.leadingAnchor.constraint(
equalTo: self.view.leadingAnchor,
constant: self.currentWidth*(1-self.messageListRatio)
),
self.listLeftMarginView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.listLeftMarginView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.listLeftMarginView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
]
self.listLeftMarginConstraints.forEach { $0.isActive = true }
if let headerComponent = self.headerComponent {
headerComponent.translatesAutoresizingMaskIntoConstraints = false
self.headerComponentConstraints = [
headerComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
headerComponent.topAnchor.constraint(equalTo: self.view.topAnchor),
headerComponent.leadingAnchor.constraint(
equalTo: self.listLeftMarginView.leadingAnchor,
constant: 0
)
]
}
default:
// Top (for portrait)
self.listTopMarginView.translatesAutoresizingMaskIntoConstraints = false
self.listTopMarginConstraints = [
self.listTopMarginView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.listTopMarginView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.listTopMarginView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.listTopMarginView.heightAnchor.constraint(
equalTo: self.view.heightAnchor,
multiplier: (1-self.messageListRatio)
)
]
self.listTopMarginConstraints.forEach { $0.isActive = true }
if let headerComponent = self.headerComponent {
// Channel info
headerComponent.translatesAutoresizingMaskIntoConstraints = false
self.weakHeaderComponentBottomConstraint = headerComponent.topAnchor.constraint(
equalTo: self.isMediaViewOverlaying
? self.listTopMarginView.bottomAnchor
: self.mediaComponent?.bottomAnchor ?? self.listTopMarginView.bottomAnchor,
constant: 0
)
self.weakHeaderComponentBottomConstraint.priority = .defaultHigh - 50
self.headerComponentConstraints = [
headerComponent.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
headerComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.weakHeaderComponentBottomConstraint
]
}
}
self.headerComponentConstraints.forEach { $0.isActive = true }
if let headerComponent = headerComponent {
let infoViewHeight: CGFloat = self.hideChannelInfoView ? 0 : headerHeight
self.headerHeightConstraint = headerComponent.heightAnchor.constraint(
equalToConstant: infoViewHeight
)
self.headerHeightConstraint.isActive = true
}
if let listComponent = listComponent {
listComponent.translatesAutoresizingMaskIntoConstraints = false
self.tableViewTopConstraint = listComponent.topAnchor.constraint(
equalTo: self.headerComponent?.bottomAnchor ?? self.view.topAnchor,
constant: 0
)
NSLayoutConstraint.activate([
self.tableViewTopConstraint,
listComponent.leftAnchor.constraint(
equalTo: self.headerComponent?.leftAnchor ?? self.view.leftAnchor,
constant: 0
),
listComponent.rightAnchor.constraint(
equalTo: self.headerComponent?.rightAnchor ?? self.view.rightAnchor,
constant: 0
),
listComponent.bottomAnchor.constraint(
equalTo: self.inputComponent?.topAnchor ?? self.view.bottomAnchor,
constant: 0
)
])
}
if let inputComponent = inputComponent {
inputComponent
.sbu_constraint(equalTo: self.listComponent ?? self.view, left: 0, right: 0)
inputComponent.translatesAutoresizingMaskIntoConstraints = false
self.messageInputViewBottomConstraint = self.inputComponent?.bottomAnchor.constraint(
equalTo: self.view.bottomAnchor,
constant: 0
)
NSLayoutConstraint.activate([
inputComponent.topAnchor.constraint(
equalTo: self.listComponent?.bottomAnchor ?? self.view.bottomAnchor,
constant: 0
),
messageInputViewBottomConstraint
])
}
}
open override func updateLayouts() {
super.updateLayouts()
if let mediaComponent = mediaComponent {
mediaComponent.translatesAutoresizingMaskIntoConstraints = false
// deactive previous constraints
self.mediaComponentConstraint.forEach { $0.isActive = false }
switch self.currentOrientation {
case .landscapeLeft, .landscapeRight:
self.mediaComponentConstraint = [
mediaComponent.leadingAnchor.constraint(
equalTo: self.isMediaViewIgnoringSafeArea
? self.view.leadingAnchor
: self.view.layoutMarginsGuide.leadingAnchor
),
mediaComponent.topAnchor.constraint(equalTo: self.view.topAnchor),
mediaComponent.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
self.isMediaViewEnabled
? mediaComponent.widthAnchor.constraint(
equalTo: self.view.widthAnchor,
multiplier: self.mediaViewRatio
)
: mediaComponent.widthAnchor.constraint(equalToConstant: 0)
]
default:
self.mediaComponentConstraint = [
mediaComponent.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
mediaComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
mediaComponent.topAnchor.constraint(
equalTo: self.isMediaViewIgnoringSafeArea
? self.view.topAnchor
: self.view.layoutMarginsGuide.topAnchor
),
self.isMediaViewEnabled
? mediaComponent.heightAnchor.constraint(
equalTo: self.view.heightAnchor,
multiplier: self.mediaViewRatio
)
: mediaComponent.heightAnchor.constraint(equalToConstant: 0)
]
}
// active new constraints
self.mediaComponentConstraint.forEach { $0.isActive = true }
}
self.headerComponentConstraints.forEach { $0.isActive = false }
switch self.currentOrientation {
case .landscapeLeft, .landscapeRight:
// Left (for landscape)
self.listLeftMarginView.translatesAutoresizingMaskIntoConstraints = false
self.listLeftMarginConstraints.forEach { $0.isActive = false }
self.listLeftMarginConstraints = [
self.listLeftMarginView.leadingAnchor.constraint(
equalTo: self.view.leadingAnchor,
constant: self.currentWidth*(1-self.messageListRatio)
),
self.listLeftMarginView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.listLeftMarginView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.listLeftMarginView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
]
self.listLeftMarginConstraints.forEach { $0.isActive = true }
if let headerComponent = self.headerComponent {
headerComponent.translatesAutoresizingMaskIntoConstraints = false
self.headerComponentConstraints = [
headerComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
headerComponent.topAnchor.constraint(equalTo: self.view.topAnchor),
headerComponent.leadingAnchor.constraint(
equalTo: self.listLeftMarginView.leadingAnchor,
constant: 0
)
]
}
default:
// Top (for portrait)
self.listTopMarginView.translatesAutoresizingMaskIntoConstraints = false
self.listTopMarginConstraints.forEach { $0.isActive = false }
self.listTopMarginConstraints = [
self.listTopMarginView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.listTopMarginView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.listTopMarginView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.listTopMarginView.heightAnchor.constraint(
equalTo: self.view.heightAnchor,
multiplier: (1-self.messageListRatio)
)
]
self.listTopMarginConstraints.forEach { $0.isActive = true }
if let headerComponent = headerComponent {
// Channel info
headerComponent.translatesAutoresizingMaskIntoConstraints = false
self.headerComponentConstraints = [
headerComponent.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
headerComponent.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
headerComponent.topAnchor.constraint(
equalTo: self.isMediaViewOverlaying
? self.listTopMarginView.bottomAnchor
: self.mediaComponent?.bottomAnchor ?? self.view.topAnchor,
constant: 0
)
]
}
}
self.headerComponentConstraints.forEach { $0.isActive = true }
if let headerComponent = headerComponent {
let infoViewHeight: CGFloat = self.hideChannelInfoView ? 0 : headerHeight
self.headerHeightConstraint = headerComponent.heightAnchor.constraint(
equalToConstant: infoViewHeight
)
self.headerHeightConstraint.isActive = true
}
if let listComponent = listComponent {
let hidden = listComponent.isScrollNearByBottom
listComponent.setScrollBottomView(hidden: hidden)
}
}
open override func setupStyles() {
let theme = self.isMediaViewOverlaying ? self.overlayTheme : self.theme
self.setupStyles(theme: theme)
}
open override func updateStyles() {
self.setupStyles()
super.updateStyles()
self.headerComponent?.updateStyles(overlaid: self.isMediaViewOverlaying)
self.inputComponent?.updateStyles(overlaid: self.isMediaViewOverlaying)
// Invokes `updateStyles(overlaid:)` instead of `updateStyles(theme:componentTheme:)`
self.listComponent?
.updateStyles(
theme: self.isMediaViewOverlaying
? self.overlayTheme
: self.theme,
componentTheme: self.isMediaViewOverlaying
? SBUTheme.overlayTheme.componentTheme
: SBUTheme.componentTheme
)
self.listComponent?.reloadTableView()
}
// MARK: - Channel
/// This function updates channel info view. If `channelDescription` is set, this value is used for channel info view configuring.
public func updateChannelInfoView() {
if let headerComponent = headerComponent {
(headerComponent.channelInfoView as? SBUChannelInfoHeaderView)?
.configure(channel: channel, description: self.channelDescription ?? nil)
}
}
// MARK: - Message: Menu
/// This function calculates the point at which to draw the menu.
/// - Parameters:
/// - indexPath: IndexPath
/// - Returns: `CGPoint` value
@available(*, deprecated, message: "Please use `calculateMessageMenuCGPoint(indexPath:)` in `SBUOpenChannelModule.List`") // 3.1.2
public func calculatorMenuPoint(indexPath: IndexPath) -> CGPoint {
guard let listComponent = listComponent else {
SBULog.error("listComponent is not set up.")
return .zero
}
return listComponent.calculateMessageMenuCGPoint(indexPath: indexPath)
}
@available(*, deprecated, message: "Please use `showMessageContextMenu(for:cell:forRowAt:)` in `SBUOpenChannelModule.List`") // 3.1.2
public override func showMenuModal(_ cell: UITableViewCell,
indexPath: IndexPath,
message: BaseMessage,
types: [MessageMenuItem]?) {
self.listComponent?.showMessageContextMenu(for: message, cell: cell, forRowAt: indexPath)
}
@available(*, deprecated, message: "Please use `showDeleteMessageAlert(on:oneTimeTheme:)` in `SBUOpenChannelModule.List` instead.") // 3.1.2
public override func showDeleteMessageMenu(message: BaseMessage,
oneTimetheme: SBUComponentTheme? = nil) {
self.listComponent?.showDeleteMessageAlert(
on: message,
oneTimeTheme: isMediaViewOverlaying ? SBUComponentTheme.dark : nil
)
}
// MARK: - Media View
/// Enable the internal media view.
/// - Parameters:
/// - enabled: If it's `true` It uses the media view.
/// ```
/// self.enableMediaView(true)
/// self.updateMessageListRatio(to: 0.7)
/// ```
public func enableMediaView(_ enabled: Bool = true) {
self.isMediaViewEnabled = enabled
if !enabled {
updateMessageListRatio(to: 1)
}
}
/// Updates a relative ratio value of the message list with `ratio` to entire screen.
///
/// The mediaView will have it's ratio accordingly, meaning
/// - normal mode : mediaView's ratio = (1 - message list's ratio). Media view & message list is side by side in landscape mode, top to bottom in portrait mode.
/// - overlay mode : mediaView's ratio = 1 (fills the whole screen). Media view fills the whole screen & message list is above the media view with transparent background.
///
/// After this method, You might need to call `setupStyles` or `updateComponentStyle`.
///
/// - Parameters:
/// - ratio: A relative ratio value of message list to entire screen. If it's `nil` or it's not in range from 0 to 1 inclusive, the value won't be set.
///
/// - Important: The ratio must be in range of `0...1`.
/// ```
/// self.updateMessageListRatio(to: 0.7)
/// ```
public func updateMessageListRatio(to ratio: CGFloat) {
guard (0...1).contains(ratio) else {
SBULog.warning("The ratio must be in range of 0...1")
return
}
self.messageListRatio = ratio
self.mediaViewRatio = self.isMediaViewOverlaying ? 1 : (1 - ratio)
}
/// Overlays the media view.
///
/// - Parameters:
/// - overlaying: If it's `true`, `mediaViewRatio` will be set to `1.0`. If it's `false`, `mediaViewRatio` will be set to `1 - messageListRatio`.
/// - messageListRatio: A relative ratio value of message list to entire screen.
///
/// ```
/// // Enable overlay mode
/// self.overlayMediaView(true, messageListRatio: 0.4)
///
/// // Disable overlay mode
/// self.overlayMediaView(false, messageListRatio: 0.3) // mediaViewRatio is 0.7
/// ```
public func overlayMediaView(_ overlaying: Bool, messageListRatio: CGFloat) {
self.isMediaViewOverlaying = overlaying
self.updateMessageListRatio(to: messageListRatio)
}
/// Changes the media view area to extend outside the screens safe areas.
///
/// - Parameters:
/// - enabled: A boolean value whther the media view ignores safe area or not.
///
/// - Note:
/// - Ignores top edge when it's on portrait mode.
/// - Ignores leading edge when it's on landscape mode.
///
/// ```
/// self.mediaViewIgnoringSafeArea(true)
/// ```
public func mediaViewIgnoringSafeArea(_ enabled: Bool = true) {
self.isMediaViewIgnoringSafeArea = enabled
}
// MARK: - ScrollView
public func configureOffset() {
guard let tableView = self.listComponent?.tableView else { return }
guard tableView.contentOffset.y < 0,
self.tableViewTopConstraint.constant <= 0 else { return }
let tempOffset = tableView.contentOffset.y
self.tableViewTopConstraint.constant -= tempOffset
}
// MARK: - Navigation
public func updateBarButton() {
guard let userId = SBUGlobals.currentUser?.userId else { return }
let isOperator = self.channel?.isOperator(userId: userId) ?? false
self.headerComponent?.updateBarButton(isOperator: isOperator)
if let headerComponent = headerComponent {
self.navigationItem.rightBarButtonItem = headerComponent.rightBarButton
}
}
// MARK: - Actions
/// This function actions to pop or dismiss.
public override func onClickBack() {
guard let channel = channel else {
super.onClickBack()
return
}
channel.exit(completionHandler: { [weak self] (error) in
guard let self = self else { return }
if let error = error {
SBULog.error("[Failed] Exit channel request: \(error.localizedDescription)")
self.errorHandler(error.localizedDescription)
}
if let navigationController = self.navigationController,
navigationController.viewControllers.count > 1 {
navigationController.popViewController(animated: true)
} else {
self.dismiss(animated: true, completion: nil)
}
})
}
open override func showChannelSettings() {
guard let channel = self.channel else { return }
let channelSettingsVC = SBUViewControllerSet.OpenChannelSettingsViewController.init(channel: channel)
self.navigationController?.pushViewController(channelSettingsVC, animated: true)
}
/// If you want to use a custom participants list, override it and implement it.
open func showParticipantsList() {
guard let channel = self.channel else { return }
let participantListVC = SBUViewControllerSet.OpenUserListViewController.init(channel: channel, userListType: .participants)
self.navigationController?.pushViewController(participantListVC, animated: true)
}
// MARK: - Orientation
public func registerOrientationChangeNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationChanged),
name: UIDevice.orientationDidChangeNotification,
object: nil
)
}
@objc private func orientationChanged(_ notification: NSNotification) {
if UIDevice.current.orientation == .faceUp || UIDevice.current.orientation == .faceDown { return }
self.currentOrientation = UIDevice.current.orientation
if prevOrientation != currentOrientation {
/// - NOTE: Methods below are called in `viewWillTransition`. (`viewWillTransition` is called first except for `.portraitUpsideDown`)
self.updateLayouts()
self.updateStyles()
}
self.prevOrientation = currentOrientation
}
// MARK: - SBUOpenChannelModuleHeaderDelegate
open override func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didTapLeftItem leftItem: UIBarButtonItem) {
onClickBack()
}
open override func baseChannelModule(_ headerComponent: SBUBaseChannelModule.Header, didTapRightItem rightItem: UIBarButtonItem) {
guard let channel = self.channel else { return }
guard let userId = SBUGlobals.currentUser?.userId else { return }
let isOperator = channel.isOperator(userId: userId)
if isOperator {
// Channel settings
self.showChannelSettings()
} else {
// ParticipantList
self.showParticipantsList()
}
}
// MARK: - SBUOpenChannelModuleListDelegate
open override func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didTapUserProfile user: SBUUser) {
self.dismissKeyboard()
if let userProfileView = listComponent.userProfileView as? SBUUserProfileView,
let baseView = self.navigationController?.view,
SBUGlobals.isOpenChannelUserProfileEnabled {
userProfileView.show(baseView: baseView, user: user, isOpenChannel: true)
}
}
// MARK: - SBUOpenChannelModuleListDataSource
open func openChannelModuleIsOverlaid(_ listComponent: SBUOpenChannelModule.List) -> Bool {
self.isMediaViewOverlaying
}
// MARK: - SBUOpenChannelModuleInputDelegate
open override func baseChannelModule(_ inputComponent: SBUBaseChannelModule.Input, didUpdateFrozenState isFrozen: Bool) {
self.listComponent?.channelStateBanner?.isHidden = !isFrozen
}
open func openChannelModule(_ inputComponent: SBUOpenChannelModule.Input, didPickFileData fileData: Data?, fileName: String, mimeType: String) {
self.viewModel?.sendFileMessage(
fileData: fileData,
fileName: fileName,
mimeType: mimeType
)
}
open override func baseChannelModule(_ listComponent: SBUBaseChannelModule.List, didScroll scrollView: UIScrollView) {
super.baseChannelModule(listComponent, didScroll: scrollView)
self.lastSeenIndexPath = nil
if listComponent.isScrollNearByBottom {
self.updateNewMessageInfo(hidden: true)
}
}
// MARK: - SBUOpenChannelModuleMediaDelegate
open func openChannelModule(_ mediaComponent: SBUOpenChannelModule.Media, didTapMediaView mediaView: UIView) {
}
// MARK: - SBUOpenChannelViewModelDelegate
open override func baseChannelViewModel(
_ viewModel: SBUBaseChannelViewModel,
didChangeChannel channel: BaseChannel?,
withContext context: MessageContext
) {
guard channel != nil else {
if self.navigationController?.viewControllers.last == self {
// If leave is called in the ChannelSettingsViewController, this logic needs to be prevented.
self.onClickBack()
}
return
}
// channel changed
switch context.source {
case .channelChangelog:
self.updateChannelTitle()
self.updateChannelStatus()
self.updateChannelInfoView()
self.updateBarButton()
self.inputComponent?.updateMessageInputModeState()
self.listComponent?.reloadTableView()
case .eventChannelChanged:
self.updateChannelTitle()
self.updateChannelStatus()
self.updateChannelInfoView()
self.updateBarButton()
self.inputComponent?.updateMessageInputModeState()
case .eventChannelFrozen, .eventChannelUnfrozen,
.eventUserMuted, .eventUserUnmuted,
.eventOperatorUpdated,
.eventUserBanned: // Other User Banned
self.updateChannelTitle()
self.updateBarButton()
self.inputComponent?.updateMessageInputModeState()
case .eventChannelMemberCountChanged:
self.updateChannelTitle()
self.listComponent?.reloadTableView()
default: break
}
}
}