element-ios/Riot/Modules/Room/TimelineCells/BaseRoomCell/BaseRoomCell.swift

412 lines
14 KiB
Swift

/*
Copyright 2020-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import UIKit
@objc protocol BaseRoomCellProtocol: Themable {
var roomCellContentView: RoomCellContentView? { get }
}
/// `BaseRoomCell` allows a room cell that inherits from this class to embed and manage the default room message outer views and add an inner content view.
@objcMembers
class BaseRoomCell: MXKRoomBubbleTableViewCell, BaseRoomCellProtocol {
// MARK: - Constants
// MARK: - Properties
private var areViewsSetup: Bool = false
// MARK: Public
weak var roomCellContentView: RoomCellContentView?
private(set) var theme: Theme?
// Overrides
override var bubbleInfoContainer: UIView! {
get {
guard let infoContainer = self.roomCellContentView?.bubbleInfoContainer else {
fatalError("[BaseRoomCell] bubbleInfoContainer should not be used before set")
}
return infoContainer
}
set {
super.bubbleInfoContainer = newValue
}
}
override var bubbleOverlayContainer: UIView! {
get {
guard let overlayContainer = self.roomCellContentView?.bubbleOverlayContainer else {
fatalError("[BaseRoomCell] bubbleOverlayContainer should not be used before set")
}
return overlayContainer
}
set {
super.bubbleInfoContainer = newValue
}
}
override var bubbleInfoContainerTopConstraint: NSLayoutConstraint! {
get {
guard let infoContainerTopConstraint = self.roomCellContentView?.bubbleInfoContainerTopConstraint else {
fatalError("[BaseRoomCell] bubbleInfoContainerTopConstraint should not be used before set")
}
return infoContainerTopConstraint
}
set {
super.bubbleInfoContainerTopConstraint = newValue
}
}
override var pictureView: MXKImageView! {
get {
guard let roomCellContentView = self.roomCellContentView,
roomCellContentView.showSenderAvatar else {
return nil
}
guard let pictureView = self.roomCellContentView?.avatarImageView else {
fatalError("[BaseRoomCell] pictureView should not be used before set")
}
return pictureView
}
set {
super.pictureView = newValue
}
}
override var userNameLabel: UILabel! {
get {
guard let roomCellContentView = self.roomCellContentView, roomCellContentView.showSenderName else {
return nil
}
guard let userNameLabel = roomCellContentView.userNameLabel else {
fatalError("[BaseRoomCell] userNameLabel should not be used before set")
}
return userNameLabel
}
set {
super.userNameLabel = newValue
}
}
override var userNameTapGestureMaskView: UIView! {
get {
guard let roomCellContentView = self.roomCellContentView,
roomCellContentView.showSenderName else {
return nil
}
guard let userNameTapGestureMaskView = self.roomCellContentView?.userNameTouchMaskView else {
fatalError("[BaseRoomCell] userNameTapGestureMaskView should not be used before set")
}
return userNameTapGestureMaskView
}
set {
super.userNameTapGestureMaskView = newValue
}
}
override var readMarkerViewLeadingConstraint: NSLayoutConstraint? {
get {
if self is RoomCellReadMarkerDisplayable {
return self.roomCellContentView?.readMarkerViewLeadingConstraint
} else {
return super.readMarkerViewLeadingConstraint
}
}
set {
if self is RoomCellReadMarkerDisplayable {
self.roomCellContentView?.readMarkerViewLeadingConstraint = newValue
} else {
super.readMarkerViewLeadingConstraint = newValue
}
}
}
override var readMarkerViewTrailingConstraint: NSLayoutConstraint? {
get {
if self is RoomCellReadMarkerDisplayable {
return self.roomCellContentView?.readMarkerViewTrailingConstraint
} else {
return super.readMarkerViewTrailingConstraint
}
}
set {
if self is RoomCellReadMarkerDisplayable {
self.roomCellContentView?.readMarkerViewTrailingConstraint = newValue
} else {
super.readMarkerViewTrailingConstraint = newValue
}
}
}
// MARK: - Setup
required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit() {
self.selectionStyle = .none
self.setupContentView()
self.update(theme: ThemeService.shared().theme)
}
// MARK: - Public
func removeDecorationViews() {
if let roomCellReadReceiptsDisplayable = self as? RoomCellReadReceiptsDisplayable {
roomCellReadReceiptsDisplayable.removeReadReceiptsView()
}
if let roomCellReactionsDisplayable = self as? RoomCellReactionsDisplayable {
roomCellReactionsDisplayable.removeReactionsView()
}
if let roomCellThreadSummaryDisplayable = self as? RoomCellThreadSummaryDisplayable {
roomCellThreadSummaryDisplayable.removeThreadSummaryView()
}
if let timestampDisplayable = self as? TimestampDisplayable {
timestampDisplayable.removeTimestampView()
}
if let urlPreviewDisplayable = self as? RoomCellURLPreviewDisplayable {
urlPreviewDisplayable.removeURLPreviewView()
}
}
// MARK: - Overrides
override var isTextViewNeedsPositioningVerticalSpace: Bool {
return false
}
override func setupViews() {
super.setupViews()
let showEncryptionStatus = roomCellContentView?.showEncryptionStatus ?? false
if showEncryptionStatus {
self.setupEncryptionStatusViewTapGestureRecognizer()
}
}
override func setupSenderNameLabel() {
guard let userNameTouchMaskView = self.roomCellContentView?.userNameTouchMaskView else {
return
}
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onSenderNameTap(_:)))
tapGesture.numberOfTouchesRequired = 1
tapGesture.numberOfTapsRequired = 1
tapGesture.delegate = self
userNameTouchMaskView.addGestureRecognizer(tapGesture)
}
override func setupAvatarView() {
guard let avatarImageView = self.roomCellContentView?.avatarImageView else {
return
}
avatarImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder
// Listen to avatar tap
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onAvatarTap(_:)))
tapGesture.numberOfTouchesRequired = 1
tapGesture.numberOfTapsRequired = 1
tapGesture.delegate = self
avatarImageView.addGestureRecognizer(tapGesture)
avatarImageView.isUserInteractionEnabled = true
// Add a long gesture recognizer on avatar (in order to display for example the member details)
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressGesture(_:)))
avatarImageView.addGestureRecognizer(longPress)
}
override class func defaultReuseIdentifier() -> String! {
return String(describing: self)
}
override func prepareForReuse() {
super.prepareForReuse()
self.removeDecorationViews()
}
override func render(_ cellData: MXKCellData!) {
// In `MXKRoomBubbleTableViewCell` setupViews() is called in awakeFromNib() that is not called here, so call it only on first render() call
self.setupViewsIfNeeded()
super.render(cellData)
guard let roomCellContentView = self.roomCellContentView else {
return
}
if let bubbleData = self.bubbleData,
let paginationDate = bubbleData.date,
roomCellContentView.showPaginationTitle {
roomCellContentView.paginationLabel.text = bubbleData.eventFormatter.dateString(from: paginationDate, withTime: false)?.uppercased()
}
if roomCellContentView.showEncryptionStatus {
self.updateEncryptionStatusViewImage()
}
self.updateUserNameColor()
}
override func customizeRendering() {
super.customizeRendering()
self.updateUserNameColor()
}
// MARK: - Themable
func update(theme: Theme) {
self.theme = theme
self.roomCellContentView?.update(theme: theme)
}
// MARK: - Private
private func setupViewsIfNeeded() {
guard self.areViewsSetup == false else {
return
}
self.setupViews()
self.areViewsSetup = true
}
private func setupContentView() {
guard self.roomCellContentView == nil else {
return
}
let roomCellContentView = RoomCellContentView.instantiate()
self.contentView.vc_addSubViewMatchingParent(roomCellContentView)
self.roomCellContentView = roomCellContentView
}
// MARK: - RoomCellURLPreviewDisplayable
// Cannot use default implementation with ObjC protocol, if self conforms to RoomCellURLPreviewDisplayable method below will be used
func addURLPreviewView(_ urlPreviewView: UIView) {
self.roomCellContentView?.addURLPreviewView(urlPreviewView)
// tmpSubviews is used for touch detection in MXKRoomBubbleTableViewCell
self.addTemporarySubview(urlPreviewView)
}
func removeURLPreviewView() {
self.roomCellContentView?.removeURLPreviewView()
}
// MARK: - RoomCellReadReceiptsDisplayable
// Cannot use default implementation with ObjC protocol, if self conforms to RoomCellReadReceiptsDisplayable method below will be used
func addReadReceiptsView(_ readReceiptsView: UIView) {
self.roomCellContentView?.addReadReceiptsView(readReceiptsView)
// tmpSubviews is used for touch detection in MXKRoomBubbleTableViewCell
self.addTemporarySubview(readReceiptsView)
}
func removeReadReceiptsView() {
self.roomCellContentView?.removeReadReceiptsView()
}
// MARK: - RoomCellReactionsDisplayable
// Cannot use default implementation with ObjC protocol, if self conforms to RoomCellReactionsDisplayable method below will be used
func addReactionsView(_ reactionsView: UIView) {
self.roomCellContentView?.addReactionsView(reactionsView)
// tmpSubviews is used for touch detection in MXKRoomBubbleTableViewCell
self.addTemporarySubview(reactionsView)
}
func removeReactionsView() {
self.roomCellContentView?.removeReactionsView()
}
// MARK: - RoomCellThreadSummaryDisplayable
func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView) {
self.roomCellContentView?.addThreadSummaryView(threadSummaryView)
// tmpSubviews is used for touch detection in MXKRoomBubbleTableViewCell
self.addTemporarySubview(threadSummaryView)
}
func removeThreadSummaryView() {
self.roomCellContentView?.removeThreadSummaryView()
}
// MARK: - RoomCellReadMarkerDisplayable
func addReadMarkerView(_ readMarkerView: UIView) {
self.roomCellContentView?.addReadMarkerView(readMarkerView)
self.readMarkerView = readMarkerView
}
override func removeReadMarkerView() {
self.roomCellContentView?.removeReadMarkerView()
super.removeReadMarkerView()
}
// Encryption status
private func updateEncryptionStatusViewImage() {
guard let component = self.bubbleData.getFirstBubbleComponentWithDisplay() else {
return
}
self.roomCellContentView?.encryptionImageView.image = RoomEncryptedDataBubbleCell.encryptionIcon(for: component)
}
private func setupEncryptionStatusViewTapGestureRecognizer() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleEncryptionStatusContainerViewTap(_:)))
tapGestureRecognizer.delegate = self
self.roomCellContentView?.encryptionImageView.isUserInteractionEnabled = true
}
@objc private func handleEncryptionStatusContainerViewTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let delegate = self.delegate else {
return
}
guard let component = self.bubbleData.getFirstBubbleComponentWithDisplay() else {
return
}
let userInfo: [AnyHashable: Any]?
if let tappedEvent = component.event {
userInfo = [kMXKRoomBubbleCellEventKey: tappedEvent]
} else {
userInfo = nil
}
delegate.cell(self, didRecognizeAction: kRoomEncryptedDataBubbleCellTapOnEncryptionIcon, userInfo: userInfo)
}
}