Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discard duplicate messages delivered by sockets #724

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class ChatViewModel: EngagementViewModel, ViewModel {
var isViewLoaded: Bool = false
var isChoiceCardInputModeEnabled: Bool = false
private (set) var siteConfiguration: CoreSdkClient.Site?
// Stored message ids retrieved from history.
// They are used to discard messages received from socket
// to avoid duplication.
private (set) var historyMessageIds: Set<ChatMessage.MessageId> = []

var mediaPickerButtonVisibility: MediaPickerButtonVisibility {
guard let site = siteConfiguration else { return .disabled }
Expand Down Expand Up @@ -372,6 +376,10 @@ extension ChatViewModel {
environment.fetchChatHistory { [weak self] result in
guard let self else { return }
let messages = (try? result.get()) ?? []
// Store message ids from history,
// to be able to discard duplicates
// delivered by sockets.
self.historyMessageIds = Set(messages.map(\.id))
let items = messages.compactMap {
ChatItem(
with: $0,
Expand Down Expand Up @@ -548,6 +556,11 @@ extension ChatViewModel {
}

private func receivedMessage(_ message: CoreSdkClient.Message) {
// Discard messages that have been already received from history
// to avoid duplication.
guard !historyMessageIds.contains(message.id) else {
return
}
switch message.sender.type {
case .operator, .system:
let message = ChatMessage(
Expand Down
3 changes: 2 additions & 1 deletion GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ enum ChatMessageSender: Int, Codable {
}

class ChatMessage: Codable {
let id: String
typealias MessageId = String
rasmustautsglia marked this conversation as resolved.
Show resolved Hide resolved
let id: MessageId
var queueID: String?
let `operator`: ChatOperator?
let sender: ChatMessageSender
Expand Down
68 changes: 68 additions & 0 deletions GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,74 @@ class ChatViewModelTests: XCTestCase {
}
XCTAssertEqual(calls, [.scrollToBottom(animated: true)])
}

func test_messagesIdsFromHistoryAreStoredInHistoryMessageIds() {
var viewModelEnv = ChatViewModel.Environment.failing()
viewModelEnv.createFileUploadListModel = { _ in .mock() }
viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] }
viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in }
viewModelEnv.loadChatMessagesFromHistory = { true }
viewModelEnv.fetchSiteConfigurations = { _ in }
let expectedMessageId = "expected_message_id"
viewModelEnv.fetchChatHistory = { callback in
callback(.success([.mock(id: expectedMessageId)]))
}
let viewModel: ChatViewModel = .mock(environment: viewModelEnv)
viewModel.start()
XCTAssertEqual(viewModel.historyMessageIds, [expectedMessageId])
}

func test_messageReceivedFromSocketIsDiscarded() {
var viewModelEnv = ChatViewModel.Environment.failing()
viewModelEnv.createFileUploadListModel = { _ in .mock() }
viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] }
viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in }
viewModelEnv.loadChatMessagesFromHistory = { true }
viewModelEnv.fetchSiteConfigurations = { _ in }
let expectedMessageId = "expected_message_id"
viewModelEnv.fetchChatHistory = { callback in
callback(.success([.mock(id: expectedMessageId)]))
}
let viewModel: ChatViewModel = .mock(environment: viewModelEnv)
viewModel.start()
var actions: [ChatViewModel.Action] = []
viewModel.action = {
actions.append($0)
}
viewModel.interactorEvent(.receivedMessage(.mock(id: expectedMessageId)))
XCTAssertTrue(actions.isEmpty)
}

func test_messageReceivedFromSocketIsNotDiscarded() {
var viewModelEnv = ChatViewModel.Environment.failing()
viewModelEnv.createFileUploadListModel = { _ in .mock() }
viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] }
viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in }
viewModelEnv.loadChatMessagesFromHistory = { true }
viewModelEnv.fetchSiteConfigurations = { _ in }
viewModelEnv.fetchChatHistory = { _ in }

let messageId = "message_id"
let viewModel: ChatViewModel = .mock(environment: viewModelEnv)
viewModel.start()
var actions: [ChatViewModel.Action] = []
viewModel.action = {
actions.append($0)
}
viewModel.interactorEvent(.receivedMessage(.mock(id: messageId)))
enum Call {
case quickReplyPropsUpdatedHidden
}
var calls: [Call] = []
switch actions[0] {
case .quickReplyPropsUpdated(.hidden):
calls.append(.quickReplyPropsUpdatedHidden)
default:
break
}
XCTAssertEqual(actions.count, 1)
XCTAssertEqual(calls, [.quickReplyPropsUpdatedHidden])
}
}

extension ChatChoiceCardOption {
Expand Down