Skip to content

Commit

Permalink
Retry logic for chat engagement
Browse files Browse the repository at this point in the history
This commit adds retry logic for regular chat engagement:
- marking failed messages;
- tap actions for undelivered messages;
- replacement logic after successful retry;
- extends Outgoing message model with `relation` property to keep association with response cards;

MOB-3597
  • Loading branch information
Egor Egorov authored and github-review-helper committed Sep 30, 2024
1 parent e7cf674 commit 1d1c18d
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 41 deletions.
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,49 @@ extension SecureConversations.ChatWithTranscriptModel {
}
// swiftlint:enable function_body_length

static func markMessageAsFailed(
_ 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))
}

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
32 changes: 29 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,22 @@ extension ChatView {
}
}

func deleteRows(_ rows: [Int], in section: Int, animated: Bool) {
let refreshBlock = {
self.tableView.beginUpdates()
let indexPaths = rows.map { IndexPath(row: $0, section: section) }
self.tableView.deleteRows(at: indexPaths, with: .fade)
self.tableView.endUpdates()
}
if animated {
refreshBlock()
} else {
UIView.performWithoutAnimation {
refreshBlock()
}
}
}

func refreshAll() {
tableView.reloadData()
}
Expand Down Expand Up @@ -345,8 +362,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 +684,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 +712,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

0 comments on commit 1d1c18d

Please sign in to comment.