sendbird-uikit-ios/Sources/ViewModel/MessageThread/SBUMessageThreadViewModel.s...

1410 lines
56 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SBUMessageThreadViewModel.swift
// SendbirdUIKit
//
// Created by Tez Park on 2022/11/01.
// Copyright © 2022 Sendbird, Inc. All rights reserved.
//
import UIKit
import SendbirdChatSDK
/// Methods to get data source for the `SBUMessageThreadViewModel`.
public protocol SBUMessageThreadViewModelDataSource: SBUBaseChannelViewModelDataSource { }
public protocol SBUMessageThreadViewModelDelegate: SBUBaseChannelViewModelDelegate {
/// Called when the message thread has received mentional member list. Please refer to `loadSuggestedMentions(with:)` in `SBUMessageThreadViewModel`.
/// - Parameters:
/// - viewModel: `SBUMessageThreadViewModel` object.
/// - members: Mentional members
func messageThreadViewModel(
_ viewModel: SBUMessageThreadViewModel,
didReceiveSuggestedMentions members: [SBUUser]?
)
/// Called when the message thread has loaded parent message.
/// - Parameters:
/// - viewModel: `SBUMessageThreadViewModel` object.
/// - parentMessage: Mentional members
func messageThreadViewModel(
_ viewModel: SBUMessageThreadViewModel,
didLoadParentMessage parentMessage: BaseMessage?
)
/// Called when the message thread has updated parent message.
/// - Parameters:
/// - viewModel: `SBUMessageThreadViewModel` object.
/// - parentMessage: Mentional members
func messageThreadViewModel(
_ viewModel: SBUMessageThreadViewModel,
didUpdateParentMessage parentMessage: BaseMessage?
)
/// Called when the message thread should be dismissed.
func messageThreadViewModelShouldDismissMessageThread(_ viewModel: SBUMessageThreadViewModel)
}
open class SBUMessageThreadViewModel: SBUBaseChannelViewModel {
/**
- Header: Channel delegate
- ParentMessage: Channel delegate, MessageCollection
- ThreadedMessage list: Channel delegate
- Pending message: MessageCollection
*/
// MARK: - Constant
private let changelogFetchLimit: Int = 100
// MARK: - Logic properties (Public)
public weak var delegate: SBUMessageThreadViewModelDelegate? {
get { self.baseDelegate as? SBUMessageThreadViewModelDelegate }
set { self.baseDelegate = newValue }
}
public weak var dataSource: SBUMessageThreadViewModelDataSource? {
get { self.baseDataSource as? SBUMessageThreadViewModelDataSource }
set { self.baseDataSource = newValue }
}
public internal(set) var customizedThreadedMessageListParams: ThreadedMessageListParams?
public internal(set) var threadedMessageListParams = ThreadedMessageListParams()
// MARK: - Logic properties (Private)
@SBUAtomic private var hasMorePrevious: Bool = true
@SBUAtomic private var hasMoreNext: Bool = false
@SBUAtomic private var changelogToken: String?
@SBUAtomic private var lastUpdatedTimestamp: Int64 = 0
private var currentTimeMillis: Int64 {
Int64(Date().timeIntervalSince1970 * 1000)
}
private var initSucceeded: Bool = false
var parentMessage: BaseMessage?
var parentMessageId: Int64?
var debouncer: SBUDebouncer?
var suggestedMemberList: [SBUUser]?
var query: MemberListQuery?
var messageCollection: MessageCollection? // for parent message's reply update
// MARK: - LifeCycle
public init(channel: BaseChannel? = nil,
channelURL: String? = nil,
parentMessage: BaseMessage? = nil,
parentMessageId: Int64? = 0,
threadedMessageListParams: ThreadedMessageListParams? = nil,
startingPoint: Int64? = .max,
delegate: SBUMessageThreadViewModelDelegate? = nil,
dataSource: SBUMessageThreadViewModelDataSource? = nil) {
super.init()
self.delegate = delegate
self.dataSource = dataSource
self.isTransformedList = false
self.isThreadMessageMode = true
self.pendingMessageManager = SBUPendingMessageManager.shared
SendbirdChat.addChannelDelegate(
self,
identifier: "\(SBUConstant.groupChannelDelegateIdentifier).\(self.description)"
)
if let channel = channel {
self.channel = channel
self.channelURL = channel.channelURL
} else if let channelURL = channelURL {
self.channelURL = channelURL
}
if let parentMessage = parentMessage {
self.parentMessage = parentMessage
self.parentMessageId = parentMessage.messageId
} else if let parentMessageId = parentMessageId {
self.parentMessageId = parentMessageId
}
self.customizedThreadedMessageListParams = threadedMessageListParams
self.startingPoint = startingPoint
self.debouncer = SBUDebouncer(
debounceTime: SBUGlobals.userMentionConfig?.debounceTime ?? SBUDebouncer.defaultTime
)
self.loadChannelAndMessages(channelURL: channelURL)
self.setupSendUserMessageCompletionHandlers()
self.setupSendFileMessageCompletionHandlers()
}
deinit {
self.messageCollection?.dispose()
SendbirdChat.removeChannelDelegate(
forIdentifier: "\(SBUConstant.groupChannelDelegateIdentifier).\(self.description)"
)
}
/// Loads channel and messages
///
/// Process order
/// ```
/// 1. Connect
/// 2. loadChannel
/// 3. loadParentMessage
/// 4. loadThreadedMessage
/// ```
///
/// - Parameter channelURL: channel URL string
public func loadChannelAndMessages(channelURL: String?) {
guard let channelURL = self.channelURL else { return }
// 1. Connect
SendbirdUI.connectIfNeeded { [weak self] _, error in
if let error = error {
self?.delegate?.didReceiveError(error, isBlocker: true)
return
}
// 2. loadChannel
self?.loadChannel(
channelURL: channelURL,
completionHandler: { channel, error in
guard error == nil,
let parentMessageId = self?.parentMessageId,
let channel = channel else {
return
}
self?.channel = channel
// 3. loadParentMessage
self?.loadParentMessage(
parentMessageId: parentMessageId,
channelURL: channelURL,
isInitilize: true,
completionHandler: { parentMessage, error in
guard error == nil, let parentMessage = parentMessage else {
return
}
self?.parentMessage = parentMessage
if let self = self {
self.delegate?.messageThreadViewModel(
self,
didLoadParentMessage: parentMessage
)
}
// 4. loadThreadedMessage
self?.loadInitialMessages(
startingPoint: self?.startingPoint,
showIndicator: true,
initialMessages: nil
)
guard let self = self,
let channel = self.channel as? GroupChannel else { return }
self.messageCollection = SendbirdChat.createMessageCollection(
channel: channel,
startingPoint: .max,
params: self.messageListParams
)
self.messageCollection?.delegate = self
}
)
}
)
}
}
// MARK: - Channel
public override func loadChannel(channelURL: String,
messageListParams: MessageListParams? = nil,
completionHandler: ((BaseChannel?, SBError?) -> Void)? = nil) {
SBULog.info("[Request] Load channel: \(String(channelURL))")
GroupChannel.getChannel(url: channelURL) { [weak self] channel, error in
guard let self = self else {
completionHandler?(nil, error)
return
}
self.channel = channel
guard self.canProceed(with: channel, error: error) else {
completionHandler?(nil, error)
return
}
SBULog.info("[Succeed] Load channel request: \(String(describing: self.channel))")
// background refresh to check if user is banned or not.
self.refreshChannel()
// for updating channel information when the connection state is closed at the time of initial load.
if SendbirdChat.getConnectState() == .closed {
let context = MessageContext(
source: .eventChannelChanged,
sendingStatus: .succeeded
)
self.delegate?.baseChannelViewModel(
self,
didChangeChannel: channel,
withContext: context
)
completionHandler?(channel, nil)
}
completionHandler?(channel, nil)
}
}
public override func refreshChannel() {
if let channel = self.channel as? GroupChannel {
channel.refresh { [weak self] error in
guard let self = self else { return }
guard self.canProceed(with: channel, error: error) == true else {
let context = MessageContext(source: .eventChannelChanged, sendingStatus: .failed)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
return
}
let context = MessageContext(
source: .eventChannelChanged,
sendingStatus: .succeeded
)
self.delegate?.baseChannelViewModel(
self,
didChangeChannel: channel,
withContext: context
)
}
} else if let channelURL = self.channelURL {
self.loadChannel(channelURL: channelURL)
}
}
private func canProceed(with channel: GroupChannel?, error: SBError?) -> Bool {
if let error = error {
SBULog.error("[Failed] Load channel request: \(error.localizedDescription)")
if error.code == ChatError.nonAuthorized.rawValue {
self.delegate?.baseChannelViewModel(self, shouldDismissForChannel: nil)
} else {
if SendbirdChat.isLocalCachingEnabled {
return true
} else {
self.delegate?.didReceiveError(error, isBlocker: true)
}
}
return false
}
guard let channel = channel,
channel.myMemberState != .none
else {
self.delegate?.baseChannelViewModel(self, shouldDismissForChannel: channel)
return false
}
return true
}
// MARK: - Parent Message
/// Loads parent message.
/// - Parameters:
/// - parentMessageId: Parent message Id
/// - channelURL: channel URL string
/// - isInitilize: For initialization process, set this value to `true`.
/// - completionHandler: completion handler
public func loadParentMessage(parentMessageId: Int64,
channelURL: String,
isInitilize: Bool? = false,
completionHandler: ((BaseMessage?, SBError?) -> Void)? = nil) {
if let parentMessage = self.parentMessage, !isInitialLoading {
// TODO: collection collection
completionHandler?(parentMessage, nil)
return
}
let params = MessageRetrievalParams()
params.messageId = parentMessageId
params.channelType = .group
params.channelURL = channelURL
params.includeThreadInfo = true
params.includeReactions = true
params.includeMetaArray = true
BaseMessage.getMessage(params: params) { (message, error) in
guard error == nil else {
completionHandler?(nil, error)
return
}
completionHandler?(message, nil)
}
}
/// Updates parent message.
public func updateParentMessage() {
guard let parentMessageId = parentMessageId,
let channelURL = channelURL else { return }
self.loadParentMessage(
parentMessageId: parentMessageId,
channelURL: channelURL
) { parentMessage, error in
guard error == nil, let parentMessage = parentMessage else {
return
}
self.parentMessage = parentMessage
self.delegate?.messageThreadViewModel(self, didUpdateParentMessage: self.parentMessage)
}
}
// MARK: - Load Threaded Messages
public override func loadInitialMessages(startingPoint: Int64?,
showIndicator: Bool,
initialMessages: [BaseMessage]?) {
SBULog.info("""
loadInitialMessages,
startingPoint : \(String(describing: startingPoint)),
initialMessages : \(String(describing: initialMessages))
"""
)
// Caution in function call order
self.startingPoint = startingPoint
self.isInitialLoading = true
self.reset()
if let initialMessages = initialMessages,
!initialMessages.isEmpty {
self.handleInitialResponse(usedParam: nil, messages: initialMessages, error: nil)
} else {
self.loadBothMessages(timestamp: startingPoint, showIndicator: showIndicator)
}
}
public override func loadPrevMessages() {
self.loadPrevMessages(timestamp: self.messageList.first?.createdAt)
}
/// Loads previous messages from given timestamp. Load messages from the latest (`Int64.max`).
public func loadPrevMessages(timestamp: Int64?) {
guard self.prevLock.try() else {
SBULog.info("Prev message already loading")
return
}
SBULog.info("[Request] Prev message list from : \(String(describing: timestamp))")
self.isLoadingPrev = true
let params = (self.threadedMessageListParams.copy() as? ThreadedMessageListParams) ?? ThreadedMessageListParams()
params.nextResultSize = 0
if params.previousResultSize == 0 {
params.previousResultSize = self.defaultFetchLimit
params.includeReactions = SBUEmojiManager.useReaction(channel: channel)
params.includeParentMessageInfo = true
}
self.parentMessage?.getThreadedMessages(
timestamp: timestamp ?? .max,
params: params,
completionHandler: { [weak self] _, messages, error in
guard let self = self else { return }
defer {
self.prevLock.unlock()
}
if let error = error {
self.delegate?.didReceiveError(error, isBlocker: false)
self.isLoadingPrev = false
return
}
guard self.isValidResponse(messages: messages, error: error),
let messages = messages else {
SBULog.warning("Prev message list request is not valid")
self.isLoadingPrev = false
return
}
SBULog.info("[Prev message response] \(messages.count) messages")
self.hasMorePrevious = messages.count >= params.previousResultSize
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: messages,
forContext: nil,
keepsScroll: false
)
self.updateLastUpdatedTimestamp(messages: messages)
self.upsertMessagesInList(messages: messages, needReload: true)
self.isLoadingPrev = false
}
)
}
public override func loadNextMessages() {
guard self.nextLock.try() else {
SBULog.info("Next message already loading")
return
}
SBULog.info("[Request] Next message list from : \(self.lastUpdatedTimestamp)")
self.isLoadingNext = true
let params = (self.threadedMessageListParams.copy() as? ThreadedMessageListParams) ?? ThreadedMessageListParams()
params.previousResultSize = 0
if params.nextResultSize == 0 {
params.nextResultSize = self.defaultFetchLimit
params.includeReactions = SBUEmojiManager.useReaction(channel: channel)
params.includeParentMessageInfo = true
}
self.parentMessage?.getThreadedMessages(
timestamp: self.lastUpdatedTimestamp,
params: params,
completionHandler: { [weak self] _, messages, error in
guard let self = self else { return }
defer {
self.nextLock.unlock()
self.isLoadingNext = false
}
guard self.isValidResponse(messages: messages, error: error),
let messages = messages else {
SBULog.warning("Next message list request is not valid")
return
}
SBULog.info("[Next message Response] \(messages.count) messages")
self.hasMoreNext = messages.count >= params.nextResultSize
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: messages,
forContext: nil,
keepsScroll: true
)
self.updateLastUpdatedTimestamp(messages: messages)
self.upsertMessagesInList(messages: messages, needReload: true)
}
)
}
/// Loads messages to both direction from given timestamp.
///
/// - 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.
public func loadBothMessages(timestamp: Int64?, showIndicator: Bool) {
SBULog.info("[Request] Both message list from : \(String(describing: timestamp))")
guard self.initialLock.try() else { return }
self.delegate?.shouldUpdateLoadingState(showIndicator)
let params = (self.threadedMessageListParams.copy() as? ThreadedMessageListParams) ?? ThreadedMessageListParams()
params.isInclusive = true
params.includeReactions = SBUEmojiManager.useReaction(channel: channel)
params.includeParentMessageInfo = true
let shouldFetchBoth: Bool = timestamp != nil
if shouldFetchBoth {
// prev & next
// if one direction is 0, half the other direction to make both direction equal
if params.previousResultSize == 0 {
params.previousResultSize = params.nextResultSize / 2
params.nextResultSize = params.nextResultSize / 2
} else if params.nextResultSize == 0 {
params.previousResultSize = params.previousResultSize / 2
params.nextResultSize = params.previousResultSize / 2
}
// if one direction is 0, make it half of default limit
if params.previousResultSize == 0 { params.previousResultSize = self.defaultFetchLimit }
if params.nextResultSize == 0 { params.nextResultSize = self.defaultFetchLimit }
} else {
// prev only
if params.previousResultSize == 0 {
params.previousResultSize = self.defaultFetchLimit
}
params.nextResultSize = 0
}
let startingTimestamp: Int64 = timestamp ?? .max
SBULog.info("""
Fetch from : \(startingTimestamp),
limit: prev = \(params.previousResultSize),
next = \(params.nextResultSize)
""")
self.isLoadingNext = true
self.parentMessage?.getThreadedMessages(
timestamp: startingTimestamp,
params: params,
completionHandler: { [weak self] _, messages, error in
guard let self = self else { return }
defer { self.initialLock.unlock() }
if let error = error {
self.delegate?.shouldUpdateLoadingState(false)
self.delegate?.didReceiveError(error, isBlocker: false)
return
}
self.handleInitialResponse(
usedParam: params,
messages: messages,
error: error
)
}
)
}
/// Handles response from initial loading request of messages (see `loadInitialMessages(startingPoint:showIndicator:initialMessages:)`).
/// - Parameters:
/// - usedParam: `ThreadedMessageListParams` used in `loadInitialMessages`, or `nil` if it was called from custom message list.
/// - messages: Messages loaded.
/// - error: `SBError` from loading messages.
private func handleInitialResponse(usedParam: ThreadedMessageListParams?,
messages: [BaseMessage]?,
error: SBError?) {
self.initSucceeded = error == nil
defer { self.isLoadingNext = false }
guard self.isValidResponse(messages: messages, error: error),
let messages = messages else {
SBULog.warning("Initial message list request is not valid")
self.delegate?.shouldUpdateLoadingState(false)
return
}
SBULog.info("[Both message response] \(messages.count) messages")
let startingTimestamp: Int64 = self.startingPoint ?? .max
if let usedParam = usedParam {
self.hasMorePrevious = messages
.filter({ $0.createdAt <= startingTimestamp })
.count >= usedParam.previousResultSize
if usedParam.nextResultSize > 0 {
// update hasNext only if message is fetched on next direction.
self.hasMoreNext = messages
.filter({ $0.createdAt >= startingTimestamp })
.count >= usedParam.nextResultSize
}
}
SBULog.info("""
[Initial message response] Prev count : \(messages.filter({ $0.createdAt <= startingTimestamp }).count),
prevLimit : \(String(describing: usedParam?.previousResultSize)),
hasPrev : \(String(describing: self.hasPrevious))
""")
SBULog.info("""
[Initial message response] Next count : \(messages.filter({ $0.createdAt >= startingTimestamp }).count),
nextLimit : \(String(describing: usedParam?.nextResultSize)),
hasNext : \(String(describing: self.hasNext))
""")
SBULog.info("""
[Initial message response] First : \(String(describing: messages.first)),
Last : \(String(describing: messages.last))
""")
self.updateLastUpdatedTimestamp(messages: messages)
self.isInitialLoading = false
self.upsertMessagesInList(messages: messages, needReload: true)
}
// MARK: - Message
/// Sets up completion handlers of send user message.
open func setupSendUserMessageCompletionHandlers() {
self.sendUserMessageCompletionHandler = { [weak self] userMessage, error in
guard let self = self else { return }
if let error = error {
self.baseDelegate?.didReceiveError(error)
SBULog.error("[Failed] Send user message request: \(error.localizedDescription)")
return
}
self.pendingMessageManager.removePendingMessage(
channelURL: userMessage?.channelURL,
requestId: userMessage?.requestId,
forMessageThread: self.isThreadMessageMode
)
guard let userMessage = userMessage else { return }
SBULog.info("[Succeed] Send user message: \(userMessage.description)")
self.upsertMessagesInList(messages: [userMessage], needReload: true)
}
}
/// Sets up completion handlers of send file message.
open func setupSendFileMessageCompletionHandlers() {
self.sendFileMessageCompletionHandler = { [weak self] fileMessage, error in
guard let self = self else { return }
if let error = error {
self.baseDelegate?.didReceiveError(error)
SBULog.error(
"""
[Failed] Send file message request:
\(error.localizedDescription)
"""
)
return
}
self.pendingMessageManager.removePendingMessage(
channelURL: fileMessage?.channelURL,
requestId: fileMessage?.requestId,
forMessageThread: self.isThreadMessageMode
)
guard let message = fileMessage else { return }
SBULog.info("[Succeed] Send file message: \(message.description)")
self.upsertMessagesInList(messages: [message], needReload: true)
}
}
override func handlePendingResendableMessage<Message: BaseMessage>(_ message: Message?,
_ error: SBError?) {
if let error = error {
self.pendingMessageManager.upsertPendingMessage(
channelURL: message?.channelURL,
message: message,
forMessageThread: self.isThreadMessageMode
)
self.sortAllMessageList(needReload: true)
self.baseDelegate?.didReceiveError(error, isBlocker: false)
SBULog.error("[Failed] Resend failed user message request: \(error.localizedDescription)")
return
} else {
self.pendingMessageManager.removePendingMessage(
channelURL: message?.channelURL,
requestId: message?.requestId,
forMessageThread: self.isThreadMessageMode
)
guard let message = message else { return }
SBULog.info("[Succeed] Resend failed file message: \(message.description)")
self.upsertMessagesInList(messages: [message], needReload: true)
}
}
// MARK: - List
public override func sortAllMessageList(needReload: Bool) {
// Generate full list for draw
let pendingMessages = self.pendingMessageManager.getPendingMessages(
channelURL: self.channel?.channelURL,
forMessageThread: self.isThreadMessageMode
).filter { $0.parentMessageId == self.parentMessageId }
self.messageList.sort { $0.createdAt < $1.createdAt }
self.fullMessageList = self.messageList
+ pendingMessages.sorted { $0.createdAt < $1.createdAt }
self.baseDelegate?.shouldUpdateLoadingState(false)
self.baseDelegate?.baseChannelViewModel(
self,
didChangeMessageList: self.fullMessageList,
needsToReload: needReload,
initialLoad: self.isInitialLoading
)
}
// MARK: - Last Updated timestamp
private func updateLastUpdatedTimestamp(messages: [BaseMessage]) {
SBULog.info("""
hasNext : \(String(describing: self.hasNext)),
first : \(String(describing: messages.first)),
last : \(String(describing: messages.last))
""")
let currentTime = self.currentTimeMillis
var newTimestamp: Int64 = 0
if self.hasNext() {
if let latestMessage = messages.last {
newTimestamp = latestMessage.createdAt
}
}
SBULog.info("""
newTimestamp : \(newTimestamp),
lastUpdatedTimestamp : \(self.lastUpdatedTimestamp),
currentTime : \(currentTime)
""")
guard newTimestamp > self.lastUpdatedTimestamp else { return }
self.setLastUpdatedTimestamp(timestamp: newTimestamp)
}
private func setLastUpdatedTimestamp(timestamp: Int64) {
SBULog.info("set to \(timestamp)")
self.lastUpdatedTimestamp = timestamp
}
private func resetLastUpdatedTimestamp() {
let currentTime = self.currentTimeMillis
self.lastUpdatedTimestamp = self.startingPoint ?? currentTime
SBULog.info("""
reset timestamp to : \(self.lastUpdatedTimestamp),
startingPoint : \(String(describing: self.startingPoint)),
currentTime : \(currentTime)
""")
}
// MARK: - Changelog
/// Loads SDK's changelog (updated + deleted) fully + new added messages (fully || once depending on `hasNext`)
private func loadMessageChangeLogs() {
guard self.initSucceeded else {
self.loadInitialMessages(
startingPoint: self.startingPoint,
showIndicator: false,
initialMessages: nil
)
return
}
/// Prevent loadNext being called if changelog is called
guard self.nextLock.try() else { return }
let changeLogsParams = MessageChangeLogsParams(
includeThreadInfo: true,
replyType: .all
)
var completion: (([BaseMessage]?, [Int64]?, Bool, String?, SBError?) -> Void)!
completion = { [weak self] updatedMessages, deletedMessageIds, hasMore, nextToken, error in
guard let self = self else { return }
let updatedMessages = updatedMessages?.filter { $0.parentMessageId == self.parentMessageId }
self.handleChangelogResponse(
updatedMessages: updatedMessages,
deletedMessageIds: deletedMessageIds,
hasMore: hasMore,
nextToken: nextToken,
error: error
)
}
if let token = self.changelogToken {
SBULog.info("[Request] Message change logs with token")
self.channel?.getMessageChangeLogs(
token: token,
params: changeLogsParams,
completionHandler: completion
)
} else {
SBULog.info("[Request] Message change logs with last updated timestamp")
self.channel?.getMessageChangeLogs(
timestamp: self.lastUpdatedTimestamp,
params: changeLogsParams,
completionHandler: completion
)
}
}
/// Separated loadNext for changelog and normal loading on scroll.
/// Difference on limit + handling response (setting hasNext, updatedAt, etc)
private func loadNextMessagesForChangelog(completion: @escaping ([BaseMessage]) -> Void) {
SBULog.info("[Request] Changelog added message list from : \(self.lastUpdatedTimestamp)")
let params = (self.threadedMessageListParams.copy() as? ThreadedMessageListParams) ?? ThreadedMessageListParams()
params.previousResultSize = 0
params.nextResultSize = self.changelogFetchLimit
params.includeParentMessageInfo = true
self.parentMessage?.getThreadedMessages(
timestamp: self.lastUpdatedTimestamp,
params: params,
completionHandler: { [weak self] _, messages, error in
guard let self = self else { return }
guard self.isValidResponse(messages: messages, error: error),
let messages = messages else {
SBULog.warning("Changelog added message list request is not valid")
self.nextLock.unlock()
return
}
SBULog.info("[Changelog added response] \(messages.count) messages")
completion(messages)
}
)
}
/// Handling response for Messaging SDK's `getMessageChangeLogs`
/// Loads SDK's changelog (updated + deleted) fully + new added messages (fully || once depending on `hasNext`)
private func handleChangelogResponse(updatedMessages: [BaseMessage]?,
deletedMessageIds: [Int64]?,
hasMore: Bool,
nextToken: String?,
error: SBError?) {
if let error = error {
SBULog.error("""
[Failed] Message change logs request:
\(error.localizedDescription)
""")
self.nextLock.unlock()
self.delegate?.didReceiveError(error, isBlocker: true)
return
}
SBULog.info("""
[Response]
\(String(format: "%d updated messages", updatedMessages?.count ?? 0)),
\(String(format: "%d deleted messages", deletedMessageIds?.count ?? 0))
""")
self.changelogToken = nextToken
self.handleChangelogResponse(
updatedMessages: updatedMessages,
deletedMessageIds: deletedMessageIds
)
if hasMore {
self.loadMessageChangeLogs()
} else {
isLoadingNext = true
var loadNextCompletion: (([BaseMessage]) -> Void)!
loadNextCompletion = { [weak self] messages in
guard let self = self else { return }
if let firstMessage = messages.first {
self.setLastUpdatedTimestamp(timestamp: firstMessage.createdAt)
}
let canLoadMore = self.handleChangelogResponse(addedMessages: messages)
guard canLoadMore else {
self.nextLock.unlock()
self.isLoadingNext = false
return
}
self.loadNextMessagesForChangelog(completion: loadNextCompletion)
}
self.loadNextMessagesForChangelog(completion: loadNextCompletion)
}
}
/// Handling updated & deleted messages
private func handleChangelogResponse(updatedMessages: [BaseMessage]?, deletedMessageIds: [Int64]?) {
if let updatedMessages = updatedMessages,
!updatedMessages.isEmpty {
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: updatedMessages,
forContext: nil,
keepsScroll: false
)
self.upsertMessagesInList(messages: updatedMessages, needReload: true)
}
if let deletedMessageIds = deletedMessageIds,
!deletedMessageIds.isEmpty {
self.deleteMessagesInList(messageIds: deletedMessageIds, needReload: true)
}
}
/// Handling added messages
///
/// - Returns: Whether there's more messages to fetch or not.
private func handleChangelogResponse(addedMessages: [BaseMessage]) -> Bool {
let hasMore = addedMessages.count >= self.changelogFetchLimit
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: addedMessages,
forContext: nil,
keepsScroll: true
)
self.upsertMessagesInList(messages: addedMessages, needReload: true)
SBULog.info("Loaded added messages : \(addedMessages.count), hasNext : \(String(describing: self.hasNext))")
return hasMore
}
// MARK: - Typing
public func startTypingMessage() {
guard let channel = self.channel as? GroupChannel else { return }
SBULog.info("[Request] Start typing")
channel.startTyping()
}
public func endTypingMessage() {
guard let channel = self.channel as? GroupChannel else { return }
SBULog.info("[Request] End typing")
channel.endTyping()
}
// MARK: - Mention
/// Loads mentionable member list.
/// When the suggested list is received, it calls `groupChannelViewModel(_:didReceiveSuggestedMembers:)` delegate method.
/// - Parameter filterText: The text that is used as filter while searching for the suggested mentions.
public func loadSuggestedMentions(with filterText: String) {
self.debouncer?.add { [weak self] in
guard let self = self else { return }
if let channel = self.channel as? GroupChannel {
if channel.isSuper {
let params = MemberListQueryParams()
params.nicknameStartsWithFilter = filterText
// +1 is buffer for when the current user is included in the search results
params.limit = UInt(SBUGlobals.userMentionConfig?.suggestionLimit ?? 0) + 1
self.query = channel.createMemberListQuery(params: params)
self.query?.loadNextPage { [weak self] members, _ in
guard let self = self else { return }
self.suggestedMemberList = SBUUser.convertUsers(members)
self.delegate?.messageThreadViewModel(
self,
didReceiveSuggestedMentions: self.suggestedMemberList
)
}
} else {
guard channel.members.count > 0 else {
self.suggestedMemberList = nil
self.delegate?.messageThreadViewModel(self, didReceiveSuggestedMentions: nil)
return
}
let sortedMembers = channel.members.sorted {
$0.nickname.lowercased() < $1.nickname.lowercased()
}
let matchedMembers = sortedMembers.filter {
return $0.nickname.lowercased().hasPrefix(filterText.lowercased())
}
let memberCount = matchedMembers.count
// +1 is buffer for when the current user is included in the search results
let limit = (SBUGlobals.userMentionConfig?.suggestionLimit ?? 0) + 1
let splitCount = min(memberCount, Int(limit))
let resultMembers = Array(matchedMembers[0..<splitCount])
self.suggestedMemberList = SBUUser.convertUsers(resultMembers)
self.delegate?.messageThreadViewModel(
self,
didReceiveSuggestedMentions: self.suggestedMemberList
)
}
}
}
}
/// Cancels loading the suggested mentions.
public func cancelLoadingSuggestedMentions() {
self.debouncer?.cancel()
}
// MARK: - Common
/// Checks if the response of loading message is valid.
/// - Parameters:
/// - messages: Messages loaded.
/// - error: `SBError` from loading messages.
/// - Returns: `true` if response is valid.
private func isValidResponse(messages: [BaseMessage]?, error: SBError?) -> Bool {
if let error = error {
SBULog.error("Couldn't retrieve thread list.: \(error)")
self.isLoadingNext = false
self.delegate?.didReceiveError(error, isBlocker: true)
return false
}
guard messages != nil else {
SBULog.warning("Response of retrieve thread list is nil")
self.isLoadingNext = false
return false
}
return true
}
public override func hasNext() -> Bool {
return self.hasMoreNext
}
public override func hasPrevious() -> Bool {
return self.hasMorePrevious
}
public override func getStartingPoint() -> Int64? {
return self.startingPoint
}
override func reset() {
self.hasMorePrevious = true
self.hasMoreNext = self.startingPoint != nil
self.resetLastUpdatedTimestamp()
self.messageCache = nil
self.resetMessageListParams()
self.isScrollToInitialPositionFinish = false
}
private func resetMessageListParams() {
self.threadedMessageListParams = self.customizedThreadedMessageListParams?.copy() as? ThreadedMessageListParams ?? ThreadedMessageListParams()
if self.threadedMessageListParams.previousResultSize <= 0 {
self.threadedMessageListParams.previousResultSize = self.defaultFetchLimit
}
if self.threadedMessageListParams.nextResultSize <= 0 {
self.threadedMessageListParams.nextResultSize = self.defaultFetchLimit
}
self.threadedMessageListParams.includeReactions = SBUEmojiManager.useReaction(channel: channel)
self.threadedMessageListParams.includeParentMessageInfo = SBUGlobals.reply.includesParentMessageInfo
self.threadedMessageListParams.includeMetaArray = true
}
}
// MARK: - ConnectionDelegate
extension SBUMessageThreadViewModel {
open override func didSucceedReconnection() {
super.didSucceedReconnection()
self.refreshChannel()
if let parentMessageId = self.parentMessageId, let channelURL = channelURL {
self.loadParentMessage(
parentMessageId: parentMessageId,
channelURL: channelURL
) { [weak self] parentMessage, error in
guard error == nil, let parentMessage = parentMessage else {
return
}
self?.parentMessage = parentMessage
if let self = self {
self.delegate?.messageThreadViewModel(self, didLoadParentMessage: parentMessage)
}
self?.loadMessageChangeLogs()
}
}
}
open func didFailReconnection() { }
}
// MARK: - GroupChannelDelegate (parent message, threaded message)
extension SBUMessageThreadViewModel: GroupChannelDelegate {
// Received message
open override func channel(_ channel: BaseChannel, didReceive message: BaseMessage) {
guard self.channel?.channelURL == channel.channelURL else { return }
guard self.parentMessageId == message.parentMessageId else { return }
super.channel(channel, didReceive: message)
self.updateParentMessage()
let isScrollNearBottom = self.dataSource?.baseChannelViewModel(
self,
isScrollNearBottomInChannel: self.channel
) ?? true
if self.hasNext() == true || isScrollNearBottom == false {
guard message is UserMessage || message is FileMessage else { return }
if let channel = self.channel {
self.delegate?.baseChannelViewModel(
self,
didReceiveNewMessage: message,
forChannel: channel
)
}
}
if self.hasNext() == false {
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: [message],
forContext: nil,
keepsScroll: !isScrollNearBottom
)
self.upsertMessagesInList(messages: [message], needReload: true)
}
}
// Updated message
open override func channel(_ channel: BaseChannel, didUpdate message: BaseMessage) {
guard self.channel?.channelURL == channel.channelURL else { return }
if message.messageId == self.parentMessageId {
SBULog.info("Did update message: \(message)")
self.delegate?.messageThreadViewModel(self, didUpdateParentMessage: message)
} else if self.parentMessageId == message.parentMessageId {
SBULog.info("Did update message: \(message)")
self.upsertMessagesInList(messages: [message], needReload: true)
}
}
open override func channel(_ channel: BaseChannel,
didUpdateThreadInfo threadInfoUpdateEvent: ThreadInfoUpdateEvent) {
if self.parentMessage?.messageId == threadInfoUpdateEvent.targetMessageId {
self.parentMessage?.apply(threadInfoUpdateEvent)
if let parentMessage = self.parentMessage {
self.delegate?.messageThreadViewModel(self, didUpdateParentMessage: parentMessage)
}
}
}
open override func channel(_ channel: BaseChannel, updatedReaction reactionEvent: ReactionEvent) {
guard self.channel?.channelURL == channel.channelURL else { return }
let message = self.fullMessageList.filter { $0.messageId == reactionEvent.messageId }.first
if reactionEvent.messageId == self.parentMessageId {
// Parent message
SBULog.info("Did update message: \(String(describing: self.parentMessage))")
if let parentMessage = parentMessage {
if reactionEvent.messageId == parentMessage.messageId {
parentMessage.apply(reactionEvent)
}
self.baseDelegate?.baseChannelViewModel(
self,
didUpdateReaction: reactionEvent,
forMessage: parentMessage
)
}
} else if self.parentMessageId == message?.parentMessageId {
guard let message = message else { return }
// threaded message
SBULog.info("Did update message: \(message.parentMessageId)")
if reactionEvent.messageId == message.messageId {
message.apply(reactionEvent)
}
self.upsertMessagesInList(messages: [message], needReload: true)
}
}
// Deleted message
open override func channel(_ channel: BaseChannel, messageWasDeleted messageId: Int64) {
guard self.channel?.channelURL == channel.channelURL else { return }
if messageId == self.parentMessageId {
self.delegate?.messageThreadViewModelShouldDismissMessageThread(self)
} else {
SBULog.info("Message was deleted: \(messageId)")
for message in self.messageList {
if message.messageId == messageId {
self.delegate?.baseChannelViewModel(self, deletedMessages: [message])
}
}
self.deleteMessagesInList(messageIds: [messageId], needReload: true)
}
}
// MARK: Channel related
open override func channelWasChanged(_ channel: BaseChannel) {
guard self.channel?.channelURL == channel.channelURL else { return }
guard let channel = channel as? GroupChannel else { return }
self.channel = channel
SBULog.info("Channel was changed, ChannelURL:\(channel.channelURL)")
let context = MessageContext(source: .eventChannelChanged, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
open override func channelWasFrozen(_ channel: BaseChannel) {
guard self.channel?.channelURL == channel.channelURL else { return }
guard let channel = channel as? GroupChannel else { return }
SBULog.info("Channel was frozen, ChannelURL:\(channel.channelURL)")
let context = MessageContext(source: .eventChannelFrozen, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
open override func channelWasUnfrozen(_ channel: BaseChannel) {
guard self.channel?.channelURL == channel.channelURL else { return }
guard let channel = channel as? GroupChannel else { return }
SBULog.info("Channel was unfrozen, ChannelURL:\(channel.channelURL)")
let context = MessageContext(source: .eventChannelUnfrozen, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
open override func channel(_ channel: BaseChannel, userWasMuted user: RestrictedUser) {
guard self.channel?.channelURL == channel.channelURL else { return }
if user.userId == SBUGlobals.currentUser?.userId {
SBULog.info("You are muted.")
let context = MessageContext(source: .eventUserMuted, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
}
open override func channel(_ channel: BaseChannel, userWasUnmuted user: User) {
guard self.channel?.channelURL == channel.channelURL else { return }
if user.userId == SBUGlobals.currentUser?.userId {
SBULog.info("You are unmuted.")
let context = MessageContext(source: .eventUserUnmuted, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
}
open override func channelDidUpdateOperators(_ channel: BaseChannel) {
guard self.channel?.channelURL == channel.channelURL else { return }
let context = MessageContext(source: .eventOperatorUpdated, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
open override func channel(_ channel: BaseChannel, userWasBanned user: RestrictedUser) {
guard self.channel?.channelURL == channel.channelURL else { return }
if user.userId == SBUGlobals.currentUser?.userId {
SBULog.info("You are banned.")
self.delegate?.baseChannelViewModel(self, shouldDismissForChannel: channel)
} else {
let context = MessageContext(source: .eventUserBanned, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
}
open func channel(_ channel: GroupChannel, userDidJoin user: User) {
guard self.channel?.channelURL == channel.channelURL else { return }
let context = MessageContext(source: .eventUserJoined, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
open func channel(_ channel: GroupChannel, userDidLeave user: User) {
guard self.channel?.channelURL == channel.channelURL else { return }
if user.userId == SBUGlobals.currentUser?.userId {
self.delegate?.baseChannelViewModel(self, shouldDismissForChannel: channel)
} else {
let context = MessageContext(source: .eventUserLeft, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: channel, withContext: context)
}
}
open override func channelWasDeleted(_ channelURL: String, channelType: ChannelType) {
guard self.channel?.channelURL == channelURL else { return }
let context = MessageContext(source: .eventChannelDeleted, sendingStatus: .succeeded)
self.delegate?.baseChannelViewModel(self, didChangeChannel: nil, withContext: context)
}
}
// MARK: - MessageCollectionDelegate
extension SBUMessageThreadViewModel: MessageCollectionDelegate {
open func messageCollection(_ collection: MessageCollection,
context: MessageContext,
channel: GroupChannel,
addedMessages messages: [BaseMessage]) {
// -> pending, -> receive new message
SBULog.info("messageCollection addedMessages : \(messages.count)")
for addedMessage in messages {
if addedMessage.sendingStatus == .succeeded
|| addedMessage.messageId == self.parentMessageId { continue }
self.pendingMessageManager.upsertPendingMessage(
channelURL: addedMessage.channelURL,
message: addedMessage,
forMessageThread: self.isThreadMessageMode
)
}
self.delegate?.baseChannelViewModel(
self,
shouldUpdateScrollInMessageList: messages,
forContext: context,
keepsScroll: true
)
self.sortAllMessageList(needReload: true)
// Parent message
self.updateParentMessage()
}
open func messageCollection(_ collection: MessageCollection,
context: MessageContext,
channel: GroupChannel,
updatedMessages messages: [BaseMessage]) {
SBULog.info("messageCollection updatedMessages : \(messages.count)")
let parentMessages = messages.filter { $0.messageId == self.parentMessageId }
if let parentMessage = parentMessages.first {
self.delegate?.messageThreadViewModel(self, didUpdateParentMessage: parentMessage)
}
// Edge case - Updates Thread message when resend finished
let threadMessages = messages.filter { $0.parentMessageId == self.parentMessageId }
if threadMessages.isEmpty { return }
self.upsertMessagesInList(messages: threadMessages, needReload: true)
}
open func messageCollection(_ collection: MessageCollection,
context: MessageContext,
channel: GroupChannel,
deletedMessages messages: [BaseMessage]) {
SBULog.info("messageCollection deletedMessages : \(messages.count)")
let parentMessages = messages.filter { $0.messageId == self.parentMessageId }
if let parentMessage = parentMessages.first {
self.delegate?.messageThreadViewModel(self, didUpdateParentMessage: parentMessage)
}
self.loadMessageChangeLogs()
}
open func messageCollection(_ collection: MessageCollection,
context: MessageContext,
updatedChannel channel: GroupChannel) {
SBULog.info("messageCollection changedChannel")
}
open func messageCollection(_ collection: MessageCollection,
context: MessageContext,
deletedChannel channelURL: String) {
SBULog.info("messageCollection deletedChannel")
}
}