Skip to content

Commit

Permalink
Make entry widget height dynamic
Browse files Browse the repository at this point in the history
In order to make entry widget height dynamic we need to observe the
mediaTypes and viewState. If mediaTypes are not present, we show the
default full scale of the EntryWidget, by switching the viewState.

MOB-3676
  • Loading branch information
rasmustautsglia committed Oct 17, 2024
1 parent cac42b4 commit aed3878
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 138 deletions.
1 change: 1 addition & 0 deletions GliaWidgets/Public/Glia/Glia+EntryWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extension Glia {
/// - `EntryWidget` instance.
public func getEntryWidget(queueIds: [String]) throws -> EntryWidget {
EntryWidget(
queueIds: queueIds,
environment: .init(
queuesMonitor: environment.queuesMonitor,
engagementLauncher: try getEngagementLauncher(queueIds: queueIds),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import UIKit

final class CustomPresentationController: UIPresentationController {
private let height: CGFloat
private var height: CGFloat
private let cornerRadius: CGFloat
private let dimmingView: UIView = {
let view = UIView()
Expand Down Expand Up @@ -82,3 +82,12 @@ final class CustomPresentationController: UIPresentationController {
}
}
}

extension CustomPresentationController {
func updateHeight(to newHeight: CGFloat) {
self.height = newHeight
UIView.animate(withDuration: 0.3) {
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
}
}
}
10 changes: 5 additions & 5 deletions GliaWidgets/Sources/EntryWidget/EntryWidget.Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import GliaCoreSDK
import SwiftUI

extension EntryWidget {
enum Channel {
case chat
case audio
enum MediaTypeItem: Int {
case video
case audio
case chat
case secureMessaging

var headline: String {
Expand All @@ -17,7 +17,7 @@ extension EntryWidget {
case .video:
return "Video"
case .secureMessaging:
return "SecureMessaging"
return "Secure Messaging"
}
}

Expand Down Expand Up @@ -49,7 +49,7 @@ extension EntryWidget {
}
}

extension EntryWidget.Channel {
extension EntryWidget.MediaTypeItem {
init?(mediaType: MediaType) {
switch mediaType {
case .audio:
Expand Down
216 changes: 129 additions & 87 deletions GliaWidgets/Sources/EntryWidget/EntryWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import SwiftUI
public final class EntryWidget: NSObject {
private var hostedViewController: UIViewController?
private var embeddedView: UIView?
private var queueIds: [String] = []
private var queueIds: [String]

@Published private var viewState: ViewState = .loading

@Published private var channels: [Channel] = []
private let sizeConstraints: SizeConstraints = .init(
singleCellHeight: 72,
singleCellIconSize: 24,
Expand All @@ -24,7 +25,11 @@ public final class EntryWidget: NSObject {

private var cancellables = CancelBag()

init(environment: Environment) {
init(
queueIds: [String],
environment: Environment
) {
self.queueIds = queueIds
self.environment = environment
super.init()

Expand Down Expand Up @@ -55,14 +60,75 @@ public final class EntryWidget: NSObject {
}
}

// MARK: - Private methods
private extension EntryWidget {
func handleQueuesMonitorUpdates(state: QueuesMonitor.State) {
switch state {
case .idle:
viewState = .loading
case .updated(let queues):
let availableMediaTypes = resolveAvailableMediaTypes(from: queues)
if availableMediaTypes.isEmpty {
viewState = .offline
} else {
viewState = .mediaTypes(availableMediaTypes)
}
case .failed:
viewState = .error
print("Failed to update queues")
}
}

func resolveAvailableMediaTypes(from queues: [Queue]) -> [MediaTypeItem] {
var availableMediaTypes: Set<MediaTypeItem> = []

queues.forEach { queue in
queue.state.media.forEach { mediaType in
if let mediaTypeItem = MediaTypeItem(mediaType: mediaType) {
availableMediaTypes.insert(mediaTypeItem)
}
}
}
return Array(availableMediaTypes).sorted(by: { $0.rawValue < $1.rawValue })
}

func mediaTypeSelected(_ mediaTypeItem: MediaTypeItem) {
do {
switch mediaTypeItem {
case .chat:
try environment.engagementLauncher.startChat()
case .audio:
try environment.engagementLauncher.startAudioCall()
case .video:
try environment.engagementLauncher.startVideoCall()
case .secureMessaging:
try environment.engagementLauncher.startSecureMessaging()
}
} catch {
viewState = .error
}
}

func makeViewModel(showHeader: Bool) -> EntryWidgetView.Model {
let viewModel = EntryWidgetView.Model(
theme: environment.theme,
showHeader: showHeader,
sizeConstraints: sizeConstraints,
viewStatePublisher: $viewState,
mediaTypeSelected: mediaTypeSelected(_:)
)

viewModel.retryMonitoring = { [weak self] in
self?.viewState = .loading
self?.environment.queuesMonitor.startMonitoring(queuesIds: self?.queueIds ?? [])
}

return viewModel
}

func showView(in parentView: UIView) {
parentView.subviews.forEach { $0.removeFromSuperview() }
let model = makeViewModel(
showHeader: false,
channels: channels,
selection: channelSelected(_:)
)
let model = makeViewModel(showHeader: false)
let view = makeView(model: model)
let hostingController = UIHostingController(rootView: view)

Expand All @@ -80,11 +146,7 @@ private extension EntryWidget {
}

func showSheet(in parentViewController: UIViewController) {
let model = makeViewModel(
showHeader: true,
channels: channels,
selection: channelSelected(_:)
)
let model = makeViewModel(showHeader: true)
let view = makeView(model: model)
let hostingController = UIHostingController(rootView: view)

Expand All @@ -106,10 +168,7 @@ private extension EntryWidget {
if #available(iOS 16.0, *) {
guard let sheet = hostingController.sheetPresentationController else { return }
let smallDetent: UISheetPresentationController.Detent = .custom { _ in
return self.calculateHeight(
channels: self.channels,
sizeConstraints: self.sizeConstraints
)
return self.calculateHeight()
}
sheet.detents = [smallDetent]
sheet.prefersScrollingExpandsWhenScrolledToEdge = true
Expand All @@ -121,53 +180,79 @@ private extension EntryWidget {

parentViewController.present(hostingController, animated: true, completion: nil)
hostedViewController = hostingController

observeViewState()
}

func makeView(model: EntryWidgetView.Model) -> EntryWidgetView {
.init(model: model)
func observeViewState() {
$viewState
.receive(on: RunLoop.main)
.sink { [weak self] state in
self?.viewStateDidChange(state)
}
.store(in: &cancellables)
}

func makeViewModel(
showHeader: Bool,
channels: [Channel],
selection: @escaping (Channel) throws -> Void
) -> EntryWidgetView.Model {
.init(
theme: environment.theme,
showHeader: showHeader,
sizeConstrainsts: sizeConstraints,
channels: $channels,
channelSelected: selection
)
func viewStateDidChange(_ state: ViewState) {
let newHeight = calculateHeight()

if #available(iOS 16.0, *) {
if let sheet = hostedViewController?.sheetPresentationController {
let smallDetent = UISheetPresentationController.Detent.custom { _ in
return newHeight
}
sheet.detents = [smallDetent]
sheet.animateChanges {
sheet.selectedDetentIdentifier = nil
}
}
} else {
if let customPresentationController = hostedViewController?.presentationController as? CustomPresentationController {
customPresentationController.updateHeight(to: newHeight)
}
}
}

func calculateHeight(
channels: [Channel],
sizeConstraints: SizeConstraints
) -> CGFloat {
func calculateHeight() -> CGFloat {
var mediaTypesCount: Int
switch viewState {
case .mediaTypes(let mediaTypes):
mediaTypesCount = mediaTypes.count
case .loading, .error, .offline:
// 4 gives the desired fixed size
mediaTypesCount = 4
}
var appliedHeight: CGFloat = 0

appliedHeight += sizeConstraints.sheetHeaderHeight
channels.forEach { _ in
appliedHeight += sizeConstraints.singleCellHeight
appliedHeight += sizeConstraints.dividerHeight
}
appliedHeight += CGFloat(mediaTypesCount) * (sizeConstraints.singleCellHeight + sizeConstraints.dividerHeight)
appliedHeight += sizeConstraints.poweredByContainerHeight

return appliedHeight
}

func makeView(model: EntryWidgetView.Model) -> EntryWidgetView {
.init(model: model)
}
}

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

// MARK: - UIViewControllerTransitioningDelegate
extension EntryWidget: UIViewControllerTransitioningDelegate {
public func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
let height = calculateHeight(
channels: channels,
sizeConstraints: sizeConstraints
)
let height = calculateHeight()
return CustomPresentationController(
presentedViewController: presented,
presenting: presenting,
Expand All @@ -176,46 +261,3 @@ extension EntryWidget: UIViewControllerTransitioningDelegate {
)
}
}

private extension EntryWidget {
func handleQueuesMonitorUpdates(state: QueuesMonitor.State) {
switch state {
case .idle:
break
case .updated(let queues):
let availableChannels = resolveAvailableChannels(from: queues)
self.channels = availableChannels
case .failed(let error):
// TODO: Handle error on EntryWidgetView
print(error)
}
}

func resolveAvailableChannels(from queues: [Queue]) -> [Channel] {
var availableChannels: Set<Channel> = []

queues.forEach { queue in
queue.state.media.forEach { mediaType in
guard let channel = Channel(mediaType: mediaType) else {
return
}
availableChannels.insert(channel)
}
}
// TODO: Add sorting for representing on UI
return Array(availableChannels)
}

func channelSelected(_ channel: Channel) throws {
switch channel {
case .chat:
try environment.engagementLauncher.startChat()
case .audio:
try environment.engagementLauncher.startAudioCall()
case .video:
try environment.engagementLauncher.startVideoCall()
case .secureMessaging:
try environment.engagementLauncher.startSecureMessaging()
}
}
}
Loading

0 comments on commit aed3878

Please sign in to comment.