From 5a92d5c168e5b24eb17afee62c8bc7481acafc8a Mon Sep 17 00:00:00 2001 From: Illia Chemolosov Date: Tue, 11 Feb 2025 19:21:26 +0100 Subject: [PATCH] 0.5.0 ### New Features - **Dialogs Screen Customization:** - Introduced the ability to modify the content of the dialogs screen, allowing greater customization for app developers. ### Deprecations - **Tab Bar Feature Removed:** - The tab bar feature has been deprecated to streamline navigation and improve UI flexibility. ### Bug Fixes - **QuickBloxUIKit Memory Leak Fix** - Fixed a memory leak issue related to the use of UIHostingController with QuickBloxUIKit navigation logic, which was caused by improper handling of environment dismiss in QuickBloxUIKit. ### Data Enhancements - **Data Source Customization:** - Added the ability to tweak UI Kits' implementation for data sources, providing more flexibility for integration. --- Package.swift | 2 +- .../DTO/AI/RemoteAnswerAssistMessageDTO.swift | 18 +- .../DTO/AI/RemoteTranslateMessageDTO.swift | 10 +- .../DTO/Dialog/LocalDialogDTO.swift | 66 ++- .../DTO/Dialog/LocalDialogsDTO.swift | 6 +- .../DTO/Dialog/RemoteDialogDTO.swift | 66 ++- .../DTO/Dialog/RemoteDialogsDTO.swift | 14 +- .../DTO/Message/LocalMessageDTO.swift | 44 +- .../DTO/Message/LocalMessagesDTO.swift | 8 +- .../DTO/Message/RemoteMessageDTO.swift | 124 +++-- .../DTO/Message/RemoteMessagesDTO.swift | 18 +- .../Message/RemoteOriginalMessageDTO.swift | 30 +- .../QuickBloxData/DTO/User/LocalUserDTO.swift | 22 +- .../DTO/User/RemoteUserDTO.swift | 22 +- .../DTO/User/RemoteUsersDTO.swift | 18 +- .../QuickBloxData/Pagination/Pagination.swift | 2 +- .../QuickBloxData/RepositoriesFabric.swift | 50 -- Sources/QuickBloxData/Repository.swift | 50 ++ .../Repository/ConnectionRepository.swift | 2 +- .../Repository/DialogsRepository.swift | 4 +- .../Repository/MessagesRepository.swift | 28 +- .../Repository/PermissionsRepository.swift | 12 +- .../Repository/UsersRepository.swift | 2 +- .../Source/Local/LocalDataSource.swift | 74 +-- .../Source/Local/LocalFilesDataSource.swift | 23 +- ...urce.swift => PermissionsDataSource.swift} | 17 +- .../Local/PermissionsDataSourceProtocol.swift | 25 + .../QuickBloxData/Source/Remote/API/API.swift | 12 +- .../Source/Remote/API/APIAI.swift | 10 +- .../Source/Remote/API/APIDialogs.swift | 16 +- .../Source/Remote/API/APIFiles.swift | 10 +- .../Source/Remote/API/APIMessages.swift | 4 +- .../Source/Remote/API/APIUsers.swift | 10 +- .../Extension/NSError+RemoteExeption.swift | 2 +- .../RemoteFileInfoDTO+QBChatAttachment.swift | 4 + .../RemoteMessageDTO+QBChatMessage.swift | 96 ++-- .../Extension/RemoteUserDTO+QBUUser.swift | 22 +- .../Source/Remote/RemoteDataSource.swift | 148 +++--- Sources/QuickBloxData/Utils.swift | 8 +- Sources/QuickBloxLog/Log.swift | 8 +- Sources/QuickBloxUIKit/QuickBloxUIKit.swift | 52 +- Sources/QuickBloxUIKit/ScreenFabric.swift | 4 - .../DialogInfo/GroupDialogInfoView.swift | 166 +++---- .../GroupDialogNonEditInfoView.swift | 98 ++-- .../Dialog/DialogViewViewModifier.swift | 23 - .../Dialog/Forward/ForwardView.swift | 18 +- .../SwiftUIView/Dialog/GroupDialogView.swift | 460 +++++++++--------- .../Dialog/Members/RemoveMembersView.swift | 2 +- .../Dialog/PrivateDialogView.swift | 46 +- .../CreateDialog/CreateDialogView.swift | 23 +- .../DialogTypeView/DialogTypeView.swift | 43 +- .../SwiftUIView/Dialogs/DialogsView.swift | 96 ++-- .../Dialogs/NewDialog/NewDialog.swift | 47 +- .../SelectDialogsListView.swift | 27 +- .../SwiftUIView/Theme/Settings/Feature.swift | 5 +- .../Theme/Settings/FeatureSettings.swift | 8 + .../ViewModel/AddMembersDialogViewModel.swift | 9 +- .../ViewModel/CreateDialogViewModel.swift | 8 +- .../ViewModel/DialogInfoViewModel.swift | 10 +- .../ViewModel/DialogViewModel.swift | 30 +- .../ViewModel/DialogsViewModel.swift | 2 +- .../ViewModel/ForwardViewModel.swift | 6 +- .../ViewModel/MembersDialogViewModel.swift | 10 +- .../ViewModel/NewDialogViewModel.swift | 4 +- .../ViewModel/Utils/TypingProvider.swift | 4 +- .../ViewModel/Utils/Utils.swift | 24 +- 66 files changed, 1310 insertions(+), 1022 deletions(-) delete mode 100644 Sources/QuickBloxData/RepositoriesFabric.swift create mode 100644 Sources/QuickBloxData/Repository.swift rename Sources/QuickBloxData/Source/Local/{PermissionsSource.swift => PermissionsDataSource.swift} (74%) create mode 100644 Sources/QuickBloxData/Source/Local/PermissionsDataSourceProtocol.swift diff --git a/Package.swift b/Package.swift index cbfbdc3..ecbaad0 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "QuickBloxUIKit", - targets: ["QuickBloxUIKit", "QuickBloxData", "QuickBloxDomain"]), + targets: ["QuickBloxUIKit", "QuickBloxData", "QuickBloxDomain", "QuickBloxLog"]), ], dependencies: [ .package(url: "https://github.com/QuickBlox/ios-quickblox-sdk", .upToNextMajor(from: "2.21.0")), diff --git a/Sources/QuickBloxData/DTO/AI/RemoteAnswerAssistMessageDTO.swift b/Sources/QuickBloxData/DTO/AI/RemoteAnswerAssistMessageDTO.swift index 7e6907f..3eb6e9f 100644 --- a/Sources/QuickBloxData/DTO/AI/RemoteAnswerAssistMessageDTO.swift +++ b/Sources/QuickBloxData/DTO/AI/RemoteAnswerAssistMessageDTO.swift @@ -11,15 +11,19 @@ import Foundation /// This is a DTO model for interactions with the Answer Assist Message models in remote storage. public struct RemoteAnswerAssistMessageDTO { - var id = "" - var smartChatAssistantId = "" - var message = "" - var history: [RemoteAnswerAssistHistoryMessageDTO] = [] + public var id = "" + public var smartChatAssistantId = "" + public var message = "" + public var history: [RemoteAnswerAssistHistoryMessageDTO] = [] + + public init () {} } /// This is a DTO model for interactions with the Answer Assist History Message models in remote storage. public struct RemoteAnswerAssistHistoryMessageDTO { - var id = "" - var role: AIMessageRole = .user - var message = "" + public var id = "" + public var role: AIMessageRole = .user + public var message = "" + + public init () {} } diff --git a/Sources/QuickBloxData/DTO/AI/RemoteTranslateMessageDTO.swift b/Sources/QuickBloxData/DTO/AI/RemoteTranslateMessageDTO.swift index f87f2d9..79a3788 100644 --- a/Sources/QuickBloxData/DTO/AI/RemoteTranslateMessageDTO.swift +++ b/Sources/QuickBloxData/DTO/AI/RemoteTranslateMessageDTO.swift @@ -9,8 +9,10 @@ import Foundation public struct RemoteTranslateMessageDTO { - var id = "" - var smartChatAssistantId = "" - var message = "" - var languageCode = "" + public var id = "" + public var smartChatAssistantId = "" + public var message = "" + public var languageCode = "" + + public init () {} } diff --git a/Sources/QuickBloxData/DTO/Dialog/LocalDialogDTO.swift b/Sources/QuickBloxData/DTO/Dialog/LocalDialogDTO.swift index cf1d974..88ee4b8 100644 --- a/Sources/QuickBloxData/DTO/Dialog/LocalDialogDTO.swift +++ b/Sources/QuickBloxData/DTO/Dialog/LocalDialogDTO.swift @@ -12,28 +12,62 @@ import Foundation /// This is a DTO model for interactions with the dialog session or conversation model in local storage. public struct LocalDialogDTO: Equatable, Identifiable, Hashable { public var id = UUID().uuidString - var type: DialogType = .private - var name = "" - var participantsIds: [String] = [] - var photo = "" - var ownerId = "" - var isOwnedByCurrentUser = false + public var type: DialogType = .private + public var name = "" + public var participantsIds: [String] = [] + public var photo = "" + public var ownerId = "" + public var isOwnedByCurrentUser = false - var createdAt = Date() - var updatedAt = Date() + public var createdAt = Date() + public var updatedAt = Date() - var messages: [LocalMessageDTO] = [] + public var messages: [LocalMessageDTO] = [] - var lastMessageId = "" - var lastMessageText = "" - var lastMessageDateSent = Date(timeIntervalSince1970: 0.0) - var lastMessageUserId: String = "" - var unreadMessagesCount: Int = 0 - var decrementCounter: Bool = false + public var lastMessageId = "" + public var lastMessageText = "" + public var lastMessageDateSent = Date(timeIntervalSince1970: 0.0) + public var lastMessageUserId: String = "" + public var unreadMessagesCount: Int = 0 + public var decrementCounter: Bool = false + + public init(id: String = UUID().uuidString, + type: DialogType = .private, + name: String = "", + participantsIds: [String] = [], + photo: String = "", + ownerId: String = "", + isOwnedByCurrentUser: Bool = false, + createdAt: Date = Date(), + updatedAt: Date = Date(), + messages: [LocalMessageDTO] = [], + lastMessageId: String = "", + lastMessageText: String = "", + lastMessageDateSent: Date = Date(timeIntervalSince1970: 0.0), + lastMessageUserId: String = "", + unreadMessagesCount: Int = 0, + decrementCounter: Bool = false) { + self.id = id + self.type = type + self.name = name + self.participantsIds = participantsIds + self.photo = photo + self.ownerId = ownerId + self.isOwnedByCurrentUser = isOwnedByCurrentUser + self.createdAt = createdAt + self.updatedAt = updatedAt + self.messages = messages + self.lastMessageId = lastMessageId + self.lastMessageText = lastMessageText + self.lastMessageDateSent = lastMessageDateSent + self.lastMessageUserId = lastMessageUserId + self.unreadMessagesCount = unreadMessagesCount + self.decrementCounter = decrementCounter + } } extension LocalDialogDTO: Dated { - var date: Date { + public var date: Date { return updatedAt } } diff --git a/Sources/QuickBloxData/DTO/Dialog/LocalDialogsDTO.swift b/Sources/QuickBloxData/DTO/Dialog/LocalDialogsDTO.swift index 47b56da..4b692a8 100644 --- a/Sources/QuickBloxData/DTO/Dialog/LocalDialogsDTO.swift +++ b/Sources/QuickBloxData/DTO/Dialog/LocalDialogsDTO.swift @@ -10,6 +10,8 @@ import QuickBloxDomain /// This is a DTO model for interactions with the dialog session or conversation models in local storage. public struct LocalDialogsDTO { - var dialogs: [LocalDialogDTO] = [] - var pagination = Pagination() + public var dialogs: [LocalDialogDTO] = [] + public var pagination = Pagination() + + public init () {} } diff --git a/Sources/QuickBloxData/DTO/Dialog/RemoteDialogDTO.swift b/Sources/QuickBloxData/DTO/Dialog/RemoteDialogDTO.swift index f51eaf9..719aead 100644 --- a/Sources/QuickBloxData/DTO/Dialog/RemoteDialogDTO.swift +++ b/Sources/QuickBloxData/DTO/Dialog/RemoteDialogDTO.swift @@ -11,22 +11,56 @@ import Foundation /// This is a DTO model for interactions with the dialog session or conversation model in remote storage. public struct RemoteDialogDTO: Equatable { - var id = "" - var type: DialogType = .private - var name = "" - var participantsIds: [String] = [] - var toDeleteIds: [String] = [] - var toAddIds: [String] = [] - var photo = "" - var ownerId = "" - var isOwnedByCurrentUser = false + public var id = "" + public var type: DialogType = .private + public var name = "" + public var participantsIds: [String] = [] + public var toDeleteIds: [String] = [] + public var toAddIds: [String] = [] + public var photo = "" + public var ownerId = "" + public var isOwnedByCurrentUser = false - var createdAt = Date() - var updatedAt = Date() + public var createdAt = Date() + public var updatedAt = Date() - var lastMessageId = "" - var lastMessageText = "" - var lastMessageDateSent = Date() - var lastMessageUserId: String = "" - var unreadMessagesCount: Int = 0 + public var lastMessageId = "" + public var lastMessageText = "" + public var lastMessageDateSent = Date() + public var lastMessageUserId: String = "" + public var unreadMessagesCount: Int = 0 + + public init(id: String = "", + type: DialogType = .private, + name: String = "", + participantsIds: [String] = [], + toDeleteIds: [String] = [], + toAddIds: [String] = [], + photo: String = "", + ownerId: String = "", + isOwnedByCurrentUser: Bool = false, + createdAt: Date = Date(), + updatedAt: Date = Date(), + lastMessageId: String = "", + lastMessageText: String = "", + lastMessageDateSent: Date = Date(), + lastMessageUserId: String = "", + unreadMessagesCount: Int = 0) { + self.id = id + self.type = type + self.name = name + self.participantsIds = participantsIds + self.toDeleteIds = toDeleteIds + self.toAddIds = toAddIds + self.photo = photo + self.ownerId = ownerId + self.isOwnedByCurrentUser = isOwnedByCurrentUser + self.createdAt = createdAt + self.updatedAt = updatedAt + self.lastMessageId = lastMessageId + self.lastMessageText = lastMessageText + self.lastMessageDateSent = lastMessageDateSent + self.lastMessageUserId = lastMessageUserId + self.unreadMessagesCount = unreadMessagesCount + } } diff --git a/Sources/QuickBloxData/DTO/Dialog/RemoteDialogsDTO.swift b/Sources/QuickBloxData/DTO/Dialog/RemoteDialogsDTO.swift index fbaf533..24a0fcb 100644 --- a/Sources/QuickBloxData/DTO/Dialog/RemoteDialogsDTO.swift +++ b/Sources/QuickBloxData/DTO/Dialog/RemoteDialogsDTO.swift @@ -10,7 +10,15 @@ import QuickBloxDomain /// This is a DTO model for interactions with the dialog session or conversation models in remote storage. public struct RemoteDialogsDTO { - var dialogs: [RemoteDialogDTO] = [] - var usersIds: [String] = [] - var pagination = Pagination() + public var dialogs: [RemoteDialogDTO] = [] + public var usersIds: [String] = [] + public var pagination = Pagination() + + public init(dialogs: [RemoteDialogDTO] = [], + usersIds: [String] = [], + pagination: Pagination = Pagination()) { + self.dialogs = dialogs + self.usersIds = usersIds + self.pagination = pagination + } } diff --git a/Sources/QuickBloxData/DTO/Message/LocalMessageDTO.swift b/Sources/QuickBloxData/DTO/Message/LocalMessageDTO.swift index 1fbacf9..6971b98 100644 --- a/Sources/QuickBloxData/DTO/Message/LocalMessageDTO.swift +++ b/Sources/QuickBloxData/DTO/Message/LocalMessageDTO.swift @@ -12,22 +12,24 @@ import Foundation /// This is a DTO model for interactions with the message model in local storage. public struct LocalMessageDTO: Identifiable, Hashable { public var id = UUID().uuidString - var dialogId = "" - var text = "" - var senderId = "" - var dateSent = Date(timeIntervalSince1970: 0) - var isOwnedByCurrentUser = false - var fileInfo: LocalFileInfoDTO? - var deliveredIds: [String] = [] - var readIds: [String] = [] - var isReaded = false - var isDelivered = false - var eventType: MessageEventType = .message - var type: MessageType = .chat - var actionType: MessageAction = .none - var originSenderName: String? - var originalMessages: [LocalMessageDTO] = [] - var relatedId: String = "" + public var dialogId = "" + public var text = "" + public var senderId = "" + public var dateSent = Date(timeIntervalSince1970: 0) + public var isOwnedByCurrentUser = false + public var fileInfo: LocalFileInfoDTO? + public var deliveredIds: [String] = [] + public var readIds: [String] = [] + public var isReaded = false + public var isDelivered = false + public var eventType: MessageEventType = .message + public var type: MessageType = .chat + public var actionType: MessageAction = .none + public var originSenderName: String? + public var originalMessages: [LocalMessageDTO] = [] + public var relatedId: String = "" + + public init () {} } extension LocalMessageDTO: Equatable { @@ -37,13 +39,15 @@ extension LocalMessageDTO: Equatable { } extension LocalMessageDTO: Dated { - var date: Date { dateSent } + public var date: Date { dateSent } } public struct LocalFileInfoDTO: Equatable, Identifiable, Hashable { public var id: String = "" - var ext: FileExtension = .json - var name: String = "" - var path: FilePath = FilePath() + public var ext: FileExtension = .json + public var name: String = "" + public var path: FilePath = FilePath() public var uid: String = "" + + public init () {} } diff --git a/Sources/QuickBloxData/DTO/Message/LocalMessagesDTO.swift b/Sources/QuickBloxData/DTO/Message/LocalMessagesDTO.swift index 795e6b1..b3e9369 100644 --- a/Sources/QuickBloxData/DTO/Message/LocalMessagesDTO.swift +++ b/Sources/QuickBloxData/DTO/Message/LocalMessagesDTO.swift @@ -11,7 +11,9 @@ import QuickBloxDomain /// This is a DTO model for interactions with messages models in local storage. public struct LocalMessagesDTO { - var dialogId = "" - var messages: [LocalMessageDTO] = [] - var pagination = Pagination() + public var dialogId = "" + public var messages: [LocalMessageDTO] = [] + public var pagination = Pagination() + + public init () {} } diff --git a/Sources/QuickBloxData/DTO/Message/RemoteMessageDTO.swift b/Sources/QuickBloxData/DTO/Message/RemoteMessageDTO.swift index 51f7982..3f36a50 100644 --- a/Sources/QuickBloxData/DTO/Message/RemoteMessageDTO.swift +++ b/Sources/QuickBloxData/DTO/Message/RemoteMessageDTO.swift @@ -11,37 +11,101 @@ import Foundation /// This is a DTO model for interactions with the message model in remote storage. public struct RemoteMessageDTO: Equatable { - var id = "" - var dialogId = "" - var text = "" - var recipientId = "" - var senderId = "" - var senderResource = "" - var dateSent = Date(timeIntervalSince1970: 0) - var customParameters: [String: String] = [:] - var filesInfo: [RemoteFileInfoDTO] = [] - var delayed = false - var markable = true - var createdAt = Date(timeIntervalSince1970: 0) - var updatedAt = Date(timeIntervalSince1970: 0) - var deliveredIds: [String] = [] - var readIds: [String] = [] - var isOwnedByCurrentUser = false - var isReaded = false - var isDelivered = false - var eventType: MessageEventType = .message - var type: MessageType = .chat - var saveToHistory: Bool = true - var actionType: MessageAction = .none - var originSenderName: String = "" - var originalMessages: [RemoteMessageDTO] = [] - var relatedId = "" + public var id: String + public var dialogId: String + public var text: String + public var recipientId: String + public var senderId: String + public var senderResource: String + public var dateSent: Date + public var customParameters: [String: String] + public var filesInfo: [RemoteFileInfoDTO] + public var delayed: Bool + public var markable: Bool + public var createdAt: Date + public var updatedAt: Date + public var deliveredIds: [String] + public var readIds: [String] + public var isOwnedByCurrentUser: Bool + public var isReaded: Bool + public var isDelivered: Bool + public var eventType: MessageEventType + public var type: MessageType + public var saveToHistory: Bool + public var actionType: MessageAction + public var originSenderName: String + public var originalMessages: [RemoteMessageDTO] + public var relatedId: String + + public init(id: String = "", + dialogId: String = "", + text: String = "", + recipientId: String = "", + senderId: String = "", + senderResource: String = "", + dateSent: Date = Date(timeIntervalSince1970: 0), + customParameters: [String : String] = [:], + filesInfo: [RemoteFileInfoDTO] = [], + delayed: Bool = false, + markable: Bool = true, + createdAt: Date = Date(timeIntervalSince1970: 0), + updatedAt: Date = Date(timeIntervalSince1970: 0), + deliveredIds: [String] = [], + readIds: [String] = [], + isOwnedByCurrentUser: Bool = false, + isReaded: Bool = false, + isDelivered: Bool = false, + eventType: MessageEventType = .message, + type: MessageType = .chat, + saveToHistory: Bool = true, + actionType: MessageAction = .none, + originSenderName: String = "", + originalMessages: [RemoteMessageDTO] = [], + relatedId: String = "") { + self.id = id + self.dialogId = dialogId + self.text = text + self.recipientId = recipientId + self.senderId = senderId + self.senderResource = senderResource + self.dateSent = dateSent + self.customParameters = customParameters + self.filesInfo = filesInfo + self.delayed = delayed + self.markable = markable + self.createdAt = createdAt + self.updatedAt = updatedAt + self.deliveredIds = deliveredIds + self.readIds = readIds + self.isOwnedByCurrentUser = isOwnedByCurrentUser + self.isReaded = isReaded + self.isDelivered = isDelivered + self.eventType = eventType + self.type = type + self.saveToHistory = saveToHistory + self.actionType = actionType + self.originSenderName = originSenderName + self.originalMessages = originalMessages + self.relatedId = relatedId + } } public struct RemoteFileInfoDTO: Equatable, Codable { - var id = "" - var name = "" - var type = "" - var path = "" - var uid = "" + public var id: String + public var name: String + public var type: String + public var path: String + public var uid: String + + public init(id: String = "", + name: String = "", + type: String = "", + path: String = "", + uid: String = "") { + self.id = id + self.name = name + self.type = type + self.path = path + self.uid = uid + } } diff --git a/Sources/QuickBloxData/DTO/Message/RemoteMessagesDTO.swift b/Sources/QuickBloxData/DTO/Message/RemoteMessagesDTO.swift index 21efa4b..8b910c0 100644 --- a/Sources/QuickBloxData/DTO/Message/RemoteMessagesDTO.swift +++ b/Sources/QuickBloxData/DTO/Message/RemoteMessagesDTO.swift @@ -10,8 +10,18 @@ import QuickBloxDomain /// This is a DTO model for interactions with messages models in remote storage. public struct RemoteMessagesDTO { - var dialogId = "" - var ids: [String] = [] - var messages: [RemoteMessageDTO] = [] - var pagination = Pagination() + public var dialogId = "" + public var ids: [String] = [] + public var messages: [RemoteMessageDTO] = [] + public var pagination = Pagination() + + public init(dialogId: String = "", + ids: [String] = [], + messages: [RemoteMessageDTO] = [], + pagination: Pagination = Pagination()) { + self.dialogId = dialogId + self.ids = ids + self.messages = messages + self.pagination = pagination + } } diff --git a/Sources/QuickBloxData/DTO/Message/RemoteOriginalMessageDTO.swift b/Sources/QuickBloxData/DTO/Message/RemoteOriginalMessageDTO.swift index b05b363..d0f2f29 100644 --- a/Sources/QuickBloxData/DTO/Message/RemoteOriginalMessageDTO.swift +++ b/Sources/QuickBloxData/DTO/Message/RemoteOriginalMessageDTO.swift @@ -10,17 +10,17 @@ import Foundation /// This is a DTO model for interactions with the original message model in remote storage. public struct RemoteOriginalMessageDTO: Codable { - var id: String - var dialogId: String - var text: String - var recipientId: UInt - var senderId: UInt - var dateSent: Int64 - var attachments: [RemoteOriginalFileInfoDTO] - var createdAt: Date - var updatedAt: Date - var deliveredIds: [UInt] - var readIds: [UInt] + public var id: String + public var dialogId: String + public var text: String + public var recipientId: UInt + public var senderId: UInt + public var dateSent: Int64 + public var attachments: [RemoteOriginalFileInfoDTO] + public var createdAt: Date + public var updatedAt: Date + public var deliveredIds: [UInt] + public var readIds: [UInt] public init(id: String, dialogId: String = "", @@ -67,10 +67,12 @@ public struct RemoteOriginalFileInfoDTO: Codable { var type: String = "" var url: String = "" var uid: String = "" + + public init () {} } extension RemoteOriginalFileInfoDTO { - init(_ value: RemoteFileInfoDTO) { + public init(_ value: RemoteFileInfoDTO) { id = value.id name = value.name type = value.type @@ -80,7 +82,7 @@ extension RemoteOriginalFileInfoDTO { } extension RemoteOriginalMessageDTO { - init(_ value: RemoteMessageDTO) { + public init(_ value: RemoteMessageDTO) { id = value.id dialogId = value.dialogId text = value.text @@ -98,7 +100,7 @@ extension RemoteOriginalMessageDTO { } private extension Date { - var timeStampInt: Int64 { + public var timeStampInt: Int64 { return Int64(self.timeIntervalSince1970 * 1000) } } diff --git a/Sources/QuickBloxData/DTO/User/LocalUserDTO.swift b/Sources/QuickBloxData/DTO/User/LocalUserDTO.swift index 1b3ea0e..4557f2e 100644 --- a/Sources/QuickBloxData/DTO/User/LocalUserDTO.swift +++ b/Sources/QuickBloxData/DTO/User/LocalUserDTO.swift @@ -11,9 +11,21 @@ import Foundation /// This is a DTO model for interactions with the user in local storage. public struct LocalUserDTO: Equatable { - var id: String = "" - var name: String = "" - var avatarPath: String = "" - var lastRequestAt: Date = Date(timeIntervalSince1970: 0) - var isCurrent: Bool = false + public var id: String + public var name: String + public var avatarPath: String + public var lastRequestAt: Date + public var isCurrent: Bool + + public init(id: String = "", + name: String = "", + avatarPath: String = "", + lastRequestAt: Date = Date(timeIntervalSince1970: 0), + isCurrent: Bool = false) { + self.id = id + self.name = name + self.avatarPath = avatarPath + self.lastRequestAt = lastRequestAt + self.isCurrent = isCurrent + } } diff --git a/Sources/QuickBloxData/DTO/User/RemoteUserDTO.swift b/Sources/QuickBloxData/DTO/User/RemoteUserDTO.swift index 48f0ef5..8b42f5a 100644 --- a/Sources/QuickBloxData/DTO/User/RemoteUserDTO.swift +++ b/Sources/QuickBloxData/DTO/User/RemoteUserDTO.swift @@ -11,9 +11,21 @@ import Foundation /// This is a DTO model for interactions with the user in remote storage. public struct RemoteUserDTO: Equatable { - var id: String = "" - var name: String = "" - var avatarPath: String = "" - var lastRequestAt: Date = Date(timeIntervalSince1970: 0) - var isCurrent: Bool = false + public var id: String + public var name: String + public var avatarPath: String + public var lastRequestAt: Date + public var isCurrent: Bool + + public init(id: String = "", + name: String = "", + avatarPath: String = "", + lastRequestAt: Date = Date(timeIntervalSince1970: 0), + isCurrent: Bool = false) { + self.id = id + self.name = name + self.avatarPath = avatarPath + self.lastRequestAt = lastRequestAt + self.isCurrent = isCurrent + } } diff --git a/Sources/QuickBloxData/DTO/User/RemoteUsersDTO.swift b/Sources/QuickBloxData/DTO/User/RemoteUsersDTO.swift index 68334cb..340150a 100644 --- a/Sources/QuickBloxData/DTO/User/RemoteUsersDTO.swift +++ b/Sources/QuickBloxData/DTO/User/RemoteUsersDTO.swift @@ -10,8 +10,18 @@ import QuickBloxDomain /// This is a DTO model for interactions with users models in remote storage. public struct RemoteUsersDTO { - var ids: [String] = [] - var name: String = "" - var users: [RemoteUserDTO] = [] - var pagination = Pagination() + public var ids: [String] + public var name: String + public var users: [RemoteUserDTO] + public var pagination: Pagination + + public init(ids: [String] = [], + name: String = "", + users: [RemoteUserDTO] = [], + pagination: Pagination = Pagination()) { + self.ids = ids + self.name = name + self.users = users + self.pagination = pagination + } } diff --git a/Sources/QuickBloxData/Pagination/Pagination.swift b/Sources/QuickBloxData/Pagination/Pagination.swift index 28e517f..b82d592 100644 --- a/Sources/QuickBloxData/Pagination/Pagination.swift +++ b/Sources/QuickBloxData/Pagination/Pagination.swift @@ -30,7 +30,7 @@ public struct Pagination: PaginationProtocol { self.total = total } - init(page: Int = 1, perPage: Int = 10, total: Int = 0) { + public init(page: Int = 1, perPage: Int = 10, total: Int = 0) { self.skip = (page - 1) * perPage self.limit = perPage self.total = total diff --git a/Sources/QuickBloxData/RepositoriesFabric.swift b/Sources/QuickBloxData/RepositoriesFabric.swift deleted file mode 100644 index 198fb0e..0000000 --- a/Sources/QuickBloxData/RepositoriesFabric.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// RepositoriesFabric.swift -// QuickBloxUIKit -// -// Created by Injoit on 28.02.2023. -// Copyright © 2023 QuickBlox. All rights reserved. -// - -import QuickBloxDomain - -public class RepositoriesFabric { - private class Service { - static let remote = RemoteDataSource() - static let local = LocalDataSource() - static let localFiles = LocalFilesDataSource() - static let permissions = PermissionsSource() - } - - static public var dialogs: DialogsRepository { - DialogsRepository(remote: Service.remote, - local: Service.local) - } - - static public var users: UsersRepository { - UsersRepository(remote: Service.remote, - local: Service.local) - } - - static public var messages: MessagesRepository { - MessagesRepository(remote: Service.remote, - local: Service.local) - } - - static public var files: FilesRepository { - FilesRepository(remote: Service.remote, - local: Service.localFiles) - } - - static public var connection: ConnectionRepository { - ConnectionRepository(remote: Service.remote) - } - - static public var permissions: PermissionsRepository { - PermissionsRepository(repo: Service.permissions) - } - - static public var ai: AIRepository { - AIRepository(remote: Service.remote) - } -} diff --git a/Sources/QuickBloxData/Repository.swift b/Sources/QuickBloxData/Repository.swift new file mode 100644 index 0000000..4c67ace --- /dev/null +++ b/Sources/QuickBloxData/Repository.swift @@ -0,0 +1,50 @@ +// +// RepositoriesFabric.swift +// QuickBloxUIKit +// +// Created by Injoit on 28.02.2023. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import QuickBloxDomain + +public class DataSource { + public static var remote = RemoteDataSource() + public static var local: LocalDataSourceProtocol = LocalDataSource() + public static var localFiles = LocalFilesDataSource() + public static var permissions = PermissionsDataSource() +} + +public class Repository { + static public var dialogs: DialogsRepository { + DialogsRepository(remote: DataSource.remote, + local: DataSource.local) + } + + static public var users: UsersRepository { + UsersRepository(remote: DataSource.remote, + local: DataSource.local) + } + + static public var messages: MessagesRepository { + MessagesRepository(remote: DataSource.remote, + local: DataSource.local) + } + + static public var files: FilesRepository { + FilesRepository(remote: DataSource.remote, + local: DataSource.localFiles) + } + + static public var connection: ConnectionRepository { + ConnectionRepository(remote: DataSource.remote) + } + + static public var permissions: PermissionsRepository { + PermissionsRepository(source: DataSource.permissions) + } + + static public var ai: AIRepository { + AIRepository(remote: DataSource.remote) + } +} diff --git a/Sources/QuickBloxData/Repository/ConnectionRepository.swift b/Sources/QuickBloxData/Repository/ConnectionRepository.swift index d349b6c..1d2724c 100644 --- a/Sources/QuickBloxData/Repository/ConnectionRepository.swift +++ b/Sources/QuickBloxData/Repository/ConnectionRepository.swift @@ -12,7 +12,7 @@ import Combine public class ConnectionRepository { private var remote: RemoteDataSourceProtocol! - init(remote: RemoteDataSourceProtocol) { + public init(remote: RemoteDataSourceProtocol) { self.remote = remote } diff --git a/Sources/QuickBloxData/Repository/DialogsRepository.swift b/Sources/QuickBloxData/Repository/DialogsRepository.swift index 096d40a..2836d92 100644 --- a/Sources/QuickBloxData/Repository/DialogsRepository.swift +++ b/Sources/QuickBloxData/Repository/DialogsRepository.swift @@ -16,8 +16,8 @@ public class DialogsRepository { private let remote: RemoteDataSourceProtocol private let local: LocalDataSourceProtocol - init(remote: RemoteDataSourceProtocol, - local: LocalDataSourceProtocol) { + public init(remote: RemoteDataSourceProtocol, + local: LocalDataSourceProtocol) { self.remote = remote self.local = local } diff --git a/Sources/QuickBloxData/Repository/MessagesRepository.swift b/Sources/QuickBloxData/Repository/MessagesRepository.swift index cde5e8f..f73e0ec 100644 --- a/Sources/QuickBloxData/Repository/MessagesRepository.swift +++ b/Sources/QuickBloxData/Repository/MessagesRepository.swift @@ -15,7 +15,7 @@ public class MessagesRepository: MessagesRepositoryProtocol { private var remote: RemoteDataSourceProtocol! private var local: LocalDataSourceProtocol! - init(remote: RemoteDataSourceProtocol, + public init(remote: RemoteDataSourceProtocol, local: LocalDataSourceProtocol) { self.remote = remote self.local = local @@ -58,15 +58,12 @@ private extension RemoteMessageDTO { id = value.id dialogId = value.dialogId text = value.text + recipientId = "" senderId = value.userId + senderResource = "" dateSent = value.date - isOwnedByCurrentUser = value.isOwnedByCurrentUser - deliveredIds = value.deliveredIds - readIds = value.readIds - isReaded = value.isRead - isDelivered = value.isDelivered - type = value.type - eventType = value.eventType + customParameters = [:] + filesInfo = [] if let file = value.fileInfo { filesInfo.append( RemoteFileInfoDTO( @@ -77,7 +74,20 @@ private extension RemoteMessageDTO { uid: file.uid )) } + delayed = false + markable = true + createdAt = Date(timeIntervalSince1970: 0) + updatedAt = Date(timeIntervalSince1970: 0) + deliveredIds = value.deliveredIds + readIds = value.readIds + isOwnedByCurrentUser = value.isOwnedByCurrentUser + isReaded = value.isRead + isDelivered = value.isDelivered + eventType = value.eventType + type = value.type + saveToHistory = true actionType = value.actionType + self.originSenderName = "" if let originSenderName = value.originSenderName { self.originSenderName = originSenderName } @@ -191,7 +201,7 @@ extension MessagesRepository { public func get(messagesFromLocal dialogId: String) async throws -> [Message] { do { - let withId = LocalMessagesDTO(dialogId: dialogId) + let withId = LocalMessagesDTO() return try await local.get(messages: withId).messages.map { Message($0)} } catch { throw try error.repositoryException diff --git a/Sources/QuickBloxData/Repository/PermissionsRepository.swift b/Sources/QuickBloxData/Repository/PermissionsRepository.swift index a0adb97..d269b2e 100644 --- a/Sources/QuickBloxData/Repository/PermissionsRepository.swift +++ b/Sources/QuickBloxData/Repository/PermissionsRepository.swift @@ -10,19 +10,17 @@ import QuickBloxDomain import AVFoundation public class PermissionsRepository { - private var repo: PermissionsRepositoryProtocol! + private let source: PermissionsDataSourceProtocol - init(repo: PermissionsRepositoryProtocol) { - self.repo = repo + public init(source: PermissionsDataSourceProtocol) { + self.source = source } - - private init() { } } extension PermissionsRepository: PermissionsRepositoryProtocol { public func openSettings() async throws { do { - try await repo.openSettings() + try await source.openSettings() } catch { throw try error.repositoryException } @@ -30,7 +28,7 @@ extension PermissionsRepository: PermissionsRepositoryProtocol { public func get(permissionTo mediaType: AVMediaType) async throws -> Bool { do { - return try await repo.get(permissionTo: mediaType) + return try await source.get(permissionTo: mediaType) } catch { throw try error.repositoryException } diff --git a/Sources/QuickBloxData/Repository/UsersRepository.swift b/Sources/QuickBloxData/Repository/UsersRepository.swift index 0b95f42..fa1210a 100644 --- a/Sources/QuickBloxData/Repository/UsersRepository.swift +++ b/Sources/QuickBloxData/Repository/UsersRepository.swift @@ -15,7 +15,7 @@ public class UsersRepository { private var remote: RemoteDataSourceProtocol! private var local: LocalDataSourceProtocol! - init(remote: RemoteDataSourceProtocol, + public init(remote: RemoteDataSourceProtocol, local: LocalDataSourceProtocol) { self.remote = remote self.local = local diff --git a/Sources/QuickBloxData/Source/Local/LocalDataSource.swift b/Sources/QuickBloxData/Source/Local/LocalDataSource.swift index 88d7da1..17e6700 100644 --- a/Sources/QuickBloxData/Source/Local/LocalDataSource.swift +++ b/Sources/QuickBloxData/Source/Local/LocalDataSource.swift @@ -14,36 +14,29 @@ import QuickBloxLog /// This is a class that implements the ``LocalDataSourceProtocol`` protocol and contains methods and properties that allow it to interact with the local data source. /// /// An object of this class provides access for local storage of ``Entity`` items at the time of the application's life cycle. Provides access to a single repository object by calling **LocalDataSource.instance** static property. -actor LocalDataSource: LocalDataSourceProtocol { +public actor LocalDataSource: LocalDataSourceProtocol { //MARK: Properties private var dialogs = CurrentValueSubject<[LocalDialogDTO], Never>([]) private var updatedDialog = CurrentValueSubject("") private var users: [String: LocalUserDTO] = [:] - var dialogsPublisher: AnyPublisher<[LocalDialogDTO], Never> { + public var dialogsPublisher: AnyPublisher<[LocalDialogDTO], Never> { get async { dialogs.eraseToAnyPublisher() } } - var dialogUpdatePublisher: AnyPublisher { + public var dialogUpdatePublisher: AnyPublisher { get async { updatedDialog.eraseToAnyPublisher() } } -} - -//MARK: Clear -extension LocalDataSource { - func cleareAll() async throws { - try await removeAllDialogs() - users.removeAll() - } -} - -//MARK: Dialogs -extension LocalDataSource { - func save(dialog dto: LocalDialogDTO) async throws { + + public init() {} + + //MARK: Dialogs + + public func save(dialog dto: LocalDialogDTO) async throws { if dialogs.value.first(where: { $0.id == dto.id } ) != nil { try await update(dialog: dto) return @@ -54,21 +47,21 @@ extension LocalDataSource { updatedDialog.value = dto.id } - func get(dialog dto: LocalDialogDTO) async throws -> LocalDialogDTO { + public func get(dialog dto: LocalDialogDTO) async throws -> LocalDialogDTO { guard let dialog = dialogs.value.first(where: { $0.id == dto.id } ) else { throw DataSourceException.notFound() } return dialog } - func delete(dialog dto: LocalDialogDTO) async throws { + public func delete(dialog dto: LocalDialogDTO) async throws { guard let index = dialogs.value.firstIndex(where: { $0.id == dto.id } ) else { throw DataSourceException.notFound() } dialogs.value.remove(at: index) } - func update(dialog dto: LocalDialogDTO) async throws { + public func update(dialog dto: LocalDialogDTO) async throws { guard let index = dialogs.value.firstIndex(where: { $0.id == dto.id } ) else { throw DataSourceException.notFound() } @@ -154,22 +147,23 @@ extension LocalDataSource { } } - func getAllDialogs() async throws -> LocalDialogsDTO { - return LocalDialogsDTO(dialogs: Array(dialogs.value)) + public func getAllDialogs() async throws -> LocalDialogsDTO { + var dto = LocalDialogsDTO() + dto.dialogs = Array(dialogs.value) + return dto } - func getAllUsers() async throws -> [LocalUserDTO] { + public func getAllUsers() async throws -> [LocalUserDTO] { return Array(users.values) } - func removeAllDialogs() async throws { + public func removeAllDialogs() async throws { dialogs.value.removeAll() } -} - -//MARK: Messages -extension LocalDataSource { - func save(message: LocalMessageDTO) async throws { + + //MARK: Messages + + public func save(message: LocalMessageDTO) async throws { guard let index = dialogs.value.firstIndex(where: { $0.id == message.dialogId }) else { let info = "Dialog not found for message with dialog id: \(message.dialogId)" throw DataSourceException.notFound(description: info) @@ -179,7 +173,7 @@ extension LocalDataSource { dialogs.value[index] = dialog } - func get(messages dto: LocalMessagesDTO) async throws -> LocalMessagesDTO { + public func get(messages dto: LocalMessagesDTO) async throws -> LocalMessagesDTO { guard let dialog = dialogs.value.first(where: { $0.id == dto.dialogId }) else { let info = "Dialog not found for messages with dialog id: \(dto.dialogId)" throw DataSourceException.notFound(description: info) @@ -192,7 +186,7 @@ extension LocalDataSource { return result } - func delete(message dto: LocalMessageDTO) async throws { + public func delete(message dto: LocalMessageDTO) async throws { guard let index = dialogs.value.firstIndex(where: { $0.id == dto.dialogId }) else { let info = "Dialog not found for messages with dialog id: \(dto.dialogId)" throw DataSourceException.notFound(description: info) @@ -203,7 +197,7 @@ extension LocalDataSource { dialogs.value[index] = dialog } - func update(message dto: LocalMessageDTO) async throws { + public func update(message dto: LocalMessageDTO) async throws { guard let index = dialogs.value.firstIndex(where: { $0.id == dto.dialogId }) else { let info = "Dialog not found for messages with dialog id: \(dto.dialogId)" throw DataSourceException.notFound(description: info) @@ -212,19 +206,25 @@ extension LocalDataSource { dialog.messages.insertElement(dto, withSorting: .orderedAscending) dialogs.value[index] = dialog } -} - -//MARK: Users -extension LocalDataSource { - func save(user dto: LocalUserDTO) async throws { + + //MARK: Users + + public func save(user dto: LocalUserDTO) async throws { users[dto.id] = dto } - func get(user dto: LocalUserDTO) async throws -> LocalUserDTO { + public func get(user dto: LocalUserDTO) async throws -> LocalUserDTO { guard let user = users[dto.id] else { throw DataSourceException.notFound() } return user } + + //MARK: Clear + + public func cleareAll() async throws { + try await removeAllDialogs() + users.removeAll() + } } diff --git a/Sources/QuickBloxData/Source/Local/LocalFilesDataSource.swift b/Sources/QuickBloxData/Source/Local/LocalFilesDataSource.swift index e1c4b23..d654a2f 100644 --- a/Sources/QuickBloxData/Source/Local/LocalFilesDataSource.swift +++ b/Sources/QuickBloxData/Source/Local/LocalFilesDataSource.swift @@ -11,7 +11,7 @@ import Foundation /// This is a class that implements the ``LocalFileDataSourceProtocol`` protocol and contains methods and properties that allow it to interact with the local file storage. /// /// Stores data cache files in the Library/Caches/ directory. Cache data can be used for any information that needs to persist for longer than temporary data, but not as long as a support file. While the application does not necessarily need the cache data to function properly, it can use it to enhance performance. The system will automatically clear the Caches/ directory to free up disk space. -class LocalFilesDataSource { +open class LocalFilesDataSource: LocalFilesDataSourceProtocol { private let manager = FileManager.default private var fileURL: URL { get throws { @@ -49,11 +49,12 @@ class LocalFilesDataSource { return try fileURL.appendingPathComponent(id) } -} - -//MARK: LocalFileDataSourceProtocol -extension LocalFilesDataSource: LocalFilesDataSourceProtocol { - func createFile(_ dto: LocalFileDTO) async throws -> LocalFileDTO { + + public init() {} + + //MARK: LocalFileDataSourceProtocol + + open func createFile(_ dto: LocalFileDTO) async throws -> LocalFileDTO { var url = try fileURL(for: dto) url = url.appendingPathExtension(dto.ext.rawValue) let encoder = JSONEncoder() @@ -71,7 +72,7 @@ extension LocalFilesDataSource: LocalFilesDataSourceProtocol { return newDTO } - func getFile(_ dto: LocalFileDTO) async throws -> LocalFileDTO { + open func getFile(_ dto: LocalFileDTO) async throws -> LocalFileDTO { let url = try searchURL(for: dto) if manager.fileExists(atPath: url.path) == false { @@ -87,7 +88,7 @@ extension LocalFilesDataSource: LocalFilesDataSourceProtocol { return result } - func deleteFile(_ dto: LocalFileDTO) async throws { + open func deleteFile(_ dto: LocalFileDTO) async throws { let url = try searchURL(for: dto) if manager.fileExists(atPath: url.path) == false { throw DataSourceException.notFound() @@ -96,14 +97,14 @@ extension LocalFilesDataSource: LocalFilesDataSourceProtocol { try manager.removeItem(at: url) } - func cleareAll() async throws { + open func cleareAll() async throws { if manager.fileExists(atPath: try fileURL.path) == false { return } try manager.removeItem(at: try fileURL) } - private func searchURL(for dto: LocalFileDTO) throws -> URL { + open func searchURL(for dto: LocalFileDTO) throws -> URL { let url = try fileURL(for: dto) guard let id = url.pathComponents.last, @@ -115,7 +116,7 @@ extension LocalFilesDataSource: LocalFilesDataSourceProtocol { return try getFirstURL(forFileName: uuid) } - private func getFirstURL(forFileName fileName: String) throws -> URL { + open func getFirstURL(forFileName fileName: String) throws -> URL { let keys: [URLResourceKey] = [.isRegularFileKey, .nameKey] let dictionaryUrl = try fileURL let enumerator = manager.enumerator(at: dictionaryUrl, diff --git a/Sources/QuickBloxData/Source/Local/PermissionsSource.swift b/Sources/QuickBloxData/Source/Local/PermissionsDataSource.swift similarity index 74% rename from Sources/QuickBloxData/Source/Local/PermissionsSource.swift rename to Sources/QuickBloxData/Source/Local/PermissionsDataSource.swift index 1a8acc3..a9c0fb7 100644 --- a/Sources/QuickBloxData/Source/Local/PermissionsSource.swift +++ b/Sources/QuickBloxData/Source/Local/PermissionsDataSource.swift @@ -10,28 +10,25 @@ import QuickBloxDomain import AVFoundation import UIKit -class PermissionsSource { +open class PermissionsDataSource: PermissionsDataSourceProtocol { + public init() {} -} - -//MARK: PermissionsRepositoryProtocol -extension PermissionsSource: PermissionsRepositoryProtocol { - func openSettings() async throws { + open func openSettings() async throws { if let url = await URL(string: UIApplication.openSettingsURLString) { await UIApplication.shared.open(url, options: [:]) } } - func get(permissionTo mediaType: AVMediaType) async throws -> Bool { + open func get(permissionTo mediaType: AVMediaType) async throws -> Bool { switch mediaType { - case .audio: return try await PermissionsSource.requestPermissionTo(.audio) - case .video: return try await PermissionsSource.requestPermissionTo(.video) + case .audio: return try await PermissionsDataSource.requestPermissionTo(.audio) + case .video: return try await PermissionsDataSource.requestPermissionTo(.video) default: return false } } } -private extension PermissionsSource { +private extension PermissionsDataSource { static func requestPermissionTo(_ mediaType: AVMediaType) async throws -> Bool { switch mediaType { diff --git a/Sources/QuickBloxData/Source/Local/PermissionsDataSourceProtocol.swift b/Sources/QuickBloxData/Source/Local/PermissionsDataSourceProtocol.swift new file mode 100644 index 0000000..59802e7 --- /dev/null +++ b/Sources/QuickBloxData/Source/Local/PermissionsDataSourceProtocol.swift @@ -0,0 +1,25 @@ +// +// PermissionsDataSourceProtocol.swift +// QuickBloxUIKit +// +// Created by Injoit on Illia Chemolosov on 28.01.2025. +// Copyright © 2023 QuickBlox. All rights reserved. +// + +import QuickBloxDomain +import AVFoundation + +public protocol PermissionsDataSourceProtocol { + + /// Request Permission for mediaType. + /// - Parameter mediaType: ``AVMediaType`` an identifier for various media types. + /// - Returns: ``Bool`` granted value. + /// + /// - Throws: An error if the permission request fails or access to the requested media type is restricted. + func get(permissionTo mediaType: AVMediaType) async throws -> Bool + + /// Open Settings. + /// + /// - Throws: An error if the settings cannot be opened (e.g., unsupported platform or restricted access). + func openSettings() async throws +} diff --git a/Sources/QuickBloxData/Source/Remote/API/API.swift b/Sources/QuickBloxData/Source/Remote/API/API.swift index 699b986..4ba36fc 100644 --- a/Sources/QuickBloxData/Source/Remote/API/API.swift +++ b/Sources/QuickBloxData/Source/Remote/API/API.swift @@ -45,10 +45,10 @@ extension QBResponsePage { } } -struct API { - let dialogs = APIDialogs() - let users = APIUsers() - let messages = APIMessages() - let files = APIFiles() - let ai = APIAI() +public struct API { + public let dialogs = APIDialogs() + public let users = APIUsers() + public let messages = APIMessages() + public let files = APIFiles() + public let ai = APIAI() } diff --git a/Sources/QuickBloxData/Source/Remote/API/APIAI.swift b/Sources/QuickBloxData/Source/Remote/API/APIAI.swift index b059cdc..e5606b9 100644 --- a/Sources/QuickBloxData/Source/Remote/API/APIAI.swift +++ b/Sources/QuickBloxData/Source/Remote/API/APIAI.swift @@ -8,16 +8,16 @@ import Quickblox -struct APIAI { +public struct APIAI { // Quickblox Server API - func answerAssist(with message: QBAIAnswerAssistMessage) async throws -> String { + public func answerAssist(with message: QBAIAnswerAssistMessage) async throws -> String { let result = try await QB.ai.answerAssist(withSmartChatAssistantId: message.smartChatAssistantId, messageToAssist: message.message, history: message.history) return result.answer } - func translate(with message: QBAITranslateMessage) async throws -> String { + public func translate(with message: QBAITranslateMessage) async throws -> String { let result = try await QB.ai.translate(withSmartChatAssistantId: message.smartChatAssistantId, textToTranslate: message.message, languageCode: message.languageCode) @@ -30,7 +30,7 @@ import QBAIAnswerAssistant extension APIAI { // Quickblox QBAIAnswerAssistant Library - func answerAssist(with content: [RemoteMessageDTO], settings: QBAIAnswerAssistant.AISettings) async throws -> String { + public func answerAssist(with content: [RemoteMessageDTO], settings: QBAIAnswerAssistant.AISettings) async throws -> String { var aiSettings = settings @@ -59,7 +59,7 @@ import QBAITranslate extension APIAI { // Quickblox QBAITranslate Library - func translate(with text: String, content: [RemoteMessageDTO], settings: QBAITranslate.AISettings) async throws -> String { + public func translate(with text: String, content: [RemoteMessageDTO], settings: QBAITranslate.AISettings) async throws -> String { var aiSettings = settings diff --git a/Sources/QuickBloxData/Source/Remote/API/APIDialogs.swift b/Sources/QuickBloxData/Source/Remote/API/APIDialogs.swift index 9dbe127..2be0d78 100644 --- a/Sources/QuickBloxData/Source/Remote/API/APIDialogs.swift +++ b/Sources/QuickBloxData/Source/Remote/API/APIDialogs.swift @@ -8,14 +8,14 @@ import Quickblox -struct DialogsPayload { +public struct DialogsPayload { let dialogs:[QBChatDialog] let usersIds: Set let page: QBResponsePage } -struct APIDialogs { - func `get`(`for` page: QBResponsePage) async throws -> DialogsPayload { +public struct APIDialogs { + public func `get`(`for` page: QBResponsePage) async throws -> DialogsPayload { return try await withCheckedThrowingContinuation { continuation in let extended = ["sort_desc": "updated_at"] QBRequest.dialogs(for: page, extendedRequest: extended) { @@ -30,7 +30,7 @@ struct APIDialogs { } } - func `get`(with id: String) async throws -> QBChatDialog { + public func `get`(with id: String) async throws -> QBChatDialog { return try await withCheckedThrowingContinuation { continuation in let extended = ["_id": id] let page = QBResponsePage() @@ -50,7 +50,7 @@ struct APIDialogs { } } - func create(new dialog: QBChatDialog) async throws -> QBChatDialog { + public func create(new dialog: QBChatDialog) async throws -> QBChatDialog { return try await withCheckedThrowingContinuation { continuation in QBRequest.createDialog(dialog) { _, dialog in continuation.resume(returning: dialog) @@ -60,7 +60,7 @@ struct APIDialogs { } } - func update(_ dialog: QBChatDialog) async throws -> QBChatDialog { + public func update(_ dialog: QBChatDialog) async throws -> QBChatDialog { return try await withCheckedThrowingContinuation { continuation in QBRequest.update(dialog) { _, dialog in continuation.resume(returning: dialog) @@ -70,13 +70,13 @@ struct APIDialogs { } } - func leave(_ dialog: QBChatDialog) async throws { + public func leave(_ dialog: QBChatDialog) async throws { let userId = QBSession.current.currentUserID dialog.pullOccupantsIDs = [(NSNumber(value: userId)).stringValue] _ = try await update(dialog) } - func delete(with id: String, force: Bool) async throws { + public func delete(with id: String, force: Bool) async throws { return try await withCheckedThrowingContinuation { continuation in QBRequest.deleteDialogs(withIDs: Set([id]), forAllUsers: force) { _,_,_,_ in diff --git a/Sources/QuickBloxData/Source/Remote/API/APIFiles.swift b/Sources/QuickBloxData/Source/Remote/API/APIFiles.swift index 0378e2f..2fad12b 100644 --- a/Sources/QuickBloxData/Source/Remote/API/APIFiles.swift +++ b/Sources/QuickBloxData/Source/Remote/API/APIFiles.swift @@ -9,8 +9,8 @@ import Quickblox import QuickBloxDomain -struct APIFiles { - func `get`(with url: URL) async throws -> RemoteFileDTO { +public struct APIFiles { + public func `get`(with url: URL) async throws -> RemoteFileDTO { let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { @@ -48,7 +48,7 @@ struct APIFiles { uid: uuid ?? "") } - func `get`(blob id: UInt) async throws -> QBCBlob { + public func `get`(blob id: UInt) async throws -> QBCBlob { return try await withCheckedThrowingContinuation { continuation in QBRequest.blob(withID: id) { _, blob in continuation.resume(returning: blob) @@ -59,7 +59,7 @@ struct APIFiles { } } - func upload(file content: RemoteFileDTO) async throws -> QBCBlob { + public func upload(file content: RemoteFileDTO) async throws -> QBCBlob { return try await withCheckedThrowingContinuation { continuation in QBRequest.tUploadFile(content.data, fileName: content.name, @@ -75,7 +75,7 @@ struct APIFiles { } } - func delete(with id: UInt) async throws { + public func delete(with id: UInt) async throws { return try await withCheckedThrowingContinuation { continuation in QBRequest.deleteBlob(withID: id) { _ in continuation.resume() diff --git a/Sources/QuickBloxData/Source/Remote/API/APIMessages.swift b/Sources/QuickBloxData/Source/Remote/API/APIMessages.swift index 5aa0210..b7f1df2 100644 --- a/Sources/QuickBloxData/Source/Remote/API/APIMessages.swift +++ b/Sources/QuickBloxData/Source/Remote/API/APIMessages.swift @@ -8,8 +8,8 @@ import Quickblox -struct APIMessages { - func `get`(for id: String, with ids: [String], page pagination: Pagination) +public struct APIMessages { + public func `get`(for id: String, with ids: [String], page pagination: Pagination) async throws -> (messages: [QBChatMessage], pagination: Pagination) { return try await withCheckedThrowingContinuation { continuation in let extended = ids.isEmpty diff --git a/Sources/QuickBloxData/Source/Remote/API/APIUsers.swift b/Sources/QuickBloxData/Source/Remote/API/APIUsers.swift index fc16ab7..ed9dae7 100644 --- a/Sources/QuickBloxData/Source/Remote/API/APIUsers.swift +++ b/Sources/QuickBloxData/Source/Remote/API/APIUsers.swift @@ -8,8 +8,8 @@ import Quickblox -struct APIUsers { - func `get`(with id: String) async throws -> QBUUser { +public struct APIUsers { + public func `get`(with id: String) async throws -> QBUUser { guard let id = UInt(id) else { let info = "Incorrect user id: \(id)" throw RemoteDataSourceException.incorrectData(info) @@ -24,7 +24,7 @@ struct APIUsers { } } - func `get`(with ids: [String], page pagination: Pagination) + public func `get`(with ids: [String], page pagination: Pagination) async throws -> (users: [QBUUser], pagination: Pagination) { return try await withCheckedThrowingContinuation { continuation in let page = QBGeneralResponsePage(pagination) @@ -36,7 +36,7 @@ struct APIUsers { } } - func `get`(for page: Pagination) + public func `get`(for page: Pagination) async throws -> (users: [QBUUser], pagination: Pagination) { let extendedRequest: [String: String] = ["order": "desc date last_request_at"] @@ -52,7 +52,7 @@ struct APIUsers { } } - func `get`(with name: String, page pagination: Pagination) + public func `get`(with name: String, page pagination: Pagination) async throws -> (users: [QBUUser], pagination: Pagination) { return try await withCheckedThrowingContinuation { continuation in let page = QBGeneralResponsePage(pagination) diff --git a/Sources/QuickBloxData/Source/Remote/Extension/NSError+RemoteExeption.swift b/Sources/QuickBloxData/Source/Remote/Extension/NSError+RemoteExeption.swift index d7342cd..4fee70a 100644 --- a/Sources/QuickBloxData/Source/Remote/Extension/NSError+RemoteExeption.swift +++ b/Sources/QuickBloxData/Source/Remote/Extension/NSError+RemoteExeption.swift @@ -9,7 +9,7 @@ import Foundation extension NSError { - var remoteException: Error { + public var remoteException: Error { get throws { var info = "Status code: \(self.code). " diff --git a/Sources/QuickBloxData/Source/Remote/Extension/RemoteFileInfoDTO+QBChatAttachment.swift b/Sources/QuickBloxData/Source/Remote/Extension/RemoteFileInfoDTO+QBChatAttachment.swift index 687ba23..73e2611 100644 --- a/Sources/QuickBloxData/Source/Remote/Extension/RemoteFileInfoDTO+QBChatAttachment.swift +++ b/Sources/QuickBloxData/Source/Remote/Extension/RemoteFileInfoDTO+QBChatAttachment.swift @@ -11,6 +11,9 @@ import QuickBloxDomain extension RemoteFileInfoDTO { init (_ value: QBChatAttachment) throws { + self.id = "" + self.uid = "" + if let id = value.id { self.id = id } else { @@ -21,6 +24,7 @@ extension RemoteFileInfoDTO { self.id = uid } if let contentType = value["content-type"] { + // TODO: Ensure the `type` value derived here aligns with the expected type logic in other parts of the code. type = contentType } } diff --git a/Sources/QuickBloxData/Source/Remote/Extension/RemoteMessageDTO+QBChatMessage.swift b/Sources/QuickBloxData/Source/Remote/Extension/RemoteMessageDTO+QBChatMessage.swift index 7fec10c..0cad78a 100644 --- a/Sources/QuickBloxData/Source/Remote/Extension/RemoteMessageDTO+QBChatMessage.swift +++ b/Sources/QuickBloxData/Source/Remote/Extension/RemoteMessageDTO+QBChatMessage.swift @@ -18,10 +18,13 @@ extension RemoteMessageDTO { recipientId = value.recipientID != 0 ? String(value.recipientID) : "" senderId = value.senderID != 0 ? String(value.senderID) : "" senderResource = value.senderResource ?? "" + dateSent = Date(timeIntervalSince1970: 0) if let date = value.dateSent { self.dateSent = date } + customParameters = [:] + saveToHistory = true if let params = value.customParameters as? [String: String] { customParameters = params if let save = params[QBChatMessage.Key.save] { @@ -32,6 +35,49 @@ extension RemoteMessageDTO { } } + delayed = value.delayed + markable = value.markable + + createdAt = value.createdAt ?? dateSent + updatedAt = value.updatedAt ?? dateSent + + eventType = value.type + type = eventType == .message ? .chat : .event + actionType = value.actionType + + originSenderName = "" + originalMessages = [] + relatedId = "" + + let current = String(QBSession.current.currentUserID) + + deliveredIds = [] + readIds = [] + isReaded = false + isDelivered = false + + isOwnedByCurrentUser = senderId == current + if isOwnedByCurrentUser { + if let ids = value.deliveredIDs { + deliveredIds = ids.map { $0.stringValue } + isDelivered = deliveredIds.filter { $0 != current }.isEmpty == false + } + if let ids = value.readIDs { + readIds = ids.map { $0.stringValue } + isReaded = readIds.filter { $0 != current }.isEmpty == false + } + } else { + if let ids = value.readIDs { + readIds = ids.map { $0.stringValue } + isReaded = readIds.contains(current) == true + } + if let ids = value.deliveredIDs { + deliveredIds = ids.map { $0.stringValue } + isDelivered = deliveredIds.contains(current) == true + } + } + + filesInfo = [] if let attachments = value.attachments { self.filesInfo = attachments.compactMap { do { @@ -53,16 +99,6 @@ extension RemoteMessageDTO { } } - delayed = value.delayed - markable = value.markable - - createdAt = value.createdAt ?? dateSent - updatedAt = value.updatedAt ?? dateSent - - eventType = value.type - type = eventType == .message ? .chat : .event - actionType = value.actionType - if actionType == .forward || actionType == .reply { var originSenderName: String = value.customParameters[QBChatMessage.Key.originSenderName] as? String ?? "Unknown" if originSenderName == "undefined" || originSenderName.isEmpty { @@ -76,24 +112,6 @@ extension RemoteMessageDTO { originSenderName: originSenderName) } } - - let current = String(QBSession.current.currentUserID) - isOwnedByCurrentUser = senderId == current - if isOwnedByCurrentUser { - if let ids = value.deliveredIDs { - isDelivered = ids.map { $0.stringValue }.filter { $0 != current }.isEmpty == false - } - if let ids = value.readIDs { - isReaded = ids.map { $0.stringValue }.filter { $0 != current }.isEmpty == false - } - } else { - if let ids = value.readIDs { - isReaded = ids.map { $0.stringValue }.contains(current) == true - } - if let ids = value.deliveredIDs { - isDelivered = ids.map { $0.stringValue }.contains(current) == true - } - } } private func originaldMessages(_ jsonString: String, @@ -219,21 +237,39 @@ extension RemoteMessageDTO { id = value.id dialogId = value.dialogId text = value.text - senderId = value.senderId != 0 ? String(value.senderId) : "" recipientId = value.recipientId != 0 ? String(value.recipientId) : "" + senderId = value.senderId != 0 ? String(value.senderId) : "" + senderResource = "" dateSent = value.dateSent.dateTimeIntervalSince1970 + customParameters = [:] + filesInfo = [] if value.attachments.isEmpty == false { self.filesInfo = value.attachments.compactMap{ RemoteFileInfoDTO($0) } } + delayed = false + markable = true + createdAt = dateSent updatedAt = dateSent + deliveredIds = value.deliveredIds.map { String($0) } + readIds = value.readIds.map { String($0) } + let current = String(QBSession.current.currentUserID) isOwnedByCurrentUser = senderId == current - isDelivered = true + isReaded = true + isDelivered = true + + eventType = .message + type = .chat + saveToHistory = true + actionType = .none + originSenderName = "" + originalMessages = [] + relatedId = "" } } diff --git a/Sources/QuickBloxData/Source/Remote/Extension/RemoteUserDTO+QBUUser.swift b/Sources/QuickBloxData/Source/Remote/Extension/RemoteUserDTO+QBUUser.swift index c241a8b..c41fc6f 100644 --- a/Sources/QuickBloxData/Source/Remote/Extension/RemoteUserDTO+QBUUser.swift +++ b/Sources/QuickBloxData/Source/Remote/Extension/RemoteUserDTO+QBUUser.swift @@ -10,14 +10,16 @@ import Quickblox import QuickBloxDomain extension RemoteUserDTO { - init (_ value: QBUUser) { - id = String(value.id) - name = value.fullName ?? "" - if (value.blobID > 0) { - avatarPath = String(value.blobID) - } - lastRequestAt = value.lastRequestAt ?? - Date(timeIntervalSince1970: 0) - isCurrent = QBSession.current.currentUserID == value.id - } + public init (_ value: QBUUser) { + id = String(value.id) + name = value.fullName ?? "" + if (value.blobID > 0) { + avatarPath = String(value.blobID) + } else { + avatarPath = "" + } + lastRequestAt = value.lastRequestAt ?? + Date(timeIntervalSince1970: 0) + isCurrent = QBSession.current.currentUserID == value.id + } } diff --git a/Sources/QuickBloxData/Source/Remote/RemoteDataSource.swift b/Sources/QuickBloxData/Source/Remote/RemoteDataSource.swift index 2b156b3..8d97e19 100644 --- a/Sources/QuickBloxData/Source/Remote/RemoteDataSource.swift +++ b/Sources/QuickBloxData/Source/Remote/RemoteDataSource.swift @@ -11,29 +11,32 @@ import QuickBloxLog import Quickblox import Combine +import QBAIAnswerAssistant +import QBAITranslate + /// This is a class that implements the ``RemoteDataSourceProtocol`` protocol and contains methods and properties that allow it to interact with the remote data source. /// -/// An object of this class provides access for remote storage of items at the time of the application's life cycle. Provides access to a single repository object by calling **RemoteDataSource.instance** static property. -class RemoteDataSource: NSObject, RemoteDataSourceProtocol { - var eventPublisher: AnyPublisher { +/// An object of this class provides access for remote storage of items at the time of the application's life cycle. +open class RemoteDataSource: NSObject, RemoteDataSourceProtocol, QBChatDelegate { + public var eventPublisher: AnyPublisher { get async { await stream.eventPublisher.eraseToAnyPublisher() } } private let connectionSubject = PassthroughSubject() - var connectionPublisher: AnyPublisher { + public var connectionPublisher: AnyPublisher { return connectionSubject.eraseToAnyPublisher() } private var cancellables: Set = Set() - private var api = API() + public var api = API() private var stream = ChatStream() private var user: QBUUser? - override init() { + public override init() { super.init() //FIXME: Must be set QBSettings.applicationID before using QBSession.currentSession QBChat.instance.addDelegate(self) @@ -62,11 +65,10 @@ class RemoteDataSource: NSObject, RemoteDataSourceProtocol { }) .store(in: &cancellables) } -} - -//MARK: Connection -extension RemoteDataSource { - func connect() async throws { + + //MARK: Connection + + public func connect() async throws { guard let details = QBSession.current.sessionDetails, let token = details.token, QBSession.current.tokenHasExpired == false else { @@ -84,7 +86,7 @@ extension RemoteDataSource { } } - func disconnect() async throws { + public func disconnect() async throws { if QBSession.current.tokenHasExpired || QBSession.current.sessionDetails?.token == nil { connectionSubject.send(.unauthorized) return @@ -110,7 +112,7 @@ extension RemoteDataSource { } } - func checkConnection() async throws -> ConnectionState { + public func checkConnection() async throws -> ConnectionState { guard let token = QBSession.current.sessionDetails?.token else { connectionSubject.send(.unauthorized) return .unauthorized @@ -142,14 +144,10 @@ extension RemoteDataSource { return .disconnected() } } -} - -//MARK: QBChatDelegate -extension RemoteDataSource: QBChatDelegate { } - -//MARK: QBChatConnectionProtocol -extension RemoteDataSource: QBChatConnectionProtocol { - func chatDidNotConnectWithError(_ error: Error) { + + //MARK: QBChatConnectionProtocol + + public func chatDidNotConnectWithError(_ error: Error) { if let exeption = try? error.repositoryException { connectionSubject.send(.connecting(exeption)) } else { @@ -157,11 +155,11 @@ extension RemoteDataSource: QBChatConnectionProtocol { } } - func chatDidConnect() { + public func chatDidConnect() { connectionSubject.send(.connected) } - func chatDidDisconnectWithError(_ error: Error?) { + public func chatDidDisconnectWithError(_ error: Error?) { if let exeption = try? error?.repositoryException { connectionSubject.send(.disconnected(exeption)) } else { @@ -169,14 +167,13 @@ extension RemoteDataSource: QBChatConnectionProtocol { } } - func chatDidReconnect() { + public func chatDidReconnect() { connectionSubject.send(.connected) } -} - -//MARK: Dialogs -extension RemoteDataSource { - func create(dialog dto: RemoteDialogDTO) async throws -> RemoteDialogDTO { + + //MARK: Dialogs + + public func create(dialog dto: RemoteDialogDTO) async throws -> RemoteDialogDTO { let dialog = QBChatDialog(dialogID: nil, type: dto.type.qbDialogType) let pIds = dto.participantsIds.map{ NSNumber(value: Int($0) ?? 0) } @@ -240,7 +237,7 @@ extension RemoteDataSource { return "\(current.fullName ?? "\(current.id)") \(actionMessage) \"\(chatName)\"" } - func update(dialog dto: RemoteDialogDTO, + public func update(dialog dto: RemoteDialogDTO, users: [RemoteUserDTO]) async throws -> RemoteDialogDTO { let dialog = try await stream.qbChat(with: dto.id) @@ -358,7 +355,7 @@ extension RemoteDataSource { return RemoteDialogDTO(updated) } - func get(dialog dto: RemoteDialogDTO) async throws -> RemoteDialogDTO { + public func get(dialog dto: RemoteDialogDTO) async throws -> RemoteDialogDTO { do { let dialog = try await api.dialogs.get(with: dto.id) await stream.update(with: dialog) @@ -374,7 +371,7 @@ extension RemoteDataSource { } } - func get(dialogs dto: RemoteDialogsDTO) async throws -> RemoteDialogsDTO { + public func get(dialogs dto: RemoteDialogsDTO) async throws -> RemoteDialogsDTO { do { let page = QBResponsePage(limit: dto.pagination.limit, skip: dto.pagination.skip) @@ -411,7 +408,7 @@ extension RemoteDataSource { } } - func getAllDialogs() async throws -> RemoteDialogsDTO { + public func getAllDialogs() async throws -> RemoteDialogsDTO { do { let page = QBResponsePage(limit: 1, skip: 0, totalEntries: 1) @@ -444,7 +441,7 @@ extension RemoteDataSource { } } - func delete(dialog dto: RemoteDialogDTO) async throws { + public func delete(dialog dto: RemoteDialogDTO) async throws { if dto.id.isEmpty { let info = "Internal. Empty dialog id" throw RepositoryException.incorrectData(info) @@ -477,22 +474,23 @@ extension RemoteDataSource { await stream.process(.leave(dto.id)) } - func subscribeToObserveTyping(dialog dialogId: String) async throws { + //MARK: Typing + + public func subscribeToObserveTyping(dialog dialogId: String) async throws { await stream.subscribeToTyping(chat: dialogId) } - func sendTyping(dialog dialogId: String) async throws { + public func sendTyping(dialog dialogId: String) async throws { await stream.sendTyping(chat: dialogId) } - func sendStopTyping(dialog dialogId: String) async throws { + public func sendStopTyping(dialog dialogId: String) async throws { await stream.sendStopTyping(chat: dialogId) } -} - -//MARK: Messages -extension RemoteDataSource { - func get(messages dto: RemoteMessagesDTO) async throws -> RemoteMessagesDTO { + + //MARK: Messages + + public func get(messages dto: RemoteMessagesDTO) async throws -> RemoteMessagesDTO { do { let result = try await api.messages.get(for: dto.dialogId, with: dto.ids, @@ -510,38 +508,37 @@ extension RemoteDataSource { } } - func send(message dto: RemoteMessageDTO) async throws { + public func send(message dto: RemoteMessageDTO) async throws { let message = QBChatMessage(dto, toSend: true) await stream.send(message) await stream.process(message) } - func update(message dto: RemoteMessageDTO) async throws -> RemoteMessageDTO { + public func update(message dto: RemoteMessageDTO) async throws -> RemoteMessageDTO { throw DataSourceException.unexpected() } - func delete(message dto: RemoteMessageDTO) async throws { + public func delete(message dto: RemoteMessageDTO) async throws { throw DataSourceException.unexpected() } - func read(message dto: RemoteMessageDTO) async throws { + public func read(message dto: RemoteMessageDTO) async throws { let userId = QBSession.current.currentUserID if dto.readIds.contains(String(userId)) == true { return } let message = QBChatMessage(dto, toSend: false) try await stream.read(message) } - func delivered(message dto: RemoteMessageDTO) async throws { + public func delivered(message dto: RemoteMessageDTO) async throws { let userId = QBSession.current.currentUserID if dto.deliveredIds.contains(String(userId)) == true { return } let message = QBChatMessage(dto, toSend: false) try await stream.delivered(message) } -} - -//MARK: Users -extension RemoteDataSource { - func get(user dto: RemoteUserDTO) async throws -> RemoteUserDTO { + + //MARK: Users + + public func get(user dto: RemoteUserDTO) async throws -> RemoteUserDTO { do { let user = try await api.users.get(with: dto.id) return RemoteUserDTO(user) @@ -550,7 +547,7 @@ extension RemoteDataSource { } } - func get(users dto: RemoteUsersDTO) async throws -> RemoteUsersDTO { + open func get(users dto: RemoteUsersDTO) async throws -> RemoteUsersDTO { do { var tuple: (users: [QBUUser], pagination: Pagination) if dto.ids.isEmpty == false { @@ -571,17 +568,16 @@ extension RemoteDataSource { if nsError.code == 404 { return RemoteUsersDTO(users: [], pagination: dto.pagination) - } + } throw try nsError.remoteException } catch { throw DataSourceException.unexpected(error.localizedDescription) } } -} - -//MARK: Files -extension RemoteDataSource { - func create(file dto: RemoteFileDTO) async throws -> RemoteFileDTO { + + //MARK: Files + + public func create(file dto: RemoteFileDTO) async throws -> RemoteFileDTO { do { let blob = try await api.files.upload(file: dto) guard let uuid = blob.uid, @@ -625,7 +621,7 @@ extension RemoteDataSource { } } - func get(file dto: RemoteFileDTO) async throws -> RemoteFileDTO { + public func get(file dto: RemoteFileDTO) async throws -> RemoteFileDTO { if dto.id.isNumber { if dto.id == "0" { let info = "Internal. Incorrect file path: \(dto.id)" @@ -734,7 +730,7 @@ extension RemoteDataSource { return try await api.files.get(with: url) } - func delete(file dto: RemoteFileDTO) async throws { + public func delete(file dto: RemoteFileDTO) async throws { do { guard let intId = UInt(dto.id) else { let info = "Internal. Incorrect id: \(dto.id)" @@ -748,12 +744,12 @@ extension RemoteDataSource { throw DataSourceException.unexpected(error.localizedDescription) } } -} - -//MARK: AI -extension RemoteDataSource { - // Quickblox Server API - func answerAssist(message dto: RemoteAnswerAssistMessageDTO) async throws -> String { + + //MARK: AI + + //MARK: Quickblox Server API + + public func answerAssist(message dto: RemoteAnswerAssistMessageDTO) async throws -> String { do { return try await api.ai.answerAssist(with: QBAIAnswerAssistMessage(dto)) } catch let nsError as NSError { @@ -764,7 +760,7 @@ extension RemoteDataSource { } - func translate(message dto: RemoteTranslateMessageDTO) async throws -> String { + public func translate(message dto: RemoteTranslateMessageDTO) async throws -> String { do { return try await api.ai.translate(with: QBAITranslateMessage(dto)) } catch let nsError as NSError { @@ -773,20 +769,16 @@ extension RemoteDataSource { throw DataSourceException.unexpected(error.localizedDescription) } } -} - -//MARK: AI Quickblox QBAIAnswerAssistant Library -import QBAIAnswerAssistant -extension RemoteDataSource { + + //MARK: AI Quickblox QBAIAnswerAssistant Library + func answerAssist(with content: [RemoteMessageDTO], settings: QBAIAnswerAssistant.AISettings) async throws -> String { return try await api.ai.answerAssist(with: content, settings: settings) } -} - -//MARK: AI Quickblox QBAITranslate Library -import QBAITranslate -extension RemoteDataSource { + + //MARK: AI Quickblox QBAITranslate Library + func translate(with text: String, content: [RemoteMessageDTO], settings: QBAITranslate.AISettings) async throws -> String { diff --git a/Sources/QuickBloxData/Utils.swift b/Sources/QuickBloxData/Utils.swift index 755059b..3feb119 100644 --- a/Sources/QuickBloxData/Utils.swift +++ b/Sources/QuickBloxData/Utils.swift @@ -8,11 +8,11 @@ import Foundation -protocol Dated { +public protocol Dated { var date: Date { get } } -extension ComparisonResult { +public extension ComparisonResult { func inverted() -> ComparisonResult { switch self { case .orderedAscending: @@ -25,8 +25,8 @@ extension ComparisonResult { } } -extension Array where Element: Dated, Element: Identifiable, Element: Hashable { - mutating func insertElement(_ new: Element, withSorting order: ComparisonResult) { +extension Array where Element: QuickBloxData.Dated, Element: Identifiable, Element: Hashable { + public mutating func insertElement(_ new: Element, withSorting order: ComparisonResult) { if isEmpty { append(new) } else { diff --git a/Sources/QuickBloxLog/Log.swift b/Sources/QuickBloxLog/Log.swift index c08eb34..a5a7a24 100644 --- a/Sources/QuickBloxLog/Log.swift +++ b/Sources/QuickBloxLog/Log.swift @@ -1,5 +1,5 @@ // -// SyncDialogsTests.swift +// Log.swift // QuickBloxUIKit // // Created by Injoit on 09.04.2023. @@ -8,13 +8,13 @@ import Foundation -enum LogType { +public enum LogType { case nothing case details } -struct LogSettings { - static var type: LogType = .details +public struct LogSettings { + public static var type: LogType = .details } public struct LogSeparator { diff --git a/Sources/QuickBloxUIKit/QuickBloxUIKit.swift b/Sources/QuickBloxUIKit/QuickBloxUIKit.swift index d6b8bce..8f123bd 100644 --- a/Sources/QuickBloxUIKit/QuickBloxUIKit.swift +++ b/Sources/QuickBloxUIKit/QuickBloxUIKit.swift @@ -34,6 +34,10 @@ var previewAware: Bool { return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } +public class Fabric { + public static var screen: ScreenFabric = ScreenFabric() +} + public var settings: ScreensProtocol = ScreenSettings(Theme()) public var feature: Feature = Feature() @@ -54,10 +58,10 @@ class Sync { private var cancellables: Set = Set() init() { - useCase = SyncData(dialogsRepo: RepositoriesFabric.dialogs, - usersRepo: RepositoriesFabric.users, - messagesRepo: RepositoriesFabric.messages, - connectRepo: RepositoriesFabric.connection) + useCase = SyncData(dialogsRepo: Repository.dialogs, + usersRepo: Repository.users, + messagesRepo: Repository.messages, + connectRepo: Repository.connection) useCase.execute() .receive(on: RunLoop.main) .sink(receiveCompletion: { _ in }, @@ -81,15 +85,47 @@ private func syncData() { } } -//FIXME: add dialogsView screen @MainActor @ViewBuilder +@available(*, deprecated, message: "Use dialogsView(onExit:) instead, as onSelect is no longer needed.") public func dialogsView(onExit: (() -> Void)? = nil, onSelect: @escaping (_ tabIndex: TabIndex) -> Void) -> some View { - DialogsView(dialogsList: DialogsViewModel(dialogsRepo: RepositoriesFabric.dialogs), + DialogsView(dialogsList: DialogsViewModel(dialogsRepo: Repository.dialogs), + onBack: { + onExit?() + }) + .onAppear { + syncData() + } +} + +/// Displays a list of dialogs with an optional exit handler. +/// +/// - Parameter onExit: A closure that is executed when the user exits the `DialogsView`. +/// - Returns: A SwiftUI `View` displaying the dialogs. +@MainActor @ViewBuilder +public func dialogsView(onExit: (() -> Void)? = nil) -> some View { + DialogsView(dialogsList: DialogsViewModel(dialogsRepo: Repository.dialogs), + onBack: { + onExit?() + }) + .onAppear { + syncData() + } +} + +/// Displays a list of dialogs with optional content modification and exit handling. +/// +/// - Parameters: +/// - onModifyContent: A closure that modifies the view's content, taking an `AnyView` and a `Binding`. +/// - onExit: A closure that is executed when the user exits the `DialogsView`. +/// - Returns: A SwiftUI `View` displaying the dialogs with customizable content. +@MainActor @ViewBuilder +public func dialogsView(onModifyContent: ((AnyView, Binding) -> AnyView)? = nil, + onExit: (() -> Void)? = nil) -> some View { + DialogsView(dialogsList: DialogsViewModel(dialogsRepo: Repository.dialogs), + modifyContent: onModifyContent, onBack: { onExit?() - }, onSelect: { tabIndex in - onSelect(tabIndex) }) .onAppear { syncData() diff --git a/Sources/QuickBloxUIKit/ScreenFabric.swift b/Sources/QuickBloxUIKit/ScreenFabric.swift index e01e583..f003ffc 100644 --- a/Sources/QuickBloxUIKit/ScreenFabric.swift +++ b/Sources/QuickBloxUIKit/ScreenFabric.swift @@ -10,10 +10,6 @@ import SwiftUI import QuickBloxData import QuickBloxDomain -public class Fabric { - public static var screen: ScreenFabric = ScreenFabric() -} - public class ScreenFabric { } // Creating the screen for adding participants to a dialogue. diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogInfoView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogInfoView.swift index cb0ec45..39e261d 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogInfoView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogInfoView.swift @@ -12,9 +12,9 @@ import QuickBloxDomain public struct GroupDialogInfoView: View { let settings = QuickBloxUIKit.settings.dialogInfoScreen - + @StateObject private var viewModel: ViewModel - + @State private var isEditDialogAlertPresented: Bool = false @State private var isSizeAlertPresented: Bool = false @State private var isMembersPresented: Bool = false @@ -40,96 +40,96 @@ public struct GroupDialogInfoView: View { @ViewBuilder private func container() -> some View { - ZStack { - settings.backgroundColor.ignoresSafeArea() - VStack { - - InfoDialogAvatar() - - ForEach(settings.groupActionSegments, id:\.self) { action in - InfoSegment(dialog: viewModel.dialog, action: action) { action in - switch action { - case .members: isMembersPresented = true - case .searchInDialog: searchPresented.toggle() - case .leaveDialog: isDeleteAlertPresented = true - case .notification: break - } - } - } - - SegmentDivider() - } + ZStack { + settings.backgroundColor.ignoresSafeArea() + VStack { - .deleteDialogAlert(isPresented: $isDeleteAlertPresented, - name: viewModel.dialog.name, - onCancel: { - isDeleteAlertPresented = false - }, onTap: { - viewModel.deleteDialog() - }) + InfoDialogAvatar() - .editDialogAlert(isPresented: $isEditDialogAlertPresented, viewModel: viewModel, - dialogName: viewModel.dialogName, - isExistingImage: viewModel.isExistingImage, - isHiddenFiles: settings.editDialogAlert.isHiddenFiles, - onRemoveImage: { - viewModel.removeExistingImage() - }, onGetAttachment: { attachmentAsset in - let sizeMB = attachmentAsset.size - if sizeMB.truncate(to: 2) > settings.maximumMB { - if attachmentAsset.image != nil { - self.attachmentAsset = attachmentAsset + ForEach(settings.groupActionSegments, id:\.self) { action in + InfoSegment(dialog: viewModel.dialog, action: action) { action in + switch action { + case .members: isMembersPresented = true + case .searchInDialog: searchPresented.toggle() + case .leaveDialog: isDeleteAlertPresented = true + case .notification: break } - isSizeAlertPresented = true - } else { - viewModel.handleOnSelect(attachmentAsset: attachmentAsset) } - }, onGetName: { name in - viewModel.handleOnSelect(newName: name) - }, onCancelName: { - viewModel.setDefaultName() - }) - - .onChange(of: viewModel.error, perform: { error in - if error.isEmpty { return } - errorPresented.toggle() - }) + } - .largeImageSizeAlert(isPresented: $isSizeAlertPresented, - onUseAttachment: { - if let attachmentAsset { - viewModel.handleOnSelect(attachmentAsset: attachmentAsset) - self.attachmentAsset = nil + SegmentDivider() + } + + .deleteDialogAlert(isPresented: $isDeleteAlertPresented, + name: viewModel.dialog.name, + onCancel: { + isDeleteAlertPresented = false + }, onTap: { + viewModel.deleteDialog() + }) + + .editDialogAlert(isPresented: $isEditDialogAlertPresented, viewModel: viewModel, + dialogName: viewModel.dialogName, + isExistingImage: viewModel.isExistingImage, + isHiddenFiles: settings.editDialogAlert.isHiddenFiles, + onRemoveImage: { + viewModel.removeExistingImage() + }, onGetAttachment: { attachmentAsset in + let sizeMB = attachmentAsset.size + if sizeMB.truncate(to: 2) > settings.maximumMB { + if attachmentAsset.image != nil { + self.attachmentAsset = attachmentAsset } - }, onCancel: { + isSizeAlertPresented = true + } else { + viewModel.handleOnSelect(attachmentAsset: attachmentAsset) + } + }, onGetName: { name in + viewModel.handleOnSelect(newName: name) + }, onCancelName: { + viewModel.setDefaultName() + }) + + .onChange(of: viewModel.error, perform: { error in + if error.isEmpty { return } + errorPresented.toggle() + }) + + .largeImageSizeAlert(isPresented: $isSizeAlertPresented, + onUseAttachment: { + if let attachmentAsset { + viewModel.handleOnSelect(attachmentAsset: attachmentAsset) self.attachmentAsset = nil - }) - - .errorAlert($viewModel.error, isPresented: $errorPresented) - .permissionAlert(isPresented: $viewModel.permissionNotGranted.notGranted, - viewModel: viewModel) - - .if(isMembersPresented == true, transform: { view in - view.navigationDestination(isPresented: $isMembersPresented) { - Fabric.screen.members(to: viewModel.dialog) - } - }) - - .modifier(DialogInfoHeader(onTapEdit: { - isEditDialogAlertPresented = true - })) - - .disabled(viewModel.isProcessing == true) - .if(viewModel.isProcessing == true) { view in - view.overlay() { - CustomProgressView() - } } - .environmentObject(viewModel) - } - .onAppear { - viewModel.sync() + }, onCancel: { + self.attachmentAsset = nil + }) + + .errorAlert($viewModel.error, isPresented: $errorPresented) + .permissionAlert(isPresented: $viewModel.permissionNotGranted.notGranted, + viewModel: viewModel) + + .if(isMembersPresented == true, transform: { view in + view.navigationDestination(isPresented: $isMembersPresented) { + Fabric.screen.members(to: viewModel.dialog) + } + }) + + .modifier(DialogInfoHeader(onTapEdit: { + isEditDialogAlertPresented = true + })) + + .disabled(viewModel.isProcessing == true) + .if(viewModel.isProcessing == true) { view in + view.overlay() { + CustomProgressView() + } } + .environmentObject(viewModel) + } + .onAppear { + viewModel.sync() + } } } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogNonEditInfoView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogNonEditInfoView.swift index 7717c1b..d1e0b47 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogNonEditInfoView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogInfo/GroupDialogNonEditInfoView.swift @@ -12,7 +12,7 @@ import QuickBloxDomain public struct GroupDialogNonEditInfoView: View { let settings = QuickBloxUIKit.settings.dialogInfoScreen - + @StateObject private var viewModel: ViewModel @State private var isMembersPresented: Bool = false @State private var searchPresented: Bool = false @@ -35,59 +35,59 @@ public struct GroupDialogNonEditInfoView: View { @ViewBuilder private func container() -> some View { - ZStack { - settings.backgroundColor.ignoresSafeArea() - VStack { - - InfoDialogAvatar() - - ForEach(settings.groupActionSegments, id:\.self) { action in - InfoSegment(dialog: viewModel.dialog, action: action) { action in - switch action { - case .members: isMembersPresented = true - case .searchInDialog: searchPresented = true - case .leaveDialog: isDeleteAlertPresented = true - case .notification: break - } - } - } - - SegmentDivider() - } - - .onChange(of: viewModel.error, perform: { error in - if error.isEmpty { return } - errorPresented.toggle() - }) - - .errorAlert($viewModel.error, isPresented: $errorPresented) - - .deleteDialogAlert(isPresented: $isDeleteAlertPresented, - name: viewModel.dialog.name, - onCancel: { - isDeleteAlertPresented = false - }, onTap: { - viewModel.deleteDialog() - }) + ZStack { + settings.backgroundColor.ignoresSafeArea() + VStack { - .if(isMembersPresented == true, transform: { view in - view.navigationDestination(isPresented: $isMembersPresented) { - Fabric.screen.members(to: viewModel.dialog) - } - }) - - .modifier(GroupDialogNonEditInfoHeader()) + InfoDialogAvatar() - .disabled(viewModel.isProcessing == true) - .if(viewModel.isProcessing == true) { view in - view.overlay() { - CustomProgressView() + ForEach(settings.groupActionSegments, id:\.self) { action in + InfoSegment(dialog: viewModel.dialog, action: action) { action in + switch action { + case .members: isMembersPresented = true + case .searchInDialog: searchPresented = true + case .leaveDialog: isDeleteAlertPresented = true + case .notification: break + } } } - .environmentObject(viewModel) + + SegmentDivider() } - .onAppear { - viewModel.sync() + + .onChange(of: viewModel.error, perform: { error in + if error.isEmpty { return } + errorPresented.toggle() + }) + + .errorAlert($viewModel.error, isPresented: $errorPresented) + + .deleteDialogAlert(isPresented: $isDeleteAlertPresented, + name: viewModel.dialog.name, + onCancel: { + isDeleteAlertPresented = false + }, onTap: { + viewModel.deleteDialog() + }) + + .if(isMembersPresented == true, transform: { view in + view.navigationDestination(isPresented: $isMembersPresented) { + Fabric.screen.members(to: viewModel.dialog) + } + }) + + .modifier(GroupDialogNonEditInfoHeader()) + + .disabled(viewModel.isProcessing == true) + .if(viewModel.isProcessing == true) { view in + view.overlay() { + CustomProgressView() + } } + .environmentObject(viewModel) } + .onAppear { + viewModel.sync() + } + } } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogViewViewModifier.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogViewViewModifier.swift index 6eda79e..39ae4d3c 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogViewViewModifier.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/DialogViewViewModifier.swift @@ -42,29 +42,6 @@ struct DialogHeaderToolbarContent: ToolbarContent { } public var body: some ToolbarContent { - - ToolbarItem(placement: .navigationBarLeading) { - - if feature.startScreen.screen == .dialog, - dialogHeaderSettings.leftButton.hidden == false, - isForward == false { - Button { - onDismiss() - } label: { - if let title = dialogHeaderSettings.leftButton.title { - Text(title).foregroundColor(dialogHeaderSettings.leftButton.color) - } else { - dialogHeaderSettings.leftButton.image - .resizable() - .scaledToFit() - .scaleEffect(dialogHeaderSettings.leftButton.scale) - .tint(dialogHeaderSettings.leftButton.color) - .padding(dialogHeaderSettings.leftButton.padding) - } - }.frame(width: 32, height: 44) - } - } - ToolbarItem(placement: .principal) { if isForward == false { diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Forward/ForwardView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Forward/ForwardView.swift index 211557e..df52ef6 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Forward/ForwardView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Forward/ForwardView.swift @@ -14,19 +14,19 @@ public struct ForwardView: View { @State public var settings = QuickBloxUIKit.settings.addMembersScreen - @Environment(\.dismiss) var dismiss - @StateObject var viewModel: ViewModel @State private var isForwardFailedPresented: Bool = false - @State var isPresented: Bool = false - let onForwardSuccess: () -> Void + @Binding var isComplete: Bool + @Binding var isPresented: Bool init(viewModel: ViewModel, - onForwardSuccess: @escaping () -> Void) { + isComplete: Binding, + isPresented: Binding) { _viewModel = StateObject(wrappedValue: viewModel) - self.onForwardSuccess = onForwardSuccess + _isComplete = isComplete + _isPresented = isPresented } public var body: some View { @@ -51,13 +51,13 @@ public struct ForwardView: View { } } .modifier(ForwardHeader(onDismiss: { - dismiss() + isPresented = false })) .onChange(of: viewModel.forwardInfo.result, perform: { forwardResult in if forwardResult == .success { - onForwardSuccess() - dismiss() + isComplete = true + isPresented = false } else { isForwardFailedPresented = true } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/GroupDialogView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/GroupDialogView.swift index 121a863..8984877 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/GroupDialogView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/GroupDialogView.swift @@ -25,8 +25,6 @@ public struct GroupDialogView let connectStatus = QuickBloxUIKit.settings.dialogsScreen.connectStatus let features = QuickBloxUIKit.feature - @Environment(\.dismiss) var dismiss - @StateObject private var viewModel: ViewModel @State private var isInfoPresented: Bool = false @@ -48,7 +46,7 @@ public struct GroupDialogView @State private var tappedMessage: ViewModel.DialogItem.MessageItem? = nil @State private var tabBarVisibility: Visibility = .visible - + public init(viewModel: ViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } @@ -57,245 +55,245 @@ public struct GroupDialogView private func container() -> some View { ZStack(alignment: .center) { settings.backgroundColor.ignoresSafeArea() - dialogContentView() + dialogContentView() } } @ViewBuilder private func dialogContentView() -> some View { - VStack(spacing: 0) { - - switch viewModel.syncState { - case .syncing(stage: let stage, error: _): - VStack { + VStack(spacing: 0) { + + switch viewModel.syncState { + case .syncing(stage: let stage, error: _): + VStack { + HStack(spacing: 12) { + ProgressView() + Text(" " + connectStatus.connectionText(stage.rawValue) ) + .foregroundColor(settings.connectForeground) + }.padding(.top) + if viewModel.dialog.displayedMessages.isEmpty { + Spacer() + } else { + messagesView() + } + } + case .synced: + VStack(spacing: 20) { + + if viewModel.isProcessing { HStack(spacing: 12) { ProgressView() - Text(" " + connectStatus.connectionText(stage.rawValue) ) + Text(" " + connectStatus.connectionText(connectStatus.update) ) .foregroundColor(settings.connectForeground) }.padding(.top) - if viewModel.dialog.displayedMessages.isEmpty { - Spacer() - } else { - messagesView() - } } - case .synced: - VStack(spacing: 20) { - - if viewModel.isProcessing { - HStack(spacing: 12) { - ProgressView() - Text(" " + connectStatus.connectionText(connectStatus.update) ) - .foregroundColor(settings.connectForeground) - }.padding(.top) - } - - aiAnswerFiled(viewModel.aiAnswerFailed.feature) - if viewModel.dialog.displayedMessages.isEmpty { - Spacer() - } else { - messagesView() - } - if settings.typing.enable == true && viewModel.typing.isEmpty == false { - TypingView(typing: viewModel.typing) - .animation(.easeInOut, value: viewModel.typing.isEmpty) - } + + aiAnswerFiled(viewModel.aiAnswerFailed.feature) + if viewModel.dialog.displayedMessages.isEmpty { + Spacer() + } else { + messagesView() } - } - - if features.forward.enable == true, viewModel.messagesActionState == .forward { - Button { - isForwardPresented = true - } label: { - Text(settings.messageRow.forward.title) + if settings.typing.enable == true && viewModel.typing.isEmpty == false { + TypingView(typing: viewModel.typing) + .animation(.easeInOut, value: viewModel.typing.isEmpty) } - .frame(height: 56) - .frame(maxWidth: .infinity) - .background(settings.textField.backgroundColor) - .overlay(Divider(), alignment: .top) - } else { - InputView(onAttachment: { - isAttachmentAlertPresented = true - }, onApplyTone: { tone, content, needToUpdate in - if content.isEmpty == false, - features.ai.rephrase.enable == true, - features.ai.rephrase.isValid == true { - viewModel.applyAIRephrase(tone, - text: content, - needToUpdate: needToUpdate) - } else { - aiFeature = .rephrase - isAIAlertPresented = true - } - }) - .background(settings.backgroundColor) } - - } - .background { - ZStack { - settings.contentBackgroundColor - if settings.backgroundImage != nil { - settings.backgroundImage? - .renderingMode(.template) - .resizable() - .scaledToFill() - .foregroundColor(settings.backgroundImageColor) - .opacity(0.8) - .edgesIgnoringSafeArea(.all) + + if features.forward.enable == true, viewModel.messagesActionState == .forward { + Button { + isForwardPresented = true + } label: { + Text(settings.messageRow.forward.title) + } + .frame(height: 56) + .frame(maxWidth: .infinity) + .background(settings.textField.backgroundColor) + .overlay(Divider(), alignment: .top) + } else { + InputView(onAttachment: { + isAttachmentAlertPresented = true + }, onApplyTone: { tone, content, needToUpdate in + if content.isEmpty == false, + features.ai.rephrase.enable == true, + features.ai.rephrase.isValid == true { + viewModel.applyAIRephrase(tone, + text: content, + needToUpdate: needToUpdate) + } else { + aiFeature = .rephrase + isAIAlertPresented = true } - }.ignoresSafeArea() + }) + .background(settings.backgroundColor) } - .mediaAlert(isAlertPresented: $isAttachmentAlertPresented, - isExistingImage: false, - isHiddenFiles: settings.isHiddenFiles, - mediaTypes: [.videos, .images], - viewModel: viewModel, - onRemoveImage: { - }, onGetAttachment: { attachmentAsset in - let sizeMB = attachmentAsset.size - if sizeMB.truncate(to: 2) > settings.maximumMB { - if attachmentAsset.image != nil { - self.attachmentAsset = attachmentAsset - } - isSizeAlertPresented = true - } else { - viewModel.handleOnSelect(attachment: attachmentAsset) - } - }) - .onChange(of: viewModel.error, perform: { error in - if error == settings.invalidFile { - isInvalidExtAlertPresented = true + } + .background { + ZStack { + settings.contentBackgroundColor + if settings.backgroundImage != nil { + settings.backgroundImage? + .renderingMode(.template) + .resizable() + .scaledToFill() + .foregroundColor(settings.backgroundImageColor) + .opacity(0.8) + .edgesIgnoringSafeArea(.all) } - }) - - .onChange(of: viewModel.aiAnswerFailed.failed, perform: { failed in - withAnimation(.easeInOut(duration: failed ? 0.4 : 0.8)) { - isAiAnswerFailedPresented = failed + }.ignoresSafeArea() + } + + .mediaAlert(isAlertPresented: $isAttachmentAlertPresented, + isExistingImage: false, + isHiddenFiles: settings.isHiddenFiles, + mediaTypes: [.videos, .images], + viewModel: viewModel, + onRemoveImage: { + }, onGetAttachment: { attachmentAsset in + let sizeMB = attachmentAsset.size + if sizeMB.truncate(to: 2) > settings.maximumMB { + if attachmentAsset.image != nil { + self.attachmentAsset = attachmentAsset } - }) + isSizeAlertPresented = true + } else { + viewModel.handleOnSelect(attachment: attachmentAsset) + } + }) + + .onChange(of: viewModel.error, perform: { error in + if error == settings.invalidFile { + isInvalidExtAlertPresented = true + } + }) - .onChange(of: viewModel.aiAnswerFailed.failed, perform: { failed in - withAnimation(.easeInOut(duration: failed ? 0.4 : 0.8)) { - isAiAnswerFailedPresented = failed + .onChange(of: viewModel.aiAnswerFailed.failed, perform: { failed in + withAnimation(.easeInOut(duration: failed ? 0.4 : 0.8)) { + isAiAnswerFailedPresented = failed + } + }) + + .onChange(of: viewModel.aiAnswerFailed.failed, perform: { failed in + withAnimation(.easeInOut(duration: failed ? 0.4 : 0.8)) { + isAiAnswerFailedPresented = failed + } + }) + + .invalidExtensionAlert(isPresented: $isInvalidExtAlertPresented) + + .if(attachmentAsset == nil && isSizeAlertPresented == true, transform: { view in + view.largeFileSizeAlert(isPresented: $isSizeAlertPresented) + }) + + .if(attachmentAsset != nil && isSizeAlertPresented == true, transform: { view in + view.largeImageSizeAlert(isPresented: $isSizeAlertPresented, + onUseAttachment: { + if let attachmentAsset { + viewModel.handleOnSelect(attachment: attachmentAsset) + self.attachmentAsset = nil } + }, onCancel: { + self.attachmentAsset = nil }) - - .invalidExtensionAlert(isPresented: $isInvalidExtAlertPresented) - - .if(attachmentAsset == nil && isSizeAlertPresented == true, transform: { view in - view.largeFileSizeAlert(isPresented: $isSizeAlertPresented) + }) + + .if(isAIAlertPresented == true && aiFeature != nil, transform: { view in + view.aiFailAlert(isPresented: $isAIAlertPresented, + feature: aiFeature ?? AIFeatureType.answerAssist, + onDismiss: { + aiFeature = nil }) - - .if(attachmentAsset != nil && isSizeAlertPresented == true, transform: { view in - view.largeImageSizeAlert(isPresented: $isSizeAlertPresented, - onUseAttachment: { - if let attachmentAsset { - viewModel.handleOnSelect(attachment: attachmentAsset) - self.attachmentAsset = nil - } - }, onCancel: { - self.attachmentAsset = nil - }) - }) - - .if(isAIAlertPresented == true && aiFeature != nil, transform: { view in - view.aiFailAlert(isPresented: $isAIAlertPresented, - feature: aiFeature ?? AIFeatureType.answerAssist, - onDismiss: { - aiFeature = nil - }) - }) - - .permissionAlert(isPresented: $viewModel.permissionNotGranted.notGranted, - viewModel: viewModel) - - .fullScreenCover(item: $attachment, content: { attachment in - FilePreviewController(url: attachment.url, onDismiss: { - self.attachment = nil - }) - }) - - .sheet(isPresented: $isFileExporterPresented, onDismiss: { - attachment = nil - }, content: { - if let fileUrl { - ActivityViewController(activityItems: [fileUrl]) - } - }) + }) - .if(isIphone == true && isInfoPresented == true, transform: { view in - view.navigationDestination(isPresented: $isInfoPresented) { - if let dialog = viewModel.dialog as? Dialog { - switch dialog.type { - case .group: - if dialog.isOwnedByCurrentUser == true { - GroupDialogInfoView(DialogInfoViewModel(dialog)) - } else { - GroupDialogNonEditInfoView(DialogInfoViewModel(dialog)) - } - default: - EmptyView() - } - } - } - }) - - .if((isIPad == true || isMac == true) && isInfoPresented == true, transform: { view in - view.sheet(isPresented: $isInfoPresented, content: { - if let dialog = viewModel.dialog as? Dialog { - switch dialog.type { - case .group: - if dialog.isOwnedByCurrentUser == true { - GroupDialogInfoView(DialogInfoViewModel(dialog)) - } else { - GroupDialogNonEditInfoView(DialogInfoViewModel(dialog)) - } - default: - EmptyView() - } - } - }) - }) + .permissionAlert(isPresented: $viewModel.permissionNotGranted.notGranted, + viewModel: viewModel) + + .fullScreenCover(item: $attachment, content: { attachment in + FilePreviewController(url: attachment.url, onDismiss: { + self.attachment = nil + }) + }) - .if(isForwardSuccess == true, transform: { view in - view.forwardSuccessAlert(isPresented: $isForwardSuccess, name: viewModel.originSenderName) - }) + .sheet(isPresented: $isFileExporterPresented, onDismiss: { + attachment = nil + }, content: { + if let fileUrl { + ActivityViewController(activityItems: [fileUrl]) + } + }) + + .if(isIphone == true && isInfoPresented == true, transform: { view in + view.navigationDestination(isPresented: $isInfoPresented) { + if let dialog = viewModel.dialog as? Dialog { + switch dialog.type { + case .group: + if dialog.isOwnedByCurrentUser == true { + GroupDialogInfoView(DialogInfoViewModel(dialog)) + } else { + GroupDialogNonEditInfoView(DialogInfoViewModel(dialog)) + } + default: + EmptyView() + } + } + } + }) - .if(isIphone == true && isForwardPresented == true, transform: { view in - view.navigationDestination(isPresented: $isForwardPresented) { - ForwardView(viewModel: ForwardViewModel(messages: viewModel.selectedMessages as? [Message] ?? [])) { - viewModel.cancelMessageAction() - isForwardSuccess = true - } - } - }) + .if((isIPad == true || isMac == true) && isInfoPresented == true, transform: { view in + view.sheet(isPresented: $isInfoPresented, content: { + if let dialog = viewModel.dialog as? Dialog { + switch dialog.type { + case .group: + if dialog.isOwnedByCurrentUser == true { + GroupDialogInfoView(DialogInfoViewModel(dialog)) + } else { + GroupDialogNonEditInfoView(DialogInfoViewModel(dialog)) + } + default: + EmptyView() + } + } + }) + }) - .if((isIPad == true || isMac == true) && isForwardPresented == true, transform: { view in - view.sheet(isPresented: $isForwardPresented, content: { - ForwardView(viewModel: ForwardViewModel(messages: viewModel.selectedMessages as? [Message] ?? [])) { - viewModel.cancelMessageAction() - isForwardSuccess = true - } - }) - }) + .if(isForwardSuccess == true, transform: { view in + view.forwardSuccessAlert(isPresented: $isForwardSuccess, name: viewModel.originSenderName) + }) - .modifier(DialogHeader(dialog: viewModel.dialog, - isForward: viewModel.messagesActionState == .forward, - selectedCount: viewModel.selectedMessages.count, - onDismiss: { - dismiss() - }, - onTapInfo: { - isInfoPresented = true - }, onTapCancel: { - viewModel.cancelMessageAction() - })) + .if(isIphone == true, transform: { view in + view.navigationDestination(isPresented: $isForwardPresented) { + forwardView() + } + .onChange(of: isForwardPresented) { newValue in + if newValue == false { + viewModel.cancelMessageAction() + } + } + }) - .environmentObject(viewModel) + .if((isIPad == true || isMac == true), transform: { view in + view.sheet(isPresented: $isForwardPresented, content: { + forwardView() + }) + .onChange(of: isForwardPresented) { newValue in + if newValue == false { + viewModel.cancelMessageAction() + } + } + }) + .environmentObject(viewModel) + } + + @ViewBuilder + private func forwardView() -> some View { + let messages = viewModel.selectedMessages as? [Message] ?? [] + let model = ForwardViewModel(messages: messages) + ForwardView(viewModel: model, + isComplete: $isForwardSuccess, + isPresented: $isForwardPresented) } @ViewBuilder @@ -328,14 +326,14 @@ public struct GroupDialogView } } } else if features.reply.enable == true && message.actionType == .reply, - message.originalMessages.isEmpty == false { + message.originalMessages.isEmpty == false { RepliedMessageRow(message: message, - isAIAlertPresented: $isAIAlertPresented, - fileUrl: $fileUrl, - aiFeature: $aiFeature, - isFileExporterPresented: $isFileExporterPresented, - tappedMessage: $tappedMessage, - attachment: $attachment) + isAIAlertPresented: $isAIAlertPresented, + fileUrl: $fileUrl, + aiFeature: $aiFeature, + isFileExporterPresented: $isFileExporterPresented, + tappedMessage: $tappedMessage, + attachment: $attachment) .onAppear { viewModel.handleOnAppear(message) } @@ -414,8 +412,19 @@ public struct GroupDialogView } public var body: some View { - if isIphone { + if isIphone { container() + .modifier(DialogHeader(dialog: viewModel.dialog, + isForward: viewModel.messagesActionState == .forward, + selectedCount: viewModel.selectedMessages.count, + onDismiss: { + // TODO: Evaluate if refactoring is needed for the onDismiss closure, possibly replacing it with a more efficient approach. + }, + onTapInfo: { + isInfoPresented = true + }, onTapCancel: { + viewModel.cancelMessageAction() + })) .onViewDidLoad { viewModel.sync() } @@ -427,6 +436,17 @@ public struct GroupDialogView } else { NavigationStack { container() + .modifier(DialogHeader(dialog: viewModel.dialog, + isForward: viewModel.messagesActionState == .forward, + selectedCount: viewModel.selectedMessages.count, + onDismiss: { + // TODO: Evaluate if refactoring is needed for the onDismiss closure, possibly replacing it with a more efficient approach. + }, + onTapInfo: { + isInfoPresented = true + }, onTapCancel: { + viewModel.cancelMessageAction() + })) .onViewDidLoad { viewModel.sync() } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Members/RemoveMembersView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Members/RemoveMembersView.swift index 5524201..a7a1ba0 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Members/RemoveMembersView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/Members/RemoveMembersView.swift @@ -12,7 +12,7 @@ import QuickBloxData public struct RemoveMembersView: View { @State public var settings = QuickBloxUIKit.settings.membersScreen - + @Environment(\.isSearching) private var isSearching: Bool @StateObject private var viewModel: ViewModel diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/PrivateDialogView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/PrivateDialogView.swift index 772ed07..548f281 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialog/PrivateDialogView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialog/PrivateDialogView.swift @@ -18,10 +18,8 @@ public struct PrivateDialogView: View { let connectStatus = QuickBloxUIKit.settings.dialogsScreen.connectStatus let features = QuickBloxUIKit.feature - @Environment(\.dismiss) var dismiss - @StateObject private var viewModel: ViewModel - + @State private var isInfoPresented: Bool = false @State private var isForwardPresented: Bool = false @State private var isForwardSuccess: Bool = false @@ -49,7 +47,7 @@ public struct PrivateDialogView: View { private func container() -> some View { ZStack(alignment: .center) { settings.backgroundColor.ignoresSafeArea() - dialogContentView() + dialogContentView() } } @@ -241,27 +239,40 @@ public struct PrivateDialogView: View { view.forwardSuccessAlert(isPresented: $isForwardSuccess, name: viewModel.originSenderName) }) - .if(isForwardPresented == true && isIphone == true, transform: { view in + .if(isIphone == true, transform: { view in view.navigationDestination(isPresented: $isForwardPresented) { - ForwardView(viewModel: ForwardViewModel(messages: viewModel.selectedMessages as? [Message] ?? [])) { + forwardView() + } + .onChange(of: isForwardPresented) { newValue in + if newValue == false { viewModel.cancelMessageAction() - isForwardSuccess = true } } }) - - .if(isForwardPresented == true && (isIPad == true || isMac == true), transform: { view in - view.sheet(isPresented: $isForwardPresented, content: { - ForwardView(viewModel: ForwardViewModel(messages: viewModel.selectedMessages as? [Message] ?? [])) { + + .if((isIPad == true || isMac == true), transform: { view in + view.navigationDestination(isPresented: $isForwardPresented) { + forwardView() + } + .onChange(of: isForwardPresented) { newValue in + if newValue == false { viewModel.cancelMessageAction() - isForwardSuccess = true } - }) + } }) .environmentObject(viewModel) } + @ViewBuilder + private func forwardView() -> some View { + let messages = viewModel.selectedMessages as? [Message] ?? [] + let model = ForwardViewModel(messages: messages) + ForwardView(viewModel: model, + isComplete: $isForwardSuccess, + isPresented: $isForwardPresented) + } + @ViewBuilder private func messagesView() -> some View { ScrollViewReader { scrollView in @@ -378,16 +389,16 @@ public struct PrivateDialogView: View { } public var body: some View { - if isIphone { + if isIphone { container() .modifier(DialogHeader(dialog: viewModel.dialog, isForward: viewModel.messagesActionState == .forward, selectedCount: viewModel.selectedMessages.count, onDismiss: { - dismiss() + // TODO: Evaluate if refactoring is needed for the onDismiss closure, possibly replacing it with a more efficient approach. }, onTapInfo: { - isInfoPresented = true + isInfoPresented = true }, onTapCancel: { viewModel.cancelMessageAction() })) @@ -406,7 +417,7 @@ public struct PrivateDialogView: View { isForward: viewModel.messagesActionState == .forward, selectedCount: viewModel.selectedMessages.count, onDismiss: { - dismiss() + // TODO: Evaluate if refactoring is needed for the onDismiss closure, possibly replacing it with a more efficient approach. }, onTapInfo: { isInfoPresented = true @@ -421,7 +432,6 @@ public struct PrivateDialogView: View { viewModel.stopPlayng() viewModel.unsync() } - } } } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/CreateDialog/CreateDialogView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/CreateDialog/CreateDialogView.swift index e0b6314..a8ec1d1 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/CreateDialog/CreateDialogView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/CreateDialog/CreateDialogView.swift @@ -11,18 +11,21 @@ import QuickBloxDomain import QuickBloxData struct CreateDialogView: View + DialogItem: DialogEntity, + UserItem: UserEntity>: View where DialogItem == ViewModel.DialogItem, UserItem == ViewModel.UserItem { let settings = QuickBloxUIKit.settings.createDialogScreen - - @Environment(\.dismiss) var dismiss + @Environment(\.isSearching) private var isSearching: Bool @StateObject private var viewModel: ViewModel - - init(viewModel: ViewModel) { + + @Binding var isPresented: Bool + + init(viewModel: ViewModel, + isPresented: Binding) { _viewModel = StateObject(wrappedValue: viewModel) + _isPresented = isPresented } public var body: some View { @@ -34,9 +37,9 @@ where DialogItem == ViewModel.DialogItem, UserItem == ViewModel.UserItem { } else { NavigationStack { container() - .onViewDidLoad { - viewModel.syncUsers() - } + .onViewDidLoad { + viewModel.syncUsers() + } }.accentColor(settings.header.leftButton.color) } } @@ -59,7 +62,7 @@ where DialogItem == ViewModel.DialogItem, UserItem == ViewModel.UserItem { } .modifier(CreateDialogHeader(onDismiss: { - dismiss() + isPresented = false }, onTapCreate: { viewModel.createDialog() }, disabled: viewModel.isProcessing == true)) diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogTypeView/DialogTypeView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogTypeView/DialogTypeView.swift index 53dfa4d..6f4f277 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogTypeView/DialogTypeView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogTypeView/DialogTypeView.swift @@ -50,33 +50,18 @@ struct DialogTypeView: View { Spacer().materialModifier() - .if(presentCreateDialog == true && isIphone == true) { view in + .if(isIphone == true) { view in view.navigationDestination(isPresented: $presentCreateDialog) { - if let selectedSegment { - if selectedSegment == .private { - CreateDialogView(viewModel: CreateDialogViewModel(modeldDialog: Dialog(type: .private))) - } else { - NewDialog(NewDialogViewModel(), type: selectedSegment) - } - } + createDialogView() } } - .if(presentCreateDialog == true && (isIPad == true || isMac == true)) { view in + .if((isIPad == true || isMac == true)) { view in view.sheet(isPresented: $presentCreateDialog, content: { - if let selectedSegment { - if selectedSegment == .private { - CreateDialogView(viewModel: CreateDialogViewModel(modeldDialog: Dialog(type: .private))) - .onDisappear { - self.selectedSegment = nil - } - } else { - NewDialog(NewDialogViewModel(), type: selectedSegment) - .onDisappear { - self.selectedSegment = nil - } + createDialogView() + .onDisappear { + self.selectedSegment = nil } - } }) } @@ -88,6 +73,22 @@ struct DialogTypeView: View { selectedSegment = nil } } + + @ViewBuilder + private func createDialogView() -> some View { + Group { + if selectedSegment == .private { + let dialog = Dialog(type: .private) + let model = CreateDialogViewModel(modeldDialog: dialog) + CreateDialogView(viewModel: model, isPresented: $presentCreateDialog) + } else if let selectedSegment { + NewDialog(NewDialogViewModel(), type: selectedSegment, isPresented: $presentCreateDialog) + } else { + EmptyView() + } + } + } + } public struct DialogTypeHeaderView: View { diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogsView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogsView.swift index e3cc73e..2094775 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogsView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/DialogsView.swift @@ -11,38 +11,38 @@ import QuickBloxData import QuickBloxDomain public struct DialogsView: View { - let settings = QuickBloxUIKit.settings.dialogsScreen let feature = QuickBloxUIKit.feature - @Environment(\.dismiss) var dismiss - let connectStatus = QuickBloxUIKit.settings.dialogsScreen.connectStatus @StateObject private var dialogsList: ViewModel + var onModifyContent: ((AnyView, Binding) -> AnyView)? + public init(dialogsList: ViewModel, - onBack: @escaping () -> Void, - onSelect: @escaping (_ tabIndex: TabIndex) -> Void) { + modifyContent: ((AnyView, Binding) -> AnyView)? = nil, + onBack: @escaping () -> Void) { _dialogsList = StateObject(wrappedValue: dialogsList) + self.onModifyContent = modifyContent self.onBack = onBack - self.onSelect = onSelect } private var onBack: () -> Void - public var onSelect: (_ tabIndex: TabIndex) -> Void - @State private var isDialogTypePresented: Bool = false + @State private var isDialogTypePresented: Bool = false { + didSet { + isNavigationBarPresented = !isDialogTypePresented + } + } @State private var isDeleteAlertPresented: Bool = false @State private var dialogForDeleting: ViewModel.Item? = nil @State private var searchText = "" @State private var submittedSearchTerm = "" + @State var isNavigationBarPresented: Bool = true @State public var isPresentedItem: Bool = false - @State private var selectedSegment: TabIndex = .dialogs - - @State private var tabBarVisibility: Visibility = .visible private var items: [ViewModel.Item] { if settings.searchBar.isSearchable == false || submittedSearchTerm.isEmpty { @@ -55,41 +55,31 @@ public struct DialogsView: View { @ViewBuilder private func container() -> some View { + var defaultView: AnyView = AnyView(EmptyView()) if isDialogTypePresented == true { - DialogTypeView(onClose: { + defaultView = AnyView(DialogTypeView(onClose: { // action onDismiss isDialogTypePresented = false - }) - } else if feature.toolbar.enable { - TabView(selection: $selectedSegment) { - if (isIPad == true || isMac == true) { - Spacer() - } - dialogsContentView().blur(radius: isDialogTypePresented ? settings.blurRadius : 0) - .toolbarBackground(settings.backgroundColor, for: .tabBar) - .toolbarBackground(tabBarVisibility, for: .tabBar) - .tabItem { - Label(TabIndex.dialogs.title, systemImage: TabIndex.dialogs.systemIcon) - } - .tag(TabIndex.dialogs) - - ForEach(feature.toolbar.externalIndexes, id:\.self) { tabIndex in - settings.backgroundColor.ignoresSafeArea() - .toolbarBackground(settings.backgroundColor, for: .tabBar) - .toolbarBackground(tabBarVisibility, for: .tabBar) - .tabItem { - Label(tabIndex.title, systemImage: tabIndex.systemIcon) - } - .tag(tabIndex) - } - } - .onChange(of: selectedSegment, perform: { newSelectedSegment in - onSelect(newSelectedSegment) - }) - .accentColor(settings.header.rightButton.color) + })) } else { - dialogsContentView().blur(radius: isDialogTypePresented ? settings.blurRadius : 0) + defaultView = AnyView(dialogsContentView().blur(radius: isDialogTypePresented ? settings.blurRadius : 0)) } + + if let modify = onModifyContent { + defaultView = modify(defaultView, $isNavigationBarPresented) + } + + return defaultView + .onChange(of: dialogsList.selectedItem, perform: { newSelectedItem in + isDialogTypePresented = false + isPresentedItem = newSelectedItem != nil + }) + .modifier(DialogListHeader(onDismiss: { + onBack() + }, onTapDialogType: { + isDialogTypePresented = true + })) + .navigationBarHidden(!isNavigationBarPresented) } @ViewBuilder @@ -187,10 +177,6 @@ public struct DialogsView: View { if isIphone { NavigationStack { container() - .onChange(of: dialogsList.selectedItem, perform: { newSelectedItem in - isDialogTypePresented = false - isPresentedItem = newSelectedItem != nil - }) .navigationDestination(isPresented: $isPresentedItem) { if let dialog = dialogsList.selectedItem as? Dialog { switch dialog.type { @@ -203,30 +189,12 @@ public struct DialogsView: View { } } } - .modifier(DialogListHeader(onDismiss: { - onBack() - dismiss() - }, onTapDialogType: { - isDialogTypePresented = true - })) - .navigationBarHidden(isDialogTypePresented) } .accentColor(settings.header.leftButton.color) } else { NavigationSplitView(columnVisibility: Binding.constant(.all)) { container() - .onChange(of: dialogsList.selectedItem, perform: { newSelectedItem in - isDialogTypePresented = false - isPresentedItem = newSelectedItem != nil - }) - .modifier(DialogListHeader(onDismiss: { - onBack() - dismiss() - }, onTapDialogType: { - isDialogTypePresented = true - })) - .navigationBarHidden(isDialogTypePresented) } detail: { if let dialog = dialogsList.selectedItem as? Dialog { switch dialog.type { @@ -235,7 +203,7 @@ public struct DialogsView: View { case .private: PrivateDialogView(viewModel: DialogViewModel(dialog: dialog)) default: - EmptyView() + EmptyDialogView() } } else { EmptyDialogView() diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/NewDialog/NewDialog.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/NewDialog/NewDialog.swift index d6cd378..bbd7f95 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/NewDialog/NewDialog.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/NewDialog/NewDialog.swift @@ -16,10 +16,8 @@ struct NewDialog: View { private var settings = QuickBloxUIKit.settings.dialogNameScreen - @Environment(\.dismiss) var dismiss - @StateObject private var viewModel: ViewModel - + private var type: DialogType @State private var isAlertPresented: Bool = false @@ -30,9 +28,13 @@ struct NewDialog: View { @State private var attachmentAsset: AttachmentAsset? = nil + @Binding var isPresented: Bool + init(_ viewModel: ViewModel, - type: DialogType) { + type: DialogType, + isPresented: Binding) { _viewModel = StateObject(wrappedValue: viewModel) + _isPresented = isPresented self.type = type } @@ -59,9 +61,9 @@ struct NewDialog: View { DialogNameTextField(dialogName: $dialogName, isValidDialogName: viewModel.isValidDialogName, isFocused: isCreatedDialog) - .onChange(of: dialogName, perform: { newValue in - viewModel.update(newValue) - }) + .onChange(of: dialogName, perform: { newValue in + viewModel.update(newValue) + }) }.padding([.leading, .trailing]) Spacer() @@ -99,26 +101,27 @@ struct NewDialog: View { .permissionAlert(isPresented: $viewModel.permissionNotGranted.notGranted, viewModel: viewModel) - .onChange(of: viewModel.modelDialog, perform: { newModelDialog in - if newModelDialog != nil { - isCreatedDialog = true + .navigationDestination(isPresented: $isCreatedDialog) { + if let dialogInfo = viewModel.modelDialog { + let dialog = Dialog(type: dialogInfo.type, + name: dialogInfo.name, + photo: dialogInfo.photo) + let viewModel = CreateDialogViewModel(modeldDialog: dialog) + + CreateDialogView(viewModel: viewModel, isPresented: $isPresented) + } else { + EmptyView() } + } + + .onChange(of: viewModel.modelDialog, perform: { newModelDialog in + isCreatedDialog = newModelDialog != nil }) - .if(isCreatedDialog == true) { view in - view.navigationDestination(isPresented: $isCreatedDialog) { - if let modelDialog = viewModel.modelDialog { - CreateDialogView(viewModel: CreateDialogViewModel(modeldDialog: Dialog(type: modelDialog.type, - name: modelDialog.name, - photo: modelDialog.photo))) - } - } - } - .modifier(DialogNameHeader(type: type, disabled: !viewModel.isValidDialogName, onDismiss: { - dismiss() - }, onNext: { + isPresented = false + }, onNext: { if type == .public { //TODO: createPublicDialog method viewModel.createPublicDialog() diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/SelectDialogsListView/SelectDialogsListView.swift b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/SelectDialogsListView/SelectDialogsListView.swift index e7ebfa2..c125add 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/SelectDialogsListView/SelectDialogsListView.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Dialogs/SelectDialogsListView/SelectDialogsListView.swift @@ -73,21 +73,20 @@ extension SelectDialogsListView: View { } .listRowInsets(EdgeInsets()) }.listStyle(.plain) + .if(settings.searchBar.isSearchable, + transform: { view in + view.searchable(text: $searchText, + prompt: settings.searchBar.searchTextField.placeholderText) + .onSubmit(of: .search) { + submittedSearchTerm = searchText + }.onChange(of: searchText) { value in + if searchText.isEmpty && !isSearching { + submittedSearchTerm = "" + } + } + .autocorrectionDisabled(true) + }) } } - - .if(settings.searchBar.isSearchable, - transform: { view in - view.searchable(text: $searchText, - prompt: settings.searchBar.searchTextField.placeholderText) - .onSubmit(of: .search) { - submittedSearchTerm = searchText - }.onChange(of: searchText) { value in - if searchText.isEmpty && !isSearching { - submittedSearchTerm = "" - } - } - .autocorrectionDisabled(true) - }) } } diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/Feature.swift b/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/Feature.swift index 8165f3d..0e78f69 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/Feature.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/Feature.swift @@ -22,8 +22,9 @@ public class Feature { public var regex: RegexFeature = RegexFeature() /// An instance of the ToolbarFeature settings and operations. + @available(*, deprecated, message: "The toolbar feature is deprecated and will be removed in future versions.") public var toolbar: ToolbarFeature = ToolbarFeature() /// An instance of the StartScreenFeature settings and operations. - public var startScreen: StartScreenFeature = StartScreenFeature() -} + @available(*, deprecated, message: "The StartScreenFeature is deprecated and will be removed in future versions.") + public var startScreen: StartScreenFeature = StartScreenFeature()} diff --git a/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/FeatureSettings.swift b/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/FeatureSettings.swift index 07c01cf..66e5d10 100644 --- a/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/FeatureSettings.swift +++ b/Sources/QuickBloxUIKit/SwiftUIView/Theme/Settings/FeatureSettings.swift @@ -37,11 +37,13 @@ public class RegexFeature { public var dialogName = "^(?=.{3,60}$)(?!.*([\\s])\\1{2})[\\w\\s]+$" } +@available(*, deprecated, message: "The ToolbarFeature class is deprecated and will be removed in future versions.") public class ToolbarFeature { public var enable: Bool = false public var externalIndexes: [TabIndex] = [] } +@available(*, deprecated, message: "Toolbar UI settings are deprecated and will be removed in future versions.") public struct ToolbarUISettings { public var backgroundColor: Color public init(_ theme: ThemeProtocol) { @@ -49,6 +51,7 @@ public struct ToolbarUISettings { } } +@available(*, deprecated, message: "TabIndex is deprecated and will be removed in future versions.") public struct TabIndex: Hashable { public var title: String public var systemIcon: String @@ -60,17 +63,22 @@ public struct TabIndex: Hashable { } public extension TabIndex { + @available(*, deprecated, message: "TabIndex.dialogs is deprecated and will be removed in future versions.") static let dialogs = TabIndex(title: "Dialogs", systemIcon: "message.fill") + + @available(*, deprecated, message: "TabIndex.settings is deprecated and will be removed in future versions.") static let settings = TabIndex(title: "Settings", systemIcon: "gearshape.fill") } +@available(*, deprecated, message: "StartScreens is deprecated and will be removed in future versions.") public enum StartScreens { case dialogs case dialog } +@available(*, deprecated, message: "StartScreenFeature is deprecated and will be removed in future versions.") public class StartScreenFeature { public var screen: StartScreens = .dialogs } diff --git a/Sources/QuickBloxUIKit/ViewModel/AddMembersDialogViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/AddMembersDialogViewModel.swift index 0a9fd4c..c105e66 100644 --- a/Sources/QuickBloxUIKit/ViewModel/AddMembersDialogViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/AddMembersDialogViewModel.swift @@ -33,7 +33,7 @@ final class AddMembersDialogViewModel: AddMembersDialogProtocol { private var dialog: Dialog - public private(set) var dialogsRepo: DialogsRepository = RepositoriesFabric.dialogs + public private(set) var dialogsRepo: DialogsRepository = Repository.dialogs private var updateDialogLocalObserve: DialogUpdateObserver! public var cancellables = Set() @@ -65,6 +65,7 @@ final class AddMembersDialogViewModel: AddMembersDialogProtocol { $search.eraseToAnyPublisher() .receive(on: RunLoop.main) + .removeDuplicates() .sink { [weak self] text in self?.displayDialogMembers(by: text) } @@ -92,11 +93,10 @@ final class AddMembersDialogViewModel: AddMembersDialogProtocol { if text.isEmpty || text.count > 2 { isSynced = false - let getUsers = GetUsers(name: text, repo: RepositoriesFabric.users) + let getUsers = GetUsers(name: text, repo: Repository.users) let ids = dialog.participantsIds taskUsers?.cancel() - taskUsers = nil taskUsers = Task { [weak self] in do { let users = try await getUsers.execute() @@ -120,6 +120,7 @@ final class AddMembersDialogViewModel: AddMembersDialogProtocol { } } } + self?.taskUsers = nil } } } @@ -140,7 +141,7 @@ final class AddMembersDialogViewModel: AddMembersDialogProtocol { dialog.pushIDs = [user.id] let updateDialog = UpdateDialog(dialog: dialog, users: [user], - repo: RepositoriesFabric.dialogs) + repo: Repository.dialogs) taskUpdate?.cancel() taskUpdate = nil diff --git a/Sources/QuickBloxUIKit/ViewModel/CreateDialogViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/CreateDialogViewModel.swift index 5bd0d44..24eb3a4 100644 --- a/Sources/QuickBloxUIKit/ViewModel/CreateDialogViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/CreateDialogViewModel.swift @@ -56,6 +56,7 @@ final class CreateDialogViewModel: CreateDialogProtocol { $search.eraseToAnyPublisher() .receive(on: RunLoop.main) + .removeDuplicates() .sink { [weak self] text in self?.displayMembers(by: text) } @@ -70,10 +71,9 @@ final class CreateDialogViewModel: CreateDialogProtocol { if text.isEmpty || text.count > 2 { isSynced = false - let getUsers = GetUsers(name: text, repo: RepositoriesFabric.users) + let getUsers = GetUsers(name: text, repo: Repository.users) taskUsers?.cancel() - taskUsers = nil taskUsers = Task { [weak self] in do { let users = try await getUsers.execute() @@ -93,7 +93,6 @@ final class CreateDialogViewModel: CreateDialogProtocol { self.displayed = toDisplay self.isSynced = true } - } catch { prettyLog(error) if error is RepositoryException { @@ -102,6 +101,7 @@ final class CreateDialogViewModel: CreateDialogProtocol { } } } + self?.taskUsers = nil } } } @@ -143,7 +143,7 @@ final class CreateDialogViewModel: CreateDialogProtocol { do { guard let dialog = self?.modeldDialog else { return } let create = CreateDialog(dialog: dialog, - repo: RepositoriesFabric.dialogs) + repo: Repository.dialogs) try await create.execute() await MainActor.run { [weak self] in diff --git a/Sources/QuickBloxUIKit/ViewModel/DialogInfoViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/DialogInfoViewModel.swift index 170a5e0..45f1b22 100644 --- a/Sources/QuickBloxUIKit/ViewModel/DialogInfoViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/DialogInfoViewModel.swift @@ -53,8 +53,8 @@ final class DialogInfoViewModel: DialogInfoProtocol { return dialog.photo.isEmpty == false } - private let dialogsRepo: DialogsRepository = RepositoriesFabric.dialogs - private let permissionsRepo: PermissionsRepository = RepositoriesFabric.permissions + private let dialogsRepo: DialogsRepository = Repository.dialogs + private let permissionsRepo: PermissionsRepository = Repository.permissions private var attachmentAsset: AttachmentAsset? = nil @@ -121,7 +121,7 @@ final class DialogInfoViewModel: DialogInfoProtocol { do { let update = UpdateDialog(dialog: modeldDialog, users: [], - repo: RepositoriesFabric.dialogs) + repo: Repository.dialogs) try await update.execute() await MainActor.run { [weak self] in @@ -201,7 +201,7 @@ final class DialogInfoViewModel: DialogInfoProtocol { let uploadAvatar = UploadFile(data: finalImageData, ext: .png, name: name, - repo: RepositoriesFabric.files) + repo: Repository.files) let fileInfo = try await uploadAvatar.execute() guard let uuid = fileInfo.info.path.uuid else { return } await MainActor.run { [weak self, uuid] in @@ -238,7 +238,7 @@ final class DialogInfoViewModel: DialogInfoProtocol { taskDelete = Task { do { let leave = LeaveDialog(dialog: dialog, - repo: RepositoriesFabric.dialogs) + repo: Repository.dialogs) try await leave.execute() self.taskDelete = nil } catch { diff --git a/Sources/QuickBloxUIKit/ViewModel/DialogViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/DialogViewModel.swift index 383115b..d40a32e 100644 --- a/Sources/QuickBloxUIKit/ViewModel/DialogViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/DialogViewModel.swift @@ -154,9 +154,9 @@ open class DialogViewModel: DialogViewModelProtocol { @Published public var filesInfo: [MessageIdsInfo: (type: String, image: UIImage?, url: URL?)] = [:] @Published public var isLoading = CurrentValueSubject(false) - private let dialogsRepo: DialogsRepository = RepositoriesFabric.dialogs - private let usersRepo: UsersRepository = RepositoriesFabric.users - private let permissionsRepo: PermissionsRepository = RepositoriesFabric.permissions + private let dialogsRepo: DialogsRepository = Repository.dialogs + private let usersRepo: UsersRepository = Repository.users + private let permissionsRepo: PermissionsRepository = Repository.permissions private var syncDialog: SyncDialog() public var tasks = Set>() @@ -124,7 +124,7 @@ open class ForwardViewModel: ForwardViewModelProtocol { originalMessages: messages) let sendForwardMessage = SendForwardMessage(message: message, - messageRepo: RepositoriesFabric.messages) + messageRepo: Repository.messages) Task { do { diff --git a/Sources/QuickBloxUIKit/ViewModel/MembersDialogViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/MembersDialogViewModel.swift index ffdb41a..7811e51 100644 --- a/Sources/QuickBloxUIKit/ViewModel/MembersDialogViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/MembersDialogViewModel.swift @@ -31,8 +31,8 @@ final class MembersDialogViewModel: MembersDialogProtocol { @Published public var dialog: Dialog @Published public var isProcessing: Bool = false - public private(set) var usersRepo: UsersRepository = RepositoriesFabric.users - public private(set) var dialogsRepo: DialogsRepository = RepositoriesFabric.dialogs + public private(set) var usersRepo: UsersRepository = Repository.users + public private(set) var dialogsRepo: DialogsRepository = Repository.dialogs private var updateDialogLocalObserve: DialogUpdateObserver! @@ -41,8 +41,8 @@ final class MembersDialogViewModel: MembersDialogProtocol { private var taskUpdate: Task? init(dialog: Dialog, - usersRepo: UsersRepository = RepositoriesFabric.users, - dialogsRepo: DialogsRepository = RepositoriesFabric.dialogs) { + usersRepo: UsersRepository = Repository.users, + dialogsRepo: DialogsRepository = Repository.dialogs) { self.dialog = dialog self.usersRepo = usersRepo self.dialogsRepo = dialogsRepo @@ -105,7 +105,7 @@ final class MembersDialogViewModel: MembersDialogProtocol { guard let dialog = self?.dialog else { return } let updateDialog = UpdateDialog(dialog: dialog, users: [user], - repo: RepositoriesFabric.dialogs) + repo: Repository.dialogs) try await updateDialog.execute() } catch { prettyLog(error) diff --git a/Sources/QuickBloxUIKit/ViewModel/NewDialogViewModel.swift b/Sources/QuickBloxUIKit/ViewModel/NewDialogViewModel.swift index 6785b8f..33e4aba 100644 --- a/Sources/QuickBloxUIKit/ViewModel/NewDialogViewModel.swift +++ b/Sources/QuickBloxUIKit/ViewModel/NewDialogViewModel.swift @@ -68,7 +68,7 @@ final class NewDialogViewModel: NewDialogProtocol { private var avatarUUID = "" - private let permissionsRepo: PermissionsRepository = RepositoriesFabric.permissions + private let permissionsRepo: PermissionsRepository = Repository.permissions private var attachmentAsset: AttachmentAsset? = nil @@ -120,7 +120,7 @@ final class NewDialogViewModel: NewDialogProtocol { let uploadAvatar = UploadFile(data: finalImageData, ext: .png, name: name, - repo: RepositoriesFabric.files) + repo: Repository.files) let fileInfo = try await uploadAvatar.execute() guard let uuid = fileInfo.info.path.uuid else { return } await MainActor.run { [weak self, uuid] in diff --git a/Sources/QuickBloxUIKit/ViewModel/Utils/TypingProvider.swift b/Sources/QuickBloxUIKit/ViewModel/Utils/TypingProvider.swift index 87ec818..7da501d 100644 --- a/Sources/QuickBloxUIKit/ViewModel/Utils/TypingProvider.swift +++ b/Sources/QuickBloxUIKit/ViewModel/Utils/TypingProvider.swift @@ -109,14 +109,14 @@ class TypingProvider: ObservableObject { stopTimer = nil Task { - let sendStopTyping = SendStopTyping(dialogId: dialogId, repo: RepositoriesFabric.dialogs) + let sendStopTyping = SendStopTyping(dialogId: dialogId, repo: Repository.dialogs) try await sendStopTyping.execute() } } public func sendTyping() { Task { - let sendTyping = SendTyping(dialogId: dialogId, repo: RepositoriesFabric.dialogs) + let sendTyping = SendTyping(dialogId: dialogId, repo: Repository.dialogs) try await sendTyping.execute() } stopTimer?.invalidate() diff --git a/Sources/QuickBloxUIKit/ViewModel/Utils/Utils.swift b/Sources/QuickBloxUIKit/ViewModel/Utils/Utils.swift index a4820cb..eba6c17 100644 --- a/Sources/QuickBloxUIKit/ViewModel/Utils/Utils.swift +++ b/Sources/QuickBloxUIKit/ViewModel/Utils/Utils.swift @@ -43,13 +43,13 @@ extension DialogEntity { func privateAvatar(scale: DialogThumbnailSize) async throws -> Image { if QuickBloxUIKit.previewAware { return placeholder } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let imageCache = ThumbnailImageCache.shared var path: String - let usersRepo = RepositoriesFabric.users + let usersRepo = Repository.users guard let userId = participantsIds.filter({ isOwnedByCurrentUser == true ? $0 != ownerId : $0 == ownerId}).first else { return placeholder } @@ -84,7 +84,7 @@ extension DialogEntity { var thumbnailKey: String = "" - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let imageCache = ThumbnailImageCache.shared @@ -106,7 +106,7 @@ extension DialogEntity { return Image(uiImage: uiImage) } case .private, .unknown: - let usersRepo = RepositoriesFabric.users + let usersRepo = Repository.users guard let userId = participantsIds.filter({ isOwnedByCurrentUser == true ? $0 != ownerId : $0 == ownerId}).first else { return placeholder } @@ -140,7 +140,7 @@ extension DialogEntity { do { if QuickBloxUIKit.previewAware, id.isEmpty { return nil } - let repo = RepositoriesFabric.messages + let repo = Repository.messages if lastMessage.id.isEmpty { return nil } @@ -159,7 +159,7 @@ extension DialogEntity { } else { return nil } } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let useCase = GetFile(id: attachmentId, repo: filesRepo) try Task.checkCancellation() @@ -270,7 +270,7 @@ extension MessageEntity { get async throws { if QuickBloxUIKit.previewAware { return "Name" } - let usersRepo = RepositoriesFabric.users + let usersRepo = Repository.users let getUser = GetUser(id: userId, repo: usersRepo) let user = try await getUser.execute() if user.name.isEmpty { return user.id } @@ -282,7 +282,7 @@ extension MessageEntity { func avatar(scale: UserThumbnailScale) async throws -> Image { if QuickBloxUIKit.previewAware { return placeholder } - let usersRepo = RepositoriesFabric.users + let usersRepo = Repository.users let getUser = GetUser(id: userId, repo: usersRepo) let user = try await getUser.execute() @@ -295,7 +295,7 @@ extension MessageEntity { return Image(uiImage: uiImage) } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let useCase = GetFile(id: user.avatarPath, repo: filesRepo) try Task.checkCancellation() @@ -321,7 +321,7 @@ extension MessageEntity { guard let file = fileInfo else { return nil } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let useCase = GetFile(id: file.id, repo: filesRepo) @@ -382,7 +382,7 @@ extension MessageEntity { guard let file = fileInfo else { return nil } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let useCase = GetFile(id: file.id, repo: filesRepo) @@ -429,7 +429,7 @@ extension UserEntity { return Image(uiImage: uiImage) } - let filesRepo = RepositoriesFabric.files + let filesRepo = Repository.files let useCase = GetFile(id: avatarPath, repo: filesRepo) try Task.checkCancellation()