Skip to content

Commit

Permalink
Handle ongoing engagement in EntryWidget
Browse files Browse the repository at this point in the history
This PR handles ongoing engagement in EntryWidget for both core engagements
and call visualizer.

MOB-3806
  • Loading branch information
rasmustautsglia committed Dec 9, 2024
1 parent 517d35c commit e01f846
Show file tree
Hide file tree
Showing 17 changed files with 364 additions and 40 deletions.
20 changes: 20 additions & 0 deletions GliaWidgets/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,16 @@ internal enum Localization {
}
}
}
internal enum CallVisualizer {
internal enum Button {
/// Call Visualizer
internal static var label: String { Localization.tr("Localizable", "entry_widget.call_visualizer.button.label", fallback: "Call Visualizer") }
internal enum Accessibility {
/// Starts call visualizer session
internal static var hint: String { Localization.tr("Localizable", "entry_widget.call_visualizer.button.accessibility.hint", fallback: "Starts call visualizer session") }
}
}
}
internal enum EmptyState {
/// We are here to assist you during our business hours.
internal static var description: String { Localization.tr("Localizable", "entry_widget.empty_state.description", fallback: "We are here to assist you during our business hours.") }
Expand Down Expand Up @@ -489,6 +499,16 @@ internal enum Localization {
internal static var label: String { Localization.tr("Localizable", "entry_widget.loading.accessibility.label", fallback: "Loading indicator. Waiting for available options.") }
}
}
internal enum OngoingEngagement {
internal enum CallVisualizer {
/// Screen sharing in progress
internal static var message: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.call_visualizer.message", fallback: "Screen sharing in progress") }
}
internal enum Core {
/// Ongoing • Tap to return
internal static var message: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.core.message", fallback: "Ongoing • Tap to return") }
}
}
internal enum SecureMessaging {
internal enum Button {
/// Start a conversation, we’ll get back to you
Expand Down
3 changes: 2 additions & 1 deletion GliaWidgets/Public/Glia/Glia+EntryWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ extension Glia {
hasPendingInteraction: { [weak self] in
guard let self else { return false }
return pendingInteraction.hasPendingInteraction
}
},
currentInteractor: { self.interactor }
)
)
}
Expand Down
4 changes: 4 additions & 0 deletions GliaWidgets/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,13 @@
"entry_widget.secure_messaging.button.label" = "Secure Messaging";
"entry_widget.secure_messaging.button.description" = "Start a conversation, we’ll get back to you";
"entry_widget.secure_messaging.button.accessibility.hint" = "Starts messaging with us";
"entry_widget.call_visualizer.button.label" = "Call Visualizer";
"entry_widget.call_visualizer.button.accessibility.hint" = "Starts call visualizer session";
"entry_widget.empty_state.title" = "Support team is currently offline";
"entry_widget.empty_state.description" = "We are here to assist you during our business hours.";
"entry_widget.error_state.title" = "Could not load the contacts";
"entry_widget.error_state.description" = "We could not load the contacts at this time. This may be due to a temporary syncing issue or network problem.";
"entry_widget.error_state.try_again.button.label" = "Try again";
"entry_widget.loading.accessibility.label" = "Loading indicator. Waiting for available options.";
"entry_widget.ongoing_engagement.core.message" = "Ongoing • Tap to return";
"entry_widget.ongoing_engagement.call_visualizer.message" = "Screen sharing in progress";
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,8 @@ extension SecureConversations.TranscriptModel {
kind = .chat
case .secureMessaging:
kind = .messaging(.welcome)
case .callVisualizer:
return
}
self?.environment.switchToEngagement(kind)
}))
Expand Down
1 change: 1 addition & 0 deletions GliaWidgets/Sources/CallVisualizer/CallVisualizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ extension CallVisualizer {
func startObservingInteractorEvents() {
environment.interactorProviding()?.addObserver(self) { [weak self] event in
if case .stateChanged(.ended(.byOperator)) = event {
self?.environment.interactorProviding()?.currentEngagement = nil
self?.endSession()
self?.environment.log.prefixed(Self.self).info("Call visualizer engagement ended")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extension EntryWidget {
var log: CoreSdkClient.Logger
var isAuthenticated: () -> Bool
var hasPendingInteraction: () -> Bool
var currentInteractor: () -> Interactor?
}
}

Expand All @@ -25,7 +26,8 @@ extension EntryWidget.Environment {
theme: .mock(),
log: .mock,
isAuthenticated: { true },
hasPendingInteraction: { false }
hasPendingInteraction: { false },
currentInteractor: { .mock() }
)
}
}
Expand Down
11 changes: 11 additions & 0 deletions GliaWidgets/Sources/EntryWidget/EntryWidget.MediaTypeItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ extension EntryWidget {
hintline: Localization.EntryWidget.SecureMessaging.Button.Accessibility.hint,
image: Asset.mcEnvelope.image
)
case .callVisualizer:
self.init(
type: type,
badgeCount: 0,
headline: "Call Visualizer",
subheadline: "",
hintline: "",
image: Asset.screensharing.image
)
}
}
}
Expand All @@ -73,13 +82,15 @@ extension EntryWidget {
case audio
case chat
case secureMessaging
case callVisualizer

var description: String {
switch self {
case .video: return "video"
case .audio: return "audio"
case .chat: return "chat"
case .secureMessaging: return "secureMessaging"
case .callVisualizer: return "callVisualizer"
}
}
}
Expand Down
76 changes: 53 additions & 23 deletions GliaWidgets/Sources/EntryWidget/EntryWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ public final class EntryWidget: NSObject {
private let environment: Environment
@Published private var unreadSecureMessageCount: Int?
private(set) var unreadSecureMessageSubscriptionId: String?

private var engagementCancellable: AnyCancellable?
@Published var viewState: ViewState = .loading
@Published private(set) var availableEngagementTypes: [EntryWidget.EngagementType] = []
private var ongoingEngagement: Engagement?

// MARK: - Initialization

Expand All @@ -43,6 +44,13 @@ public final class EntryWidget: NSObject {
Publishers.CombineLatest(environment.queuesMonitor.$state, $unreadSecureMessageCount)
.sink(receiveValue: handleQueuesMonitorUpdates(state:unreadSecureMessagesCount:))
.store(in: &cancellables)

engagementCancellable = environment.currentInteractor()?.$currentEngagement
.sink { [weak self] engagement in
guard let self = self else { return }
self.ongoingEngagement = engagement
handleQueuesMonitorUpdates(state: environment.queuesMonitor.state, unreadSecureMessagesCount: unreadSecureMessageCount)
}
}

deinit {
Expand Down Expand Up @@ -96,6 +104,8 @@ extension EntryWidget {
case .loading, .error, .offline:
// 4 gives the desired fixed size
mediaTypesCount = 4
case .ongoingEngagement:
mediaTypesCount = 1
}
var appliedHeight: CGFloat = 0
appliedHeight += configuration.sizeConstraints.sheetHeaderHeight
Expand All @@ -108,30 +118,47 @@ extension EntryWidget {

// MARK: - Private methods
private extension EntryWidget {
func handleQueuesMonitorUpdates(state: QueuesMonitor.State, unreadSecureMessagesCount: Int?) {
switch state {
case .idle:
viewState = .loading
case .updated(let queues):
let availableEngagementTypes = resolveAvailableEngagementTypes(from: queues)
if availableEngagementTypes.isEmpty {
viewState = .offline
} else {
let mediaTypes = availableEngagementTypes.map { type in
if type == .secureMessaging {
return EntryWidget.MediaTypeItem(
type: type,
badgeCount: unreadSecureMessagesCount ?? 0
)
func handleQueuesMonitorUpdates(
state: QueuesMonitor.State,
unreadSecureMessagesCount: Int?
) {
if let ongoingEngagement {
if ongoingEngagement.source == .callVisualizer {
viewState = .ongoingEngagement(.callVisualizer)
} else if ongoingEngagement.source == .coreEngagement {
if ongoingEngagement.mediaStreams.video != nil {
viewState = .ongoingEngagement(.video)
} else if ongoingEngagement.mediaStreams.audio != nil {
viewState = .ongoingEngagement(.audio)
} else {
viewState = .ongoingEngagement(.chat)
}
}
} else {
switch state {
case .idle:
viewState = .loading
case .updated(let queues):
let availableEngagementTypes = resolveAvailableEngagementTypes(from: queues)
if availableEngagementTypes.isEmpty {
viewState = .offline
} else {
let mediaTypes = availableEngagementTypes.map { type in
if type == .secureMessaging {
return EntryWidget.MediaTypeItem(
type: type,
badgeCount: unreadSecureMessagesCount ?? 0
)
}
return EntryWidget.MediaTypeItem(type: type)
}
return EntryWidget.MediaTypeItem(type: type)
viewState = .mediaTypes(mediaTypes)
}
viewState = .mediaTypes(mediaTypes)
self.availableEngagementTypes = availableEngagementTypes
case .failed(let error):
viewState = .error
environment.log.prefixed(Self.self).error("Setting up queues. Failed to get site queues \(error)")
}
self.availableEngagementTypes = availableEngagementTypes
case .failed(let error):
viewState = .error
environment.log.prefixed(Self.self).error("Setting up queues. Failed to get site queues \(error)")
}
}

Expand Down Expand Up @@ -178,6 +205,8 @@ private extension EntryWidget {
try self.environment.engagementLauncher.startVideoCall()
case .secureMessaging:
try self.environment.engagementLauncher.startSecureMessaging()
case .callVisualizer:
break
}
} catch {
self.viewState = .error
Expand Down Expand Up @@ -314,11 +343,12 @@ private extension EntryWidget {

// MARK: - View State
extension EntryWidget {
enum ViewState {
enum ViewState: Equatable {
case loading
case mediaTypes([MediaTypeItem])
case offline
case error
case ongoingEngagement(EngagementType)
}
}

Expand Down
65 changes: 64 additions & 1 deletion GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct EntryWidgetView: View {
mediaTypesView(types)
case .offline:
offilineView()
case let .ongoingEngagement(engagementType):
ongoingEngagementTypeView(engagementType)
}
}
}
Expand Down Expand Up @@ -156,7 +158,7 @@ private extension EntryWidgetView {
func headerView() -> some View {
VStack {
Capsule(style: .continuous)
.fill(model.style.dividerColor.swiftUIColor())
.fill(model.style.dividerColor.swiftUIColor().opacity(0.4))
.width(model.configuration.sizeConstraints.sheetHeaderDraggerWidth)
.height(model.configuration.sizeConstraints.sheetHeaderDraggerHeight)
}
Expand Down Expand Up @@ -222,6 +224,7 @@ private extension EntryWidgetView {
func icon(_ image: UIImage) -> some View {
image.asSwiftUIImage()
.resizable()
.renderingMode(.template)
.fit()
.width(model.configuration.sizeConstraints.singleCellIconSize)
.height(model.configuration.sizeConstraints.singleCellIconSize)
Expand Down Expand Up @@ -287,3 +290,63 @@ private extension EntryWidgetView {
}
}
}

// Views related to ongoing engagement view state
private extension EntryWidgetView {
@ViewBuilder
func ongoingEngagementMediaTypes(
mediaType: EntryWidget.MediaTypeItem,
engagementType: EntryWidget.EngagementType
) -> some View {
VStack(spacing: 0) {
HStack(spacing: 16) {
icon(mediaType.image)
VStack(alignment: .leading, spacing: 2) {
headlineText(model.style.mediaTypeItem.title(for: mediaType))
if !sizeCategory.isAccessibilityCategory {
Text(model.ongoingEngagementLabel(for: engagementType))
.setFont(model.style.mediaTypeItem.ongoingEngagementMessageFont)
.setColor(model.style.mediaTypeItem.ongoingEngagementMessageColor)
}
}
unreadMessageCountView(for: mediaType)
}
.maxWidth(alignment: .leading)
.height(model.configuration.sizeConstraints.singleCellHeight)
.applyColorTypeBackground(model.style.mediaTypeItem.backgroundColor)
.contentShape(.rect)
.accessibilityElement(children: .combine)
.accessibility(addTraits: .isButton)
.accessibilityHint(model.style.mediaTypeItem.accessibility.hint(for: mediaType))
.accessibilityIdentifier("entryWidget_\(mediaType.type)_item")
.onTapGesture {
model.selectMediaType(mediaType)
}
.padding(.horizontal)
Divider()
.height(model.configuration.sizeConstraints.dividerHeight)
.setColor(model.style.dividerColor)
.padding(.horizontal, model.configuration.sizeConstraints.dividerHorizontalPadding)
}
}

@ViewBuilder
func ongoingEngagementTypeView(_ engagementType: EntryWidget.EngagementType) -> some View {
VStack(spacing: 0) {
if model.showHeader {
headerView()
.padding(.horizontal)
}
ongoingEngagementMediaTypes(
mediaType: .init(type: engagementType),
engagementType: engagementType
)
if model.showPoweredBy {
poweredByView()
.padding(.horizontal)
}
}
.maxSize()
.applyColorTypeBackground(model.style.backgroundColor)
}
}
9 changes: 9 additions & 0 deletions GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,14 @@ extension EntryWidgetView {
func onTryAgainTapped() {
retryMonitoring?()
}

func ongoingEngagementLabel(for engagementType: EntryWidget.EngagementType) -> String {
switch engagementType {
case .video, .audio, .chat, .secureMessaging:
return style.mediaTypeItem.ongoingCoreEngagementMessage
case .callVisualizer:
return style.mediaTypeItem.ongoingCallVisualizerMessage
}
}
}
}
Loading

0 comments on commit e01f846

Please sign in to comment.