diff --git a/Podfile b/Podfile index b034a21..ad1a7ed 100644 --- a/Podfile +++ b/Podfile @@ -17,7 +17,7 @@ target 'ZelloSDKExampleApp' do use_frameworks! # Pods for ZelloSDKExampleApp - pod "ZelloSDK", "~> 0.4.0" + pod "ZelloSDK", "~> 1.0.0" end @@ -26,7 +26,7 @@ target 'SDKNotificationServiceExtension' do use_frameworks! # Pods for SDKNotificationServiceExtension - pod "ZelloSDK", "~> 0.4.0" + pod "ZelloSDK", "~> 1.0.0" end diff --git a/Podfile.lock b/Podfile.lock index 974bce6..8777fda 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -11,18 +11,20 @@ PODS: - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) + - SnowplowTracker (6.0.8) - zello-opus-ios (1.0.1) - - ZelloSDK (0.4.0): + - ZelloSDK (1.0.0): - CocoaAsyncSocket (~> 7.6) - CocoaLumberjack/Swift (~> 3.7) - OpenSSL-Universal (~> 1.1) - PhoneNumberKit/PhoneNumberKitCore (~> 3.6) - PromisesSwift (~> 2.1) - SDWebImage (~> 5.13) + - SnowplowTracker (~> 6.0) - zello-opus-ios (~> 1.0) DEPENDENCIES: - - ZelloSDK (~> 0.4.0) + - ZelloSDK (~> 1.0.0) SPEC REPOS: trunk: @@ -33,6 +35,7 @@ SPEC REPOS: - PromisesObjC - PromisesSwift - SDWebImage + - SnowplowTracker - zello-opus-ios - ZelloSDK @@ -44,9 +47,10 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 + SnowplowTracker: c7dcb63a584038ac3459bc803bfedec98cf87de9 zello-opus-ios: 8c1f08799d7a147c86f21ec2a18f7b19f8c2ceed - ZelloSDK: 85bfe4d3f07077163ff0711b7fd24aa9b28789f9 + ZelloSDK: 38bd1581f93072d521ee708064577fe009349f02 -PODFILE CHECKSUM: 60c1219dc719fa1a13b42ac86949a576482ab6e0 +PODFILE CHECKSUM: 3a005f8a86cb592f251f81749205f739c39b7541 COCOAPODS: 1.15.2 diff --git a/ZelloSDKExampleApp.xcodeproj/project.pbxproj b/ZelloSDKExampleApp.xcodeproj/project.pbxproj index 6379fe8..2146315 100644 --- a/ZelloSDKExampleApp.xcodeproj/project.pbxproj +++ b/ZelloSDKExampleApp.xcodeproj/project.pbxproj @@ -43,6 +43,10 @@ 41C742882BB4974A00D519F7 /* ChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C742862BB4974A00D519F7 /* ChannelsViewModel.swift */; }; 41C742A32BB498E500D519F7 /* SessionConnectButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C742A22BB498E500D519F7 /* SessionConnectButton.swift */; }; 41C742A52BB49B3300D519F7 /* ListItemTalkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C742A42BB49B3300D519F7 /* ListItemTalkButton.swift */; }; + BA1D5B4C2C9A446C00C703D8 /* ConversationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1D5B492C9A446C00C703D8 /* ConversationsView.swift */; }; + BA1D5B4D2C9A446C00C703D8 /* ConversationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1D5B4A2C9A446C00C703D8 /* ConversationsViewModel.swift */; }; + BA1D5B502C9A461400C703D8 /* UserSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1D5B4E2C9A461400C703D8 /* UserSelectionView.swift */; }; + BA1D5B512C9A461400C703D8 /* ConnectButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1D5B4F2C9A461400C703D8 /* ConnectButton.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,6 +119,10 @@ 41FCD7172BB32EF900F06DDE /* ZelloSDKExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ZelloSDKExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; B0C5266FA4CEED302DDF44FE /* Pods_SDKNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SDKNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B350D77533A95A10E3863DA7 /* Pods-ZelloSDKExampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ZelloSDKExampleApp.debug.xcconfig"; path = "Target Support Files/Pods-ZelloSDKExampleApp/Pods-ZelloSDKExampleApp.debug.xcconfig"; sourceTree = ""; }; + BA1D5B492C9A446C00C703D8 /* ConversationsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationsView.swift; sourceTree = ""; }; + BA1D5B4A2C9A446C00C703D8 /* ConversationsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationsViewModel.swift; sourceTree = ""; }; + BA1D5B4E2C9A461400C703D8 /* UserSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSelectionView.swift; sourceTree = ""; }; + BA1D5B4F2C9A461400C703D8 /* ConnectButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectButton.swift; sourceTree = ""; }; BB9FBB5493D793D4A736E2C6 /* Pods-SDKNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDKNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-SDKNotificationServiceExtension/Pods-SDKNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; CB79221BBBB40DCD8D0ECCA1 /* Pods_ZelloSDKExampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ZelloSDKExampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D00DED424D00407A04E92276 /* Pods-SDKNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDKNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-SDKNotificationServiceExtension/Pods-SDKNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; @@ -187,6 +195,7 @@ isa = PBXGroup; children = ( 00E5E4282C0F95000028F485 /* AlertPopupView.swift */, + BA1D5B4F2C9A461400C703D8 /* ConnectButton.swift */, 00B26A1B2BED169300BBD78D /* ConnectivityProvider.swift */, 00D7B9A12BF665BB005AB5F1 /* ConnectNavBarButton.swift */, 00D7B99B2BF409A8005AB5F1 /* ImagePopupView.swift */, @@ -199,6 +208,7 @@ 00B26A192BED0EBC00BBD78D /* StatusMessageView.swift */, 00D7B9B62BFD3A81005AB5F1 /* TextPopupView.swift */, 419D06FB2C46D99E0028FB3C /* HistoryPopupView.swift */, + BA1D5B4E2C9A461400C703D8 /* UserSelectionView.swift */, 416751012BB3AEA90095410A /* Types */, ); path = Shared; @@ -232,6 +242,7 @@ 41B79CB22C3C7B0E001DD052 /* Recents */, 41FCD72C2BB3377C00F06DDE /* Users */, 41FCD72D2BB3378A00F06DDE /* Channels */, + BA1D5B4B2C9A446C00C703D8 /* Conversations */, ); path = UI; sourceTree = ""; @@ -307,6 +318,15 @@ path = Channels; sourceTree = ""; }; + BA1D5B4B2C9A446C00C703D8 /* Conversations */ = { + isa = PBXGroup; + children = ( + BA1D5B492C9A446C00C703D8 /* ConversationsView.swift */, + BA1D5B4A2C9A446C00C703D8 /* ConversationsViewModel.swift */, + ); + path = Conversations; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -485,6 +505,7 @@ 00D7B9B72BFD3A81005AB5F1 /* TextPopupView.swift in Sources */, 41B79CB42C3C7B1A001DD052 /* RecentsView.swift in Sources */, 0071CC4B2C08B722002533CB /* MapView.swift in Sources */, + BA1D5B512C9A461400C703D8 /* ConnectButton.swift in Sources */, 00B26A232BED1BF200BBD78D /* SignInView.swift in Sources */, 00D7B9AB2BF7DB53005AB5F1 /* StatusActionSheet.swift in Sources */, 41C742802BB4969C00D519F7 /* IncomingVoiceMessageViewState.swift in Sources */, @@ -496,15 +517,18 @@ 00D7B99C2BF409A8005AB5F1 /* ImagePopupView.swift in Sources */, 00B26A1C2BED169300BBD78D /* ConnectivityProvider.swift in Sources */, 41C742A32BB498E500D519F7 /* SessionConnectButton.swift in Sources */, + BA1D5B4D2C9A446C00C703D8 /* ConversationsViewModel.swift in Sources */, 0071CC492C08B706002533CB /* LocationPopupView.swift in Sources */, 41C742832BB4973300D519F7 /* UsersView.swift in Sources */, 00B26A1A2BED0EBC00BBD78D /* StatusMessageView.swift in Sources */, + BA1D5B502C9A461400C703D8 /* UserSelectionView.swift in Sources */, 41B79CB62C3C7B40001DD052 /* RecentsViewModel.swift in Sources */, 00E5E4292C0F95000028F485 /* AlertPopupView.swift in Sources */, 41C742872BB4974A00D519F7 /* ChannelsView.swift in Sources */, 419D06FE2C46D9BA0028FB3C /* HistoryViewState.swift in Sources */, 41C7427C2BB4964700D519F7 /* ConnectionViewState.swift in Sources */, 41C742762BB492F500D519F7 /* ZelloService.swift in Sources */, + BA1D5B4C2C9A446C00C703D8 /* ConversationsView.swift in Sources */, 41C742882BB4974A00D519F7 /* ChannelsViewModel.swift in Sources */, 41C742642BB48FE800D519F7 /* ZelloSDKExampleAppApp.swift in Sources */, 41C742842BB4973300D519F7 /* UsersViewModel.swift in Sources */, diff --git a/ZelloSDKExampleApp/Services/ZelloService.swift b/ZelloSDKExampleApp/Services/ZelloService.swift index 2520f02..35fba7c 100644 --- a/ZelloSDKExampleApp/Services/ZelloService.swift +++ b/ZelloSDKExampleApp/Services/ZelloService.swift @@ -11,6 +11,7 @@ class ZelloRepository: Zello.Delegate, ObservableObject { @Published var connectionState: Zello.ConnectionState = .disconnected @Published var users: [ZelloUser] = [] @Published var channels: [ZelloChannel] = [] + @Published var groupConversations: [ZelloGroupConversation] = [] @Published var selectedContact: ZelloContact? = nil @Published var statusMessage: String? = nil { didSet { @@ -69,6 +70,7 @@ class ZelloRepository: Zello.Delegate, ObservableObject { func zelloDidUpdateContactList(_ zello: Zello) { users = zello.users channels = zello.channels + groupConversations = zello.groupConversations emergencyChannel = zello.emergencyChannel } @@ -124,7 +126,7 @@ class ZelloRepository: Zello.Delegate, ObservableObject { } func zello(_ zello: Zello, didFailToSend textMessage: ZelloTextMessage) { - statusMessage = "Failed to send image message" + statusMessage = "Failed to send text message" } func zello(_ zello: Zello, didReceive locationMessage: ZelloLocationMessage) { @@ -136,7 +138,7 @@ class ZelloRepository: Zello.Delegate, ObservableObject { } func zello(_ zello: Zello, didFailToSend locationMessage: ZelloLocationMessage) { - statusMessage = "Failed to send image message" + statusMessage = "Failed to send location message" } func zello(_ zello: Zello, didReceive alertMessage: ZelloAlertMessage) { @@ -148,7 +150,7 @@ class ZelloRepository: Zello.Delegate, ObservableObject { } func zello(_ zello: Zello, didFailToSend alertMessage: ZelloAlertMessage) { - statusMessage = "Failed to send image message" + statusMessage = "Failed to send alert message" } func zello(_ zello: Zello, didStart outgoingEmergency: ZelloOutgoingEmergency) { @@ -196,4 +198,29 @@ class ZelloRepository: Zello.Delegate, ObservableObject { func clearHistory() { history = nil } + + func zello(_ zello: Zello, didRename conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + + func zello(_ zello: Zello, didJoin conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + + func zello(_ zello: Zello, didCreate conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + + func zello(_ zello: Zello, didAdd users: [ZelloGroupConversationUser], to conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + + func zello(_ zello: Zello, didLeave conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + + func zello(_ zello: Zello, didRemove users: [ZelloGroupConversationUser], from conversation: ZelloGroupConversation) { + groupConversations = zello.groupConversations + } + } diff --git a/ZelloSDKExampleApp/Shared/AlertPopupView.swift b/ZelloSDKExampleApp/Shared/AlertPopupView.swift index 9e9f8f3..ac7f7cd 100644 --- a/ZelloSDKExampleApp/Shared/AlertPopupView.swift +++ b/ZelloSDKExampleApp/Shared/AlertPopupView.swift @@ -54,7 +54,11 @@ struct AlertPopupView: View { guard let alertMessage else { return nil } if let author = alertMessage.channelUser { - return "\(author.name) -> \(alertMessage.contact.name)" + var displayName = alertMessage.contact.name + if let conversation = alertMessage.contact.asZelloGroupConversation() { + displayName = conversation.displayName + } + return "\(author.name) -> \(displayName)" } return alertMessage.contact.name } diff --git a/ZelloSDKExampleApp/Shared/ConnectButton.swift b/ZelloSDKExampleApp/Shared/ConnectButton.swift new file mode 100644 index 0000000..b4a559b --- /dev/null +++ b/ZelloSDKExampleApp/Shared/ConnectButton.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ConnectButton: View { + @ObservedObject var viewModel: ViewModel + let onConnectButtonTapped: () -> Void + + var body: some View { + let isConnected = viewModel.isConnected + let isConnecting = viewModel.isConnecting + + SessionConnectButton(isConnected: isConnected, isConnecting: isConnecting) { + if isConnected { + viewModel.disconnect() + } else { + onConnectButtonTapped() + } + } + } +} diff --git a/ZelloSDKExampleApp/Shared/ImagePopupView.swift b/ZelloSDKExampleApp/Shared/ImagePopupView.swift index 25d5938..5842b5a 100644 --- a/ZelloSDKExampleApp/Shared/ImagePopupView.swift +++ b/ZelloSDKExampleApp/Shared/ImagePopupView.swift @@ -64,7 +64,11 @@ struct ImagePopupView: View { guard let imageMessage else { return nil } if let author = imageMessage.channelUser { - return "\(author.name) -> \(imageMessage.contact.name)" + var displayName = imageMessage.contact.name + if let conversation = imageMessage.contact.asZelloGroupConversation() { + displayName = conversation.displayName + } + return "\(author.name) -> \(displayName)" } return imageMessage.contact.name } diff --git a/ZelloSDKExampleApp/Shared/InputDialog.swift b/ZelloSDKExampleApp/Shared/InputDialog.swift index 3c814ff..f8fe3f2 100644 --- a/ZelloSDKExampleApp/Shared/InputDialog.swift +++ b/ZelloSDKExampleApp/Shared/InputDialog.swift @@ -4,6 +4,7 @@ import ZelloSDK enum InputAction: String { case alert = "Alert" case text = "Text" + case rename = "Rename Conversation" } struct InputDialog: View { @@ -12,6 +13,7 @@ struct InputDialog: View { @Binding var selectedLevel: ZelloAlertMessage.ChannelLevel? let action: InputAction let contact: ZelloContact + let conversation: ZelloGroupConversation? var onSend: () -> Void init(isVisible: Binding, @@ -19,6 +21,7 @@ struct InputDialog: View { selectedLevel: Binding = .constant(nil), action: InputAction, contact: ZelloContact, + conversation: ZelloGroupConversation?, onSend: @escaping () -> Void) { self._isVisible = isVisible self._text = text @@ -26,16 +29,26 @@ struct InputDialog: View { self.action = action self.contact = contact self.onSend = onSend + self.conversation = conversation } var body: some View { VStack { - Text("Send \(action) to \(contact.name)") - .padding() + if let conversation, action == .rename { + Text("Rename \(conversation.displayName)") + .padding() - TextField("Enter your message", text: $text) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding() + TextField("New name", text: $text) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + } else { + Text("Send \(action) to \(displayName())") + .padding() + + TextField("Enter your message", text: $text) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + } if selectedLevel != nil { Picker("Select Level", selection: $selectedLevel.unwrap(defaultValue: .connected)) { @@ -53,7 +66,7 @@ struct InputDialog: View { } .padding() - Button("Send") { + Button("Continue") { onSend() isVisible = false } @@ -65,6 +78,13 @@ struct InputDialog: View { .shadow(radius: 10) .padding() } + + func displayName() -> String { + if let conversation, !conversation.displayName.isEmpty { + return conversation.displayName + } + return contact.name + } } extension Binding where Value: ExpressibleByNilLiteral { diff --git a/ZelloSDKExampleApp/Shared/LocationPopupView.swift b/ZelloSDKExampleApp/Shared/LocationPopupView.swift index 3614338..f0b1128 100644 --- a/ZelloSDKExampleApp/Shared/LocationPopupView.swift +++ b/ZelloSDKExampleApp/Shared/LocationPopupView.swift @@ -61,7 +61,11 @@ struct LocationPopupView: View { guard let locationMessage else { return nil } if let author = locationMessage.channelUser { - return "\(author.name) -> \(locationMessage.contact.name)" + var displayName = locationMessage.contact.name + if let conversation = locationMessage.contact.asZelloGroupConversation() { + displayName = conversation.displayName + } + return "\(author.name) -> \(displayName)" } return locationMessage.contact.name } diff --git a/ZelloSDKExampleApp/Shared/TextPopupView.swift b/ZelloSDKExampleApp/Shared/TextPopupView.swift index 0562908..9339c48 100644 --- a/ZelloSDKExampleApp/Shared/TextPopupView.swift +++ b/ZelloSDKExampleApp/Shared/TextPopupView.swift @@ -54,7 +54,11 @@ struct TextPopupView: View { guard let textMessage else { return nil } if let author = textMessage.channelUser { - return "\(author.name) -> \(textMessage.contact.name)" + var displayName = textMessage.contact.name + if let conversation = textMessage.contact.asZelloGroupConversation() { + displayName = conversation.displayName + } + return "\(author.name) -> \(displayName)" } return textMessage.contact.name } diff --git a/ZelloSDKExampleApp/Shared/UserSelectionView.swift b/ZelloSDKExampleApp/Shared/UserSelectionView.swift new file mode 100644 index 0000000..08bb4f6 --- /dev/null +++ b/ZelloSDKExampleApp/Shared/UserSelectionView.swift @@ -0,0 +1,83 @@ +import SwiftUI +import ZelloSDK + +struct UserSelectionView: View { + @Binding var isVisible: Bool + @Binding var selectedUsers: [ZelloUser] + let allUsers: [ZelloUser] + let title: String + let onCreate: () -> Void + + var body: some View { + VStack { + Text(title) + .font(.headline) + .padding() + .frame(maxWidth: .infinity) + .background(Color(UIColor.white)) + + if allUsers.isEmpty { + Text("No users available") + } else { + List(allUsers, id: \.id) { user in + HStack { + Text(user.displayName) + Spacer() + CheckBoxView( + isChecked: selectedUsers.contains(where: { $0.id == user.id }), + onTap: { + if let index = selectedUsers.firstIndex(where: { $0.id == user.id }) { + selectedUsers.remove(at: index) + } else { + selectedUsers.append(user) + } + } + ) + } + } + } + + HStack { + Button("Cancel") { + isVisible = false + } + .padding() + Spacer() + Button("Continue") { + onCreate() + isVisible = false + } + .padding() + } + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(radius: 10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + ) + .padding(.horizontal, 20) + .onAppear { + selectedUsers.removeAll() + } + } +} + + +struct CheckBoxView: View { + let isChecked: Bool + let onTap: () -> Void + + var body: some View { + Button(action: { + onTap() + }) { + Image(systemName: isChecked ? "checkmark.circle.fill" : "circle") + .foregroundColor(isChecked ? .blue : .gray) + .font(.system(size: 24)) + } + .buttonStyle(PlainButtonStyle()) + } +} diff --git a/ZelloSDKExampleApp/UI/Channels/ChannelsView.swift b/ZelloSDKExampleApp/UI/Channels/ChannelsView.swift index 8e70fc6..e7e0ea7 100644 --- a/ZelloSDKExampleApp/UI/Channels/ChannelsView.swift +++ b/ZelloSDKExampleApp/UI/Channels/ChannelsView.swift @@ -31,6 +31,10 @@ struct ChannelsView: View { channel: channel, isMuted: channel.isMuted, showEndCallButton: showEndCallButton, + showSendLocation: channel.channelOptions.allowLocations && viewModel.settings?.allowsLocationMessages == true && channel.status == .connected, + showSendImage: viewModel.settings?.allowsImageMessages == true && channel.status == .connected, + showSendAlert: channel.channelOptions.allowAlerts && viewModel.settings?.allowsAlertMessages == true && channel.status == .connected, + showSendText: channel.channelOptions.allowTextMessages && viewModel.settings?.allowsTextMessages == true && channel.status == .connected, showTextInputDialog: $showTextInputDialog, showAlertInputDialog: $showAlertInputDialog, channelInputText: $channelInputText, @@ -91,6 +95,7 @@ struct ChannelsView: View { text: $channelInputText, action: .text, contact: contact, + conversation: nil, onSend: { viewModel.sendText(channel: selectedChannel, message: channelInputText) } @@ -106,6 +111,7 @@ struct ChannelsView: View { selectedLevel: $selectedLevel, action: .alert, contact: contact, + conversation: nil, onSend: { viewModel.sendAlert(channel: selectedChannel, message: channelInputText, level: selectedLevel) } @@ -191,9 +197,9 @@ struct ChannelsView: View { .disabled(channel.status == .connecting) .onTapGesture { if channel.status == .connected { - viewModel.disconnectChannel(channel: channel) + viewModel.disconnect(from: channel) } else { - viewModel.connectChannel(channel: channel) + viewModel.connect(to: channel) } } } @@ -223,6 +229,10 @@ struct ChannelsView: View { let channel: ZelloChannel let isMuted: Bool let showEndCallButton: Bool + let showSendLocation: Bool + let showSendImage: Bool + let showSendAlert: Bool + let showSendText: Bool @Binding var showTextInputDialog: Bool @Binding var showAlertInputDialog: Bool @Binding var channelInputText: String @@ -242,19 +252,29 @@ struct ChannelsView: View { .buttonStyle(PlainButtonStyle()) .contentShape(Rectangle()) .contextMenu { - Button("Send Image", action: sendImage) - Button("Send Text", action: { - selectedChannelForText = channel - channelInputText = "" - showTextInputDialog = true - }) - Button("Send Alert", action: { - selectedChannelForText = channel - channelInputText = "" - showAlertInputDialog = true - }) - Button("Send Location", action: sendLocation) - Button(isMuted ? "Unmute" : "Mute", action: toggleMute) + if showSendImage { + Button("Send Image", action: sendImage) + } + if showSendText { + Button("Send Text", action: { + selectedChannelForText = channel + channelInputText = "" + showTextInputDialog = true + }) + } + if showSendAlert { + Button("Send Alert", action: { + selectedChannelForText = channel + channelInputText = "" + showAlertInputDialog = true + }) + } + if showSendLocation { + Button("Send Location", action: sendLocation) + } + if channel.status == .connected { + Button(isMuted ? "Unmute" : "Mute", action: toggleMute) + } if viewModel.emergencyChannel == channel { if viewModel.outgoingEmergency != nil { Button("Stop Emergency", action: stopEmergency) diff --git a/ZelloSDKExampleApp/UI/Channels/ChannelsViewModel.swift b/ZelloSDKExampleApp/UI/Channels/ChannelsViewModel.swift index 3d767ca..4b98380 100644 --- a/ZelloSDKExampleApp/UI/Channels/ChannelsViewModel.swift +++ b/ZelloSDKExampleApp/UI/Channels/ChannelsViewModel.swift @@ -200,12 +200,12 @@ class ChannelsViewModel: ObservableObject, ConnectivityProvider { ZelloRepository.instance.zello.stopVoiceMessage() } - func connectChannel(channel: ZelloChannel) { - ZelloRepository.instance.zello.connectChannel(channel: channel) + func connect(to channel: ZelloChannel) { + ZelloRepository.instance.zello.connect(to: channel) } - func disconnectChannel(channel: ZelloChannel) { - ZelloRepository.instance.zello.disconnectChannel(channel: channel) + func disconnect(from channel: ZelloChannel) { + ZelloRepository.instance.zello.disconnect(from: channel) } func sendImage(channel: ZelloChannel, image: UIImage) { diff --git a/ZelloSDKExampleApp/UI/Conversations/ConversationsView.swift b/ZelloSDKExampleApp/UI/Conversations/ConversationsView.swift new file mode 100644 index 0000000..d8e154f --- /dev/null +++ b/ZelloSDKExampleApp/UI/Conversations/ConversationsView.swift @@ -0,0 +1,356 @@ +import SwiftUI +import ZelloSDK + +struct Conversation: Identifiable { + let id = UUID() + let title: String +} + +struct ConversationsView: View { + @StateObject private var viewModel = ConversationsViewModel() + @State private var showSignInDialog = false + @State private var credentials = ZelloCredentials(username: "", network: "", password: "") + @State private var showStatusMenu = false + @State private var showTextInputDialog = false + @State private var showAlertInputDialog = false + @State private var conversationInputText = "" + @State private var selectedConversation: ZelloGroupConversation? + @State private var selectedLevel: ZelloAlertMessage.ChannelLevel? = .connected + @State private var showRenameDialog = false + @State private var renameInputText = "" + @State private var showCreateConversationDialog = false + @State private var showAddUserSelectionView = false + @State private var selectedUsers = [ZelloUser]() + @State private var addUsers = [ZelloUser]() + + var body: some View { + NavigationView { + ZStack { + VStack { + ScrollView { + LazyVStack { + ForEach(viewModel.groupConversations, id: \.name) { conversation in + let isConnecting = viewModel.connectionViewState.connectionState == .connecting + + HStack { + Details(viewModel: viewModel, conversation: conversation) + ConnectionToggle(viewModel: viewModel, conversation: conversation) + Spacer() + ActionsButton(viewModel: viewModel, + conversation: conversation, + isMuted: conversation.isMuted, + showTextInputDialog: $showTextInputDialog, + showAlertInputDialog: $showAlertInputDialog, + conversationInputText: $conversationInputText, + selectedConversation: $selectedConversation, + showRenameDialog: $showRenameDialog, + renameInputText: $renameInputText, + showAddUserSelectionView: $showAddUserSelectionView, + selectedUsers: $selectedUsers, + addUsers: $addUsers + ) + Spacer() + TalkButton(viewModel: viewModel, conversation: conversation) + } + .background(isConnecting ? Color.yellow.opacity(0.5) : Color.clear) + .cornerRadius(5) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, alignment: .leading) + .onTapGesture { + viewModel.setSelectedContact(conversation: conversation) + } + .cornerRadius(5) + .padding(.horizontal, 12) + .padding(.vertical, 2) + } + } + } + StatusMessageView(statusMessage: viewModel.statusMessage) + } + + VStack { + Spacer() + HStack { + Spacer() + Button(action: { + showCreateConversationDialog = true + }) { + Image(systemName: "plus") + .font(.system(size: 24)) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .clipShape(Circle()) + .shadow(radius: 4) + } + .padding() + } + } + + if showSignInDialog { + SignInPrompt( + showDialog: $showSignInDialog, + credentials: $credentials + ) { + viewModel.connect(credentials: credentials) + } + } + + if viewModel.showImagePopup { + ImagePopupView(isVisible: $viewModel.showImagePopup, + imageMessage: viewModel.incomingImageMessage) + } + + if viewModel.showTextPopup { + TextPopupView(isVisible: $viewModel.showTextPopup, + textMessage: viewModel.incomingTextMessage) + } + + if viewModel.showAlertPopup { + AlertPopupView(isVisible: $viewModel.showAlertPopup, + alertMessage: viewModel.incomingAlertMessage) + } + + if viewModel.showLocationPopup { + LocationPopupView(isVisible: $viewModel.showLocationPopup, + locationMessage: viewModel.incomingLocationMessage) + } + + if showTextInputDialog, let selectedConversation { + let contact = ZelloContact.conversation(selectedConversation) + + InputDialog( + isVisible: $showTextInputDialog, + text: $conversationInputText, + action: .text, + contact: contact, + conversation: selectedConversation, + onSend: { + viewModel.sendText(conversation: selectedConversation, message: conversationInputText) + } + ) + } + + if showAlertInputDialog, let selectedConversation = selectedConversation { + let contact = ZelloContact.conversation(selectedConversation) + + InputDialog( + isVisible: $showAlertInputDialog, + text: $conversationInputText, + selectedLevel: $selectedLevel, + action: .alert, + contact: contact, + conversation: selectedConversation, + onSend: { + viewModel.sendAlert(conversation: selectedConversation, message: conversationInputText, level: selectedLevel) + } + ) + } + + if viewModel.showHistoryPopup, let messages = viewModel.history?.messages { + HistoryPopupView(isVisible: $viewModel.showHistoryPopup, messages: messages) + } + + if showCreateConversationDialog { + UserSelectionView( + isVisible: $showCreateConversationDialog, + selectedUsers: $selectedUsers, + allUsers: viewModel.users.filter { $0.supportedFeatures.groupConversations }, + title: "Create Conversation", + onCreate: { + viewModel.createConversation(users: selectedUsers) + } + ) + } + + if showAddUserSelectionView, let selectedConversation { + UserSelectionView( + isVisible: $showAddUserSelectionView, + selectedUsers: $selectedUsers, + allUsers: addUsers.filter { $0.supportedFeatures.groupConversations }, + title: "Add Users", + onCreate: { + viewModel.add(selectedUsers, to: selectedConversation) + } + ) + } + + if showRenameDialog, let selectedConversation { + InputDialog( + isVisible: $showRenameDialog, + text: $renameInputText, + action: .rename, + contact: ZelloContact.conversation(selectedConversation), + conversation: selectedConversation, + onSend: { + viewModel.rename(selectedConversation, to: renameInputText) + } + ) + } + } + .navigationTitle("Conversations") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems( + leading: viewModel.isConnected ? AnyView(StatusActionSheet( + showStatusMenu: $showStatusMenu, + accountStatus: $viewModel.accountStatus, + updateAccountStatus: viewModel.setAccountStatus + )) : AnyView(EmptyView()), + trailing: ConnectNavBarButton(viewModel: viewModel, onConnectButtonTapped: { + showSignInDialog = true + }) + ) + } + } + + private func updateAccountStatus(_ status: ZelloAccountStatus) { + viewModel.setAccountStatus(status: status) + } + + struct Details: View { + @ObservedObject var viewModel: ConversationsViewModel + let conversation: ZelloGroupConversation + + var body: some View { + VStack { + let isSelectedContact = viewModel.selectedContact == .conversation(conversation) + Text(conversation.displayName) + .bold(isSelectedContact) + .frame(maxWidth: .infinity, alignment: .leading) + Text(conversation.status == .connected ? "Connected" : conversation.status == .connecting ? "Connecting" : "Disconnected") + .bold(isSelectedContact) + .frame(maxWidth: .infinity, alignment: .leading) + Text("\(conversation.onlineUsers.count) users connected") + .bold(isSelectedContact) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + struct ConnectionToggle: View { + @ObservedObject var viewModel: ConversationsViewModel + let conversation: ZelloGroupConversation + + var body: some View { + Toggle("", isOn: .constant(conversation.status == .connected)) + .disabled(conversation.status == .connecting) + .onTapGesture { + if conversation.status == .connected { + viewModel.disconnect(from: conversation) + } else { + viewModel.connect(to: conversation) + } + } + } + } + + struct TalkButton: View { + @ObservedObject var viewModel: ConversationsViewModel + let conversation: ZelloGroupConversation + + var body: some View { + let incomingVoiceMessageViewState = viewModel.incomingVoiceMessageViewState + let outgoingVoiceMessageViewState = viewModel.outgoingVoiceMessageViewState + let isSameOutgoingContact = outgoingVoiceMessageViewState?.contact == .conversation(conversation) + let isSending = isSameOutgoingContact && outgoingVoiceMessageViewState?.state == .sending + let isReceiving = incomingVoiceMessageViewState?.contact == .conversation(conversation) + let isConnecting = isSameOutgoingContact && outgoingVoiceMessageViewState?.state == .connecting + ListItemTalkButton(isSending: isSending, isReceiving: isReceiving, isConnecting: isConnecting, isEnabled: true) { + viewModel.startSendingMessage(conversation: conversation) + } onUp: { + viewModel.stopSendingMessage() + } + } + } + + struct ActionsButton: View { + @ObservedObject var viewModel: ConversationsViewModel + let conversation: ZelloGroupConversation + let isMuted: Bool + @Binding var showTextInputDialog: Bool + @Binding var showAlertInputDialog: Bool + @Binding var conversationInputText: String + @Binding var selectedConversation: ZelloGroupConversation? + @Binding var showRenameDialog: Bool + @Binding var renameInputText: String + @Binding var showAddUserSelectionView: Bool + @Binding var selectedUsers: [ZelloUser] + @Binding var addUsers: [ZelloUser] + + var body: some View { + Button(action: {}) { + Image(systemName: "ellipsis").font(.title) + } + .cornerRadius(5) + .padding(.horizontal, 10) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(red: 0.8, green: 0.8, blue: 0.8)) + ) + .buttonStyle(PlainButtonStyle()) + .contentShape(Rectangle()) + .contextMenu { + if viewModel.settings?.allowsImageMessages == true && conversation.status == .connected { + Button("Send Image", action: sendImage) + } + if viewModel.settings?.allowsTextMessages == true && conversation.status == .connected { + Button("Send Text", action: { + selectedConversation = conversation + conversationInputText = "" + showTextInputDialog = true + }) + } + + if viewModel.settings?.allowsAlertMessages == true && conversation.status == .connected { + Button("Send Alert", action: { + selectedConversation = conversation + conversationInputText = "" + showAlertInputDialog = true + }) + } + if viewModel.settings?.allowsLocationMessages == true && conversation.status == .connected { + Button("Send Location", action: sendLocation) + } + Button(isMuted ? "Unmute" : "Mute", action: toggleMute) + Button("Show History", action: showHistory) + Button("Leave Conversation", action: leaveConversation) + Button("Rename Conversation", action: { + selectedConversation = conversation + renameInputText = conversation.displayName + showRenameDialog = true + }) + Button("Add Users", action: { + selectedConversation = conversation + addUsers = viewModel.users.filter { user in + !conversation.users.contains(where: { $0.name == user.name }) + } + showAddUserSelectionView = true + }) + } + } + + private func sendImage() { + print("Send Image selected") + if let image = UIImage(named: "TeamHonda") { + viewModel.sendImage(conversation: conversation, image: image) + } + } + + private func sendLocation() { + viewModel.sendLocationTo(conversation: conversation) + } + + private func toggleMute() { + viewModel.toggleMute(conversation: conversation) + } + + private func showHistory() { + viewModel.getHistory(conversation: conversation) + } + + private func leaveConversation() { + viewModel.leave(conversation) + } + } +} diff --git a/ZelloSDKExampleApp/UI/Conversations/ConversationsViewModel.swift b/ZelloSDKExampleApp/UI/Conversations/ConversationsViewModel.swift new file mode 100644 index 0000000..b587402 --- /dev/null +++ b/ZelloSDKExampleApp/UI/Conversations/ConversationsViewModel.swift @@ -0,0 +1,248 @@ +import Combine +import UIKit +import ZelloSDK + +class ConversationsViewModel: ObservableObject, ConnectivityProvider { + @Published var connectionViewState: ConnectionViewState + @Published var groupConversations: [ZelloGroupConversation] = [] + @Published var users: [ZelloUser] = [] + @Published var statusMessage: String? + @Published var selectedContact: ZelloContact? = nil + @Published var outgoingVoiceMessageViewState: OutgoingVoiceMessageViewState? + @Published var incomingVoiceMessageViewState: IncomingVoiceMessageViewState? + @Published var incomingImageMessage: ZelloImageMessage? + @Published var incomingLocationMessage: ZelloLocationMessage? + @Published var incomingTextMessage: ZelloTextMessage? + @Published var incomingAlertMessage: ZelloAlertMessage? + @Published var accountStatus: ZelloAccountStatus? = nil + @Published var showLocationPopup = false + @Published var showTextPopup = false + @Published var showImagePopup = false + @Published var showAlertPopup = false + @Published var showHistoryPopup = false + @Published var history: HistoryViewState? = nil + @Published var settings: ZelloConsoleSettings? = nil + + private var cancellables: Set = [] + + var isConnected: Bool { + connectionViewState.connectionState == .connected + } + + var isConnecting: Bool { + connectionViewState.connectionState == .connecting + } + + init() { + connectionViewState = ConnectionViewState(connectionState: ZelloRepository.instance.connectionState) + ZelloRepository.instance.$connectionState + .sink { [weak self] connectionState in + guard let weakSelf = self else { return } + var state = weakSelf.connectionViewState + state.connectionState = connectionState + weakSelf.connectionViewState = state + } + .store(in: &cancellables) + + groupConversations = ZelloRepository.instance.groupConversations + ZelloRepository.instance.$groupConversations + .sink { [weak self] groupConversations in + self?.groupConversations = groupConversations + } + .store(in: &cancellables) + + users = ZelloRepository.instance.users + ZelloRepository.instance.$users + .sink { [weak self] users in + self?.users = users + } + .store(in: &cancellables) + + statusMessage = ZelloRepository.instance.statusMessage + ZelloRepository.instance.$statusMessage + .sink { [weak self] statusMessage in + self?.statusMessage = statusMessage + } + .store(in: &cancellables) + + selectedContact = ZelloRepository.instance.selectedContact + ZelloRepository.instance.$selectedContact + .sink { [weak self] selectedContact in + self?.selectedContact = selectedContact + } + .store(in: &cancellables) + + if let message = ZelloRepository.instance.outgoingVoiceMessage { + outgoingVoiceMessageViewState = OutgoingVoiceMessageViewState(contact: message.contact, state: message.state) + } + ZelloRepository.instance.$outgoingVoiceMessage + .sink { [weak self] outgoingVoiceMessage in + if let message = outgoingVoiceMessage { + self?.outgoingVoiceMessageViewState = OutgoingVoiceMessageViewState(contact: message.contact, state: message.state) + } else { + self?.outgoingVoiceMessageViewState = nil + } + } + .store(in: &cancellables) + + if let message = ZelloRepository.instance.incomingVoiceMessage { + incomingVoiceMessageViewState = IncomingVoiceMessageViewState(contact: message.contact) + } + ZelloRepository.instance.$incomingVoiceMessage + .sink { [weak self] incomingVoiceMessage in + if let message = incomingVoiceMessage { + self?.incomingVoiceMessageViewState = IncomingVoiceMessageViewState(contact: message.contact) + } else { + self?.incomingVoiceMessageViewState = nil + } + } + .store(in: &cancellables) + + ZelloRepository.instance.$lastIncomingImageMessage + .sink { [weak self] incomingImageMessage in + if incomingImageMessage?.channelUser != nil { + self?.incomingImageMessage = incomingImageMessage + self?.showImagePopup = incomingImageMessage != nil + } else { + self?.incomingImageMessage = nil + } + } + .store(in: &cancellables) + + ZelloRepository.instance.$lastIncomingTextMessage + .sink { [weak self] incomingTextMessage in + if incomingTextMessage?.channelUser != nil { + self?.incomingTextMessage = incomingTextMessage + self?.showTextPopup = incomingTextMessage != nil + } else { + self?.incomingTextMessage = nil + } + } + .store(in: &cancellables) + + ZelloRepository.instance.$lastIncomingAlertMessage + .sink { [weak self] incomingAlertMessage in + if incomingAlertMessage?.channelUser != nil { + self?.incomingAlertMessage = incomingAlertMessage + self?.showAlertPopup = incomingAlertMessage != nil + } else { + self?.incomingAlertMessage = nil + } + } + .store(in: &cancellables) + + ZelloRepository.instance.$lastIncomingLocationMessage + .sink { [weak self] incomingLocationMessage in + if incomingLocationMessage?.channelUser != nil { + self?.incomingLocationMessage = incomingLocationMessage + self?.showLocationPopup = incomingLocationMessage != nil + } else { + self?.incomingLocationMessage = nil + } + } + .store(in: &cancellables) + + accountStatus = ZelloRepository.instance.accountStatus + ZelloRepository.instance.$accountStatus + .sink { [weak self] accountStatus in + self?.accountStatus = accountStatus + } + .store(in: &cancellables) + + history = ZelloRepository.instance.history + ZelloRepository.instance.$history + .sink { [weak self] history in + self?.showHistoryPopup = history != nil + self?.history = history + } + .store(in: &cancellables) + + settings = ZelloRepository.instance.settings + ZelloRepository.instance.$settings + .sink { [weak self] settings in + self?.settings = settings + } + .store(in: &cancellables) + } + + func connect(credentials: ZelloSDK.ZelloCredentials) { + ZelloRepository.instance.zello.connect(credentials: credentials) + } + + func disconnect() { + ZelloRepository.instance.zello.disconnect() + } + + func setSelectedContact(conversation: ZelloGroupConversation) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.setSelectedContact(contact: contact) + } + + func startSendingMessage(conversation: ZelloGroupConversation) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.startVoiceMessage(contact: contact) + } + + func stopSendingMessage() { + ZelloRepository.instance.zello.stopVoiceMessage() + } + + func connect(to conversation: ZelloGroupConversation) { + ZelloRepository.instance.zello.connect(to: conversation) + } + + func disconnect(from conversation: ZelloGroupConversation) { + ZelloRepository.instance.zello.disconnect(from: conversation) + } + + func sendImage(conversation: ZelloGroupConversation, image: UIImage) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.send(image, to: contact) + } + + func setAccountStatus(status: ZelloAccountStatus) { + ZelloRepository.instance.zello.setAccountStatus(status: status) + } + + func sendText(conversation: ZelloGroupConversation, message: String) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.send(textMessage: message, to: contact) + } + + func sendAlert(conversation: ZelloGroupConversation, message: String, level: ZelloAlertMessage.ChannelLevel? = nil) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.send(alertMessage: message, to: contact, using: level) + } + + func sendLocationTo(conversation: ZelloGroupConversation) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.zello.sendLocation(to: contact) + } + + func rename(_ conversation: ZelloGroupConversation, to newName: String) { + ZelloRepository.instance.zello.rename(conversation, to: newName) + } + + func createConversation(users: [ZelloUser]) { + ZelloRepository.instance.zello.createGroupConversation(users: users, displayName: nil) + } + + func add(_ users: [ZelloUser], to conversation: ZelloGroupConversation) { + ZelloRepository.instance.zello.add(users, to: conversation) + } + + func toggleMute(conversation: ZelloGroupConversation) { + let contact = ZelloContact.conversation(conversation) + contact.isMuted ? ZelloRepository.instance.zello.unmuteContact(contact: contact) : ZelloRepository.instance.zello.muteContact(contact: contact) + } + + func getHistory(conversation: ZelloGroupConversation) { + let contact = ZelloContact.conversation(conversation) + ZelloRepository.instance.getHistory(contact: contact) + } + + func leave(_ conversation: ZelloGroupConversation) { + ZelloRepository.instance.zello.leave(conversation) + } + +} diff --git a/ZelloSDKExampleApp/UI/Recents/RecentsView.swift b/ZelloSDKExampleApp/UI/Recents/RecentsView.swift index 37985c9..5ae3f4d 100644 --- a/ZelloSDKExampleApp/UI/Recents/RecentsView.swift +++ b/ZelloSDKExampleApp/UI/Recents/RecentsView.swift @@ -84,6 +84,8 @@ struct RecentsView: View { return "\(channelUser) : \(recent.contact.name)" } else if let user = recent.contact.asZelloUser() { return "\(user.displayName) (\(user.name))" + } else if let conversation = recent.contact.asZelloGroupConversation() { + return conversation.displayName } return recent.contact.name } diff --git a/ZelloSDKExampleApp/UI/TabBar.swift b/ZelloSDKExampleApp/UI/TabBar.swift index 9d8af94..e7d4f98 100644 --- a/ZelloSDKExampleApp/UI/TabBar.swift +++ b/ZelloSDKExampleApp/UI/TabBar.swift @@ -15,6 +15,10 @@ struct TabBar: View { .tabItem { Label("Channels", systemImage: "person.3") } + ConversationsView() + .tabItem { + Label("Conversations", systemImage: "person.2") + } } } } diff --git a/ZelloSDKExampleApp/UI/Users/UsersView.swift b/ZelloSDKExampleApp/UI/Users/UsersView.swift index 23ec42d..df7b8a2 100644 --- a/ZelloSDKExampleApp/UI/Users/UsersView.swift +++ b/ZelloSDKExampleApp/UI/Users/UsersView.swift @@ -11,6 +11,8 @@ struct UsersView: View { @State private var userInputText = "" @State private var selectedUserForText: ZelloUser? + private let profilePictureSize: CGFloat = 40 + var body: some View { NavigationView { ZStack { @@ -19,11 +21,34 @@ struct UsersView: View { LazyVStack { ForEach(viewModel.users, id: \.name) { user in HStack { + if let picture = user.profilePictureThumbnailURL { + AsyncImage(url: picture) { phase in + switch phase { + case .empty: + Color.gray // Placeholder while loading + case .success(let image): + image + .resizable() + .scaledToFill() + .clipShape(Circle()) + case .failure(_): + Color.red // Display red if image fails to load + @unknown default: + Color.blue // Fallback for future cases + } + } + .frame(width: profilePictureSize, height: profilePictureSize) + } + Details(viewModel: viewModel, user: user) Spacer() - ActionsButton(viewModel: viewModel, + ActionsButton(viewModel: viewModel, user: user, isMuted: user.isMuted, + showSendLocation: viewModel.settings?.allowsLocationMessages == true, + showSendImage: viewModel.settings?.allowsImageMessages == true, + showSendAlert: viewModel.settings?.allowsAlertMessages == true, + showSendText: viewModel.settings?.allowsTextMessages == true, showTextInputDialog: $showTextInputDialog, showAlertInputDialog: $showAlertInputDialog, userInputText: $userInputText, @@ -79,6 +104,7 @@ struct UsersView: View { text: $userInputText, action: .text, contact: contact, + conversation: nil, onSend: { viewModel.sendText(user: selectedUser, message: userInputText) } @@ -93,6 +119,7 @@ struct UsersView: View { text: $userInputText, action: .alert, contact: contact, + conversation: nil, onSend: { viewModel.sendAlert(user: selectedUser, message: userInputText) } @@ -180,6 +207,10 @@ struct UsersView: View { @ObservedObject var viewModel: UsersViewModel let user: ZelloUser let isMuted: Bool + let showSendLocation: Bool + let showSendImage: Bool + let showSendAlert: Bool + let showSendText: Bool @Binding var showTextInputDialog: Bool @Binding var showAlertInputDialog: Bool @Binding var userInputText: String @@ -199,18 +230,26 @@ struct UsersView: View { .buttonStyle(PlainButtonStyle()) .contentShape(Rectangle()) .contextMenu { - Button("Send Image", action: sendImage) - Button("Send Text", action: { - selectedUserForText = user - userInputText = "" - showTextInputDialog = true - }) - Button("Send Alert", action: { - selectedUserForText = user - userInputText = "" - showAlertInputDialog = true - }) - Button("Send Location", action: sendLocation) + if showSendImage { + Button("Send Image", action: sendImage) + } + if showSendText { + Button("Send Text", action: { + selectedUserForText = user + userInputText = "" + showTextInputDialog = true + }) + } + if showSendAlert { + Button("Send Alert", action: { + selectedUserForText = user + userInputText = "" + showAlertInputDialog = true + }) + } + if showSendLocation { + Button("Send Location", action: sendLocation) + } Button(isMuted ? "Unmute" : "Mute", action: toggleMute) Button("Show History", action: showHistory) } diff --git a/ZelloSDKExampleApp/UI/Users/UsersViewModel.swift b/ZelloSDKExampleApp/UI/Users/UsersViewModel.swift index 6fa248e..93e7e9b 100644 --- a/ZelloSDKExampleApp/UI/Users/UsersViewModel.swift +++ b/ZelloSDKExampleApp/UI/Users/UsersViewModel.swift @@ -20,6 +20,7 @@ class UsersViewModel: ObservableObject, ConnectivityProvider { @Published var incomingLocationMessage: ZelloLocationMessage? @Published var accountStatus: ZelloAccountStatus? @Published var history: HistoryViewState? = nil + @Published var settings: ZelloConsoleSettings? = nil private var cancellables: Set = [] @@ -147,6 +148,13 @@ class UsersViewModel: ObservableObject, ConnectivityProvider { self?.history = history } .store(in: &cancellables) + + settings = ZelloRepository.instance.settings + ZelloRepository.instance.$settings + .sink { [weak self] settings in + self?.settings = settings + } + .store(in: &cancellables) } func connect(credentials: ZelloCredentials) {