AzureCommunicationUI/AzureCommunicationUIDemoApp/Sources/Views/ChatDemoView.swift (229 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import AzureCommunicationUIChat import AzureCommunicationCommon import SwiftUI struct ChatDemoView: View { private enum Constant { static let oneMillisecond: UInt64 = 10_000_000 } @State var isErrorDisplayed = false @ObservedObject var envConfigSubject: EnvConfigSubject @State var isShowingChatView = false @State var errorMessage: String = "" let verticalPadding: CGFloat = 5 let horizontalPadding: CGFloat = 10 @State var chatAdapter: ChatAdapter? var body: some View { NavigationView { VStack { NavigationLink(destination: chatView, isActive: $isShowingChatView) { EmptyView() } launchView } .navigationTitle("UI Library - Chat Sample") .navigationBarTitleDisplayMode(.inline) }.onAppear { AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait }.onDisappear { AppDelegate.orientationLock = UIInterfaceOrientationMask.all }.modifier(ErrorView(isPresented: $isErrorDisplayed, errorMessage: errorMessage, onDismiss: { isErrorDisplayed = false self.isShowingChatView = false self.chatAdapter?.disconnect(completionHandler: { result in self.onDisconnectFromChat(with: result) }) })) } var launchView: some View { VStack { acsTokenSelector displayNameTextField userIdField endpointUrlField meetingSelector startExperience Spacer() } .padding() } var chatView: some View { VStack { if let chatAdapter = chatAdapter { ChatCompositeView(with: chatAdapter) .navigationTitle("Chat") .navigationBarTitleDisplayMode(.inline) } else { EmptyView() .navigationTitle("Failed to start chat") .navigationBarTitleDisplayMode(.inline) } } } var acsTokenSelector: some View { Group { Picker("Token Type", selection: $envConfigSubject.selectedAcsTokenType) { Text("Token URL").tag(ACSTokenType.tokenUrl) Text("Token").tag(ACSTokenType.token) }.pickerStyle(.segmented) switch envConfigSubject.selectedAcsTokenType { case .tokenUrl: TextField("ACS Token URL", text: $envConfigSubject.acsTokenUrl) .disableAutocorrection(true) .autocapitalization(.none) .textFieldStyle(.roundedBorder) case .token: TextField("ACS Token", text: !envConfigSubject.useExpiredToken ? $envConfigSubject.acsToken : $envConfigSubject.expiredAcsToken) .modifier(TextFieldClearButton(text: $envConfigSubject.acsToken)) .disableAutocorrection(true) .autocapitalization(.none) .textFieldStyle(.roundedBorder) } } .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) } var displayNameTextField: some View { TextField("Display Name", text: $envConfigSubject.displayName) .disableAutocorrection(true) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) .textFieldStyle(.roundedBorder) } var userIdField: some View { TextField("Communication User Id", text: $envConfigSubject.userId) .disableAutocorrection(true) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) .textFieldStyle(.roundedBorder) } var endpointUrlField: some View { TextField("ACS Endpoint Url", text: $envConfigSubject.endpointUrl) .disableAutocorrection(true) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) .textFieldStyle(.roundedBorder) } var meetingSelector: some View { TextField( "Group Chat ThreadId", text: $envConfigSubject.threadId) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) } var startExperience: some View { Group { HStack { Button("Start Experience") { if self.chatAdapter == nil { self.startChatComposite() } self.isShowingChatView = true } .buttonStyle(DemoButtonStyle()) .disabled(isStartExperienceDisabled) .accessibility(identifier: AccessibilityId.startExperienceAccessibilityID.rawValue) Button("Stop") { self.chatAdapter?.disconnect(completionHandler: { result in self.onDisconnectFromChat(with: result) }) } .buttonStyle(DemoButtonStyle()) .disabled(self.chatAdapter == nil) .accessibility(identifier: AccessibilityId.stopChatAccessibilityID.rawValue) } } } var isStartExperienceDisabled: Bool { let acsToken = envConfigSubject.useExpiredToken ? envConfigSubject.expiredAcsToken : envConfigSubject.acsToken if (envConfigSubject.selectedAcsTokenType == .token && acsToken.isEmpty) || envConfigSubject.selectedAcsTokenType == .tokenUrl && envConfigSubject.acsTokenUrl.isEmpty || envConfigSubject.threadId.isEmpty { return true } return false } } extension ChatDemoView { func startChatComposite(headless: Bool = false) { let communicationIdentifier = CommunicationUserIdentifier(envConfigSubject.userId) guard let communicationTokenCredential = try? CommunicationTokenCredential( token: envConfigSubject.acsToken) else { return } self.chatAdapter = ChatAdapter( endpoint: envConfigSubject.endpointUrl, identifier: communicationIdentifier, credential: communicationTokenCredential, threadId: envConfigSubject.threadId, displayName: envConfigSubject.displayName) guard let chatAdapter = self.chatAdapter else { return } chatAdapter.events.onError = showError(error:) chatAdapter.connect { _ in print("Chat connect completionHandler called") } } private func getTokenCredential() throws -> CommunicationTokenCredential { switch envConfigSubject.selectedAcsTokenType { case .token: let acsToken = envConfigSubject.useExpiredToken ? envConfigSubject.expiredAcsToken : envConfigSubject.acsToken if let communicationTokenCredential = try? CommunicationTokenCredential(token: acsToken) { return communicationTokenCredential } else { throw DemoError.invalidToken } case .tokenUrl: if let url = URL(string: envConfigSubject.acsTokenUrl) { let tokenRefresher = AuthenticationHelper.getCommunicationToken(tokenUrl: url) let communicationTokenRefreshOptions = CommunicationTokenRefreshOptions(initialToken: nil, refreshProactively: true, tokenRefresher: tokenRefresher) if let credential = try? CommunicationTokenCredential(withOptions: communicationTokenRefreshOptions) { return credential } } throw DemoError.invalidToken } } private func onDisconnectFromChat(with result: Result<Void, ChatCompositeError>) { switch result { case .success: self.chatAdapter = nil Task { @MainActor in try await Task.sleep(nanoseconds: Constant.oneMillisecond) self.isShowingChatView = false } case .failure(let error): print("disconnect error \(error)") } } private func showError(error: ChatCompositeError) { print("::::SwiftUIChatDemoView::showError \(error)") print("::::SwiftUIChatDemoView error.code \(error.code)") print("Error - \(error.code): \(error.error?.localizedDescription ?? error.localizedDescription)") switch error.code { case ChatCompositeErrorCode.joinFailed: errorMessage = "Connection Failed" case ChatCompositeErrorCode.disconnectFailed: errorMessage = "Disconnect Failed" case ChatCompositeErrorCode.sendMessageFailed, ChatCompositeErrorCode.fetchMessagesFailed, ChatCompositeErrorCode.requestParticipantsFetchFailed, ChatCompositeErrorCode.sendReadReceiptFailed, ChatCompositeErrorCode.sendTypingIndicatorFailed: // no alert return default: errorMessage = "Unknown error" } isErrorDisplayed = true } }