sendbird-uikit-ios/Sources/View/Channel/MessageCell/NotificationChannel/SBUNotificationCell.swift

451 lines
17 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.

//
// SBUNotificationCell.swift
// SendbirdUIKit
//
// Created by Jaesung Lee on 2022/12/07.
// Copyright © 2022 Sendbird, Inc. All rights reserved.
//
import UIKit
import SendbirdChatSDK
protocol SBUNotificationCellDelegate: AnyObject {
func notificationCellShouldReload(_ cell: SBUNotificationCell)
}
@IBDesignable
class SBUNotificationCell: SBUBaseMessageCell {
// MARK: - UI Layouts
var baseStackView = SBUStackView(axis: .horizontal, alignment: .bottom, spacing: 4)
var contentStackView = SBUStackView(axis: .vertical, spacing: 4)
var captionStackView = SBUStackView(axis: .horizontal, alignment: .center, spacing: 4)
let feedNotificationMaxWidth: CGFloat = 380.0
let chatNotificationMaxWidth: CGFloat = 276.0
let feedNotificationDownloadingHeight: CGFloat = 294.0
let chatNotificationDownloadingHeight: CGFloat = 274.0
// MARK: - UI Views (Public)
var profileView: UIView = SBUMessageProfileView()
var categoryLabel = UILabel()
var newNotificationBadge: UIView?
var dateLabel = UILabel()
/// Specifies the theme object thats used as the theme of the message template view. The theme must inherit the ``SBUNotificationTheme.NotificationCell`` class.
var notificationCellTheme: SBUNotificationTheme.NotificationCell {
switch SBUTheme.colorScheme {
case .light: return .light
case .dark: return .dark
}
}
// MARK: - UI Views (Private)
private var notificationTemplateRenderer: MessageTemplateRenderer?
/// Shows `message.message` or ``SBUStringSet/Notification_Template_Error_Title``, ``SBUStringSet/Notification_Template_Error_Subtitle`` if the `message.message` is `nil`
private var parsingErrorNotificationRenderer: MessageTemplateRenderer {
if let notification = self.message?.message, notification.count > 0 {
return MessageTemplateRenderer(
body: .parsingError(text: notification),
fontFamily: SBUFontSet.FontFamily.notifications
)
} else {
return MessageTemplateRenderer(
body: .parsingError(
text: SBUStringSet.Notification_Template_Error_Title,
subText: SBUStringSet.Notification_Template_Error_Subtitle
),
fontFamily: SBUFontSet.FontFamily.notifications
)
}
}
private var categoryMargin = UIView()
private var profileMargin = UIView()
/// The green dot icon
private lazy var defaultNewNotificationBadge: UIView = {
let length = CGFloat(6)
let iconView = UIView()
.sbu_constraint(width: length, height: length)
iconView.layer.cornerRadius = length / 2
iconView.layer.masksToBounds = true
iconView.backgroundColor = self.notificationCellTheme.unreadIndicatorColor
return iconView
}()
var availableTemplateWidth: CGFloat = 0.0
// MARK: - Logic
var type: NotificationType = .none
// MARK: - Delegate properties
weak var delegate: SBUNotificationCellDelegate?
// MARK: - Actions
var notificationActionHandler: ((SBUMessageTemplate.Action) -> Void)?
// MARK: - Sendbird Life cycle
/// Configures a cell with ``SBUBaseMessageCellParams`` object.
override func configure(with configuration: SBUBaseMessageCellParams) {
super.configure(with: configuration)
self.dateLabel.text = Date
.sbu_from(configuration.message.createdAt)
.sbu_toString(dateFormat: SBUDateFormatSet.Message.sentTimeFormat)
self.categoryLabel.text = configuration.message.customType ?? ""
self.setupNotificationTemplate(with: self.message)
if type == .chat {
if let profileView = self.profileView as? SBUMessageProfileView {
var urlString = ""
if let profileURLString = configuration.profileImageURL {
urlString = profileURLString
}
profileView.configure(urlString: urlString)
}
}
self.setupLayouts()
self.layoutIfNeeded()
}
override func setupViews() {
self.dateView = SBUNotificationTimelineView() // timeline
self.dateView.isHidden = true
if self.newNotificationBadge == nil {
self.newNotificationBadge = self.defaultNewNotificationBadge
}
self.newNotificationBadge?.isHidden = true
self.contentView.addSubview(
self.stackView.setVStack([
self.dateView,
self.messageContentView
])
)
switch type {
case .none, .feed:
self.messageContentView.addSubview(
self.baseStackView.setHStack([
self.contentStackView.setVStack([
self.captionStackView.setHStack([
self.categoryLabel,
UIView(),
self.newNotificationBadge,
self.dateLabel
]),
// messageTemplateRenderer will be located here...
]),
])
)
case .chat:
self.messageContentView.addSubview(
self.baseStackView.setHStack([
self.profileView,
self.profileMargin,
self.contentStackView.setVStack([
self.captionStackView.setHStack([
self.categoryMargin,
self.categoryLabel,
]),
// messageTemplateRenderer will be located here...
]),
self.dateLabel
])
)
}
}
override func setupLayouts() {
self.stackView
.sbu_constraint(
equalTo: self.contentView,
left: 0,
right: 0,
top: 16,
bottom: 0
)
let windowBounds = UIApplication.shared.currentWindow?.bounds ?? .zero
let screenWidth = min(windowBounds.width, windowBounds.height)
var maxTemplateWidth: CGFloat = 0.0
var leftMargin = 0.0
var rightMargin = 0.0
let dateLabelWidth = self.dateLabel.textWidth()
switch type {
case .none, .feed:
maxTemplateWidth = feedNotificationMaxWidth
leftMargin = 16
rightMargin = 16
self.availableTemplateWidth = screenWidth - (leftMargin + rightMargin)
case .chat:
maxTemplateWidth = chatNotificationMaxWidth
leftMargin = 12
rightMargin = 12
self.availableTemplateWidth = screenWidth - (leftMargin + 26 + 12 + 4 + dateLabelWidth + rightMargin)
// leftMargin + (profile) + (caption) + (profile/caption margin) + dataLabel + rightMargin
}
self.baseStackView
.sbu_constraint(
equalTo: self.messageContentView,
leading: leftMargin,
top: 0,
bottom: 0,
priority: .required
)
self.baseStackView.sbu_constraint(
equalTo: self.messageContentView,
trailing: -rightMargin,
priority: .defaultHigh
)
self.contentStackView.sbu_constraint(
width: min(self.availableTemplateWidth, maxTemplateWidth),
priority: .required
)
if self.type == .chat {
self.dateLabel.sbu_constraint(width: dateLabelWidth, priority: .defaultLow)
self.dateLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal)
self.profileView.sbu_constraint(width: 26, height: 26)
self.profileMargin.sbu_constraint(width: 4)
self.categoryMargin.sbu_constraint(width: 4)
}
// TODO: need to check
self.captionStackView
.sbu_constraint_greaterThan(height: 12)
}
/// This function handles the initialization of actions.
/// - NOTE: It is called from intializer of ``SBUTableViewCell``
/// - NOTE: To customize the action of message template view, please overrides regarding delegate methods in ``SBUFeedNotificationChannelModuleListDelegate``
/// such as ``SBUFeedNotificationChannelModuleListDelegate/feedNotificationChannelModule(_:shouldHandleWebAction:message:forRowAt:)``,
/// ``SBUFeedNotificationChannelModuleListDelegate/feedNotificationChannelModule(_:shouldHandlePreDefinedAction:message:forRowAt:)``,
/// or ``SBUFeedNotificationChannelModuleListDelegate/feedNotificationChannelModule(_:shouldHandleCustomAction:message:forRowAt:)``
override func setupActions() {
super.setupActions()
}
override func setupStyles() {
self.backgroundColor = .clear
if let dateView = self.dateView as? SBUNotificationTimelineView {
dateView.setupStyles()
}
if self.type == .chat {
if let profileView = self.profileView as? SBUMessageProfileView {
profileView.setupStyles()
}
}
self.categoryLabel.font = self.notificationCellTheme.categoryTextFont
self.categoryLabel.textColor = self.notificationCellTheme.categoryTextColor
self.dateLabel.font = self.notificationCellTheme.sentAtTextFont // SBUFontSet.caption4
self.dateLabel.textColor = self.notificationCellTheme.sentAtTextColor
self.dateLabel.adjustsFontSizeToFitWidth = true
}
/// Creates the message template view and updates the views hierarchy.
/// If the `message.extendedMessage["sub_data"]` is invalid, the message template view shows `message.message` or ``SBUStringSet/Notification_Template_Error_Title``, ``SBUStringSet/Notification_Template_Error_Subtitle`` if the `message.message` is `nil`
/// - Parameters:
/// - notification: If it's `nil`, it uses message value in ``SBUNotificationCell``. The default value is `nil`.
func setupNotificationTemplate(with notification: BaseMessage? = nil) {
let notification = notification ?? self.message
let subType = Int(notification?.extendedMessage["sub_type"] as? String ?? "0")
guard subType == 0 else { return } // subType: 0 is template type
let subData = notification?.extendedMessage["sub_data"] as? String
var (bindedTemplate, isNewTemplateDownloading) = SBUNotificationChannelManager.generateTemplate(
with: subData
) { [weak self, weak notification] success in
guard success else { return }
self?.setupNotificationTemplate(with: notification)
self?.reloadCell()
}
bindedTemplate = bindedTemplate?.replacingOccurrences(of: "\\n", with: "\\\\n")
bindedTemplate = bindedTemplate?.replacingOccurrences(of: "\n", with: "\\n")
var template: MessageTemplateData?
do {
template = try JSONDecoder().decode(MessageTemplateData.self, from: Data((bindedTemplate ?? "").utf8))
} catch {
SBULog.error(error)
}
var showFallback = false
let version = template?.version ?? 0
if version != 1 { // Not used now
// bindedData = subData // v0.2
showFallback = true
}
self.notificationTemplateRenderer = nil
if isNewTemplateDownloading {
self.notificationTemplateRenderer = MessageTemplateRenderer(
body: .downloadingTemplate(
height: (type == .chat)
? chatNotificationDownloadingHeight
: feedNotificationDownloadingHeight
),
fontFamily: SBUFontSet.FontFamily.notifications
)
} else if let bindedTemplate = bindedTemplate, !showFallback {
self.notificationTemplateRenderer = MessageTemplateRenderer(
with: bindedTemplate,
delegate: self,
maxWidth: self.availableTemplateWidth,
fontFamily: SBUFontSet.FontFamily.notifications,
actionHandler: { [weak self] action in
self?.statisticsForAction(with: subData)
self?.notificationActionHandler?(action)
}
) ?? parsingErrorNotificationRenderer
} else {
self.notificationTemplateRenderer = parsingErrorNotificationRenderer
}
self.notificationTemplateRenderer?.delegate = self
guard let notificationTemplateRenderer = self.notificationTemplateRenderer else { return }
notificationTemplateRenderer.backgroundColor = self.notificationCellTheme.backgroundColor
notificationTemplateRenderer.roundCorners(corners: .allCorners, radius: self.notificationCellTheme.radius)
notificationTemplateRenderer.clipsToBounds = true
self.baseStackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
switch type {
case .none, .feed:
self.baseStackView.setHStack([
self.contentStackView.setVStack([
self.captionStackView.setHStack([
self.categoryLabel,
UIView(),
self.newNotificationBadge,
self.dateLabel
]),
notificationTemplateRenderer
]),
])
case .chat:
self.baseStackView.setHStack([
self.profileView,
self.profileMargin,
self.contentStackView.setVStack([
self.captionStackView.setHStack([
self.categoryMargin,
self.categoryLabel,
]),
notificationTemplateRenderer
]),
self.dateLabel
])
}
self.updateLayouts()
}
/// As a default, it follows the condition: `message.createdAt <= listComponent.lastSeenAt`
func updateReadStatus(_ read: Bool) {
self.newNotificationBadge?.isHidden = read
}
func reloadCell() {
if Thread.isMainThread {
self.delegate?.notificationCellShouldReload(self)
} else {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.notificationCellShouldReload(self)
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
if let profileView = self.profileView as? SBUMessageProfileView {
profileView.imageDownloadTask?.cancel()
profileView.urlString = ""
profileView.imageView.image = nil
}
}
// MARK: - Common
/// Adds stat for action of notification to SendbirdStatistics.
/// - Parameter subData: for exporting templateKey and tags.
/// - Since: 3.5.7
@discardableResult func statisticsForAction(with subData: String?) -> Bool {
guard let subData = subData else { return false }
// data scheme
var templateKey: String?
var tags: [String] = []
do {
if let subDataDic = try JSONSerialization.jsonObject(
with: Data(subData.utf8),
options: []
) as? [String: Any] {
templateKey = subDataDic["template_key"] as? String
tags = subDataDic["tags"] as? [String] ?? []
}
} catch {
SBULog.error(error.localizedDescription)
return false
}
guard let templateKey = templateKey,
let message = self.message else { return false }
let type = "noti:stats"
let action = "clicked"
let source = "notification"
let isSucceed = __SendbirdStatistics.__appendStat(
type: type,
data: [
"action": action,
"template_key": templateKey,
"channel_url": message.channelURL,
"tags": tags,
"message_id": message.messageId,
"source": source,
"message_ts": message.createdAt
]
)
SBULog.info("[\(isSucceed ? "Succeed" : "Failed")] SendbirdStatistics - \(type), \(action)")
return isSucceed
}
}
// MARK: - MessageTemplateRendererDelegate
extension SBUNotificationCell: MessageTemplateRendererDelegate {
func messageTemplateRender(_ renderer: MessageTemplateRenderer, didFinishLoadingImage imageView: UIImageView) {
self.reloadCell()
}
func messageTemplateNeedReloadCell() {
self.reloadCell()
}
}