981 lines
41 KiB
Swift
981 lines
41 KiB
Swift
//
|
|
// SBUBaseChannelViewModel.swift
|
|
// SendbirdUIKit
|
|
//
|
|
// Created by Tez Park on 2021/07/22.
|
|
// Copyright © 2021 Sendbird, Inc. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import AVKit
|
|
import SendbirdChatSDK
|
|
|
|
/// Methods to get data source for the `SBUBaseChannelViewModel`.
|
|
public protocol SBUBaseChannelViewModelDataSource: AnyObject {
|
|
/// Asks to data source whether the channel is scrolled to bottom.
|
|
/// - Parameters:
|
|
/// - viewModel: `SBUBaseChannelViewModel` object.
|
|
/// - channel: `BaseChannel` object.
|
|
/// - Returns:
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
isScrollNearBottomInChannel channel: BaseChannel?
|
|
) -> Bool
|
|
}
|
|
|
|
/// Methods for notifying the data updates from the `SBUBaseChannelViewModel`.
|
|
public protocol SBUBaseChannelViewModelDelegate: SBUCommonViewModelDelegate {
|
|
/// Called when the the channel has been changed.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didChangeChannel channel: BaseChannel?,
|
|
withContext context: MessageContext
|
|
)
|
|
|
|
/// Called when the channel has received a new message.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didReceiveNewMessage message: BaseMessage,
|
|
forChannel channel: BaseChannel
|
|
)
|
|
|
|
/// Called when the channel should finish editing mode
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldFinishEditModeForChannel channel: BaseChannel
|
|
)
|
|
|
|
/// Called when the channel should be dismissed.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldDismissForChannel channel: BaseChannel?
|
|
)
|
|
|
|
/// Called when the messages has been changed. If they're the first loaded messages, `initialLoad` is `true`.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didChangeMessageList messages: [BaseMessage],
|
|
needsToReload: Bool,
|
|
initialLoad: Bool
|
|
)
|
|
|
|
/// Called when the messages has been deleted.
|
|
/// - Since: 3.4.0
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
deletedMessages messages: [BaseMessage]
|
|
)
|
|
|
|
/// Called when it should be updated scroll status for messages.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
shouldUpdateScrollInMessageList messages: [BaseMessage],
|
|
forContext context: MessageContext?,
|
|
keepsScroll: Bool
|
|
)
|
|
|
|
/// Called when it has updated the reaction event for a message.
|
|
func baseChannelViewModel(
|
|
_ viewModel: SBUBaseChannelViewModel,
|
|
didUpdateReaction reaction: ReactionEvent,
|
|
forMessage message: BaseMessage
|
|
)
|
|
}
|
|
|
|
open class SBUBaseChannelViewModel: NSObject {
|
|
// MARK: - Constant
|
|
let defaultFetchLimit: Int = 30
|
|
let initPolicy: MessageCollectionInitPolicy = .cacheAndReplaceByApi
|
|
|
|
// MARK: - Logic properties (Public)
|
|
/// The current channel object. It's `BaseChannel` type.
|
|
public internal(set) var channel: BaseChannel?
|
|
/// The URL of the current channel.
|
|
public internal(set) var channelURL: String?
|
|
/// The starting point of the message list in the `channel`.
|
|
public internal(set) var startingPoint: Int64?
|
|
|
|
/// This user message object that is being edited.
|
|
public internal(set) var inEditingMessage: UserMessage?
|
|
|
|
/// This object has a list of all success messages synchronized with the server.
|
|
@SBUAtomic public internal(set) var messageList: [BaseMessage] = []
|
|
/// This object has a list of all messages.
|
|
@SBUAtomic public internal(set) var fullMessageList: [BaseMessage] = []
|
|
|
|
/// This object is used to check if current user is an operator.
|
|
public var isOperator: Bool {
|
|
if let groupChannel = self.channel as? GroupChannel {
|
|
return groupChannel.myRole == .operator
|
|
} else if let openChannel = self.channel as? OpenChannel {
|
|
guard let userId = SBUGlobals.currentUser?.userId else { return false }
|
|
return openChannel.isOperator(userId: userId)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Custom param set by user.
|
|
public var customizedMessageListParams: MessageListParams?
|
|
public internal(set) var messageListParams = MessageListParams()
|
|
|
|
public var sendFileMessageCompletionHandler: SendbirdChatSDK.FileMessageHandler?
|
|
public var sendUserMessageCompletionHandler: SendbirdChatSDK.UserMessageHandler?
|
|
|
|
public var pendingMessageManager = SBUPendingMessageManager.shared
|
|
|
|
// MARK: - Logic properties (Private)
|
|
weak var baseDataSource: SBUBaseChannelViewModelDataSource?
|
|
weak var baseDelegate: SBUBaseChannelViewModelDelegate?
|
|
|
|
let prevLock = NSLock()
|
|
let nextLock = NSLock()
|
|
let initialLock = NSLock()
|
|
|
|
var isInitialLoading = false
|
|
var isScrollToInitialPositionFinish = false
|
|
|
|
@SBUAtomic var isLoadingNext = false
|
|
@SBUAtomic var isLoadingPrev = false
|
|
|
|
/// Memory cache of newest messages to be used when message has loaded from specific timestamp.
|
|
var messageCache: SBUMessageCache?
|
|
|
|
var isTransformedList: Bool = true
|
|
var isThreadMessageMode: Bool = false
|
|
|
|
// MARK: - LifeCycle
|
|
public override init() {
|
|
super.init()
|
|
|
|
SendbirdChat.addConnectionDelegate(
|
|
self,
|
|
identifier: "\(SBUConstant.connectionDelegateIdentifier).\(self.description)"
|
|
)
|
|
}
|
|
|
|
func reset() {
|
|
self.messageCache = nil
|
|
self.resetMessageListParams()
|
|
self.isScrollToInitialPositionFinish = false
|
|
}
|
|
|
|
deinit {
|
|
self.baseDelegate = nil
|
|
self.baseDataSource = nil
|
|
|
|
SendbirdChat.removeConnectionDelegate(
|
|
forIdentifier: "\(SBUConstant.connectionDelegateIdentifier).\(self.description)"
|
|
)
|
|
}
|
|
|
|
// MARK: - Channel related
|
|
|
|
/// This function loads channel information and message list.
|
|
/// - Parameters:
|
|
/// - channelURL: channel url
|
|
/// - messageListParams: (Optional) The parameter to be used when getting channel information.
|
|
public func loadChannel(channelURL: String, messageListParams: MessageListParams? = nil, completionHandler: ((BaseChannel?, SBError?) -> Void)? = nil) {}
|
|
|
|
/// This function refreshes channel.
|
|
public func refreshChannel() {}
|
|
|
|
// MARK: - Load Messages
|
|
|
|
/// Loads initial messages in channel.
|
|
/// `NOT` using `initialMessages` here since `MessageCollection` handles messages from db.
|
|
/// Only used in `SBUOpenChannelViewModel` where `MessageCollection` is not suppoorted.
|
|
///
|
|
/// - Parameters:
|
|
/// - startingPoint: Starting point to load messages from, or `nil` to load from the latest. (`Int64.max`)
|
|
/// - showIndicator: Whether to show indicator on load or not.
|
|
/// - initialMessages: Custom messages to start the messages from.
|
|
public func loadInitialMessages(startingPoint: Int64?,
|
|
showIndicator: Bool,
|
|
initialMessages: [BaseMessage]?) {}
|
|
|
|
/// Loads previous messages.
|
|
public func loadPrevMessages() {}
|
|
|
|
/// Loads next messages from `lastUpdatedTimestamp`.
|
|
public func loadNextMessages() {}
|
|
|
|
/// This function resets list and reloads message lists.
|
|
public func reloadMessageList() {
|
|
self.loadInitialMessages(
|
|
startingPoint: nil,
|
|
showIndicator: false,
|
|
initialMessages: []
|
|
)
|
|
}
|
|
|
|
// MARK: - Message
|
|
|
|
/// Sends a user message with text and parentMessageId.
|
|
/// - Parameters:
|
|
/// - text: String value
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
open func sendUserMessage(text: String, parentMessage: BaseMessage? = nil) {
|
|
let text = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let messageParams = UserMessageCreateParams(message: text)
|
|
|
|
SBUGlobalCustomParams.userMessageParamsSendBuilder?(messageParams)
|
|
|
|
if let parentMessage = parentMessage, SBUGlobals.reply.replyType != .none {
|
|
messageParams.parentMessageId = parentMessage.messageId
|
|
messageParams.isReplyToChannel = true
|
|
}
|
|
messageParams.mentionedMessageTemplate = ""
|
|
messageParams.mentionedUserIds = []
|
|
|
|
self.sendUserMessage(messageParams: messageParams, parentMessage: parentMessage)
|
|
}
|
|
|
|
/// Sends a user message with mentionedMessageTemplate and mentionedUserIds.
|
|
/// - Parameters:
|
|
/// - mentionedMessageTemplate: Mentioned message string value that is generated by `text` and `mentionedUsers`.
|
|
/// - mentionedUserIds: Mentioned user Id array
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
/// ```swift
|
|
/// print(text) // "Hi @Nickname"
|
|
/// print(mentionedMessageTemplate) // "Hi @{UserID}"
|
|
/// print(mentionedUserIds) // ["{UserID}"]
|
|
/// ```
|
|
open func sendUserMessage(text: String, mentionedMessageTemplate: String, mentionedUserIds: [String], parentMessage: BaseMessage? = nil) {
|
|
let messageParams = UserMessageCreateParams(message: text)
|
|
|
|
SBUGlobalCustomParams.userMessageParamsSendBuilder?(messageParams)
|
|
|
|
if let parentMessage = parentMessage, SBUGlobals.reply.replyType != .none {
|
|
messageParams.parentMessageId = parentMessage.messageId
|
|
messageParams.isReplyToChannel = true
|
|
}
|
|
messageParams.mentionedMessageTemplate = mentionedMessageTemplate
|
|
messageParams.mentionedUserIds = mentionedUserIds
|
|
self.sendUserMessage(messageParams: messageParams, parentMessage: parentMessage)
|
|
}
|
|
|
|
/// Sends a user messag with messageParams.
|
|
///
|
|
/// You can send a message by setting various properties of MessageParams.
|
|
/// - Parameters:
|
|
/// - messageParams: `UserMessageCreateParams` class object
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
/// - Since: 1.0.9
|
|
open func sendUserMessage(messageParams: UserMessageCreateParams, parentMessage: BaseMessage? = nil) {
|
|
SBULog.info("[Request] Send user message")
|
|
|
|
let preSendMessage = self.channel?.sendUserMessage(params: messageParams) { [weak self] userMessage, error in
|
|
self?.sendUserMessageCompletionHandler?(userMessage, error)
|
|
}
|
|
|
|
if let preSendMessage = preSendMessage,
|
|
self.messageListParams.belongsTo(preSendMessage) {
|
|
preSendMessage.parentMessage = parentMessage
|
|
self.pendingMessageManager.upsertPendingMessage(
|
|
channelURL: self.channel?.channelURL,
|
|
message: preSendMessage,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
} else {
|
|
SBULog.info("A filtered user message has been sent.")
|
|
}
|
|
|
|
self.sortAllMessageList(needReload: true)
|
|
|
|
if let channel = self.channel as? GroupChannel {
|
|
channel.endTyping()
|
|
}
|
|
|
|
let context = MessageContext(source: .eventMessageSent, sendingStatus: .succeeded)
|
|
self.baseDelegate?.baseChannelViewModel(
|
|
self,
|
|
shouldUpdateScrollInMessageList: self.fullMessageList,
|
|
forContext: context,
|
|
keepsScroll: false
|
|
)
|
|
}
|
|
|
|
/// Sends a file message with file data, file name, mime type.
|
|
/// - Parameters:
|
|
/// - fileData: `Data` class object
|
|
/// - fileName: file name. Used when displayed in channel list.
|
|
/// - mimeType: file's mime type.
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
open func sendFileMessage(fileData: Data?, fileName: String, mimeType: String, parentMessage: BaseMessage? = nil) {
|
|
guard let fileData = fileData else { return }
|
|
let messageParams = FileMessageCreateParams(file: fileData)
|
|
messageParams.fileName = fileName
|
|
messageParams.mimeType = mimeType
|
|
messageParams.fileSize = UInt(fileData.count)
|
|
|
|
// Image size
|
|
if let image = UIImage(data: fileData) {
|
|
let thumbnailSize = ThumbnailSize.make(maxSize: image.size)
|
|
messageParams.thumbnailSizes = [thumbnailSize]
|
|
}
|
|
|
|
// Video thumbnail size
|
|
else if let asset = fileData.getAVAsset() {
|
|
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
|
|
avAssetImageGenerator.appliesPreferredTrackTransform = true
|
|
let cmTime = CMTimeMake(value: 2, timescale: 1)
|
|
if let cgImage = try? avAssetImageGenerator.copyCGImage(at: cmTime, actualTime: nil) {
|
|
let image = UIImage(cgImage: cgImage)
|
|
let thumbnailSize = ThumbnailSize.make(maxSize: image.size)
|
|
messageParams.thumbnailSizes = [thumbnailSize]
|
|
}
|
|
}
|
|
|
|
SBUGlobalCustomParams.fileMessageParamsSendBuilder?(messageParams)
|
|
|
|
if let parentMessage = parentMessage, SBUGlobals.reply.replyType != .none {
|
|
messageParams.parentMessageId = parentMessage.messageId
|
|
messageParams.isReplyToChannel = true
|
|
}
|
|
self.sendFileMessage(messageParams: messageParams, parentMessage: parentMessage)
|
|
}
|
|
|
|
/// Sends a voice message with ``SBUVoiceFileInfo`` object that contains essential information of a voice message.
|
|
/// - Parameters:
|
|
/// - voiceFileInfo: ``SBUVoiceFileInfo`` class object
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
open func sendVoiceMessage(voiceFileInfo: SBUVoiceFileInfo, parentMessage: BaseMessage? = nil) {
|
|
guard let filePath = voiceFileInfo.filePath,
|
|
let fileName = voiceFileInfo.fileName,
|
|
let fileData = SBUCacheManager.File.diskCache.get(fullPath: filePath) else { return }
|
|
let playtime = String(Int(voiceFileInfo.playtime ?? 0))
|
|
let durationMetaArray = MessageMetaArray(key: SBUConstant.voiceMessageDurationKey, value: [playtime])
|
|
let typeMetaArray = MessageMetaArray(key: SBUConstant.internalMessageTypeKey, value: [SBUConstant.voiceMessageType])
|
|
|
|
let messageParams = FileMessageCreateParams(file: fileData)
|
|
messageParams.fileName = fileName // Maintain the file name used for recording to erase the recording file cache
|
|
messageParams.mimeType = "\(SBUConstant.voiceMessageType);\(SBUConstant.voiceMessageTypeVoiceParameter)"
|
|
messageParams.fileSize = UInt(fileData.count)
|
|
messageParams.metaArrays = [durationMetaArray, typeMetaArray]
|
|
|
|
SBUGlobalCustomParams.voiceFileMessageParamsSendBuilder?(messageParams)
|
|
|
|
if let parentMessage = parentMessage, SBUGlobals.reply.replyType != .none {
|
|
messageParams.parentMessageId = parentMessage.messageId
|
|
messageParams.isReplyToChannel = true
|
|
}
|
|
|
|
self.sendFileMessage(messageParams: messageParams, parentMessage: parentMessage)
|
|
}
|
|
|
|
/// Sends a file message with messageParams.
|
|
///
|
|
/// You can send a file message by setting various properties of MessageParams.
|
|
/// - Parameters:
|
|
/// - messageParams: `FileMessageCreateParams` class object
|
|
/// - parentMessage: The parent message. The default value is `nil` when there's no parent message.
|
|
/// - Since: 1.0.9
|
|
open func sendFileMessage(messageParams: FileMessageCreateParams, parentMessage: BaseMessage? = nil) {
|
|
guard let channel = self.channel else { return }
|
|
|
|
SBULog.info("[Request] Send file message")
|
|
|
|
// for voice message
|
|
let fileName = messageParams.fileName ?? ""
|
|
|
|
if SBUUtils.getFileType(by: messageParams.mimeType ?? "") == .voice {
|
|
let extensiontype = URL(fileURLWithPath: fileName).pathExtension
|
|
if extensiontype.count > 0 {
|
|
messageParams.fileName = "\(SBUStringSet.VoiceMessage.fileName).\(extensiontype)"
|
|
} else {
|
|
messageParams.fileName = "\(SBUStringSet.VoiceMessage.fileName)"
|
|
}
|
|
}
|
|
|
|
var preSendMessage: FileMessage?
|
|
preSendMessage = channel.sendFileMessage(
|
|
params: messageParams,
|
|
progressHandler: { requestId, _, totalBytesSent, totalBytesExpectedToSend in
|
|
//// If need reload cell for progress, call reload action in here.
|
|
guard let requestId = requestId else { return }
|
|
let fileTransferProgress = CGFloat(totalBytesSent)/CGFloat(totalBytesExpectedToSend)
|
|
SBULog.info("File message transfer progress: \(requestId) - \(fileTransferProgress)")
|
|
},
|
|
completionHandler: { [weak self] fileMessage, error in
|
|
if let error = error {
|
|
SBULog.error(error.localizedDescription)
|
|
}
|
|
self?.sendFileMessageCompletionHandler?(fileMessage, error)
|
|
}
|
|
)
|
|
|
|
if let preSendMessage = preSendMessage {
|
|
switch SBUUtils.getFileType(by: preSendMessage) {
|
|
case .image:
|
|
SBUCacheManager.Image.preSave(fileMessage: preSendMessage)
|
|
case .video:
|
|
SBUCacheManager.Image.preSave(fileMessage: preSendMessage) // for Thumbnail
|
|
SBUCacheManager.File.preSave(fileMessage: preSendMessage, fileName: messageParams.fileName)
|
|
case .voice:
|
|
// voice file's fileName is "Voice message". not have path extension.
|
|
let extensiontype = URL(fileURLWithPath: fileName).pathExtension
|
|
let voiceFileName = "\(SBUStringSet.VoiceMessage.fileName).\(extensiontype)"
|
|
let tempFileName = "\(fileName).\(extensiontype)"
|
|
|
|
SBUCacheManager.File.preSave(fileMessage: preSendMessage, fileName: voiceFileName)
|
|
SBUCacheManager.File.removeVoiceTemp(fileName: tempFileName)
|
|
default:
|
|
SBUCacheManager.File.preSave(fileMessage: preSendMessage, fileName: messageParams.fileName)
|
|
}
|
|
}
|
|
|
|
if let preSendMessage = preSendMessage, self.messageListParams.belongsTo(preSendMessage) {
|
|
preSendMessage.parentMessage = parentMessage
|
|
self.pendingMessageManager.upsertPendingMessage(
|
|
channelURL: self.channel?.channelURL,
|
|
message: preSendMessage,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
|
|
self.pendingMessageManager.addFileInfo(
|
|
requestId: preSendMessage.requestId,
|
|
params: messageParams,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
} else {
|
|
SBULog.info("A filtered file message has been sent.")
|
|
}
|
|
|
|
self.sortAllMessageList(needReload: true)
|
|
|
|
let context = MessageContext(source: .eventMessageSent, sendingStatus: .succeeded)
|
|
self.baseDelegate?.baseChannelViewModel(
|
|
self,
|
|
shouldUpdateScrollInMessageList: self.fullMessageList,
|
|
forContext: context,
|
|
keepsScroll: false
|
|
)
|
|
}
|
|
|
|
/// Updates a user message with message object.
|
|
/// - Parameters:
|
|
/// - message: `UserMessage` object to update
|
|
/// - text: String to be updated
|
|
/// - Since: 1.0.9
|
|
public func updateUserMessage(message: UserMessage, text: String) {
|
|
let text = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let messageParams = UserMessageUpdateParams(message: text)
|
|
|
|
SBUGlobalCustomParams.userMessageParamsUpdateBuilder?(messageParams)
|
|
messageParams.mentionedMessageTemplate = ""
|
|
messageParams.mentionedUserIds = []
|
|
|
|
self.updateUserMessage(message: message, messageParams: messageParams)
|
|
}
|
|
|
|
/// Sends a user message with mentionedMessageTemplate and mentionedUserIds.
|
|
/// - Parameters:
|
|
/// - message: `UserMessage` object to update
|
|
/// - text: A `String` value to update `message.message`
|
|
/// - mentionedMessageTemplate: Mentioned message string value that is generated by `text` and `mentionedUsers`
|
|
/// - mentionedUserIds: Mentioned user Id array
|
|
/// ```swift
|
|
/// print(text) // "Hi @Nickname"
|
|
/// print(mentionedMessageTemplate) // "Hi @{UserID}"
|
|
/// print(mentionedUserIds) // ["{UserID}"]
|
|
/// ```
|
|
open func updateUserMessage(message: UserMessage, text: String, mentionedMessageTemplate: String, mentionedUserIds: [String]) {
|
|
let messageParams = UserMessageUpdateParams(message: text)
|
|
|
|
SBUGlobalCustomParams.userMessageParamsUpdateBuilder?(messageParams)
|
|
|
|
messageParams.mentionedMessageTemplate = mentionedMessageTemplate
|
|
messageParams.mentionedUserIds = mentionedUserIds
|
|
self.updateUserMessage(message: message, messageParams: messageParams)
|
|
}
|
|
|
|
/// Updates a user message with message object and messageParams.
|
|
///
|
|
/// You can update messages by setting various properties of MessageParams.
|
|
/// - Parameters:
|
|
/// - message: `UserMessage` object to update
|
|
/// - messageParams: `UserMessageUpdateParams` class object
|
|
/// - Since: 1.0.9
|
|
public func updateUserMessage(message: UserMessage, messageParams: UserMessageUpdateParams) {
|
|
SBULog.info("[Request] Update user message")
|
|
self.channel?.updateUserMessage(
|
|
messageId: message.messageId,
|
|
params: messageParams
|
|
) { [weak self] _, _ in
|
|
guard let self = self else { return }
|
|
guard let channel = self.channel else { return }
|
|
self.baseDelegate?.baseChannelViewModel(self, shouldFinishEditModeForChannel: channel)
|
|
}
|
|
}
|
|
|
|
func handlePendingResendableMessage<Message: BaseMessage>(_ message: Message?, _ error: SBError?) { }
|
|
|
|
/// Resends a message with failedMessage object.
|
|
/// - Parameter failedMessage: `BaseMessage` class based failed object
|
|
/// - Since: 1.0.9
|
|
public func resendMessage(failedMessage: BaseMessage) {
|
|
if let failedMessage = failedMessage as? UserMessage {
|
|
SBULog.info("[Request] Resend failed user message")
|
|
|
|
let pendingMessage = self.channel?.resendUserMessage(
|
|
failedMessage
|
|
) { [weak self] message, error in
|
|
guard let self = self else { return }
|
|
self.handlePendingResendableMessage(message, error)
|
|
}
|
|
|
|
self.pendingMessageManager.upsertPendingMessage(
|
|
channelURL: self.channel?.channelURL,
|
|
message: pendingMessage,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
|
|
if let failedMessage = pendingMessage {
|
|
self.deleteMessagesInList(
|
|
messageIds: [failedMessage.messageId],
|
|
excludeResendableMessages: true,
|
|
needReload: true
|
|
)
|
|
}
|
|
|
|
} else if let failedMessage = failedMessage as? FileMessage {
|
|
var data: Data?
|
|
|
|
if let fileInfo = self.pendingMessageManager.getFileInfo(
|
|
requestId: failedMessage.requestId,
|
|
forMessageThread: self.isThreadMessageMode
|
|
) {
|
|
data = fileInfo.file
|
|
}
|
|
|
|
SBULog.info("[Request] Resend failed file message")
|
|
|
|
let pendingMessage = self.channel?.resendFileMessage(
|
|
failedMessage,
|
|
binaryData: data
|
|
) { (_, _, _, _) in
|
|
//// If need reload cell for progress, call reload action in here.
|
|
// self.tableView.reloadData()
|
|
} completionHandler: { [weak self] message, error in
|
|
guard let self = self else { return }
|
|
self.handlePendingResendableMessage(message, error)
|
|
}
|
|
|
|
self.pendingMessageManager.upsertPendingMessage(
|
|
channelURL: self.channel?.channelURL,
|
|
message: pendingMessage,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
|
|
if let failedMessage = pendingMessage {
|
|
self.deleteMessagesInList(
|
|
messageIds: [failedMessage.messageId],
|
|
excludeResendableMessages: true,
|
|
needReload: true
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Deletes a message with message object.
|
|
/// - Parameter message: `BaseMessage` based class object
|
|
/// - Since: 1.0.9
|
|
public func deleteMessage(message: BaseMessage) {
|
|
SBULog.info("[Request] Delete message: \(message.description)")
|
|
self.channel?.deleteMessage(message, completionHandler: nil)
|
|
}
|
|
|
|
// MARK: - List
|
|
|
|
/// This function updates the messages in the list.
|
|
///
|
|
/// It is updated only if the messages already exist in the list, and if not, it is ignored.
|
|
/// And, after updating the messages, a function to sort the message list is called.
|
|
/// - Parameters:
|
|
/// - messages: Message array to update
|
|
/// - needReload: If set to `true`, the tableview will be call reloadData.
|
|
/// - Since: 1.2.5
|
|
public func updateMessagesInList(messages: [BaseMessage]?, needReload: Bool) {
|
|
messages?.forEach { message in
|
|
if let index = SBUUtils.findIndex(of: message, in: self.messageList) {
|
|
if !self.messageListParams.belongsTo(message) {
|
|
self.messageList.remove(at: index)
|
|
} else {
|
|
self.messageList[index] = message
|
|
}
|
|
}
|
|
}
|
|
|
|
self.sortAllMessageList(needReload: needReload)
|
|
}
|
|
|
|
func filteredForThreadMessageView(messages: [BaseMessage]?) -> [BaseMessage]? {
|
|
let pendingMessages = self.pendingMessageManager.getPendingMessages(
|
|
channelURL: self.channelURL,
|
|
forMessageThread: true
|
|
)
|
|
let refinedResult = messages?.filter { message in
|
|
var existInPendingThreadMessage = false
|
|
pendingMessages.forEach {
|
|
if $0.requestId == message.requestId {
|
|
existInPendingThreadMessage = true
|
|
}
|
|
}
|
|
return !existInPendingThreadMessage
|
|
}
|
|
return refinedResult
|
|
}
|
|
|
|
/// This function upserts the messages in the list.
|
|
/// - Parameters:
|
|
/// - messages: Message array to upsert
|
|
/// - needUpdateNewMessage: If set to `true`, increases new message count.
|
|
/// - needReload: If set to `true`, the tableview will be call reloadData.
|
|
/// - Since: 1.2.5
|
|
public func upsertMessagesInList(messages: [BaseMessage]?,
|
|
needUpdateNewMessage: Bool = false,
|
|
needReload: Bool) {
|
|
SBULog.info("First : \(String(describing: messages?.first)), Last : \(String(describing: messages?.last))")
|
|
|
|
var needMarkAsRead = false
|
|
|
|
messages?.forEach { message in
|
|
if let index = SBUUtils.findIndex(of: message, in: self.messageList) {
|
|
self.messageList.remove(at: index)
|
|
}
|
|
|
|
guard self.messageListParams.belongsTo(message) else {
|
|
self.sortAllMessageList(needReload: needReload)
|
|
return
|
|
}
|
|
|
|
guard message is UserMessage || message is FileMessage else {
|
|
if message is AdminMessage {
|
|
self.messageList.append(message)
|
|
}
|
|
return
|
|
}
|
|
|
|
if needUpdateNewMessage {
|
|
guard let channel = self.channel else { return }
|
|
self.baseDelegate?.baseChannelViewModel(self, didReceiveNewMessage: message, forChannel: channel)
|
|
}
|
|
|
|
if message.sendingStatus == .succeeded {
|
|
self.messageList.append(message)
|
|
|
|
self.pendingMessageManager.removePendingMessageAllTypes(
|
|
channelURL: channelURL,
|
|
requestId: message.requestId
|
|
)
|
|
|
|
needMarkAsRead = true
|
|
|
|
} else if message.sendingStatus == .failed ||
|
|
message.sendingStatus == .pending {
|
|
self.pendingMessageManager.upsertPendingMessage(
|
|
channelURL: channelURL,
|
|
message: message,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
}
|
|
}
|
|
|
|
if needMarkAsRead, let channel = self.channel as? GroupChannel, !self.isThreadMessageMode {
|
|
channel.markAsRead(completionHandler: nil)
|
|
}
|
|
|
|
self.sortAllMessageList(needReload: needReload)
|
|
}
|
|
|
|
/// This function deletes the messages in the list using the message ids. (Resendable messages are also delete together.)
|
|
/// - Parameters:
|
|
/// - messageIds: Message id array to delete
|
|
/// - needReload: If set to `true`, the tableview will be call reloadData.
|
|
/// - Since: 1.2.5
|
|
public func deleteMessagesInList(messageIds: [Int64]?, needReload: Bool) {
|
|
self.deleteMessagesInList(
|
|
messageIds: messageIds,
|
|
excludeResendableMessages: false,
|
|
needReload: needReload
|
|
)
|
|
}
|
|
|
|
/// This function deletes the messages in the list using the message ids.
|
|
/// - Parameters:
|
|
/// - messageIds: Message id array to delete
|
|
/// - excludeResendableMessages: If set to `true`, the resendable messages are not deleted.
|
|
/// - needReload: If set to `true`, the tableview will be call reloadData.
|
|
/// - Since: 2.1.8
|
|
public func deleteMessagesInList(messageIds: [Int64]?,
|
|
excludeResendableMessages: Bool,
|
|
needReload: Bool) {
|
|
guard let messageIds = messageIds else { return }
|
|
|
|
// if deleted message contains the currently editing message,
|
|
// end edit mode.
|
|
if let editMessage = inEditingMessage,
|
|
messageIds.contains(editMessage.messageId),
|
|
let channel = self.channel {
|
|
self.baseDelegate?.baseChannelViewModel(self, shouldFinishEditModeForChannel: channel)
|
|
}
|
|
|
|
var toBeDeleteIndexes: [Int] = []
|
|
var toBeDeleteRequestIds: [String] = []
|
|
|
|
for (index, message) in self.messageList.enumerated() {
|
|
for messageId in messageIds {
|
|
guard message.messageId == messageId else { continue }
|
|
toBeDeleteIndexes.append(index)
|
|
|
|
guard message.requestId.count > 0 else { continue }
|
|
|
|
switch message {
|
|
case let userMessage as UserMessage:
|
|
let requestId = userMessage.requestId
|
|
toBeDeleteRequestIds.append(requestId)
|
|
|
|
case let fileMessage as FileMessage:
|
|
let requestId = fileMessage.requestId
|
|
toBeDeleteRequestIds.append(requestId)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
// for remove from last
|
|
let sortedIndexes = toBeDeleteIndexes.sorted().reversed()
|
|
|
|
for index in sortedIndexes {
|
|
self.messageList.remove(at: index)
|
|
}
|
|
|
|
if excludeResendableMessages {
|
|
self.sortAllMessageList(needReload: needReload)
|
|
} else {
|
|
self.deleteResendableMessages(requestIds: toBeDeleteRequestIds, needReload: needReload)
|
|
}
|
|
}
|
|
|
|
/// This functions deletes the resendable message.
|
|
/// If `baseChannel` is type of `GroupChannel`, it deletes the message by using local caching.
|
|
/// If `baseChannel` is not type of `GroupChannel` that not using local caching, it calls `deleteResendableMessages(requestIds:needReload:)`.
|
|
/// - Parameters:
|
|
/// - message: The resendable`BaseMessage` object such as failed message.
|
|
/// - needReload: If `true`, the table view will call `reloadData()`.
|
|
/// - Since: 2.2.1
|
|
public func deleteResendableMessage(_ message: BaseMessage, needReload: Bool) {
|
|
self.deleteResendableMessages(requestIds: [message.requestId], needReload: needReload)
|
|
}
|
|
|
|
/// This functions deletes the resendable messages using the request ids.
|
|
/// - Parameters:
|
|
/// - requestIds: Request id array to delete
|
|
/// - needReload: If set to `true`, the tableview will be call reloadData.
|
|
/// - Since: 1.2.5
|
|
public func deleteResendableMessages(requestIds: [String], needReload: Bool) {
|
|
for requestId in requestIds {
|
|
self.pendingMessageManager.removePendingMessageAllTypes(
|
|
channelURL: self.channel?.channelURL,
|
|
requestId: requestId
|
|
)
|
|
}
|
|
|
|
self.sortAllMessageList(needReload: needReload)
|
|
}
|
|
|
|
/// This function sorts the all message list. (Included `presendMessages`, `messageList` and `resendableMessages`.)
|
|
/// - Parameter needReload: If set to `true`, the tableview will be call reloadData and, scroll to last seen index.
|
|
/// - Since: 1.2.5
|
|
public func sortAllMessageList(needReload: Bool) {
|
|
// Generate full list for draw
|
|
let pendingMessages = self.pendingMessageManager.getPendingMessages(
|
|
channelURL: self.channel?.channelURL,
|
|
forMessageThread: self.isThreadMessageMode
|
|
)
|
|
|
|
let refinedPendingMessages = pendingMessages.filter { pendingMessage in
|
|
var isInMessageList = false
|
|
self.messageList.forEach { message in
|
|
if message.requestId == pendingMessage.requestId {
|
|
isInMessageList = true
|
|
return
|
|
}
|
|
}
|
|
return !isInMessageList
|
|
}
|
|
|
|
if isTransformedList {
|
|
self.messageList.sort { $0.createdAt > $1.createdAt }
|
|
self.fullMessageList = refinedPendingMessages.sorted { $0.createdAt > $1.createdAt }
|
|
+ self.messageList
|
|
} else {
|
|
self.messageList.sort { $0.createdAt < $1.createdAt }
|
|
self.fullMessageList = self.messageList
|
|
+ refinedPendingMessages.sorted { $0.createdAt < $1.createdAt }
|
|
}
|
|
|
|
self.baseDelegate?.shouldUpdateLoadingState(false)
|
|
self.baseDelegate?.baseChannelViewModel(
|
|
self,
|
|
didChangeMessageList: self.fullMessageList,
|
|
needsToReload: needReload,
|
|
initialLoad: self.isInitialLoading
|
|
)
|
|
}
|
|
|
|
/// This functions clears current message lists
|
|
///
|
|
/// - Since: 2.1.0
|
|
public func clearMessageList() {
|
|
self.fullMessageList.removeAll(where: { SBUUtils.findIndex(of: $0, in: messageList) != nil })
|
|
self.messageList = []
|
|
}
|
|
|
|
// MARK: - MessageListParams
|
|
private func resetMessageListParams() {
|
|
self.messageListParams = self.customizedMessageListParams?.copy() as? MessageListParams
|
|
?? MessageListParams()
|
|
|
|
if self.messageListParams.previousResultSize <= 0 {
|
|
self.messageListParams.previousResultSize = self.defaultFetchLimit
|
|
}
|
|
if self.messageListParams.nextResultSize <= 0 {
|
|
self.messageListParams.nextResultSize = self.defaultFetchLimit
|
|
}
|
|
|
|
self.messageListParams.reverse = true
|
|
self.messageListParams.includeReactions = SBUEmojiManager.useReaction(channel: channel)
|
|
|
|
self.messageListParams.includeThreadInfo = SBUGlobals.reply.includesThreadInfo
|
|
self.messageListParams.includeParentMessageInfo = SBUGlobals.reply.includesParentMessageInfo
|
|
self.messageListParams.replyType = SBUGlobals.reply.replyType.filterValue
|
|
|
|
self.messageListParams.includeMetaArray = true
|
|
}
|
|
|
|
// MARK: - Reactions
|
|
/// This function is used to add or delete reactions.
|
|
/// - Parameters:
|
|
/// - message: `BaseMessage` object to update
|
|
/// - emojiKey: set emoji key
|
|
/// - didSelect: set reaction state
|
|
/// - Since: 1.1.0
|
|
public func setReaction(message: BaseMessage, emojiKey: String, didSelect: Bool) {
|
|
if didSelect {
|
|
SBULog.info("[Request] Add Reaction")
|
|
self.channel?.addReaction(with: message, key: emojiKey) { reactionEvent, error in
|
|
if let error = error {
|
|
self.baseDelegate?.didReceiveError(error, isBlocker: false)
|
|
}
|
|
|
|
SBULog.info("[Response] \(reactionEvent?.key ?? "") reaction")
|
|
guard let reactionEvent = reactionEvent else { return }
|
|
if reactionEvent.messageId == message.messageId {
|
|
message.apply(reactionEvent)
|
|
}
|
|
self.baseDelegate?.baseChannelViewModel(self, didUpdateReaction: reactionEvent, forMessage: message)
|
|
}
|
|
} else {
|
|
SBULog.info("[Request] Delete Reaction")
|
|
self.channel?.deleteReaction(with: message, key: emojiKey) { reactionEvent, error in
|
|
if let error = error {
|
|
self.baseDelegate?.didReceiveError(error, isBlocker: false)
|
|
}
|
|
|
|
SBULog.info("[Response] \(reactionEvent?.key ?? "") reaction")
|
|
guard let reactionEvent = reactionEvent else { return }
|
|
if reactionEvent.messageId == message.messageId {
|
|
message.apply(reactionEvent)
|
|
}
|
|
self.baseDelegate?.baseChannelViewModel(self, didUpdateReaction: reactionEvent, forMessage: message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Common
|
|
|
|
/// This function checks that have the following list.
|
|
/// - Returns: This function returns `true` if there is the following list.
|
|
public func hasNext() -> Bool { return false }
|
|
|
|
/// This function checks that have the previous list.
|
|
/// - Returns: This function returns `true` if there is the previous list.
|
|
public func hasPrevious() -> Bool { return false }
|
|
|
|
public func getStartingPoint() -> Int64? { return .max }
|
|
|
|
// MARK: - Cache
|
|
func setupCache() {
|
|
guard let channel = channel else { return }
|
|
self.messageCache = SBUMessageCache(
|
|
channel: channel,
|
|
messageListParam: self.messageListParams
|
|
)
|
|
self.messageCache?.loadInitial()
|
|
}
|
|
|
|
func flushCache(with messages: [BaseMessage]) -> [BaseMessage] {
|
|
SBULog.info("flushing cache with : \(messages.count)")
|
|
guard let messageCache = self.messageCache else { return messages }
|
|
|
|
let mergedList = messageCache.flush(with: messages)
|
|
self.messageCache = nil
|
|
|
|
return mergedList
|
|
}
|
|
}
|
|
|
|
// MARK: - ConnectionDelegate
|
|
extension SBUBaseChannelViewModel: ConnectionDelegate {
|
|
open func didSucceedReconnection() {
|
|
SBULog.info("Did succeed reconnection")
|
|
|
|
SendbirdUI.updateUserInfo { error in
|
|
if let error = error {
|
|
SBULog.error("[Failed] Update user info: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
self.refreshChannel()
|
|
}
|
|
}
|
|
|
|
// MARK: - ChannelDelegate
|
|
extension SBUBaseChannelViewModel: BaseChannelDelegate {
|
|
// Received message
|
|
open func channel(_ channel: BaseChannel, didReceive message: BaseMessage) {
|
|
guard self.channel?.channelURL == channel.channelURL else { return }
|
|
|
|
switch message {
|
|
case is UserMessage:
|
|
SBULog.info("Did receive user message: \(message)")
|
|
case is FileMessage:
|
|
SBULog.info("Did receive file message: \(message)")
|
|
case is AdminMessage:
|
|
SBULog.info("Did receive admin message: \(message)")
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// If channel type is Group, please do not use belows any more.
|
|
open func channel(_ channel: BaseChannel, didUpdate message: BaseMessage) {}
|
|
open func channel(_ channel: BaseChannel, messageWasDeleted messageId: Int64) {}
|
|
open func channel(_ channel: BaseChannel, didUpdateThreadInfo threadInfoUpdateEvent: ThreadInfoUpdateEvent) {}
|
|
open func channel(_ channel: BaseChannel, updatedReaction reactionEvent: ReactionEvent) {}
|
|
// open func channelDidUpdateReadReceipt(_ channel: GroupChannel) {}
|
|
// open func channelDidUpdateDeliveryReceipt(_ channel: GroupChannel) {}
|
|
// open func channelDidUpdateTypingStatus(_ channel: GroupChannel) {}
|
|
open func channelWasChanged(_ channel: BaseChannel) {}
|
|
open func channelWasFrozen(_ channel: BaseChannel) {}
|
|
open func channelWasUnfrozen(_ channel: BaseChannel) {}
|
|
open func channel(_ channel: BaseChannel, userWasMuted user: RestrictedUser) {}
|
|
open func channel(_ channel: BaseChannel, userWasUnmuted user: User) {}
|
|
open func channelDidUpdateOperators(_ channel: BaseChannel) {}
|
|
open func channel(_ channel: BaseChannel, userWasBanned user: RestrictedUser) {}
|
|
open func channel(_ channel: BaseChannel, userWasUnbanned user: User) {}
|
|
open func channelWasDeleted(_ channelURL: String, channelType: ChannelType) {}
|
|
}
|