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

Handle ongoing engagement in EntryWidget #1147

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
18 changes: 18 additions & 0 deletions GliaWidgets/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,14 @@ internal enum Localization {
}
}
}
internal enum CallVisualizer {
/// Screen sharing in progress
internal static var desciption: String { Localization.tr("Localizable", "entry_widget.call_visualizer.desciption", fallback: "Screen sharing in progress") }
internal enum Button {
/// Call Visualizer
internal static var label: String { Localization.tr("Localizable", "entry_widget.call_visualizer.button.label", fallback: "Call Visualizer") }
}
}
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 +497,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 {
/// Ongoing • Tap to return
internal static var description: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.description", fallback: "Ongoing • Tap to return") }
internal enum Button {
internal enum Accessibility {
/// Returns to ongoing engagement
internal static var hint: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.button.accessibility.hint", fallback: "Returns to ongoing engagement") }
}
}
}
internal enum SecureMessaging {
internal enum Button {
/// Start a conversation, we’ll get back to you
Expand Down
4 changes: 4 additions & 0 deletions GliaWidgets/Public/Glia/Glia+EntryWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ extension Glia {
hasPendingInteraction: { [weak self] in
guard let self else { return false }
return pendingInteraction?.hasPendingInteraction ?? false
},
currentInteractor: { [weak self] in
guard let self else { return nil }
return 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.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.description" = "Ongoing • Tap to return";
"entry_widget.call_visualizer.desciption" = "The support team can provide interactive assistance";
"entry_widget.ongoing_engagement.button.accessibility.hint" = "Returns to ongoing engagement";
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,8 @@ extension SecureConversations.TranscriptModel {
kind = .chat
case .secureMessaging:
kind = .messaging(.welcome)
case .callVisualizer:
return
}
self?.environment.switchToEngagement(kind)
}))
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 else { return }
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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ extension EntryWidgetStyle.MediaTypeItemStyle {
/// The accessibility hint for secure messaging media type.
public var secureMessagingHint: String

/// The accessibility hint for call visualizer.
public var callVisualizerHint: String

/// - Parameters:
/// - chatHint: The accessibility hint for chat media type.
/// - audioHint: The accessibility hint for audio media type.
/// - videoHint: The accessibility hint for video media type.
/// - secureMessagingHint: The accessibility hint for secure messaging media type.
/// - callVisualizer: The accessibility hint for call visualizer.
///
public init(
chatHint: String,
audioHint: String,
videoHint: String,
secureMessagingHint: String
secureMessagingHint: String,
callVisualizerHint: String
) {
self.chatHint = chatHint
self.audioHint = audioHint
self.videoHint = videoHint
self.secureMessagingHint = secureMessagingHint
self.callVisualizerHint = callVisualizerHint
}

func hint(for item: EntryWidget.MediaTypeItem) -> String {
Expand All @@ -43,6 +49,8 @@ extension EntryWidgetStyle.MediaTypeItemStyle {
return videoHint
case .secureMessaging:
return secureMessagingHint
case .callVisualizer:
return callVisualizerHint
}
}
}
Expand All @@ -54,6 +62,7 @@ extension EntryWidgetStyle.MediaTypeItemStyle.Accessibility {
chatHint: "",
audioHint: "",
videoHint: "",
secureMessagingHint: ""
secureMessagingHint: "",
callVisualizerHint: ""
)
}
Loading
Loading