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

Retry logic for chat engagement #1056

Merged
merged 1 commit into from
Sep 30, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ extension SecureConversations.ChatWithTranscriptModel {
guard let index = section.items
.enumerated()
.first(where: {
guard case .outgoingMessage(let message) = $0.element.kind else { return false }
guard case .outgoingMessage(let message, _) = $0.element.kind else { return false }
return message.payload.messageId == outgoingMessage.payload.messageId
})?.offset
else { return }
Expand Down Expand Up @@ -197,7 +197,7 @@ extension SecureConversations.ChatWithTranscriptModel {
messageId: ChatMessage.MessageId
) -> Bool {
switch chatItem.kind {
case let .outgoingMessage(outgoingMessage):
case let .outgoingMessage(outgoingMessage, _):
return outgoingMessage.payload.messageId.rawValue.uppercased() == messageId.uppercased()
case let .visitorMessage(message, _):
return message.id.uppercased() == messageId.uppercased()
Expand Down Expand Up @@ -311,6 +311,51 @@ extension SecureConversations.ChatWithTranscriptModel {
}
// swiftlint:enable function_body_length

// TODO: - This will be covered with unit tests in next PR
static func markMessageAsFailed(
igorkravchenko marked this conversation as resolved.
Show resolved Hide resolved
_ outgoingMessage: OutgoingMessage,
in section: Section<ChatItem>,
message: String,
action: ActionCallback?
) {
guard let index = section.items
.enumerated()
.first(where: { _, element in
Self.chatItemMatchesMessageId(
chatItem: element,
messageId: outgoingMessage.payload.messageId.rawValue
)
})?.offset
else { return }

let item = ChatItem(kind: .outgoingMessage(
outgoingMessage,
error: message
))
section.replaceItem(at: index, with: item)
action?(.refreshRows([index], in: section.index, animated: false))
}

// TODO: - This will be covered with unit tests in next PR
static func removeMessage(
_ outgoingMessage: OutgoingMessage,
in section: Section<ChatItem>,
action: ActionCallback?
) {
guard let index = section.items
.enumerated()
.first(where: { _, element in
Self.chatItemMatchesMessageId(
chatItem: element,
messageId: outgoingMessage.payload.messageId.rawValue
)
})?.offset
else { return }

section.removeItem(at: index)
action?(.deleteRows([index], in: section.index, animated: true))
}

static private func shouldShowOperatorImage(
for row: Int,
in section: Section<ChatItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private extension SecureConversations.TranscriptModel {
let outgoingMessage = OutgoingMessage(payload: payload)

appendItem(
.init(kind: .outgoingMessage(outgoingMessage)),
.init(kind: .outgoingMessage(outgoingMessage, error: nil)),
to: pendingSection,
animated: true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ extension SecureConversations {
break
case let .gvaButtonTapped(option):
gvaOptionAction(for: option)()
case let .retryMessageTapped(message):
// Will be handled in next PR
break
}
}

Expand Down Expand Up @@ -265,7 +268,7 @@ extension SecureConversations.TranscriptModel {
)

appendItem(
.init(kind: .outgoingMessage(outgoingMessage)),
.init(kind: .outgoingMessage(outgoingMessage, error: nil)),
to: pendingSection,
animated: true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ extension ChatCoordinator {
isWindowVisible: isWindowVisible,
startAction: startAction,
deliveredStatusText: viewFactory.theme.chat.visitorMessageStyle.delivered,
failedToDeliverStatusText: viewFactory.theme.chat.visitorMessageStyle.failedToDeliver,
chatType: chatType,
environment: .create(
with: environment,
Expand Down
16 changes: 16 additions & 0 deletions GliaWidgets/Sources/Extensions/UITableView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ internal extension UITableView {
self.scrollToRow(at: indexPath, at: .bottom, animated: animated)
}
}

func deleteRows(_ rows: [Int], in section: Int, animated: Bool) {
let refreshBlock = {
self.beginUpdates()
let indexPaths = rows.map { IndexPath(row: $0, section: section) }
self.deleteRows(at: indexPaths, with: .fade)
self.endUpdates()
}
if animated {
refreshBlock()
} else {
UIView.performWithoutAnimation {
refreshBlock()
}
}
}
}
20 changes: 17 additions & 3 deletions GliaWidgets/Sources/View/Chat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ChatView: EngagementView {
var linkTapped: ((URL) -> Void)?
var selectCustomCardOption: ((HtmlMetadata.Option, MessageRenderer.Message.Identifier) -> Void)?
var gvaButtonTapped: ((GvaOption) -> Void)?
var retryMessageTapped: ((OutgoingMessage) -> Void)?

let style: ChatStyle
let environment: Environment
Expand Down Expand Up @@ -310,6 +311,10 @@ extension ChatView {
}
}

func deleteRows(_ rows: [Int], in section: Int, animated: Bool) {
EgorovEI marked this conversation as resolved.
Show resolved Hide resolved
tableView.deleteRows(rows, in: section, animated: animated)
}

func refreshAll() {
tableView.reloadData()
}
Expand Down Expand Up @@ -345,8 +350,8 @@ extension ChatView {
switch item.kind {
case .queueOperator:
return .queueOperator(connectView)
case let .outgoingMessage(message):
return outgoingMessageContent(message)
case let .outgoingMessage(message, error):
return outgoingMessageContent(message, error: error)
case let .visitorMessage(message, status):
return visitorMessageContent(message, status: status)
case let .operatorMessage(message, showsImage, imageUrl):
Expand Down Expand Up @@ -667,7 +672,10 @@ extension ChatView {
return .systemMessage(view)
}

private func outgoingMessageContent(_ message: OutgoingMessage) -> ChatItemCell.Content {
private func outgoingMessageContent(
_ message: OutgoingMessage,
error: String?
) -> ChatItemCell.Content {
let view = VisitorChatMessageView(
with: style.visitorMessageStyle,
environment: .create(with: environment)
Expand All @@ -692,6 +700,12 @@ extension ChatView {
)
view.fileTapped = { [weak self] in self?.fileTapped?($0) }
view.linkTapped = { [weak self] in self?.linkTapped?($0) }
view.error = error
if error != nil {
view.messageTapped = { [weak self] in
self?.retryMessageTapped?(message)
}
}
return .outgoingMessage(view)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ class VisitorChatMessageView: ChatMessageView {
set { statusLabel.text = newValue }
}

var error: String? {
get { return errorLabel.text }
set { errorLabel.text = newValue }
}

var messageTapped: (() -> Void)?

private let statusLabel = UILabel().makeView()
private let errorLabel = UILabel().makeView()
private let contentInsets = UIEdgeInsets(top: 8, left: 88, bottom: 8, right: 16)

init(
Expand Down Expand Up @@ -42,6 +50,13 @@ class VisitorChatMessageView: ChatMessageView {
style.accessibility.isFontScalingEnabled,
for: statusLabel
)

errorLabel.font = style.error.font
errorLabel.textColor = UIColor(hex: style.error.color)
setFontScalingEnabled(
style.accessibility.isFontScalingEnabled,
for: errorLabel
)
}

override func defineLayout() {
Expand All @@ -55,6 +70,24 @@ class VisitorChatMessageView: ChatMessageView {
addSubview(statusLabel)
constraints += statusLabel.topAnchor.constraint(equalTo: contentViews.bottomAnchor, constant: 2)
constraints += statusLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -contentInsets.right)
constraints += statusLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -contentInsets.bottom)

addSubview(errorLabel)
constraints += errorLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 0)
constraints += errorLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -contentInsets.right)
constraints += errorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -contentInsets.bottom)

defineTapGestureRecognizer()
}

func defineTapGestureRecognizer() {
let tapRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(tapped)
)
addGestureRecognizer(tapRecognizer)
}

@objc private func tapped() {
messageTapped?()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ final class ChatViewController: EngagementViewController, PopoverPresenter {
viewModel.event(.gvaButtonTapped(option))
}

view.retryMessageTapped = { message in
viewModel.event(.retryMessageTapped(message))
}

var viewModel = viewModel

viewModel.action = { [weak self, weak view] action in
Expand Down Expand Up @@ -131,6 +135,8 @@ final class ChatViewController: EngagementViewController, PopoverPresenter {
view?.refreshRows(rows, in: section, animated: animated)
case let .refreshSection(section, animated):
view?.refreshSection(section, animated: animated)
case let .deleteRows(rows, in: section, animated: animated):
view?.deleteRows(rows, in: section, animated: animated)
case .refreshAll:
view?.refreshAll()
case .scrollToBottom(let animated):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension ChatViewModel {
}
}

private func respond(to choiceCardId: String, with selection: String?) {
func respond(to choiceCardId: String, with selection: String?) {
// In the case of upgrading a secure conversation to a live chat,
// there's a bug (MSG-483) that sends the welcome message web socket event before the
// start engagement event. This means that we display it in the `pendingSection`
Expand Down
26 changes: 16 additions & 10 deletions GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+CustomCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ extension ChatViewModel {
)

let payload = environment.createSendMessagePayload(option.text, attachment)
let outgoingMessage = OutgoingMessage(payload: payload)
let outgoingMessage = OutgoingMessage(
payload: payload,
relation: .customCard(messageId: messageId)
)

registerReceivedMessage(messageId: payload.messageId.rawValue)

Expand All @@ -31,7 +34,7 @@ extension ChatViewModel {

self.updateCustomCard(
messageId: messageId,
selectedOption: option,
selectedOptionValue: option.value,
isActive: false
)
self.replace(
Expand All @@ -43,29 +46,32 @@ extension ChatViewModel {
self.action?(.scrollToBottom(animated: true))
}

let failure: (CoreSdkClient.SalemoveError) -> Void = { [weak self] error in
let failure: () -> Void = { [weak self] in
guard let self = self else { return }
self.updateCustomCard(
messageId: messageId,
selectedOption: nil,
selectedOptionValue: nil,
isActive: true
)
self.engagementAction?(.showAlert(.error(error: error.error)))
self.markMessageAsFailed(
outgoingMessage,
in: self.messagesSection
)
}

interactor.send(messagePayload: payload) { result in
switch result {
case let .success(message):
success(message)
case let .failure(error):
failure(error)
case .failure:
failure()
}
}
}

private func updateCustomCard(
func updateCustomCard(
messageId: MessageRenderer.Message.Identifier,
selectedOption: HtmlMetadata.Option?,
selectedOptionValue: String?,
isActive: Bool
) {
guard let index = messagesSection.items
Expand All @@ -85,7 +91,7 @@ extension ChatViewModel {
_
) = customCardItem.kind else { return }

message.attachment?.selectedOption = selectedOption?.value
message.attachment?.selectedOption = selectedOptionValue
let item = ChatItem(kind: .customCard(
message,
showsImage: showsImage,
Expand Down
7 changes: 5 additions & 2 deletions GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ private extension ChatViewModel {
in: self.messagesSection
)
}
case let .failure(error):
self.engagementAction?(.showAlert(.error(error: error.error)))
case .failure:
self.markMessageAsFailed(
outgoingMessage,
in: self.messagesSection
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension ChatViewModel: ViewModel {
messageId: MessageRenderer.Message.Identifier
)
case gvaButtonTapped(GvaOption)
case retryMessageTapped(OutgoingMessage)
}

enum Action {
Expand All @@ -37,6 +38,7 @@ extension ChatViewModel: ViewModel {
case refreshRow(Int, in: Int, animated: Bool)
case refreshRows([Int], in: Int, animated: Bool)
case refreshSection(Int, animated: Bool = false)
case deleteRows([Int], in: Int, animated: Bool)
case refreshAll
case scrollToBottom(animated: Bool)
case updateItemsUserImage(animated: Bool)
Expand Down
2 changes: 2 additions & 0 deletions GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension ChatViewModel {
isWindowVisible: ObservableValue<Bool> = .init(with: true),
startAction: StartAction = .startEngagement,
deliveredStatusText: String = "Delivered",
failedToDeliverStatusText: String = "Failed",
chatType: ChatViewModel.ChatType = .nonAuthenticated,
environment: Environment = .mock,
maximumUploads: () -> Int = { 2 }
Expand All @@ -25,6 +26,7 @@ extension ChatViewModel {
isWindowVisible: isWindowVisible,
startAction: startAction,
deliveredStatusText: deliveredStatusText,
failedToDeliverStatusText: failedToDeliverStatusText,
chatType: chatType,
environment: environment,
maximumUploads: maximumUploads
Expand Down
Loading
Loading