From 22db9a8e122c12ca9bacf9cebd99c1369039deaf Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 15 Mar 2023 11:06:42 +0300 Subject: [PATCH 001/136] [trello.com/c/v9nCB1Sx] Implemented Increase fee --- Adamant.xcodeproj/project.pbxproj | 8 +++ .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Assets/l18n/de.lproj/Localizable.strings | 3 + .../Assets/l18n/en.lproj/Localizable.strings | 3 + .../Assets/l18n/ru.lproj/Localizable.strings | 3 + .../ServiceProtocols/IncreaseFeeService.swift | 14 ++++ .../Services/AdamantIncreaseFeeService.swift | 72 +++++++++++++++++++ Adamant/SwinjectDependencies.swift | 7 ++ .../Adamant/AdmTransferViewController.swift | 10 ++- Adamant/Wallets/Adamant/AdmWalletRoutes.swift | 14 ++-- .../Bitcoin/BtcTransferViewController.swift | 33 +++++++-- Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift | 16 +++-- .../Wallets/Bitcoin/BtcWalletService.swift | 30 ++++++-- .../Dash/DashTransferViewController.swift | 34 ++++++++- Adamant/Wallets/Dash/DashWalletRouter.swift | 16 +++-- .../Doge/DogeTransferViewController.swift | 34 ++++++++- Adamant/Wallets/Doge/DogeWalletRoutes.swift | 16 +++-- .../ERC20/ERC20TransferViewController.swift | 33 +++++++-- Adamant/Wallets/ERC20/ERC20WalletRouter.swift | 16 +++-- .../Wallets/ERC20/ERC20WalletService.swift | 25 +++++-- .../Ethereum/EthTransferViewController.swift | 33 +++++++-- .../Wallets/Ethereum/EthWalletRoutes.swift | 16 +++-- .../Wallets/Ethereum/EthWalletService.swift | 23 +++++- .../Lisk/LskTransferViewController.swift | 33 +++++++-- Adamant/Wallets/Lisk/LskWalletRoutes.swift | 16 +++-- .../Wallets/TransferViewControllerBase.swift | 58 +++++++++++++-- Adamant/Wallets/WalletService.swift | 12 ++++ AdamantShared/Core/SecuredStore.swift | 4 ++ 28 files changed, 487 insertions(+), 97 deletions(-) create mode 100644 Adamant/ServiceProtocols/IncreaseFeeService.swift create mode 100644 Adamant/Services/AdamantIncreaseFeeService.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index aad3f86b6..4fdae0fab 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4150136329B225CC0037F834 /* Double+adamant.swift */; }; + 4153045929C09902000E4BEA /* AdamantIncreaseFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */; }; + 4153045B29C09C6C000E4BEA /* IncreaseFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */; }; 415441372923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; 415441382923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; 415441392923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; @@ -755,6 +757,8 @@ 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; 4150136329B225CC0037F834 /* Double+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+adamant.swift"; sourceTree = ""; }; + 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantIncreaseFeeService.swift; sourceTree = ""; }; + 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreaseFeeService.swift; sourceTree = ""; }; 415441362923AB3700824478 /* BtcProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcProvider.swift; sourceTree = ""; }; 4154413A2923AED000824478 /* bitcoin_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bitcoin_notificationContent.png; sourceTree = ""; }; 415CFD38294310680008AEE8 /* AdamantDynamicResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantDynamicResources.swift; sourceTree = ""; }; @@ -1748,6 +1752,7 @@ 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */, 9324C75D297170600022D7EA /* RichTransactionStatusService.swift */, 41047B73294C61D10039E956 /* VisibleWalletsService.swift */, + 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -1768,6 +1773,7 @@ E9A03FD720DC0ABA007653A1 /* AdamantNodesSource.swift */, E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */, 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */, + 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */, E921597420611A6A0000CA5C /* AdamantReachability.swift */, E950273F202E257E002C1098 /* RepeaterService.swift */, E9E7CDB42002BA6900DFC4DB /* SwinjectedRouter.swift */, @@ -3080,6 +3086,7 @@ E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, 550066C7284D682D0044C0B1 /* AdamantHealthCheckService.swift in Sources */, 640EFAA62558613F00E9724B /* LskProvider.swift in Sources */, + 4153045929C09902000E4BEA /* AdamantIncreaseFeeService.swift in Sources */, E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */, E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */, E91947B420002809001362F8 /* AdamantAccount.swift in Sources */, @@ -3267,6 +3274,7 @@ 551F66E628959A5300DE5D69 /* LoadingView.swift in Sources */, E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, + 4153045B29C09C6C000E4BEA /* IncreaseFeeService.swift in Sources */, E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */, 9324C760297171040022D7EA /* AdamantRichTransactionStatusService.swift in Sources */, E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */, diff --git a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d07ea46c..1d60d0b16 100644 --- a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -174,7 +174,7 @@ }, { "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream.git", + "repositoryURL": "https://github.com/daltoniam/Starscream", "state": { "branch": null, "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index c226e27d6..d9f2d62b4 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -988,6 +988,9 @@ /* Transfer: comment for transfer in chat */ "TransferScene.Row.Comments" = "Bemerkungen"; +/* Transfer: transfer increase fee */ +"TransferScene.Row.IncreaseFee" = "Gebühr erhöhen"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Transaktionsinfo"; diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index e2d54d11b..1769fdad5 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -964,6 +964,9 @@ /* Transfer: comment for transfer in chat */ "TransferScene.Row.Comments" = "Comments"; +/* Transfer: transfer increase fee */ +"TransferScene.Row.IncreaseFee" = "Increase fee"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Transfer Info"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index 6b436c453..e215e13a5 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -961,6 +961,9 @@ /* Transfer: comment for transfer in chat */ "TransferScene.Row.Comments" = "Комментарии"; +/* Transfer: transfer increase fee */ +"TransferScene.Row.IncreaseFee" = "Увеличить комиссию"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Перевод"; diff --git a/Adamant/ServiceProtocols/IncreaseFeeService.swift b/Adamant/ServiceProtocols/IncreaseFeeService.swift new file mode 100644 index 000000000..e6f583374 --- /dev/null +++ b/Adamant/ServiceProtocols/IncreaseFeeService.swift @@ -0,0 +1,14 @@ +// +// IncreaseFeeService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 14.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol IncreaseFeeService: AnyObject { + func isIncreaseFeeEnabled(for id: String) -> Bool + func setIncreaseFeeEnabled(for id: String, value: Bool) +} diff --git a/Adamant/Services/AdamantIncreaseFeeService.swift b/Adamant/Services/AdamantIncreaseFeeService.swift new file mode 100644 index 000000000..8f8e0ed03 --- /dev/null +++ b/Adamant/Services/AdamantIncreaseFeeService.swift @@ -0,0 +1,72 @@ +// +// AdamantIncreaseFeeService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 14.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Combine + +final class AdamantIncreaseFeeService: IncreaseFeeService { + + // MARK: Dependencies + + let securedStore: SecuredStore + + // MARK: Proprieties + + private var data: [String: Bool] = [:] + private var notificationsSet: Set = [] + + // MARK: Lifecycle + + init(securedStore: SecuredStore) { + self.securedStore = securedStore + + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedOut) + .sink { [weak self] _ in + self?.userLoggedOut() + } + .store(in: ¬ificationsSet) + + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedIn) + .sink { [weak self] _ in + self?.userLoggedIn() + } + .store(in: ¬ificationsSet) + } + + // MARK: Notification actions + + private func userLoggedIn() { + data = getIncreaseFeeDictionary() + } + + private func userLoggedOut() { + securedStore.remove(StoreKey.increaseFee.increaseFee) + data = [:] + } + + // MARK: Check + + func isIncreaseFeeEnabled(for id: String) -> Bool { + return data[id] ?? false + } + + func setIncreaseFeeEnabled(for id: String, value: Bool) { + data[id] = value + securedStore.set(data, for: StoreKey.increaseFee.increaseFee) + } + + private func getIncreaseFeeDictionary() -> [String: Bool] { + guard let result: [String: Bool] = securedStore.get(StoreKey.increaseFee.increaseFee) else { + return [:] + } + + return result + } +} diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index 1966e62be..1411e60e4 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -59,6 +59,13 @@ extension Container { ) }.inObjectScope(.container) + // MARK: IncreaseFeeService + self.register(IncreaseFeeService.self) { r in + AdamantIncreaseFeeService( + securedStore: r.resolve(SecuredStore.self)! + ) + }.inObjectScope(.container) + // MARK: PushNotificationsTokenService self.register(PushNotificationsTokenService.self) { r in AdamantPushNotificationsTokenService( diff --git a/Adamant/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Wallets/Adamant/AdmTransferViewController.swift index aa3374497..7c6ccc77b 100644 --- a/Adamant/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Wallets/Adamant/AdmTransferViewController.swift @@ -102,7 +102,7 @@ class AdmTransferViewController: TransferViewControllerBase { service.update() dialogService.dismissProgress() - dialogService?.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) openDetailVC( result: result, @@ -111,8 +111,8 @@ class AdmTransferViewController: TransferViewControllerBase { comments: comments ) } catch { - dialogService?.dismissProgress() - dialogService?.showRichError(error: error) + dialogService.dismissProgress() + dialogService.showRichError(error: error) } } } @@ -132,7 +132,7 @@ class AdmTransferViewController: TransferViewControllerBase { if let recipientName = recipientName { detailsVC?.recipientName = recipientName vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) - } else if let accountsProvider = accountsProvider { + } else { Task { do { let account = try await accountsProvider.getAccount(byAddress: recipient) @@ -142,8 +142,6 @@ class AdmTransferViewController: TransferViewControllerBase { vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) } } - } else { - vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) } } diff --git a/Adamant/Wallets/Adamant/AdmWalletRoutes.swift b/Adamant/Wallets/Adamant/AdmWalletRoutes.swift index 121920a9e..3b3b5567a 100644 --- a/Adamant/Wallets/Adamant/AdmWalletRoutes.swift +++ b/Adamant/Wallets/Adamant/AdmWalletRoutes.swift @@ -22,12 +22,14 @@ extension AdamantScene.Wallets { /// Send money static let transfer = AdamantScene(identifier: "AdmTransferViewController") { r in - let c = AdmTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = AdmTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)! + ) return c } diff --git a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift index a3ab65dbb..5abc5309f 100644 --- a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift +++ b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift @@ -13,7 +13,7 @@ class BtcTransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider // MARK: Properties @@ -29,6 +29,33 @@ class BtcTransferViewController: TransferViewControllerBase { static let invalidCharacters: CharacterSet = CharacterSet.decimalDigits.inverted + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: Send @MainActor @@ -44,10 +71,6 @@ class BtcTransferViewController: TransferViewControllerBase { return } - guard let dialogService = dialogService else { - return - } - dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) Task { diff --git a/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift b/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift index b408f4638..ed8d4e950 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send BTC tokens static let transfer = AdamantScene(identifier: "BtcTransferViewController") { r in - let c = BtcTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = BtcTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Wallets/Bitcoin/BtcWalletService.swift index 511fa2ba9..f242623c7 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService.swift @@ -74,6 +74,14 @@ class BtcWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var isSupportIncreaseFee: Bool { + return true + } + + var isIncreaseFeeEnabled: Bool { + return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + } + var wallet: WalletAccount? { return btcWallet } var walletViewController: WalletViewController { @@ -93,6 +101,7 @@ class BtcWalletService: WalletService { var accountService: AccountService! var dialogService: DialogService! var router: Router! + var increaseFeeService: IncreaseFeeService! // MARK: - Constants static var currencyLogo = #imageLiteral(resourceName: "bitcoin_wallet") @@ -201,15 +210,25 @@ class BtcWalletService: WalletService { feeRate = rate } + if let height = try? await getCurrentHeight() { + currentHeight = height + } + if let transactions = try? await getUnspentTransactions() { let feeRate = feeRate let fee = Decimal(transactions.count * 181 + 78) * feeRate - transactionFee = fee / BtcWalletService.multiplier - } - - if let height = try? await getCurrentHeight() { - currentHeight = height + var newTransactionFee = fee / BtcWalletService.multiplier + + newTransactionFee = isIncreaseFeeEnabled + ? newTransactionFee * defaultIncreaseFee + : newTransactionFee + + guard transactionFee != newTransactionFee else { return } + + transactionFee = newTransactionFee + + NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) } } @@ -378,6 +397,7 @@ extension BtcWalletService: SwinjectDependentService { apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) + increaseFeeService = container.resolve(IncreaseFeeService.self) } } diff --git a/Adamant/Wallets/Dash/DashTransferViewController.swift b/Adamant/Wallets/Dash/DashTransferViewController.swift index d23af4722..78b5cf045 100644 --- a/Adamant/Wallets/Dash/DashTransferViewController.swift +++ b/Adamant/Wallets/Dash/DashTransferViewController.swift @@ -18,12 +18,39 @@ class DashTransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider // MARK: Properties static let invalidCharacters: CharacterSet = CharacterSet.decimalDigits.inverted + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: Send @MainActor @@ -35,7 +62,10 @@ class DashTransferViewController: TransferViewControllerBase { comments = "" } - guard let service = service as? DashWalletService, let recipient = recipientAddress, let amount = amount, let dialogService = dialogService else { + guard let service = service as? DashWalletService, + let recipient = recipientAddress, + let amount = amount + else { return } diff --git a/Adamant/Wallets/Dash/DashWalletRouter.swift b/Adamant/Wallets/Dash/DashWalletRouter.swift index ed321d962..7b4d193f0 100644 --- a/Adamant/Wallets/Dash/DashWalletRouter.swift +++ b/Adamant/Wallets/Dash/DashWalletRouter.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send tokens static let transfer = AdamantScene(identifier: "DashTransferViewController") { r in - let c = DashTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = DashTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index 7b49e8d47..3d1ca71af 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -14,7 +14,34 @@ class DogeTransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider + + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: Properties @@ -31,7 +58,10 @@ class DogeTransferViewController: TransferViewControllerBase { comments = "" } - guard let service = service as? DogeWalletService, let recipient = recipientAddress, let amount = amount, let dialogService = dialogService else { + guard let service = service as? DogeWalletService, + let recipient = recipientAddress, + let amount = amount + else { return } diff --git a/Adamant/Wallets/Doge/DogeWalletRoutes.swift b/Adamant/Wallets/Doge/DogeWalletRoutes.swift index 1e7c5d9b7..c332f058f 100644 --- a/Adamant/Wallets/Doge/DogeWalletRoutes.swift +++ b/Adamant/Wallets/Doge/DogeWalletRoutes.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send tokens static let transfer = AdamantScene(identifier: "DogeTransferViewController") { r in - let c = DogeTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = DogeTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift index 72ee777c3..7943b6348 100644 --- a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift +++ b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift @@ -14,7 +14,7 @@ class ERC20TransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider // MARK: Properties @@ -28,6 +28,33 @@ class ERC20TransferViewController: TransferViewControllerBase { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: EthWalletService.currencySymbol) } + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: Send @MainActor @@ -43,10 +70,6 @@ class ERC20TransferViewController: TransferViewControllerBase { return } - guard let dialogService = dialogService else { - return - } - dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) Task { diff --git a/Adamant/Wallets/ERC20/ERC20WalletRouter.swift b/Adamant/Wallets/ERC20/ERC20WalletRouter.swift index f5d8b6af8..694586677 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletRouter.swift +++ b/Adamant/Wallets/ERC20/ERC20WalletRouter.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send money static let transfer = AdamantScene(identifier: "ERC20TransferViewController") { r in - let c = ERC20TransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = ERC20TransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Wallets/ERC20/ERC20WalletService.swift index 2099d212a..ca6f1619d 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Wallets/ERC20/ERC20WalletService.swift @@ -61,6 +61,14 @@ class ERC20WalletService: WalletService { return token?.defaultOrdinalLevel } + var isSupportIncreaseFee: Bool { + return true + } + + var isIncreaseFeeEnabled: Bool { + return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + } + private (set) var blockchainSymbol: String = "ETH" private (set) var isDynamicFee: Bool = true private (set) var transactionFee: Decimal = 0.0 @@ -83,6 +91,7 @@ class ERC20WalletService: WalletService { var apiService: ApiService! var dialogService: DialogService! var router: Router! + var increaseFeeService: IncreaseFeeService! // MARK: - Notifications var walletUpdatedNotification = Notification.Name("adamant.erc20Wallet.walletUpdated") @@ -277,13 +286,20 @@ class ERC20WalletService: WalletService { ? gasLimit : gasLimit + gasLimitPercent - let newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) + var newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) + newFee = isIncreaseFeeEnabled + ? newFee * defaultIncreaseFee + : newFee + guard transactionFee != newFee else { return } transactionFee = newFee - gasPrice = price - isWarningGasPrice = price >= BigUInt(token.warningGasPriceGwei).toWei() + gasPrice = isIncreaseFeeEnabled + ? price * BigUInt(defaultIncreaseFee.doubleValue) + : price + + isWarningGasPrice = gasPrice >= BigUInt(token.warningGasPriceGwei).toWei() self.gasLimit = gasLimit NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) @@ -318,7 +334,7 @@ class ERC20WalletService: WalletService { } do { - var transaction = try await erc20.transfer( + let transaction = try await erc20.transfer( from: ethWallet.ethAddress, to: ethWallet.ethAddress, amount: "\(ethWallet.balance)" @@ -423,6 +439,7 @@ extension ERC20WalletService: SwinjectDependentService { apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) + increaseFeeService = container.resolve(IncreaseFeeService.self) } } diff --git a/Adamant/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Wallets/Ethereum/EthTransferViewController.swift index 0f17889d1..ef1dd918d 100644 --- a/Adamant/Wallets/Ethereum/EthTransferViewController.swift +++ b/Adamant/Wallets/Ethereum/EthTransferViewController.swift @@ -14,7 +14,7 @@ class EthTransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider // MARK: Properties @@ -24,6 +24,33 @@ class EthTransferViewController: TransferViewControllerBase { CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").inverted }() + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: Send @MainActor @@ -39,10 +66,6 @@ class EthTransferViewController: TransferViewControllerBase { return } - guard let dialogService = dialogService else { - return - } - dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) Task { diff --git a/Adamant/Wallets/Ethereum/EthWalletRoutes.swift b/Adamant/Wallets/Ethereum/EthWalletRoutes.swift index b5e277570..ffad9088d 100644 --- a/Adamant/Wallets/Ethereum/EthWalletRoutes.swift +++ b/Adamant/Wallets/Ethereum/EthWalletRoutes.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send money static let transfer = AdamantScene(identifier: "EthTransferViewController") { r in - let c = EthTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = EthTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/Ethereum/EthWalletService.swift b/Adamant/Wallets/Ethereum/EthWalletService.swift index 7591ed44a..0c9653ae0 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Wallets/Ethereum/EthWalletService.swift @@ -89,6 +89,14 @@ class EthWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var isSupportIncreaseFee: Bool { + return true + } + + var isIncreaseFeeEnabled: Bool { + return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + } + private (set) var isDynamicFee: Bool = true private (set) var transactionFee: Decimal = 0.0 private (set) var gasPrice: BigUInt = 0 @@ -106,6 +114,7 @@ class EthWalletService: WalletService { var apiService: ApiService! var dialogService: DialogService! var router: Router! + var increaseFeeService: IncreaseFeeService! // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.ethWallet.walletUpdated") @@ -293,13 +302,20 @@ class EthWalletService: WalletService { ? gasLimit : gasLimit + gasLimitPercent - let newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) + var newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) + + newFee = isIncreaseFeeEnabled + ? newFee * defaultIncreaseFee + : newFee guard transactionFee != newFee else { return } transactionFee = newFee - gasPrice = price - isWarningGasPrice = price >= warningGasPriceGwei.toWei() + gasPrice = isIncreaseFeeEnabled + ? price * BigUInt(defaultIncreaseFee.doubleValue) + : price + + isWarningGasPrice = gasPrice >= warningGasPriceGwei.toWei() self.gasLimit = gasLimit NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) @@ -509,6 +525,7 @@ extension EthWalletService: SwinjectDependentService { apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) + increaseFeeService = container.resolve(IncreaseFeeService.self) } } diff --git a/Adamant/Wallets/Lisk/LskTransferViewController.swift b/Adamant/Wallets/Lisk/LskTransferViewController.swift index cfda23ee4..18ed41899 100644 --- a/Adamant/Wallets/Lisk/LskTransferViewController.swift +++ b/Adamant/Wallets/Lisk/LskTransferViewController.swift @@ -14,7 +14,34 @@ class LskTransferViewController: TransferViewControllerBase { // MARK: Dependencies - var chatsProvider: ChatsProvider! + var chatsProvider: ChatsProvider + + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService, + chatsProvider: ChatsProvider + ) { + self.chatsProvider = chatsProvider + + super.init( + accountService: accountService, + accountsProvider: accountsProvider, + dialogService: dialogService, + router: router, + currencyInfoService: currencyInfoService, + increaseFeeService: increaseFeeService + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: Send @@ -31,10 +58,6 @@ class LskTransferViewController: TransferViewControllerBase { return } - guard let dialogService = dialogService else { - return - } - dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) Task { diff --git a/Adamant/Wallets/Lisk/LskWalletRoutes.swift b/Adamant/Wallets/Lisk/LskWalletRoutes.swift index da136ecf0..fb08febc6 100644 --- a/Adamant/Wallets/Lisk/LskWalletRoutes.swift +++ b/Adamant/Wallets/Lisk/LskWalletRoutes.swift @@ -21,13 +21,15 @@ extension AdamantScene.Wallets { /// Send LSK tokens static let transfer = AdamantScene(identifier: "LskTransferViewController") { r in - let c = LskTransferViewController() - c.dialogService = r.resolve(DialogService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) + let c = LskTransferViewController( + accountService: r.resolve(AccountService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)!, + dialogService: r.resolve(DialogService.self)!, + router: r.resolve(Router.self)!, + currencyInfoService: r.resolve(CurrencyInfoService.self)!, + increaseFeeService: r.resolve(IncreaseFeeService.self)!, + chatsProvider: r.resolve(ChatsProvider.self)! + ) return c } diff --git a/Adamant/Wallets/TransferViewControllerBase.swift b/Adamant/Wallets/TransferViewControllerBase.swift index 789209198..7a12ac371 100644 --- a/Adamant/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Wallets/TransferViewControllerBase.swift @@ -62,6 +62,7 @@ class TransferViewControllerBase: FormViewController { case maxToTransfer case name case address + case increaseFee case fee case total case comments @@ -75,6 +76,7 @@ class TransferViewControllerBase: FormViewController { case .maxToTransfer: return "max" case .name: return "name" case .address: return "recipient" + case .increaseFee: return "increaseFee" case .fee: return "fee" case .total: return "total" case .comments: return "comments" @@ -94,6 +96,7 @@ class TransferViewControllerBase: FormViewController { case .total: return NSLocalizedString("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") case .comments: return NSLocalizedString("TransferScene.Row.Comments", comment: "Transfer: transfer comment") case .sendButton: return String.adamantLocalized.transfer.send + case .increaseFee: return NSLocalizedString("TransferScene.Row.IncreaseFee", comment: "Transfer: transfer increase fee") } } } @@ -125,11 +128,12 @@ class TransferViewControllerBase: FormViewController { // MARK: - Dependencies - var accountService: AccountService! - var accountsProvider: AccountsProvider! - var dialogService: DialogService! - var router: Router! - var currencyInfoService: CurrencyInfoService! + var accountService: AccountService + var accountsProvider: AccountsProvider + var dialogService: DialogService + var router: Router + var currencyInfoService: CurrencyInfoService + var increaseFeeService: IncreaseFeeService // MARK: - Properties @@ -250,6 +254,30 @@ class TransferViewControllerBase: FormViewController { var progressView: UIView? var alertView: UIView? + // MARK: - Init + + init( + accountService: AccountService, + accountsProvider: AccountsProvider, + dialogService: DialogService, + router: Router, + currencyInfoService: CurrencyInfoService, + increaseFeeService: IncreaseFeeService + ) { + self.accountService = accountService + self.accountsProvider = accountsProvider + self.dialogService = dialogService + self.router = router + self.currencyInfoService = currencyInfoService + self.increaseFeeService = increaseFeeService + + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -421,6 +449,11 @@ class TransferViewControllerBase: FormViewController { section.append(defaultRowFor(baseRow: .amount)) section.append(defaultRowFor(baseRow: .fiat)) + + if service?.isSupportIncreaseFee == true { + section.append(defaultRowFor(baseRow: .increaseFee)) + } + section.append(defaultRowFor(baseRow: .fee)) section.append(defaultRowFor(baseRow: .total)) @@ -896,7 +929,20 @@ extension TransferViewControllerBase { return self?.rate == nil } } - + case .increaseFee: + return SwitchRow { [weak self] in + $0.tag = BaseRows.increaseFee.tag + $0.title = BaseRows.increaseFee.localized + $0.value = self?.service?.isIncreaseFeeEnabled ?? false + }.onChange { [weak self] row in + guard let id = self?.service?.tokenUnicID, + let value = row.value + else { + return + } + self?.increaseFeeService.setIncreaseFeeEnabled(for: id, value: value) + self?.service?.update() + } case .fee: return DoubleDetailsRow { [weak self] in let estimateSymbol = service?.isDynamicFee == true ? " ~" : "" diff --git a/Adamant/Wallets/WalletService.swift b/Adamant/Wallets/WalletService.swift index c82fc06e1..682924207 100644 --- a/Adamant/Wallets/WalletService.swift +++ b/Adamant/Wallets/WalletService.swift @@ -271,6 +271,9 @@ protocol WalletServiceWithSend: WalletService { var isWarningGasPrice : Bool { get } var isTransactionFeeValid : Bool { get } var commentsEnabledForRichMessages: Bool { get } + var isSupportIncreaseFee: Bool { get } + var isIncreaseFeeEnabled: Bool { get } + var defaultIncreaseFee: Decimal { get } func transferViewController() -> UIViewController } @@ -290,6 +293,15 @@ extension WalletServiceWithSend { var isDynamicFee: Bool { return false } + var isSupportIncreaseFee: Bool { + return false + } + var isIncreaseFeeEnabled: Bool { + return false + } + var defaultIncreaseFee: Decimal { + return 1.5 + } } protocol WalletServiceSimpleSend: WalletServiceWithSend { diff --git a/AdamantShared/Core/SecuredStore.swift b/AdamantShared/Core/SecuredStore.swift index 1fac6b0aa..9feeb1136 100644 --- a/AdamantShared/Core/SecuredStore.swift +++ b/AdamantShared/Core/SecuredStore.swift @@ -38,6 +38,10 @@ extension StoreKey { static let useCustomIndexes = "visible.wallets.useCustomIndexes" static let useCustomVisibility = "visible.wallets.useCustomVisibility" } + + enum increaseFee { + static let increaseFee = "increaseFee" + } } protocol SecuredStore: AnyObject { From d1872a4fc6093d071f46eefcdfa9a61e952789b0 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 24 Mar 2023 17:49:36 +0300 Subject: [PATCH 002/136] [trello.com/c/TGBBXBeX] Starting the creation of reply message processing --- Adamant.xcodeproj/project.pbxproj | 40 ++++++ .../Adamant.xcdatamodel/contents | 1 + ...essageTransaction+CoreDataProperties.swift | 1 + .../AdamantChatTransactionService.swift | 10 ++ .../Chat/View/ChatViewController.swift | 2 + .../View/Managers/ChatDataSourceManager.swift | 16 ++- .../View/Managers/ChatLayoutManager.swift | 2 +- .../ChatReplyContainerView+Model.swift | 23 ++++ .../Container/ChatReplyContainerView.swift | 95 ++++++++++++++ .../Content/ChatReplyContentView+Model.swift | 25 ++++ .../Content/ChatReplyContentView.swift | 117 ++++++++++++++++++ .../ChatTransactionCellSizeCalculator.swift | 26 ++-- .../Chat/ViewModel/ChatMessageFactory.swift | 28 +++++ .../Chat/ViewModel/ChatViewModel.swift | 7 +- .../Chat/ViewModel/Models/ChatMessage.swift | 3 + AdamantShared/Models/RichMessage.swift | 14 +++ 16 files changed, 395 insertions(+), 15 deletions(-) create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index c3eef5c34..cacc1c2ed 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -19,6 +19,10 @@ 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */; }; 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; + 413AD21D29CDDD970025F255 /* ChatReplyContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */; }; + 413AD21F29CDDDF20025F255 /* ChatReplyContainerView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */; }; + 413AD22229CDDE380025F255 /* ChatReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */; }; + 413AD22429CDDF510025F255 /* ChatReplyContentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */; }; 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4150136329B225CC0037F834 /* Double+adamant.swift */; }; 415441372923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; 415441382923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; @@ -755,6 +759,10 @@ 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVisibleWalletsService.swift; sourceTree = ""; }; 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; + 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReplyContainerView.swift; sourceTree = ""; }; + 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatReplyContainerView+Model.swift"; sourceTree = ""; }; + 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReplyContentView.swift; sourceTree = ""; }; + 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatReplyContentView+Model.swift"; sourceTree = ""; }; 4150136329B225CC0037F834 /* Double+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+adamant.swift"; sourceTree = ""; }; 415441362923AB3700824478 /* BtcProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcProvider.swift; sourceTree = ""; }; 4154413A2923AED000824478 /* bitcoin_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bitcoin_notificationContent.png; sourceTree = ""; }; @@ -1368,6 +1376,33 @@ path = SocketService; sourceTree = ""; }; + 413AD21A29CDDD750025F255 /* ChatReply */ = { + isa = PBXGroup; + children = ( + 413AD21B29CDDD820025F255 /* Container */, + 413AD22029CDDE240025F255 /* Content */, + ); + path = ChatReply; + sourceTree = ""; + }; + 413AD21B29CDDD820025F255 /* Container */ = { + isa = PBXGroup; + children = ( + 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */, + 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */, + ); + path = Container; + sourceTree = ""; + }; + 413AD22029CDDE240025F255 /* Content */ = { + isa = PBXGroup; + children = ( + 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */, + 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */, + ); + path = Content; + sourceTree = ""; + }; 4186B32A2940CBF9006594A3 /* Scripts */ = { isa = PBXGroup; children = ( @@ -1568,6 +1603,7 @@ 93996A9829682690008D080B /* Subviews */ = { isa = PBXGroup; children = ( + 413AD21A29CDDD750025F255 /* ChatReply */, 9377FBE0296C2AB700C9211B /* ChatTransaction */, 938F7D652955C966001915CA /* ChatInputBar.swift */, 93A91FD0297972B7001DB1F8 /* ChatScrollDownButton.swift */, @@ -3155,6 +3191,7 @@ 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */, E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, + 413AD22229CDDE380025F255 /* ChatReplyContentView.swift in Sources */, 646DF92422C29F6E00DCC864 /* ERC20Token.swift in Sources */, E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */, 550698B628C00AED000E8A2B /* CollectionUtilites.swift in Sources */, @@ -3163,6 +3200,7 @@ 640EFA9E2558613700E9724B /* DashProvider.swift in Sources */, E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */, + 413AD21D29CDDD970025F255 /* ChatReplyContainerView.swift in Sources */, 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */, E96D64B22295BED600CA5587 /* TransactionAsset.swift in Sources */, E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */, @@ -3174,6 +3212,7 @@ 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */, 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */, E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */, + 413AD22429CDDF510025F255 /* ChatReplyContentView+Model.swift in Sources */, 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */, 93F391482962EEDC00BFD6AE /* CGSize+adamant.swift in Sources */, 938F7D612955C92B001915CA /* ChatDataSourceManager.swift in Sources */, @@ -3204,6 +3243,7 @@ E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, 640EFAA52558613D00E9724B /* ERC20Provider.swift in Sources */, + 413AD21F29CDDDF20025F255 /* ChatReplyContainerView+Model.swift in Sources */, 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, 934B9BE9296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift in Sources */, diff --git a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents index a5cb28f78..cb942e930 100644 --- a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents +++ b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents @@ -54,6 +54,7 @@ + diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift index e25b775f2..fdc044f3c 100644 --- a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift +++ b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift @@ -18,6 +18,7 @@ extension RichMessageTransaction { @NSManaged public var richContent: [String:String]? @NSManaged public var richType: String? + @NSManaged public var isReply: Bool @NSManaged public var transferStatusRaw: NSNumber? } diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index 41c33d25e..6e47d1253 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -113,8 +113,18 @@ actor AdamantChatTransactionService: ChatTransactionService { let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) trs.richContent = richContent trs.richType = type + trs.isReply = false trs.transactionStatus = richProviders[type] != nil ? .notInitiated : nil messageTransaction = trs + } else if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + richContent["replyto_id"] != nil { + let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) + trs.richContent = richContent + trs.richType = "reply" + trs.isReply = true + trs.transactionStatus = nil + messageTransaction = trs } else { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = decodedMessage diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index c1e4c490b..c0480faf0 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -16,6 +16,7 @@ import SnapKit final class ChatViewController: MessagesViewController { typealias SpinnerCell = MessageCellWrapper typealias TransactionCell = CollectionCellWrapper + typealias ReplyCell = CollectionCellWrapper typealias SendTransaction = (UIViewController & ComplexTransferViewControllerDelegate) -> Void // MARK: Dependencies @@ -337,6 +338,7 @@ private extension ChatViewController { let collection = ChatMessagesCollectionView() collection.refreshControl = ChatRefreshMock() collection.register(TransactionCell.self) + collection.register(ReplyCell.self) collection.register( SpinnerCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index a2f2cf218..451af2059 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -67,11 +67,25 @@ final class ChatDataSourceManager: MessagesDataSource { at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> UICollectionViewCell { + + if case let .custom(model) = message.kind, + let model = model as? ChatReplyContainerView.Model { + let cell = messagesCollectionView.dequeueReusableCell( + ChatViewController.ReplyCell.self, + for: indexPath + ) + + cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } + cell.wrappedView.model = model + + return cell + } + let cell = messagesCollectionView.dequeueReusableCell( ChatViewController.TransactionCell.self, for: indexPath ) - + if case let .transaction(model) = message.fullModel.content { cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.model = model diff --git a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift index 63a37c791..3c665a529 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift @@ -81,7 +81,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { at _: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> CellSizeCalculator { - ChatTransactionCellSizeCalculator( + ChatCellSizeCalculator( layout: messagesCollectionView.messagesCollectionViewFlowLayout, getCurrentSender: { [sender = viewModel.sender] in sender }, getMessages: { [messages = viewModel.messages] in messages } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift new file mode 100644 index 000000000..7b4dbec5f --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift @@ -0,0 +1,23 @@ +// +// ChatReplyContainerView+Model.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 24.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +extension ChatReplyContainerView { + struct Model: Equatable { + let id: String + let isFromCurrentSender: Bool + let content: ChatReplyContentView.Model + + static let `default` = Self( + id: "", + isFromCurrentSender: true, + content: .default + ) + } +} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift new file mode 100644 index 000000000..13b7263fa --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift @@ -0,0 +1,95 @@ +// +// ChatReplyContainerView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 24.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import SnapKit +import Combine + +final class ChatReplyContainerView: UIView { + var model: Model = .default { + didSet { + guard model != oldValue else { return } + update() + } + } + + var actionHandler: (ChatAction) -> Void = { _ in } { + didSet { contentView.actionHandler = actionHandler } + } + + private let contentView = ChatReplyContentView() + + private let spacingView: UIView = { + let view = UIView() + view.setContentCompressionResistancePriority(.dragThatCanResizeScene, for: .horizontal) + return view + }() + + private let horizontalStack: UIStackView = { + let stack = UIStackView() + stack.alignment = .center + stack.axis = .horizontal + stack.spacing = 12 + return stack + }() + + override init(frame: CGRect) { + super.init(frame: frame) + print("init") + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +private extension ChatReplyContainerView { + func configure() { + addSubview(horizontalStack) + horizontalStack.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(12) + } + } + + func update() { + contentView.model = model.content + updateLayout() + } + + func updateLayout() { + var viewsList = [spacingView, contentView] + + viewsList = model.isFromCurrentSender + ? viewsList + : viewsList.reversed() + + guard horizontalStack.arrangedSubviews != viewsList else { return } + horizontalStack.arrangedSubviews.forEach(horizontalStack.removeArrangedSubview) + viewsList.forEach(horizontalStack.addArrangedSubview) + } + + @objc func buttonTap() { + // actionHandler(.scrollToMessage(id: model.id)) + } +} + +extension ChatReplyContainerView.Model { + func height(for width: CGFloat) -> CGFloat { + content.height(for: width) + } +} + +extension ChatReplyContainerView: ReusableView { + func prepareForReuse() { + model = .default + actionHandler = { _ in } + } +} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift new file mode 100644 index 000000000..da27d2661 --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift @@ -0,0 +1,25 @@ +// +// ChatReplyContentView+Model.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 24.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +extension ChatReplyContentView { + struct Model: Equatable { + let id: String + let message: String + let messageReply: String + let backgroundColor: ChatMessageBackgroundColor + + static let `default` = Self( + id: "", + message: "", + messageReply: "", + backgroundColor: .failed + ) + } +} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift new file mode 100644 index 000000000..326a4f7d7 --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift @@ -0,0 +1,117 @@ +// +// ChatReplyContentView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 24.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import SnapKit + +final class ChatReplyContentView: UIView { + var model: Model = .default { + didSet { + guard oldValue != model else { return } + update() + } + } + + var actionHandler: (ChatAction) -> Void = { _ in } + + private let messageLabel = UILabel(font: messageFont, textColor: .adamant.textColor, numberOfLines: 0) + private let replyLabel = UILabel(font: replyFont, textColor: .adamant.textColor, numberOfLines: 0) + + private lazy var replyView: UIView = { + let view = UIView() + let colorView = UIView() + colorView.backgroundColor = .adamant.active + + view.addSubview(colorView) + view.addSubview(replyLabel) + + colorView.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.width.equalTo(2) + } + replyLabel.snp.makeConstraints { + $0.top.bottom.trailing.equalToSuperview() + $0.leading.equalTo(colorView.snp.trailing).offset(3) + } + return view + }() + + private lazy var verticalStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [replyView, messageLabel]) + stack.axis = .vertical + stack.spacing = verticalStackSpacing + return stack + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension ChatReplyContentView.Model { + func height(for width: CGFloat) -> CGFloat { + let maxSize = CGSize(width: width, height: .infinity) + let titleString = NSAttributedString(string: message, attributes: [.font: messageFont]) + let dateString = NSAttributedString(string: messageReply, attributes: [.font: replyFont]) + + let titleHeight = titleString.boundingRect( + with: maxSize, + options: .usesLineFragmentOrigin, + context: nil + ).height + + let dateHeight = dateString.boundingRect( + with: maxSize, + options: .usesLineFragmentOrigin, + context: nil + ).height + + return verticalInsets * 2 + + verticalStackSpacing * 3 + + titleHeight + + dateHeight + } +} + +private extension ChatReplyContentView { + func configure() { + layer.cornerRadius = 16 + + addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(didTap) + )) + + addSubview(verticalStack) + verticalStack.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(verticalInsets) + $0.leading.trailing.equalToSuperview().inset(12) + } + } + + func update() { + backgroundColor = model.backgroundColor.uiColor + messageLabel.text = model.message + replyLabel.text = model.messageReply + } + + @objc func didTap() { + // actionHandler(.scrollToMessage(id: model.id)) + } +} + +private let messageFont = UIFont.systemFont(ofSize: 17) +private let replyFont = UIFont.systemFont(ofSize: 16) +private let verticalStackSpacing: CGFloat = 6 +private let verticalInsets: CGFloat = 8 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index 815512f94..2182681af 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -1,5 +1,5 @@ // -// ChatTransactionCellSizeCalculator.swift +// ChatCellSizeCalculator.swift // Adamant // // Created by Andrey Golubenko on 10.01.2023. @@ -9,8 +9,7 @@ import MessageKit import UIKit -final class ChatTransactionCellSizeCalculator: CellSizeCalculator { - private let measuredView = ChatTransactionContainerView() +final class ChatCellSizeCalculator: CellSizeCalculator { private let getCurrentSender: () -> SenderType private let getMessages: () -> [ChatMessage] private let messagesFlowLayout: MessagesCollectionViewFlowLayout @@ -28,13 +27,20 @@ final class ChatTransactionCellSizeCalculator: CellSizeCalculator { } override func sizeForItem(at indexPath: IndexPath) -> CGSize { - guard - case let .transaction(model) = getMessages()[indexPath.section].fullModel.content - else { return .zero } + if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { + return .init( + width: messagesFlowLayout.itemWidth, + height: model.height(for: messagesFlowLayout.itemWidth) + ) + } - return .init( - width: messagesFlowLayout.itemWidth, - height: model.height(for: messagesFlowLayout.itemWidth) - ) + if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { + return .init( + width: messagesFlowLayout.itemWidth, + height: model.height(for: messagesFlowLayout.itemWidth) + ) + } + + return .zero } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index baea5684b..159f16ef9 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -94,6 +94,10 @@ private extension ChatMessageFactory { case let transaction as MessageTransaction: return makeContent(transaction) case let transaction as RichMessageTransaction: + if transaction.isReply { + return makeContent(transaction, backgroundColor: backgroundColor) + } + return makeContent( transaction, isFromCurrentSender: isFromCurrentSender, @@ -116,6 +120,30 @@ private extension ChatMessageFactory { } ?? .default } + func makeContent( + _ transaction: RichMessageTransaction, + backgroundColor: ChatMessageBackgroundColor + ) -> ChatMessage.Content { + guard let content = transaction.richContent, + let message = content["message"], + let replyId = content["replyto_id"], + let replyMessage = content["reply_message"] + else { + return .default + } + + return .reply(.init( + id: replyId, + isFromCurrentSender: true, + content: .init( + id: replyId, + message: message, + messageReply: replyMessage, + backgroundColor: backgroundColor + ) + )) + } + func makeContent( _ transaction: RichMessageTransaction, isFromCurrentSender: Bool, diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index cf50b7054..3e4599623 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -171,9 +171,10 @@ final class ChatViewModel: NSObject { } Task { - let message: AdamantMessage = markdownParser.parse(text).length == text.count - ? .text(text) - : .markdownText(text) +// let message: AdamantMessage = markdownParser.parse(text).length == text.count +// ? .text(text) +// : .markdownText(text) + let message: AdamantMessage = .richMessage(payload: RichMessageReply(type: "")) guard await validateSendingMessage(message: message) else { return } diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift index b527df2d5..cb6cce9fc 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift @@ -43,6 +43,7 @@ extension ChatMessage { enum Content: Equatable { case message(ComparableAttributedString) case transaction(ChatTransactionContainerView.Model) + case reply(ChatReplyContainerView.Model) static let `default` = Self.message(.init(string: .init())) } @@ -58,6 +59,8 @@ extension ChatMessage: MessageType { return .attributedText(text.string) case let .transaction(model): return .custom(model) + case let .reply(model): + return .custom(model) } } } diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index 296a61c2c..452235c6a 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -33,6 +33,20 @@ struct RichContentKeys { private init() {} } +// MARK: - RichMessageReply + +struct RichMessageReply: RichMessage { + var type: String = "reply" + + func content() -> [String : String] { + return [ + "replyto_id": "9839400464901626037", + "reply_message": "123", + "message": "test reply test reply test reply test reply test reply\ntest replytest reply" + ] + } +} + // MARK: - RichMessageTransfer struct RichMessageTransfer: RichMessage { From eb3fe9be8fe4aca3de64df1fd02e8fe934a3f78a Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 28 Mar 2023 09:50:15 +0300 Subject: [PATCH 003/136] [trello.com/c/TGBBXBeX] implemented swipe --- .../Chat/View/ChatViewController.swift | 53 +++++++++++++++++++ .../View/Managers/ChatDataSourceManager.swift | 18 +++++++ .../Container/ChatReplyContainerView.swift | 1 - .../Chat/ViewModel/ChatViewModel.swift | 20 +++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index c0480faf0..74053b09d 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -240,6 +240,10 @@ private extension ChatViewController { viewModel.didTapAdmSend .sink { [weak self] in self?.didTapAdmSend(to: $0) } .store(in: &subscriptions) + + viewModel.swipeAction + .sink { [weak self] in self?.swipeGestureCellAction($0) } + .store(in: &subscriptions) } } @@ -503,6 +507,52 @@ private extension ChatViewController { } } +// MARK: Swipe +private extension ChatViewController { + func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { + guard let movingView = recognizer.view?.superview as? UIView else { return } + + let translation = recognizer.translation(in: messagesCollectionView) + + if movingView.frame.origin.x == messagePadding && translation.x > 0 { return } + + if movingView.frame.origin.x <= messagePadding { + print("movingView.center.x= \(movingView.center.x)") + movingView.center = CGPoint( + x: movingView.center.x + translation.x, + y: movingView.center.y + ) + recognizer.setTranslation(CGPoint(x: 0, y: 0), in: view) + if abs(movingView.frame.origin.x) > UIScreen.main.bounds.size.width * 0.18 { + replyAction = true + if canReplyVibrate { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + } + canReplyVibrate = false + } else { + replyAction = false + } + } + + if recognizer.state == .ended { + canReplyVibrate = true + + if replyAction { + print("reply!") + } + + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { + movingView.frame = CGRect( + x: messagePadding, + y: movingView.frame.origin.y, + width: movingView.frame.size.width, + height: movingView.frame.size.height + ) + } + } + } +} + // MARK: Markdown private extension ChatViewController { @@ -533,3 +583,6 @@ private extension ChatViewController { } private let scrollDownButtonInset: CGFloat = 20 +private let messagePadding: CGFloat = 12 +private var replyAction: Bool = false +private var canReplyVibrate: Bool = true diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 451af2059..badd326c1 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -78,6 +78,13 @@ final class ChatDataSourceManager: MessagesDataSource { cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.model = model + let panGestureRecognizer = UIPanGestureRecognizer( + target: self, + action: #selector(swipeGestureCellAction(_:)) + ) + panGestureRecognizer.delegate = viewModel + cell.contentView.addGestureRecognizer(panGestureRecognizer) + return cell } @@ -91,6 +98,13 @@ final class ChatDataSourceManager: MessagesDataSource { cell.wrappedView.model = model } + let panGestureRecognizer = UIPanGestureRecognizer( + target: self, + action: #selector(swipeGestureCellAction(_ :)) + ) + panGestureRecognizer.delegate = viewModel + cell.contentView.addGestureRecognizer(panGestureRecognizer) + return cell } } @@ -104,4 +118,8 @@ private extension ChatDataSourceManager { viewModel.forceUpdateTransactionStatus(id: id) } } + + @objc func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { + viewModel.swipeAction.send(recognizer) + } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift index 13b7263fa..cebb96c8f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift @@ -40,7 +40,6 @@ final class ChatReplyContainerView: UIView { override init(frame: CGRect) { super.init(frame: frame) - print("init") configure() } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 3e4599623..a8fbd3ab4 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -9,6 +9,7 @@ import Combine import CoreData import MarkdownKit +import UIKit @MainActor final class ChatViewModel: NSObject { @@ -46,6 +47,7 @@ final class ChatViewModel: NSObject { let didTapAdmChat = ObservableSender<(Chatroom, String?)>() let didTapAdmSend = ObservableSender() let closeScreen = ObservableSender() + let swipeAction = ObservableSender() @ObservableValue private(set) var fullscreenLoading = false @ObservableValue private(set) var messages = [ChatMessage]() @@ -335,6 +337,24 @@ extension ChatViewModel: NSFetchedResultsControllerDelegate { } } +extension ChatViewModel: UIGestureRecognizerDelegate { + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + return true + } + +// func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { +// guard let gesture = gestureRecognizer as? UIPanGestureRecognizer else { +// return true +// } +// +// let translation = gesture.translation(in: messagesCollectionView) +// return scrollView.contentOffset.y == 0 +// } +} + private extension ChatViewModel { func setupObservers() { $inputText From e124f5033ff86d4e2986f22a30c9772976841480 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 28 Mar 2023 10:31:40 +0300 Subject: [PATCH 004/136] [trello.com/c/TGBBXBeX] Temporary corrected additional Y offset --- .../Chat/View/ChatViewController.swift | 18 +++++++++++-- .../View/Managers/ChatDataSourceManager.swift | 2 +- .../Chat/ViewModel/ChatViewModel.swift | 27 +++++++------------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 74053b09d..9702c319e 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -508,6 +508,7 @@ private extension ChatViewController { } // MARK: Swipe + private extension ChatViewController { func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { guard let movingView = recognizer.view?.superview as? UIView else { return } @@ -516,13 +517,21 @@ private extension ChatViewController { if movingView.frame.origin.x == messagePadding && translation.x > 0 { return } + if recognizer.state == .began { + oldContentOffset = messagesCollectionView.contentOffset + } + if movingView.frame.origin.x <= messagePadding { - print("movingView.center.x= \(movingView.center.x)") movingView.center = CGPoint( - x: movingView.center.x + translation.x, + x: movingView.center.x + translation.x / 2, y: movingView.center.y ) recognizer.setTranslation(CGPoint(x: 0, y: 0), in: view) + + if let oldContentOffset = oldContentOffset { + messagesCollectionView.setContentOffset(oldContentOffset, animated: false) + } + if abs(movingView.frame.origin.x) > UIScreen.main.bounds.size.width * 0.18 { replyAction = true if canReplyVibrate { @@ -537,6 +546,10 @@ private extension ChatViewController { if recognizer.state == .ended { canReplyVibrate = true + if let oldContentOffset = oldContentOffset { + messagesCollectionView.setContentOffset(oldContentOffset, animated: false) + } + if replyAction { print("reply!") } @@ -586,3 +599,4 @@ private let scrollDownButtonInset: CGFloat = 20 private let messagePadding: CGFloat = 12 private var replyAction: Bool = false private var canReplyVibrate: Bool = true +private var oldContentOffset: CGPoint? diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index badd326c1..2c2b0e091 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -100,7 +100,7 @@ final class ChatDataSourceManager: MessagesDataSource { let panGestureRecognizer = UIPanGestureRecognizer( target: self, - action: #selector(swipeGestureCellAction(_ :)) + action: #selector(swipeGestureCellAction(_:)) ) panGestureRecognizer.delegate = viewModel cell.contentView.addGestureRecognizer(panGestureRecognizer) diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index a8fbd3ab4..846d5a62e 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -337,24 +337,6 @@ extension ChatViewModel: NSFetchedResultsControllerDelegate { } } -extension ChatViewModel: UIGestureRecognizerDelegate { - func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - return true - } - -// func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { -// guard let gesture = gestureRecognizer as? UIPanGestureRecognizer else { -// return true -// } -// -// let translation = gesture.translation(in: messagesCollectionView) -// return scrollView.contentOffset.y == 0 -// } -} - private extension ChatViewModel { func setupObservers() { $inputText @@ -564,3 +546,12 @@ private extension ChatViewModel { didTapAdmChat.send((chatroom, message)) } } + +extension ChatViewModel: UIGestureRecognizerDelegate { + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + return true + } +} From d71108cdfac17e31c3f5bea4b5deb6d49228df7d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 28 Mar 2023 11:25:16 +0300 Subject: [PATCH 005/136] [trello.com/c/TGBBXBeX] Passed data to swipe gesture --- Adamant.xcodeproj/project.pbxproj | 8 ++++ .../Helpers/SwipePanGestureRecognizer.swift | 44 +++++++++++++++++++ Adamant/Models/MessageModel.swift | 16 +++++++ .../AdamantChatTransactionService.swift | 4 +- .../Chat/View/ChatViewController.swift | 8 +++- .../View/Managers/ChatDataSourceManager.swift | 37 ++++++++-------- .../ChatReplyContainerView+Model.swift | 6 ++- .../Content/ChatReplyContentView+Model.swift | 10 +++-- .../Content/ChatReplyContentView.swift | 8 ++-- .../ChatTransactionContainerView+Model.swift | 13 +++++- .../Chat/ViewModel/ChatMessageFactory.swift | 9 ++-- .../Chat/ViewModel/ChatViewModel.swift | 9 ---- 12 files changed, 127 insertions(+), 45 deletions(-) create mode 100644 Adamant/Helpers/SwipePanGestureRecognizer.swift create mode 100644 Adamant/Models/MessageModel.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index cacc1c2ed..089cfd270 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -57,6 +57,8 @@ 4198D57D28C8B7FA009337F2 /* relax-message-tone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */; }; 4198D57F28C8B834009337F2 /* short-success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57E28C8B834009337F2 /* short-success.mp3 */; }; 4198D58128C8B8D1009337F2 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; + 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */; }; + 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994329D2D3CF0031AD75 /* MessageModel.swift */; }; 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */; }; 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */; }; 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */; }; @@ -785,6 +787,8 @@ 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "relax-message-tone.mp3"; sourceTree = ""; }; 4198D57E28C8B834009337F2 /* short-success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "short-success.mp3"; sourceTree = ""; }; 4198D58028C8B8D1009337F2 /* default.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = default.mp3; sourceTree = ""; }; + 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipePanGestureRecognizer.swift; sourceTree = ""; }; + 41A1994329D2D3CF0031AD75 /* MessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = ""; }; 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsResetTableViewCell.swift; sourceTree = ""; }; 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3Swift+Adamant.swift"; sourceTree = ""; }; @@ -1840,6 +1844,7 @@ 9304F8BD292F88F900173F18 /* ANSPayload.swift */, 9304F8C5292F971600173F18 /* ApiServiceResult.swift */, 9304F8C7292F972600173F18 /* ApiServiceError.swift */, + 41A1994329D2D3CF0031AD75 /* MessageModel.swift */, ); path = Models; sourceTree = ""; @@ -1866,6 +1871,7 @@ 9345769428FD0C34004E6C7A /* UIViewController+email.swift */, 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */, 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */, + 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */, ); path = Helpers; sourceTree = ""; @@ -3006,6 +3012,7 @@ E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */, 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */, + 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */, 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */, @@ -3180,6 +3187,7 @@ E91947B22000246A001362F8 /* AdamantError.swift in Sources */, 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */, E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */, + 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */, 6416B1A721B024B6006089AC /* LskWalletService+Send.swift in Sources */, E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */, E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, diff --git a/Adamant/Helpers/SwipePanGestureRecognizer.swift b/Adamant/Helpers/SwipePanGestureRecognizer.swift new file mode 100644 index 000000000..8583d0978 --- /dev/null +++ b/Adamant/Helpers/SwipePanGestureRecognizer.swift @@ -0,0 +1,44 @@ +// +// SwipePanGesture.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +class SwipePanGestureRecognizer: UIPanGestureRecognizer, UIGestureRecognizerDelegate { + + private var initialTouchLocation: CGPoint? + private let minHorizontalOffset: CGFloat = 5 + + var message: MessageModel + + init(target: Any?, action: Selector?, message: MessageModel) { + self.message = message + super.init(target: target, action: action) + delegate = self + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + self.initialTouchLocation = touches.first?.location(in: self.view) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if self.state == .possible, + abs((touches.first?.location(in: self.view).x ?? 0) - (self.initialTouchLocation?.x ?? 0)) >= self.minHorizontalOffset { + self.state = .changed + } + } + +// func gestureRecognizer( +// _ gestureRecognizer: UIGestureRecognizer, +// shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer +// ) -> Bool { +// return true +// } +} diff --git a/Adamant/Models/MessageModel.swift b/Adamant/Models/MessageModel.swift new file mode 100644 index 000000000..f94237c5b --- /dev/null +++ b/Adamant/Models/MessageModel.swift @@ -0,0 +1,16 @@ +// +// MessageModel.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol MessageModel { + var id: String { get } + var isFromCurrentSender: Bool { get } + + func makeReplyContent() -> NSAttributedString +} diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index 6e47d1253..63b0f73c9 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -157,7 +157,7 @@ actor AdamantChatTransactionService: ChatTransactionService { messageTransaction.isOutgoing = isOutgoing messageTransaction.blockId = transaction.blockId messageTransaction.confirmations = transaction.confirmations - messageTransaction.chatMessageId = UUID().uuidString + messageTransaction.chatMessageId = String(transaction.id) messageTransaction.fee = transaction.fee as NSDecimalNumber messageTransaction.statusEnum = MessageStatus.delivered messageTransaction.partner = partner @@ -195,7 +195,7 @@ actor AdamantChatTransactionService: ChatTransactionService { transfer.isOutgoing = isOut transfer.blockId = transaction.blockId transfer.confirmations = transaction.confirmations - transfer.chatMessageId = UUID().uuidString + transfer.chatMessageId = String(transaction.id) transfer.fee = transaction.fee as NSDecimalNumber transfer.statusEnum = MessageStatus.delivered transfer.partner = partner diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 9702c319e..fb9258552 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -511,7 +511,11 @@ private extension ChatViewController { private extension ChatViewController { func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { - guard let movingView = recognizer.view?.superview as? UIView else { return } + guard let movingView = recognizer.view?.superview as? UIView, + let panGesture = recognizer as? SwipePanGestureRecognizer + else { + return + } let translation = recognizer.translation(in: messagesCollectionView) @@ -551,7 +555,7 @@ private extension ChatViewController { } if replyAction { - print("reply!") + print("reply id =\(panGesture.message.id), message = \(panGesture.message.makeReplyContent().string)") } UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 2c2b0e091..6f5532db0 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -68,8 +68,7 @@ final class ChatDataSourceManager: MessagesDataSource { in messagesCollectionView: MessagesCollectionView ) -> UICollectionViewCell { - if case let .custom(model) = message.kind, - let model = model as? ChatReplyContainerView.Model { + if case let .reply(model) = message.fullModel.content { let cell = messagesCollectionView.dequeueReusableCell( ChatViewController.ReplyCell.self, for: indexPath @@ -78,34 +77,36 @@ final class ChatDataSourceManager: MessagesDataSource { cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.model = model - let panGestureRecognizer = UIPanGestureRecognizer( + let panGestureRecognizer = SwipePanGestureRecognizer( target: self, - action: #selector(swipeGestureCellAction(_:)) + action: #selector(swipeGestureCellAction(_:)), + message: model ) - panGestureRecognizer.delegate = viewModel cell.contentView.addGestureRecognizer(panGestureRecognizer) return cell } - let cell = messagesCollectionView.dequeueReusableCell( - ChatViewController.TransactionCell.self, - for: indexPath - ) - if case let .transaction(model) = message.fullModel.content { + let cell = messagesCollectionView.dequeueReusableCell( + ChatViewController.TransactionCell.self, + for: indexPath + ) + cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.model = model + + let panGestureRecognizer = SwipePanGestureRecognizer( + target: self, + action: #selector(swipeGestureCellAction(_:)), + message: model + ) + cell.contentView.addGestureRecognizer(panGestureRecognizer) + + return cell } - let panGestureRecognizer = UIPanGestureRecognizer( - target: self, - action: #selector(swipeGestureCellAction(_:)) - ) - panGestureRecognizer.delegate = viewModel - cell.contentView.addGestureRecognizer(panGestureRecognizer) - - return cell + return UICollectionViewCell() } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift index 7b4dbec5f..c0292174f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift @@ -9,7 +9,7 @@ import Foundation extension ChatReplyContainerView { - struct Model: Equatable { + struct Model: Equatable, MessageModel { let id: String let isFromCurrentSender: Bool let content: ChatReplyContentView.Model @@ -19,5 +19,9 @@ extension ChatReplyContainerView { isFromCurrentSender: true, content: .default ) + + func makeReplyContent() -> NSAttributedString { + return content.message + } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift index da27d2661..1f0e25ca5 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift @@ -11,14 +11,16 @@ import UIKit extension ChatReplyContentView { struct Model: Equatable { let id: String - let message: String - let messageReply: String + let replyId: String + let message: NSAttributedString + let messageReply: NSAttributedString let backgroundColor: ChatMessageBackgroundColor static let `default` = Self( id: "", - message: "", - messageReply: "", + replyId: "", + message: NSAttributedString(string: ""), + messageReply: NSAttributedString(string: ""), backgroundColor: .failed ) } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift index 326a4f7d7..bfe24ee78 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift @@ -62,8 +62,8 @@ final class ChatReplyContentView: UIView { extension ChatReplyContentView.Model { func height(for width: CGFloat) -> CGFloat { let maxSize = CGSize(width: width, height: .infinity) - let titleString = NSAttributedString(string: message, attributes: [.font: messageFont]) - let dateString = NSAttributedString(string: messageReply, attributes: [.font: replyFont]) + let titleString = NSAttributedString(string: message.string, attributes: [.font: messageFont]) + let dateString = NSAttributedString(string: messageReply.string, attributes: [.font: replyFont]) let titleHeight = titleString.boundingRect( with: maxSize, @@ -102,8 +102,8 @@ private extension ChatReplyContentView { func update() { backgroundColor = model.backgroundColor.uiColor - messageLabel.text = model.message - replyLabel.text = model.messageReply + messageLabel.attributedText = model.message + replyLabel.attributedText = model.messageReply } @objc func didTap() { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift index 6be06cdef..09e9ef3b6 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift @@ -9,7 +9,7 @@ import Foundation extension ChatTransactionContainerView { - struct Model: Equatable { + struct Model: Equatable, MessageModel { let id: String let isFromCurrentSender: Bool let content: ChatTransactionContentView.Model @@ -21,5 +21,16 @@ extension ChatTransactionContainerView { content: .default, status: .notInitiated ) + + func makeReplyContent() -> NSAttributedString { + let commentRaw = content.comment ?? "" + let comment = commentRaw.isEmpty + ? commentRaw + : ": \(commentRaw)" + + let content = "\(content.title) \(content.currency) \(content.amount)\(comment)" + + return NSAttributedString(string: content) + } } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 159f16ef9..ef7ae4c29 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -133,12 +133,13 @@ private extension ChatMessageFactory { } return .reply(.init( - id: replyId, + id: transaction.txId, isFromCurrentSender: true, content: .init( - id: replyId, - message: message, - messageReply: replyMessage, + id: transaction.txId, + replyId: replyId, + message: Self.markdownParser.parse(message), + messageReply: Self.markdownParser.parse(replyMessage), backgroundColor: backgroundColor ) )) diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 846d5a62e..1e9454a3d 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -546,12 +546,3 @@ private extension ChatViewModel { didTapAdmChat.send((chatroom, message)) } } - -extension ChatViewModel: UIGestureRecognizerDelegate { - func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - return true - } -} From b7732efe6a653048f026f36dbcbfbda6dc988076 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 28 Mar 2023 12:13:03 +0300 Subject: [PATCH 006/136] [trello.com/c/TGBBXBeX] Base text cell made swipeable --- Adamant/Models/MessageModel.swift | 16 +++++++++++ .../View/Managers/ChatDataSourceManager.swift | 27 +++++++++++++++++++ .../Chat/ViewModel/ChatMessageFactory.swift | 2 +- .../Models/ComparableAttributedString.swift | 11 ++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Adamant/Models/MessageModel.swift b/Adamant/Models/MessageModel.swift index f94237c5b..7ae9524de 100644 --- a/Adamant/Models/MessageModel.swift +++ b/Adamant/Models/MessageModel.swift @@ -14,3 +14,19 @@ protocol MessageModel { func makeReplyContent() -> NSAttributedString } + +struct BaseMessageModel: Equatable, MessageModel { + let id: String + let isFromCurrentSender: Bool + let text: NSAttributedString + + static let `default` = Self( + id: "", + isFromCurrentSender: true, + text: NSAttributedString(string: "") + ) + + func makeReplyContent() -> NSAttributedString { + return text + } +} diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 6f5532db0..420c02a14 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -62,6 +62,33 @@ final class ChatDataSourceManager: MessagesDataSource { message.fullModel.dateHeader?.string } + func textCell( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView + ) -> UICollectionViewCell? { + let cell = messagesCollectionView.dequeueReusableCell( + TextMessageCell.self, + for: indexPath + ) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + + if case let .message(model) = message.fullModel.content { + let panGestureRecognizer = SwipePanGestureRecognizer( + target: self, + action: #selector(swipeGestureCellAction(_:)), + message: BaseMessageModel( + id: model.id, + isFromCurrentSender: true, + text: model.string + ) + ) + cell.contentView.addGestureRecognizer(panGestureRecognizer) + } + + return cell + } + func customCell( for message: MessageType, at indexPath: IndexPath, diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index ef7ae4c29..53390b98f 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -116,7 +116,7 @@ private extension ChatMessageFactory { func makeContent(_ transaction: MessageTransaction) -> ChatMessage.Content { transaction.message.map { - .message(.init(string: Self.markdownParser.parse($0))) + .message(.init(string: Self.markdownParser.parse($0), id: transaction.txId)) } ?? .default } diff --git a/AdamantShared/Models/ComparableAttributedString.swift b/AdamantShared/Models/ComparableAttributedString.swift index d1ff9a03a..37e038fbc 100644 --- a/AdamantShared/Models/ComparableAttributedString.swift +++ b/AdamantShared/Models/ComparableAttributedString.swift @@ -10,6 +10,17 @@ import Foundation struct ComparableAttributedString: Equatable { let string: NSAttributedString + let id: String + + init(string: NSAttributedString, id: String) { + self.string = string + self.id = id + } + + init(string: NSAttributedString) { + self.string = string + self.id = "" + } static func == (lhs: Self, rhs: Self) -> Bool { guard lhs.string.hash == rhs.string.hash else { return false } From 8ce34d517e38d2b294634dc1095deab8432f5619 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 30 Mar 2023 11:35:52 +0300 Subject: [PATCH 007/136] [trello.com/c/TGBBXBeX] Fixed swipe/scroll bugs --- Adamant.xcodeproj/project.pbxproj | 64 ++++----- .../Helpers/SwipePanGestureRecognizer.swift | 12 +- Adamant/Models/MessageModel.swift | 19 +-- Adamant/SharedViews/SwipeableView.swift | 119 +++++++++++++++ .../Chat/View/ChatViewController.swift | 96 ++++--------- .../View/Managers/ChatDataSourceManager.swift | 67 +++------ .../ChatMessageCell+Model.swift | 45 ++++++ .../ChatBaseMessage/ChatMessageCell.swift | 45 ++++++ ...swift => ChatMessageReplyCell+Model.swift} | 12 +- .../ChatReply/ChatMessageReplyCell.swift | 136 ++++++++++++++++++ .../ChatReplyContainerView+Model.swift | 27 ---- .../Container/ChatReplyContainerView.swift | 94 ------------ .../Content/ChatReplyContentView.swift | 117 --------------- .../ChatTransactionContainerView.swift | 16 +++ .../Chat/ViewModel/ChatMessageFactory.swift | 12 +- .../Chat/ViewModel/ChatViewModel.swift | 1 - .../Chat/ViewModel/Models/ChatMessage.swift | 8 +- 17 files changed, 468 insertions(+), 422 deletions(-) create mode 100644 Adamant/SharedViews/SwipeableView.swift create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift rename Adamant/Stories/Chat/View/Subviews/ChatReply/{Content/ChatReplyContentView+Model.swift => ChatMessageReplyCell+Model.swift} (66%) create mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift delete mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift delete mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift delete mode 100644 Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 089cfd270..36563169a 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -19,10 +19,6 @@ 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */; }; 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; - 413AD21D29CDDD970025F255 /* ChatReplyContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */; }; - 413AD21F29CDDDF20025F255 /* ChatReplyContainerView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */; }; - 413AD22229CDDE380025F255 /* ChatReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */; }; - 413AD22429CDDF510025F255 /* ChatReplyContentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */; }; 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4150136329B225CC0037F834 /* Double+adamant.swift */; }; 415441372923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; 415441382923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; @@ -59,6 +55,12 @@ 4198D58128C8B8D1009337F2 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */; }; 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994329D2D3CF0031AD75 /* MessageModel.swift */; }; + 41A1994629D2FCF80031AD75 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994529D2FCF80031AD75 /* ReplyView.swift */; }; + 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994729D325800031AD75 /* SwipeableView.swift */; }; + 41A1995229D42C460031AD75 /* ChatMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995129D42C460031AD75 /* ChatMessageCell.swift */; }; + 41A1995429D56E340031AD75 /* ChatMessageReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */; }; + 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */; }; + 41A1995829D5733D0031AD75 /* ChatMessageCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */; }; 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */; }; 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */; }; 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */; }; @@ -761,10 +763,6 @@ 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVisibleWalletsService.swift; sourceTree = ""; }; 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; - 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReplyContainerView.swift; sourceTree = ""; }; - 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatReplyContainerView+Model.swift"; sourceTree = ""; }; - 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReplyContentView.swift; sourceTree = ""; }; - 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatReplyContentView+Model.swift"; sourceTree = ""; }; 4150136329B225CC0037F834 /* Double+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+adamant.swift"; sourceTree = ""; }; 415441362923AB3700824478 /* BtcProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcProvider.swift; sourceTree = ""; }; 4154413A2923AED000824478 /* bitcoin_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bitcoin_notificationContent.png; sourceTree = ""; }; @@ -789,6 +787,12 @@ 4198D58028C8B8D1009337F2 /* default.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = default.mp3; sourceTree = ""; }; 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipePanGestureRecognizer.swift; sourceTree = ""; }; 41A1994329D2D3CF0031AD75 /* MessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = ""; }; + 41A1994529D2FCF80031AD75 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = ""; }; + 41A1994729D325800031AD75 /* SwipeableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeableView.swift; sourceTree = ""; }; + 41A1995129D42C460031AD75 /* ChatMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageCell.swift; sourceTree = ""; }; + 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageReplyCell.swift; sourceTree = ""; }; + 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageReplyCell+Model.swift"; sourceTree = ""; }; + 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageCell+Model.swift"; sourceTree = ""; }; 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsResetTableViewCell.swift; sourceTree = ""; }; 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3Swift+Adamant.swift"; sourceTree = ""; }; @@ -1383,30 +1387,12 @@ 413AD21A29CDDD750025F255 /* ChatReply */ = { isa = PBXGroup; children = ( - 413AD21B29CDDD820025F255 /* Container */, - 413AD22029CDDE240025F255 /* Content */, + 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */, + 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */, ); path = ChatReply; sourceTree = ""; }; - 413AD21B29CDDD820025F255 /* Container */ = { - isa = PBXGroup; - children = ( - 413AD21C29CDDD970025F255 /* ChatReplyContainerView.swift */, - 413AD21E29CDDDF20025F255 /* ChatReplyContainerView+Model.swift */, - ); - path = Container; - sourceTree = ""; - }; - 413AD22029CDDE240025F255 /* Content */ = { - isa = PBXGroup; - children = ( - 413AD22129CDDE380025F255 /* ChatReplyContentView.swift */, - 413AD22329CDDF510025F255 /* ChatReplyContentView+Model.swift */, - ); - path = Content; - sourceTree = ""; - }; 4186B32A2940CBF9006594A3 /* Scripts */ = { isa = PBXGroup; children = ( @@ -1427,6 +1413,15 @@ path = VisibleWallets; sourceTree = ""; }; + 41A1995029D42C160031AD75 /* ChatBaseMessage */ = { + isa = PBXGroup; + children = ( + 41A1995129D42C460031AD75 /* ChatMessageCell.swift */, + 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */, + ); + path = ChatBaseMessage; + sourceTree = ""; + }; 5551CC8D28A8B72D00B52AD0 /* Stubs */ = { isa = PBXGroup; children = ( @@ -1607,6 +1602,7 @@ 93996A9829682690008D080B /* Subviews */ = { isa = PBXGroup; children = ( + 41A1995029D42C160031AD75 /* ChatBaseMessage */, 413AD21A29CDDD750025F255 /* ChatReply */, 9377FBE0296C2AB700C9211B /* ChatTransaction */, 938F7D652955C966001915CA /* ChatInputBar.swift */, @@ -2453,6 +2449,8 @@ 551F66E528959A5200DE5D69 /* LoadingView.swift */, 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */, 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */, + 41A1994529D2FCF80031AD75 /* ReplyView.swift */, + 41A1994729D325800031AD75 /* SwipeableView.swift */, ); path = SharedViews; sourceTree = ""; @@ -3020,6 +3018,7 @@ E908472F2196FEA80095825D /* BaseTransaction+CoreDataProperties.swift in Sources */, 640EFA902558612100E9724B /* RichMessageNotificationProvider.swift in Sources */, E96D64B92295BED700CA5587 /* StateType.swift in Sources */, + 41A1995229D42C460031AD75 /* ChatMessageCell.swift in Sources */, E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, 64A223D820F7A08E005157CB /* LskApiService.swift in Sources */, E94008702114EA6800CD2D67 /* AdamantBalanceFormat.swift in Sources */, @@ -3044,6 +3043,7 @@ E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */, E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */, E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */, + 41A1995829D5733D0031AD75 /* ChatMessageCell+Model.swift in Sources */, 932F77592989F999006D8801 /* ChatCellManager.swift in Sources */, 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */, E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */, @@ -3107,6 +3107,7 @@ E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, 6489794E24CE00C000C33A68 /* SwiftyOnboard.swift in Sources */, 9399F5ED29A85A48006C3E30 /* ChatCacheService.swift in Sources */, + 41A1995429D56E340031AD75 /* ChatMessageReplyCell.swift in Sources */, 6449BA6B235CA0930033B936 /* ERC20TransactionDetailsViewController.swift in Sources */, E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */, 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */, @@ -3199,7 +3200,6 @@ 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */, E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, - 413AD22229CDDE380025F255 /* ChatReplyContentView.swift in Sources */, 646DF92422C29F6E00DCC864 /* ERC20Token.swift in Sources */, E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */, 550698B628C00AED000E8A2B /* CollectionUtilites.swift in Sources */, @@ -3208,7 +3208,6 @@ 640EFA9E2558613700E9724B /* DashProvider.swift in Sources */, E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */, - 413AD21D29CDDD970025F255 /* ChatReplyContainerView.swift in Sources */, 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */, E96D64B22295BED600CA5587 /* TransactionAsset.swift in Sources */, E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */, @@ -3220,7 +3219,6 @@ 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */, 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */, E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */, - 413AD22429CDDF510025F255 /* ChatReplyContentView+Model.swift in Sources */, 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */, 93F391482962EEDC00BFD6AE /* CGSize+adamant.swift in Sources */, 938F7D612955C92B001915CA /* ChatDataSourceManager.swift in Sources */, @@ -3251,7 +3249,7 @@ E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, 640EFAA52558613D00E9724B /* ERC20Provider.swift in Sources */, - 413AD21F29CDDDF20025F255 /* ChatReplyContainerView+Model.swift in Sources */, + 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */, 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, 934B9BE9296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift in Sources */, @@ -3266,6 +3264,7 @@ E908472E2196FEA80095825D /* BaseTransaction+CoreDataClass.swift in Sources */, 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */, E91E5BF220DAF05500B06B3C /* EurekaNodeRow.swift in Sources */, + 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */, E90847312196FEA80095825D /* ChatTransaction+CoreDataProperties.swift in Sources */, E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */, E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */, @@ -3364,6 +3363,7 @@ E9771DA722997F310099AAC7 /* ServerResponseWithTimestamp.swift in Sources */, E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, 648CE3AA229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, + 41A1994629D2FCF80031AD75 /* ReplyView.swift in Sources */, E90847342196FEA80095825D /* MessageTransaction+CoreDataClass.swift in Sources */, E9960B3521F5154300C840A8 /* DummyAccount+CoreDataClass.swift in Sources */, E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */, diff --git a/Adamant/Helpers/SwipePanGestureRecognizer.swift b/Adamant/Helpers/SwipePanGestureRecognizer.swift index 8583d0978..b32f89049 100644 --- a/Adamant/Helpers/SwipePanGestureRecognizer.swift +++ b/Adamant/Helpers/SwipePanGestureRecognizer.swift @@ -35,10 +35,10 @@ class SwipePanGestureRecognizer: UIPanGestureRecognizer, UIGestureRecognizerDele } } -// func gestureRecognizer( -// _ gestureRecognizer: UIGestureRecognizer, -// shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer -// ) -> Bool { -// return true -// } + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + return true + } } diff --git a/Adamant/Models/MessageModel.swift b/Adamant/Models/MessageModel.swift index 7ae9524de..5db2c42bb 100644 --- a/Adamant/Models/MessageModel.swift +++ b/Adamant/Models/MessageModel.swift @@ -6,27 +6,10 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation +import UIKit protocol MessageModel { var id: String { get } - var isFromCurrentSender: Bool { get } func makeReplyContent() -> NSAttributedString } - -struct BaseMessageModel: Equatable, MessageModel { - let id: String - let isFromCurrentSender: Bool - let text: NSAttributedString - - static let `default` = Self( - id: "", - isFromCurrentSender: true, - text: NSAttributedString(string: "") - ) - - func makeReplyContent() -> NSAttributedString { - return text - } -} diff --git a/Adamant/SharedViews/SwipeableView.swift b/Adamant/SharedViews/SwipeableView.swift new file mode 100644 index 000000000..35ce2edb8 --- /dev/null +++ b/Adamant/SharedViews/SwipeableView.swift @@ -0,0 +1,119 @@ +// +// SwipeableView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import SnapKit + +class SwipeableView: UIView { + + // MARK: Proprieties + + weak var viewForSwipe: UIView? + + private var panGestureRecognizer: SwipePanGestureRecognizer? + private var messagePadding: CGFloat = 0 + private var replyAction: Bool = false + private var canReplyVibrate: Bool = true + private var oldContentOffset: CGPoint? + + var action: ((MessageModel) -> Void)? + + // MARK: Init + + override init(frame: CGRect) { + super.init(frame: frame) + viewForSwipe = self + setup() + } + + init(frame: CGRect, view: UIView, messagePadding: CGFloat = 0) { + super.init(frame: frame) + self.messagePadding = messagePadding + viewForSwipe = view + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + // MARK: Setup + + private func setup() { + panGestureRecognizer = SwipePanGestureRecognizer( + target: self, + action: #selector(swipeGestureCellAction(_:)), + message: ChatMessageCell.Model.default + ) + viewForSwipe?.addGestureRecognizer(panGestureRecognizer!) + } + + func update(_ model: MessageModel) { + panGestureRecognizer?.message = model + } + + // MARK: Actions + + func didSwipe(_ message: MessageModel) { + action?(message) + } +} + +// MARK: UIPanGestureRecognizer + +private extension SwipeableView { + @objc func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { + let translation = recognizer.translation(in: viewForSwipe) + + guard let movingView = recognizer.view?.superview as? UIView, + let panGesture = recognizer as? SwipePanGestureRecognizer + else { + return + } + + let isOnStartPosition = movingView.frame.origin.x == 0 || movingView.frame.origin.x == messagePadding + + if isOnStartPosition && translation.x > 0 { return } + + if movingView.frame.origin.x <= messagePadding { + movingView.center = CGPoint( + x: movingView.center.x + translation.x / 2, + y: movingView.center.y + ) + recognizer.setTranslation(CGPoint(x: 0, y: 0), in: viewForSwipe) + + if abs(movingView.frame.origin.x) > UIScreen.main.bounds.size.width * 0.18 { + replyAction = true + if canReplyVibrate { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + } + canReplyVibrate = false + } else { + replyAction = false + } + } + + if recognizer.state == .ended { + canReplyVibrate = true + + if replyAction { + didSwipe(panGesture.message) + } + + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { + movingView.frame = CGRect( + x: self.messagePadding, + y: movingView.frame.origin.y, + width: movingView.frame.size.width, + height: movingView.frame.size.height + ) + } + } + } +} diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index fb9258552..f8f8e208d 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -16,7 +16,6 @@ import SnapKit final class ChatViewController: MessagesViewController { typealias SpinnerCell = MessageCellWrapper typealias TransactionCell = CollectionCellWrapper - typealias ReplyCell = CollectionCellWrapper typealias SendTransaction = (UIViewController & ComplexTransferViewControllerDelegate) -> Void // MARK: Dependencies @@ -40,6 +39,7 @@ final class ChatViewController: MessagesViewController { private lazy var loadingView = LoadingView() private lazy var scrollDownButton = makeScrollDownButton() private lazy var chatMessagesCollectionView = makeChatMessagesCollectionView() + private lazy var replyView = ReplyView() // swiftlint:disable unused_setter_value override var messageInputBar: InputBarAccessoryView { @@ -84,6 +84,11 @@ final class ChatViewController: MessagesViewController { configureLayout() setupObservers() viewModel.loadFirstMessagesIfNeeded() + + let panGesture = UIPanGestureRecognizer() + panGesture.delegate = self + messagesCollectionView.addGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = false } override func viewWillLayoutSubviews() { @@ -141,6 +146,18 @@ final class ChatViewController: MessagesViewController { } } +extension ChatViewController { + override func gestureRecognizerShouldBegin( + _ gestureRecognizer: UIGestureRecognizer + ) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + let velocity = panGesture.velocity(in: messagesCollectionView) + return abs(velocity.x) > abs(velocity.y) + } +} + // MARK: Delegate Protocols extension ChatViewController: ComplexTransferViewControllerDelegate { @@ -240,10 +257,6 @@ private extension ChatViewController { viewModel.didTapAdmSend .sink { [weak self] in self?.didTapAdmSend(to: $0) } .store(in: &subscriptions) - - viewModel.swipeAction - .sink { [weak self] in self?.swipeGestureCellAction($0) } - .store(in: &subscriptions) } } @@ -275,6 +288,13 @@ private extension ChatViewController { loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } + +// view.addSubview(replyView) +// replyView.snp.makeConstraints { make in +// make.leading.trailing.equalToSuperview() +// make.bottom.equalTo(messageInputBar.snp.top) +// make.height.equalTo(40) +// } } func configureHeader() { @@ -342,7 +362,8 @@ private extension ChatViewController { let collection = ChatMessagesCollectionView() collection.refreshControl = ChatRefreshMock() collection.register(TransactionCell.self) - collection.register(ReplyCell.self) + collection.register(ChatMessageCell.self) + collection.register(ChatMessageReplyCell.self) collection.register( SpinnerCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader @@ -507,69 +528,6 @@ private extension ChatViewController { } } -// MARK: Swipe - -private extension ChatViewController { - func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { - guard let movingView = recognizer.view?.superview as? UIView, - let panGesture = recognizer as? SwipePanGestureRecognizer - else { - return - } - - let translation = recognizer.translation(in: messagesCollectionView) - - if movingView.frame.origin.x == messagePadding && translation.x > 0 { return } - - if recognizer.state == .began { - oldContentOffset = messagesCollectionView.contentOffset - } - - if movingView.frame.origin.x <= messagePadding { - movingView.center = CGPoint( - x: movingView.center.x + translation.x / 2, - y: movingView.center.y - ) - recognizer.setTranslation(CGPoint(x: 0, y: 0), in: view) - - if let oldContentOffset = oldContentOffset { - messagesCollectionView.setContentOffset(oldContentOffset, animated: false) - } - - if abs(movingView.frame.origin.x) > UIScreen.main.bounds.size.width * 0.18 { - replyAction = true - if canReplyVibrate { - UIImpactFeedbackGenerator(style: .heavy).impactOccurred() - } - canReplyVibrate = false - } else { - replyAction = false - } - } - - if recognizer.state == .ended { - canReplyVibrate = true - - if let oldContentOffset = oldContentOffset { - messagesCollectionView.setContentOffset(oldContentOffset, animated: false) - } - - if replyAction { - print("reply id =\(panGesture.message.id), message = \(panGesture.message.makeReplyContent().string)") - } - - UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { - movingView.frame = CGRect( - x: messagePadding, - y: movingView.frame.origin.y, - width: movingView.frame.size.width, - height: movingView.frame.size.height - ) - } - } - } -} - // MARK: Markdown private extension ChatViewController { diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 420c02a14..4005c704e 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -67,53 +67,43 @@ final class ChatDataSourceManager: MessagesDataSource { at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> UICollectionViewCell? { - let cell = messagesCollectionView.dequeueReusableCell( - TextMessageCell.self, - for: indexPath - ) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) if case let .message(model) = message.fullModel.content { - let panGestureRecognizer = SwipePanGestureRecognizer( - target: self, - action: #selector(swipeGestureCellAction(_:)), - message: BaseMessageModel( - id: model.id, - isFromCurrentSender: true, - text: model.string - ) + let cell = messagesCollectionView.dequeueReusableCell( + ChatMessageCell.self, + for: indexPath ) - cell.contentView.addGestureRecognizer(panGestureRecognizer) + let model = ChatMessageCell.Model(id: model.id, text: model.string) + + cell.model = model + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + cell.actionHandler = { [weak self] in self?.handleAction($0) } + + return cell } - return cell - } - - func customCell( - for message: MessageType, - at indexPath: IndexPath, - in messagesCollectionView: MessagesCollectionView - ) -> UICollectionViewCell { - if case let .reply(model) = message.fullModel.content { let cell = messagesCollectionView.dequeueReusableCell( - ChatViewController.ReplyCell.self, + ChatMessageReplyCell.self, for: indexPath ) - cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } - cell.wrappedView.model = model - - let panGestureRecognizer = SwipePanGestureRecognizer( - target: self, - action: #selector(swipeGestureCellAction(_:)), - message: model - ) - cell.contentView.addGestureRecognizer(panGestureRecognizer) + cell.model = model + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + cell.actionHandler = { [weak self] in self?.handleAction($0) } return cell } + return UICollectionViewCell() + } + + func customCell( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView + ) -> UICollectionViewCell { + if case let .transaction(model) = message.fullModel.content { let cell = messagesCollectionView.dequeueReusableCell( ChatViewController.TransactionCell.self, @@ -123,13 +113,6 @@ final class ChatDataSourceManager: MessagesDataSource { cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.model = model - let panGestureRecognizer = SwipePanGestureRecognizer( - target: self, - action: #selector(swipeGestureCellAction(_:)), - message: model - ) - cell.contentView.addGestureRecognizer(panGestureRecognizer) - return cell } @@ -146,8 +129,4 @@ private extension ChatDataSourceManager { viewModel.forceUpdateTransactionStatus(id: id) } } - - @objc func swipeGestureCellAction(_ recognizer: UIPanGestureRecognizer) { - viewModel.swipeAction.send(recognizer) - } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift new file mode 100644 index 000000000..764ef6500 --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift @@ -0,0 +1,45 @@ +// +// ChatMessageCell+Model.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 30.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +extension ChatMessageCell { + struct Model: Equatable, MessageModel { + let id: String + let text: NSAttributedString + + static let `default` = Self( + id: "", + text: NSAttributedString(string: "") + ) + + func makeReplyContent() -> NSAttributedString { + return text + } + + func height(for width: CGFloat) -> CGFloat { + let maxSize = CGSize(width: width, height: .infinity) + let titleString = NSAttributedString(string: text.string, attributes: [.font: messageFont]) + + let titleHeight = titleString.boundingRect( + with: maxSize, + options: .usesLineFragmentOrigin, + context: nil + ).height + + return verticalInsets * 2 + + verticalStackSpacing * 3 + + titleHeight + } + } +} + +private let messageFont = UIFont.systemFont(ofSize: 17) +private let replyFont = UIFont.systemFont(ofSize: 16) +private let verticalStackSpacing: CGFloat = 6 +private let verticalInsets: CGFloat = 8 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift new file mode 100644 index 000000000..fe46f8aba --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -0,0 +1,45 @@ +// +// ChatBaseTextMessageView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 29.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import SnapKit +import MessageKit + +class ChatMessageCell: TextMessageCell { + private lazy var swipeView: SwipeableView = { + let view = SwipeableView(frame: .zero, view: contentView, messagePadding: 8) + return view + }() + + // MARK: - Properties + + var model: Model = .default { + didSet { + guard model != oldValue else { return } + swipeView.update(model) + } + } + + var actionHandler: (ChatAction) -> Void = { _ in } + + // MARK: - Methods + + override func setupSubviews() { + super.setupSubviews() + + contentView.addSubview(swipeView) + swipeView.snp.makeConstraints { make in + make.leading.trailing.bottom.top.equalToSuperview() + } + + swipeView.action = { [weak self] message in + print("message id \(message.id), text = \(message.makeReplyContent().string)") + // actionHandler(.scrollToMessage(id: model.id)) + } + } +} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift similarity index 66% rename from Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift rename to Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 1f0e25ca5..2074361aa 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -1,15 +1,15 @@ // -// ChatReplyContentView+Model.swift +// ChatMessageReplyCell+Model.swift // Adamant // -// Created by Stanislav Jelezoglo on 24.03.2023. +// Created by Stanislav Jelezoglo on 30.03.2023. // Copyright © 2023 Adamant. All rights reserved. // import UIKit -extension ChatReplyContentView { - struct Model: Equatable { +extension ChatMessageReplyCell { + struct Model: Equatable, MessageModel { let id: String let replyId: String let message: NSAttributedString @@ -23,5 +23,9 @@ extension ChatReplyContentView { messageReply: NSAttributedString(string: ""), backgroundColor: .failed ) + + func makeReplyContent() -> NSAttributedString { + return message + } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift new file mode 100644 index 000000000..78eda5423 --- /dev/null +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -0,0 +1,136 @@ +// +// ChatMessageReplyCell.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 30.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import MessageKit +import SnapKit + +class ChatMessageReplyCell: MessageContentCell { + /// The labels used to display the message's text. + private var messageLabel = MessageLabel() + private var replyMessageLabel = MessageLabel() + + private lazy var swipeView: SwipeableView = { + let view = SwipeableView(frame: .zero, view: contentView, messagePadding: 8) + return view + }() + + private lazy var replyView: UIView = { + let view = UIView() + let colorView = UIView() + colorView.backgroundColor = .adamant.active + + view.addSubview(colorView) + view.addSubview(replyMessageLabel) + + colorView.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.width.equalTo(2) + } + replyMessageLabel.snp.makeConstraints { + $0.top.bottom.trailing.equalToSuperview() + $0.leading.equalTo(colorView.snp.trailing).offset(3) + } + return view + }() + + private lazy var verticalStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [replyView, messageLabel]) + stack.axis = .vertical + stack.spacing = 10 + return stack + }() + + // MARK: - Properties + + /// The `MessageCellDelegate` for the cell. + override weak var delegate: MessageCellDelegate? { + didSet { + messageLabel.delegate = delegate + } + } + + var model: Model = .default { + didSet { + guard model != oldValue else { return } + swipeView.update(model) + } + } + + var actionHandler: (ChatAction) -> Void = { _ in } + + // MARK: - Methods + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { + messageLabel.font = attributes.messageLabelFont + } + } + + override func prepareForReuse() { + super.prepareForReuse() + messageLabel.attributedText = nil + messageLabel.text = nil + replyMessageLabel.attributedText = nil + } + + override func setupSubviews() { + super.setupSubviews() + + contentView.addSubview(swipeView) + swipeView.snp.makeConstraints { make in + make.leading.trailing.bottom.top.equalToSuperview() + } + + swipeView.action = { [weak self] message in + print("message id \(message.id), text = \(message.makeReplyContent().string)") + // actionHandler(.scrollToMessage(id: model.id)) + } + + messageContainerView.addSubview(verticalStack) + messageLabel.numberOfLines = 0 + replyMessageLabel.numberOfLines = 1 + verticalStack.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(8) + $0.leading.trailing.equalToSuperview().inset(12) + } + } + + override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView + ) { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + return + } + + let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView) + + messageLabel.configure { + messageLabel.enabledDetectors = enabledDetectors + for detector in enabledDetectors { + let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath) + messageLabel.setAttributes(attributes, detector: detector) + } + + messageLabel.attributedText = model.message + } + + replyMessageLabel.attributedText = model.messageReply + } + + /// Used to handle the cell's contentView's tap gesture. + /// Return false when the contentView does not need to handle the gesture. + override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { + messageLabel.handleGesture(touchPoint) + } +} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift deleted file mode 100644 index c0292174f..000000000 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView+Model.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ChatReplyContainerView+Model.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 24.03.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import Foundation - -extension ChatReplyContainerView { - struct Model: Equatable, MessageModel { - let id: String - let isFromCurrentSender: Bool - let content: ChatReplyContentView.Model - - static let `default` = Self( - id: "", - isFromCurrentSender: true, - content: .default - ) - - func makeReplyContent() -> NSAttributedString { - return content.message - } - } -} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift deleted file mode 100644 index cebb96c8f..000000000 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Container/ChatReplyContainerView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// ChatReplyContainerView.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 24.03.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import UIKit -import SnapKit -import Combine - -final class ChatReplyContainerView: UIView { - var model: Model = .default { - didSet { - guard model != oldValue else { return } - update() - } - } - - var actionHandler: (ChatAction) -> Void = { _ in } { - didSet { contentView.actionHandler = actionHandler } - } - - private let contentView = ChatReplyContentView() - - private let spacingView: UIView = { - let view = UIView() - view.setContentCompressionResistancePriority(.dragThatCanResizeScene, for: .horizontal) - return view - }() - - private let horizontalStack: UIStackView = { - let stack = UIStackView() - stack.alignment = .center - stack.axis = .horizontal - stack.spacing = 12 - return stack - }() - - override init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } -} - -private extension ChatReplyContainerView { - func configure() { - addSubview(horizontalStack) - horizontalStack.snp.makeConstraints { - $0.top.bottom.equalToSuperview() - $0.leading.trailing.equalToSuperview().inset(12) - } - } - - func update() { - contentView.model = model.content - updateLayout() - } - - func updateLayout() { - var viewsList = [spacingView, contentView] - - viewsList = model.isFromCurrentSender - ? viewsList - : viewsList.reversed() - - guard horizontalStack.arrangedSubviews != viewsList else { return } - horizontalStack.arrangedSubviews.forEach(horizontalStack.removeArrangedSubview) - viewsList.forEach(horizontalStack.addArrangedSubview) - } - - @objc func buttonTap() { - // actionHandler(.scrollToMessage(id: model.id)) - } -} - -extension ChatReplyContainerView.Model { - func height(for width: CGFloat) -> CGFloat { - content.height(for: width) - } -} - -extension ChatReplyContainerView: ReusableView { - func prepareForReuse() { - model = .default - actionHandler = { _ in } - } -} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift deleted file mode 100644 index bfe24ee78..000000000 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/Content/ChatReplyContentView.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ChatReplyContentView.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 24.03.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import UIKit -import SnapKit - -final class ChatReplyContentView: UIView { - var model: Model = .default { - didSet { - guard oldValue != model else { return } - update() - } - } - - var actionHandler: (ChatAction) -> Void = { _ in } - - private let messageLabel = UILabel(font: messageFont, textColor: .adamant.textColor, numberOfLines: 0) - private let replyLabel = UILabel(font: replyFont, textColor: .adamant.textColor, numberOfLines: 0) - - private lazy var replyView: UIView = { - let view = UIView() - let colorView = UIView() - colorView.backgroundColor = .adamant.active - - view.addSubview(colorView) - view.addSubview(replyLabel) - - colorView.snp.makeConstraints { - $0.top.leading.bottom.equalToSuperview() - $0.width.equalTo(2) - } - replyLabel.snp.makeConstraints { - $0.top.bottom.trailing.equalToSuperview() - $0.leading.equalTo(colorView.snp.trailing).offset(3) - } - return view - }() - - private lazy var verticalStack: UIStackView = { - let stack = UIStackView(arrangedSubviews: [replyView, messageLabel]) - stack.axis = .vertical - stack.spacing = verticalStackSpacing - return stack - }() - - override init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } -} - -extension ChatReplyContentView.Model { - func height(for width: CGFloat) -> CGFloat { - let maxSize = CGSize(width: width, height: .infinity) - let titleString = NSAttributedString(string: message.string, attributes: [.font: messageFont]) - let dateString = NSAttributedString(string: messageReply.string, attributes: [.font: replyFont]) - - let titleHeight = titleString.boundingRect( - with: maxSize, - options: .usesLineFragmentOrigin, - context: nil - ).height - - let dateHeight = dateString.boundingRect( - with: maxSize, - options: .usesLineFragmentOrigin, - context: nil - ).height - - return verticalInsets * 2 - + verticalStackSpacing * 3 - + titleHeight - + dateHeight - } -} - -private extension ChatReplyContentView { - func configure() { - layer.cornerRadius = 16 - - addGestureRecognizer(UITapGestureRecognizer( - target: self, - action: #selector(didTap) - )) - - addSubview(verticalStack) - verticalStack.snp.makeConstraints { - $0.top.bottom.equalToSuperview().inset(verticalInsets) - $0.leading.trailing.equalToSuperview().inset(12) - } - } - - func update() { - backgroundColor = model.backgroundColor.uiColor - messageLabel.attributedText = model.message - replyLabel.attributedText = model.messageReply - } - - @objc func didTap() { - // actionHandler(.scrollToMessage(id: model.id)) - } -} - -private let messageFont = UIFont.systemFont(ofSize: 17) -private let replyFont = UIFont.systemFont(ofSize: 16) -private let verticalStackSpacing: CGFloat = 6 -private let verticalInsets: CGFloat = 8 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 954ffa44e..dfae046ec 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -45,6 +45,11 @@ final class ChatTransactionContainerView: UIView { return stack }() + private lazy var swipeView: SwipeableView = { + let view = SwipeableView(frame: .zero, view: self) + return view + }() + override init(frame: CGRect) { super.init(frame: frame) configure() @@ -70,9 +75,20 @@ private extension ChatTransactionContainerView { $0.top.bottom.equalToSuperview() $0.leading.trailing.equalToSuperview().inset(12) } + + addSubview(swipeView) + swipeView.snp.makeConstraints { make in + make.top.bottom.leading.trailing.equalToSuperview() + } + + swipeView.action = { [weak self] message in + print("message id \(message.id), text = \(message.makeReplyContent().string)") + // actionHandler(.scrollToMessage(id: model.id)) + } } func update() { + swipeView.update(model) contentView.model = model.content updateStatus(model.status) updateLayout() diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 53390b98f..19b74cd72 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -134,14 +134,10 @@ private extension ChatMessageFactory { return .reply(.init( id: transaction.txId, - isFromCurrentSender: true, - content: .init( - id: transaction.txId, - replyId: replyId, - message: Self.markdownParser.parse(message), - messageReply: Self.markdownParser.parse(replyMessage), - backgroundColor: backgroundColor - ) + replyId: replyId, + message: Self.markdownParser.parse(message), + messageReply: Self.markdownParser.parse(replyMessage), + backgroundColor: backgroundColor )) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 1e9454a3d..97911a7e9 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -47,7 +47,6 @@ final class ChatViewModel: NSObject { let didTapAdmChat = ObservableSender<(Chatroom, String?)>() let didTapAdmSend = ObservableSender() let closeScreen = ObservableSender() - let swipeAction = ObservableSender() @ObservableValue private(set) var fullscreenLoading = false @ObservableValue private(set) var messages = [ChatMessage]() diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift index cb6cce9fc..eeedab3ce 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift @@ -43,7 +43,7 @@ extension ChatMessage { enum Content: Equatable { case message(ComparableAttributedString) case transaction(ChatTransactionContainerView.Model) - case reply(ChatReplyContainerView.Model) + case reply(ChatMessageReplyCell.Model) static let `default` = Self.message(.init(string: .init())) } @@ -60,7 +60,11 @@ extension ChatMessage: MessageType { case let .transaction(model): return .custom(model) case let .reply(model): - return .custom(model) + let result = NSMutableAttributedString() + result.append(model.message) + result.append(NSAttributedString(string: "\n\n")) + result.append(model.messageReply) + return .attributedText(result) } } } From 70486d28f64bc79a8c1e099af990687a70ed7ce9 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 30 Mar 2023 13:19:10 +0300 Subject: [PATCH 008/136] [trello.com/c/TGBBXBeX] Implemented reply view --- .../Swipe actions/Contents.json | 6 +- .../reply.imageset/Contents.json | 23 ++++ .../Swipe actions/reply.imageset/reply-2.png | Bin 0 -> 615 bytes .../Swipe actions/reply.imageset/reply-3.png | Bin 0 -> 1221 bytes .../Swipe actions/reply.imageset/reply.png | Bin 0 -> 520 bytes Adamant/SharedViews/ReplyView.swift | 105 ++++++++++++++++++ .../Chat/View/ChatViewController.swift | 13 ++- .../ChatTransactionCellSizeCalculator.swift | 12 +- 8 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-2.png create mode 100644 Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-3.png create mode 100644 Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply.png create mode 100644 Adamant/SharedViews/ReplyView.swift diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json b/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json index da4a164c9..73c00596a 100644 --- a/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json +++ b/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/Contents.json new file mode 100644 index 000000000..8e2b2eec1 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "reply.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "reply-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "reply-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-2.png b/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-2.png new file mode 100644 index 0000000000000000000000000000000000000000..dacfc95f180d9a3b118fbee1223048683f87813d GIT binary patch literal 615 zcmV-t0+{`YP)trQP4&Vf*+MwXuwVp0vb>Qns{gN&T*R~d%4}*tq6JGWx0K4 z=6QB!-q|@;@SkD}Ug7D#Am5HDG-6eX-T0ItuT;Q(%mnG9ss#*VHVE%weU$=E;CGPS z!a&@wM8FyR38JfQdwmyhF$n57zsP>y1Pn;8x`8>I%&}_<_<_%OhY4K65e$_fUyJbw zJc~meY@b_r9NX8tbXpE$RX5}tw2s$|bnLnjJcu3GhoiWH$M}^IGl^ZD0uACt7=OXu zZWJ$pAzYBs)4+Ee$TNN!Mjx@W2jp37!h?wa6ML!_K&pW|!SNQW+YS1Wf_BvKJ~&P< zW>BUGi((|=zsWNwattS>CiNB5(yM)l>o|_BIq|2%Z*yMYqKznT)umoBVqwnj#{RJm z4+q9FfzEXhP{THDjnB;IRf%f!zp~w>Wrf}7{4dF z5g>xhDgQZMhszoJ8*zQztpHLnHV2Y%shEm>4M(+7+K@)OLg>Xvc01$ut*8K&Ka_5i z8eOw3m%?(#@@6XL%L<_N$K*)~-!*tv(su>D$6qrJxN6C9ZJPi9002ovPDHLkV1f~s B2oV4P literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-3.png b/Adamant/Assets/Assets.xcassets/Swipe actions/reply.imageset/reply-3.png new file mode 100644 index 0000000000000000000000000000000000000000..eb29fdae30cacfe3facd8a3cf58098fee40e350e GIT binary patch literal 1221 zcmV;$1UmbPP)>m&i8Bp7GRUCvwITPdXBTPMb^Uc+Fbe@_Kq zHU5=#>UqqmGp1((u-5dvzS^E?Fs>&8kjm>P@KBDivpN9v^7^4 zr!;2D>z6bkFO@76Z3l$Aak=`tO-mTUmk9&D!*G)_uhW_!RNGeEqRBwav<3|016&YY z|4O(G_cfz1;$$3OvEz)IHY5~I7va*%=SB{W;6Z%Y0=q1cE?qYXQ=)867apLl7pls7 zcf&-b&)j2CwoF*uQh9x;i_Z)!#Y@8Cm?Z)9yB!6|u#>`Vw)*J?}K$f?*U>%;4L1I-yo%i1bVE6O1YZbQ->o>SfWCJkS!_a}vlF~!AE{~ZNv%`itujAA1W z3-1W>5RaWF6fNiDN@0yygU9fOu;``|w7BiZqq6zRfNV~1W&xj*tVL+ziRs&mqg<~iux?isdq-=15ee~sD5ZB05OY%Z@b=tdJs3&^pB_RT}|rE zuv-24p^R5!gTku5Hz9Xim`d<;^tUIDQSUU0)(@==K$Qj9kdQrrJA@$BUf<9ny;Bsa zAKDRsDr;o#$V0Msz{VEqMg9k=?F;~1kAET^W6j*fbgDRrY-Z^Ht9w8 z9Fj`qE7kUW8}*_)j>!$;(}ccB{M<&pQ*^^Y$tz^%|0I5Cv)(DX;i&8j37vnIpm&OT z^+Qp<&UwD) z`+c7GeV^y>KVbyVx+Ok_0~Fm5pAO;OP8-zlFhpPAK_?APV?G2Q;}m8(WpEZtAvSyL z{fP{EvgcmBMjexQTJp^@w&VJaclcbz>@q%LAEPDlJ6H+MI(|wO75IdEIMYge0AHk& za<@u0QuQ}6fe&G{hHE*o4_ny5?TX@M7zvF{TGtx%B$5&}7nx|+3Q#c>o23A{@cuqR&g*1n9 zgC=UYE)Bv4j_?h`(pcHgD*FvRSO~#KLCg(u)TRFTfhWPe^as9Lh!?aGnRZUe$oqsb#~j4E=b8i|rWvoA?FGY-nVv504`N0000< KMNUMnLSTaW*xH={ literal 0 HcmV?d00001 diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift new file mode 100644 index 000000000..22181c31e --- /dev/null +++ b/Adamant/SharedViews/ReplyView.swift @@ -0,0 +1,105 @@ +// +// ReplyView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.03.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import SnapKit + +final class ReplyView: UIView { + + private let messageLabel = UILabel(font: messageFont, textColor: .adamant.textColor, numberOfLines: 1) + + private lazy var replyView: UIView = { + let view = UIView() + let colorView = UIView() + colorView.backgroundColor = .adamant.active + + view.addSubview(colorView) + view.addSubview(messageLabel) + + colorView.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.width.equalTo(2) + } + messageLabel.snp.makeConstraints { + $0.top.bottom.trailing.equalToSuperview() + $0.leading.equalTo(colorView.snp.trailing).offset(5) + } + return view + }() + + private var replyIV: UIImageView = { + let iv = UIImageView( + image: UIImage(named: "reply")?.withTintColor(.adamant.active) + ) + + iv.snp.makeConstraints { make in + make.height.equalTo(30) + make.width.equalTo(24) + } + + return iv + }() + + private lazy var closeBtn: UIButton = { + let btn = UIButton() + btn.setImage( + UIImage(systemName: "xmark")?.withTintColor(.adamant.alert), + for: .normal + ) + btn.addTarget(self, action: #selector(didTapCloseBtn), for: .touchUpInside) + + btn.snp.makeConstraints { make in + make.height.width.equalTo(30) + } + return btn + }() + + private lazy var horizontalStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [replyIV, replyView, closeBtn]) + stack.axis = .horizontal + stack.spacing = horizontalStackSpacing + return stack + }() + + // MARK: Init + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + func configure() { + addSubview(horizontalStack) + horizontalStack.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(verticalInsets) + $0.leading.trailing.equalToSuperview().inset(12) + } + } + + // MARK: Actions + + @objc private func didTapCloseBtn() { + removeFromSuperview() + } +} + +extension ReplyView { + func update(with model: MessageModel) { + backgroundColor = .clear + messageLabel.attributedText = model.makeReplyContent() + } +} + +private let messageFont = UIFont.systemFont(ofSize: 14) +private let horizontalStackSpacing: CGFloat = 20 +private let verticalInsets: CGFloat = 8 diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index f8f8e208d..a239df94a 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -289,12 +289,13 @@ private extension ChatViewController { $0.directionalEdges.equalToSuperview() } -// view.addSubview(replyView) -// replyView.snp.makeConstraints { make in -// make.leading.trailing.equalToSuperview() -// make.bottom.equalTo(messageInputBar.snp.top) -// make.height.equalTo(40) -// } + messageInputBar.topStackView.addArrangedSubview(replyView) + + replyView.update(with: ChatMessageCell.Model.init(id: "1", text: NSAttributedString.init(string: "heelllooo"))) + + replyView.snp.makeConstraints { make in + make.height.equalTo(40) + } } func configureHeader() { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index 2182681af..12557fd59 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -34,10 +34,18 @@ final class ChatCellSizeCalculator: CellSizeCalculator { ) } - if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { +// if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { +// return .init( +// width: messagesFlowLayout.itemWidth, +// height: model.height(for: messagesFlowLayout.itemWidth) +// ) +// } + + if case let .message(model) = getMessages()[indexPath.section].fullModel.content { + let newModel = ChatMessageCell.Model(id: "", text: model.string) return .init( width: messagesFlowLayout.itemWidth, - height: model.height(for: messagesFlowLayout.itemWidth) + height: newModel.height(for: messagesFlowLayout.itemWidth) ) } From ae31ede149b6131d2a19c32c2e640f2a111cb744 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 31 Mar 2023 11:44:05 +0300 Subject: [PATCH 009/136] [trello.com/c/TGBBXBeX] Made reply view dynamic --- Adamant/SharedViews/ReplyView.swift | 8 +++- .../Chat/View/ChatViewController.swift | 23 ++++++++-- .../Chat/View/Managers/ChatAction.swift | 1 + .../View/Managers/ChatDataSourceManager.swift | 2 + .../ChatBaseMessage/ChatMessageCell.swift | 2 +- .../ChatReply/ChatMessageReplyCell.swift | 2 +- .../ChatTransactionContainerView+Model.swift | 2 +- .../ChatTransactionContainerView.swift | 2 +- .../Chat/ViewModel/ChatMessageFactory.swift | 43 ++++++++++--------- .../Chat/ViewModel/ChatViewModel.swift | 1 + 10 files changed, 56 insertions(+), 30 deletions(-) diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift index 22181c31e..b9c835f91 100644 --- a/Adamant/SharedViews/ReplyView.swift +++ b/Adamant/SharedViews/ReplyView.swift @@ -89,7 +89,13 @@ final class ReplyView: UIView { // MARK: Actions @objc private func didTapCloseBtn() { - removeFromSuperview() + UIView.transition( + with: self, + duration: 0.25, + options: [.transitionCrossDissolve], + animations: { + self.removeFromSuperview() + }) } } diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index a239df94a..fca4cb722 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -257,6 +257,10 @@ private extension ChatViewController { viewModel.didTapAdmSend .sink { [weak self] in self?.didTapAdmSend(to: $0) } .store(in: &subscriptions) + + viewModel.didSwipeMessage + .sink { [weak self] in self?.didSwipeMessage($0) } + .store(in: &subscriptions) } } @@ -288,10 +292,6 @@ private extension ChatViewController { loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - - messageInputBar.topStackView.addArrangedSubview(replyView) - - replyView.update(with: ChatMessageCell.Model.init(id: "1", text: NSAttributedString.init(string: "heelllooo"))) replyView.snp.makeConstraints { make in make.height.equalTo(40) @@ -458,6 +458,21 @@ private extension ChatViewController { viewModel.inputText = inputBar.text } + func didSwipeMessage(_ message: MessageModel) { + if !messageInputBar.topStackView.subviews.contains(replyView) { + UIView.transition( + with: messageInputBar.topStackView, + duration: 0.25, + options: [.transitionCrossDissolve], + animations: { + self.messageInputBar.topStackView.addArrangedSubview(self.replyView) + }) + messageInputBar.inputTextView.becomeFirstResponder() + } + + replyView.update(with: message) + } + func didTapTransfer(id: String) { guard let transaction = viewModel.chatTransactions.first( diff --git a/Adamant/Stories/Chat/View/Managers/ChatAction.swift b/Adamant/Stories/Chat/View/Managers/ChatAction.swift index 9f2212ca6..c82a42417 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatAction.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatAction.swift @@ -9,4 +9,5 @@ enum ChatAction { case forceUpdateTransactionStatus(id: String) case openTransactionDetails(id: String) + case reply(message: MessageModel) } diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 4005c704e..101fe312c 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -127,6 +127,8 @@ private extension ChatDataSourceManager { viewModel.didTapTransfer.send(id) case let .forceUpdateTransactionStatus(id): viewModel.forceUpdateTransactionStatus(id: id) + case let .reply(message): + viewModel.didSwipeMessage.send(message) } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index fe46f8aba..2ed137935 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -39,7 +39,7 @@ class ChatMessageCell: TextMessageCell { swipeView.action = { [weak self] message in print("message id \(message.id), text = \(message.makeReplyContent().string)") - // actionHandler(.scrollToMessage(id: model.id)) + self?.actionHandler(.reply(message: message)) } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 78eda5423..851f201d8 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -90,7 +90,7 @@ class ChatMessageReplyCell: MessageContentCell { swipeView.action = { [weak self] message in print("message id \(message.id), text = \(message.makeReplyContent().string)") - // actionHandler(.scrollToMessage(id: model.id)) + self?.actionHandler(.reply(message: message)) } messageContainerView.addSubview(verticalStack) diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift index 09e9ef3b6..80d7c65c0 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift @@ -30,7 +30,7 @@ extension ChatTransactionContainerView { let content = "\(content.title) \(content.currency) \(content.amount)\(comment)" - return NSAttributedString(string: content) + return ChatMessageFactory.markdownParser.parse(content) } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index dfae046ec..881b1eba3 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -83,7 +83,7 @@ private extension ChatTransactionContainerView { swipeView.action = { [weak self] message in print("message id \(message.id), text = \(message.makeReplyContent().string)") - // actionHandler(.scrollToMessage(id: model.id)) + self?.actionHandler(.reply(message: message)) } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 19b74cd72..433d2a64e 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -13,6 +13,28 @@ import MessageKit struct ChatMessageFactory { private let richMessageProviders: [String: RichMessageProvider] + static let markdownParser = MarkdownParser( + font: .adamantChatDefault, + color: .adamant.primary, + enabledElements: [ + .header, + .list, + .quote, + .bold, + .italic, + .code, + .strikethrough + ], + customElements: [ + MarkdownSimpleAdm(), + MarkdownLinkAdm(), + MarkdownAdvancedAdm( + font: .adamantChatDefault, + color: .adamant.active + ) + ] + ) + init(richMessageProviders: [String: RichMessageProvider]) { self.richMessageProviders = richMessageProviders } @@ -63,27 +85,6 @@ struct ChatMessageFactory { } private extension ChatMessageFactory { - static let markdownParser = MarkdownParser( - font: .adamantChatDefault, - color: .adamant.primary, - enabledElements: [ - .header, - .list, - .quote, - .bold, - .italic, - .code, - .strikethrough - ], - customElements: [ - MarkdownSimpleAdm(), - MarkdownLinkAdm(), - MarkdownAdvancedAdm( - font: .adamantChatDefault, - color: .adamant.active - ) - ] - ) func makeContent( _ transaction: ChatTransaction, diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 97911a7e9..42b8a3330 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -47,6 +47,7 @@ final class ChatViewModel: NSObject { let didTapAdmChat = ObservableSender<(Chatroom, String?)>() let didTapAdmSend = ObservableSender() let closeScreen = ObservableSender() + let didSwipeMessage = ObservableSender() @ObservableValue private(set) var fullscreenLoading = false @ObservableValue private(set) var messages = [ChatMessage]() From 75522c60659d163e2bd6082fcd29ca7879f5a582 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 31 Mar 2023 14:11:07 +0300 Subject: [PATCH 010/136] [trello.com/c/TGBBXBeX] Fixed reply cell dynamic height --- Adamant/SharedViews/ReplyView.swift | 12 ++--- .../Chat/View/ChatViewController.swift | 49 ++++++++++++++----- .../View/Managers/ChatDataSourceManager.swift | 2 +- .../View/Managers/ChatLayoutManager.swift | 12 +++++ .../ChatMessageCell+Model.swift | 20 -------- .../ChatMessageReplyCell+Model.swift | 29 +++++++++++ .../ChatReply/ChatMessageReplyCell.swift | 19 +++++++ .../ChatTransactionCellSizeCalculator.swift | 36 +++++++++----- .../Chat/ViewModel/ChatViewModel.swift | 23 +++++++-- .../Chat/ViewModel/Models/ChatMessage.swift | 9 ++-- AdamantShared/Models/RichMessage.swift | 18 +++++-- 11 files changed, 164 insertions(+), 65 deletions(-) diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift index b9c835f91..b53ee893d 100644 --- a/Adamant/SharedViews/ReplyView.swift +++ b/Adamant/SharedViews/ReplyView.swift @@ -66,6 +66,10 @@ final class ReplyView: UIView { return stack }() + // MARK: Proprieties + + var closeAction: (() -> Void)? + // MARK: Init override init(frame: CGRect) { @@ -89,13 +93,7 @@ final class ReplyView: UIView { // MARK: Actions @objc private func didTapCloseBtn() { - UIView.transition( - with: self, - duration: 0.25, - options: [.transitionCrossDissolve], - animations: { - self.removeFromSuperview() - }) + closeAction?() } } diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index fca4cb722..6adb8b810 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -82,13 +82,10 @@ final class ChatViewController: MessagesViewController { configureMessageActions() configureHeader() configureLayout() + configureReplyView() + configureGesture() setupObservers() viewModel.loadFirstMessagesIfNeeded() - - let panGesture = UIPanGestureRecognizer() - panGesture.delegate = self - messagesCollectionView.addGestureRecognizer(panGesture) - messagesCollectionView.clipsToBounds = false } override func viewWillLayoutSubviews() { @@ -258,8 +255,8 @@ private extension ChatViewController { .sink { [weak self] in self?.didTapAdmSend(to: $0) } .store(in: &subscriptions) - viewModel.didSwipeMessage - .sink { [weak self] in self?.didSwipeMessage($0) } + viewModel.$replyMessage + .sink { [weak self] in self?.processSwipeMessage($0) } .store(in: &subscriptions) } } @@ -292,10 +289,6 @@ private extension ChatViewController { loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - - replyView.snp.makeConstraints { make in - make.height.equalTo(40) - } } func configureHeader() { @@ -307,6 +300,23 @@ private extension ChatViewController { action: #selector(showMenu) ) } + + func configureReplyView() { + replyView.snp.makeConstraints { make in + make.height.equalTo(40) + } + + replyView.closeAction = { [weak self] in + self?.viewModel.replyMessage = nil + } + } + + func configureGesture() { + let panGesture = UIPanGestureRecognizer() + panGesture.delegate = self + messagesCollectionView.addGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = false + } } // MARK: Content updating @@ -458,7 +468,12 @@ private extension ChatViewController { viewModel.inputText = inputBar.text } - func didSwipeMessage(_ message: MessageModel) { + func processSwipeMessage(_ message: MessageModel?) { + guard let message = message else { + closeReplyView() + return + } + if !messageInputBar.topStackView.subviews.contains(replyView) { UIView.transition( with: messageInputBar.topStackView, @@ -473,6 +488,16 @@ private extension ChatViewController { replyView.update(with: message) } + func closeReplyView() { + UIView.transition( + with: messageInputBar.topStackView, + duration: 0.25, + options: [.transitionCrossDissolve], + animations: { + self.replyView.removeFromSuperview() + }) + } + func didTapTransfer(id: String) { guard let transaction = viewModel.chatTransactions.first( diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 101fe312c..438bce204 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -128,7 +128,7 @@ private extension ChatDataSourceManager { case let .forceUpdateTransactionStatus(id): viewModel.forceUpdateTransactionStatus(id: id) case let .reply(message): - viewModel.didSwipeMessage.send(message) + viewModel.replyMessage = message } } } diff --git a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift index 3c665a529..1e668ef56 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift @@ -96,6 +96,18 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ? SpinnerView.size : .zero } + + func attributedTextCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView + ) -> CellSizeCalculator? { + ChatTextCellSizeCalculator( + layout: messagesCollectionView.messagesCollectionViewFlowLayout, + getCurrentSender: { [sender = viewModel.sender] in sender }, + getMessages: { [messages = viewModel.messages] in messages } + ) + } } private extension ChatLayoutManager { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift index 764ef6500..9a5358dae 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift @@ -21,25 +21,5 @@ extension ChatMessageCell { func makeReplyContent() -> NSAttributedString { return text } - - func height(for width: CGFloat) -> CGFloat { - let maxSize = CGSize(width: width, height: .infinity) - let titleString = NSAttributedString(string: text.string, attributes: [.font: messageFont]) - - let titleHeight = titleString.boundingRect( - with: maxSize, - options: .usesLineFragmentOrigin, - context: nil - ).height - - return verticalInsets * 2 - + verticalStackSpacing * 3 - + titleHeight - } } } - -private let messageFont = UIFont.systemFont(ofSize: 17) -private let replyFont = UIFont.systemFont(ofSize: 16) -private let verticalStackSpacing: CGFloat = 6 -private let verticalInsets: CGFloat = 8 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 2074361aa..3f077f5bb 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -29,3 +29,32 @@ extension ChatMessageReplyCell { } } } + +extension ChatMessageReplyCell.Model { + func containerHeight(for width: CGFloat) -> CGFloat { + let height = contentHeight(for: width) + + return height + + otherLabelsHeight + } + + func contentHeight(for width: CGFloat) -> CGFloat { + let maxSize = CGSize(width: width, height: .infinity) + + let messageHeight = message.boundingRect( + with: maxSize, + options: [.usesLineFragmentOrigin, .usesFontLeading], + context: nil + ).height + + return verticalInsets * 2 + + verticalStackSpacing + + messageHeight + + messageReplyHeight + } +} + +private let verticalStackSpacing: CGFloat = 12 +private let verticalInsets: CGFloat = 8 +private let messageReplyHeight: CGFloat = 20 +private let otherLabelsHeight: CGFloat = 40 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 851f201d8..626463464 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -126,6 +126,25 @@ class ChatMessageReplyCell: MessageContentCell { } replyMessageLabel.attributedText = model.messageReply + + updateFrames() + } + + func updateFrames() { + let size = messageContainerView.frame.size + messageContainerView.frame = CGRect( + origin: messageContainerView.frame.origin, + size: CGSize( + width: size.width, + height: model.contentHeight(for: size.width) + ) + ) + + let origin = CGPoint( + x: 0, + y: messageContainerView.frame.maxY + ) + messageBottomLabel.frame = CGRect(origin: origin, size: messageBottomLabel.frame.size) } /// Used to handle the cell's contentView's tap gesture. diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index 12557fd59..792c8d52a 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -34,21 +34,35 @@ final class ChatCellSizeCalculator: CellSizeCalculator { ) } -// if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { -// return .init( -// width: messagesFlowLayout.itemWidth, -// height: model.height(for: messagesFlowLayout.itemWidth) -// ) -// } - - if case let .message(model) = getMessages()[indexPath.section].fullModel.content { - let newModel = ChatMessageCell.Model(id: "", text: model.string) + return .zero + } +} + +final class ChatTextCellSizeCalculator: TextMessageSizeCalculator { + private let getCurrentSender: () -> SenderType + private let getMessages: () -> [ChatMessage] + private let messagesFlowLayout: MessagesCollectionViewFlowLayout + + init( + layout: MessagesCollectionViewFlowLayout, + getCurrentSender: @escaping () -> SenderType, + getMessages: @escaping () -> [ChatMessage] + ) { + self.getMessages = getMessages + self.getCurrentSender = getCurrentSender + self.messagesFlowLayout = layout + super.init() + self.layout = layout + } + + override func sizeForItem(at indexPath: IndexPath) -> CGSize { + if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { return .init( width: messagesFlowLayout.itemWidth, - height: newModel.height(for: messagesFlowLayout.itemWidth) + height: model.containerHeight(for: messagesFlowLayout.itemWidth) ) } - return .zero + return super.sizeForItem(at: indexPath) } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 42b8a3330..35b78a246 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -47,7 +47,6 @@ final class ChatViewModel: NSObject { let didTapAdmChat = ObservableSender<(Chatroom, String?)>() let didTapAdmSend = ObservableSender() let closeScreen = ObservableSender() - let didSwipeMessage = ObservableSender() @ObservableValue private(set) var fullscreenLoading = false @ObservableValue private(set) var messages = [ChatMessage]() @@ -56,6 +55,7 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var fee = "" @ObservableValue private(set) var partnerName: String? @ObservableValue var inputText = "" + @ObservableValue var replyMessage: MessageModel? var startPosition: ChatStartPosition? { if let messageIdToShow = messageIdToShow { @@ -173,10 +173,23 @@ final class ChatViewModel: NSObject { } Task { -// let message: AdamantMessage = markdownParser.parse(text).length == text.count -// ? .text(text) -// : .markdownText(text) - let message: AdamantMessage = .richMessage(payload: RichMessageReply(type: "")) + let message: AdamantMessage + + if let replyMessage = replyMessage { + message = .richMessage( + payload: RichMessageReply( + replyto_id: replyMessage.id, + reply_message: replyMessage.makeReplyContent().string, + message: text + ) + ) + } else { + message = markdownParser.parse(text).length == text.count + ? .text(text) + : .markdownText(text) + } + + self.replyMessage = nil guard await validateSendingMessage(message: message) else { return } diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift index eeedab3ce..201bb0a8b 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift @@ -60,11 +60,10 @@ extension ChatMessage: MessageType { case let .transaction(model): return .custom(model) case let .reply(model): - let result = NSMutableAttributedString() - result.append(model.message) - result.append(NSAttributedString(string: "\n\n")) - result.append(model.messageReply) - return .attributedText(result) + let message = model.message.string.count > model.messageReply.string.count + ? model.message + : model.messageReply + return .attributedText(message) } } } diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index 452235c6a..4e66f0e00 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -36,13 +36,23 @@ struct RichContentKeys { // MARK: - RichMessageReply struct RichMessageReply: RichMessage { - var type: String = "reply" + var type: String + var replyto_id: String + var message: String + var reply_message: String + + init(replyto_id: String, reply_message: String, message: String) { + self.type = "reply" + self.replyto_id = replyto_id + self.message = message + self.reply_message = reply_message + } func content() -> [String : String] { return [ - "replyto_id": "9839400464901626037", - "reply_message": "123", - "message": "test reply test reply test reply test reply test reply\ntest replytest reply" + "replyto_id": replyto_id, + "reply_message": reply_message, + "message": message ] } } From f81453832bf2e2150a7cae12d6be174ffd526873 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 31 Mar 2023 15:52:03 +0300 Subject: [PATCH 011/136] [trello.com/c/TGBBXBeX] Fixed send rich message & reply container height --- .../Services/DataProviders/AdamantChatsProvider.swift | 4 ++++ Adamant/Stories/Chat/View/ChatViewController.swift | 1 + .../ChatReply/ChatMessageReplyCell+Model.swift | 8 -------- .../ChatTransactionCellSizeCalculator.swift | 11 ++++++++++- Adamant/Stories/Chat/ViewModel/ChatViewModel.swift | 4 ++-- AdamantShared/Models/RichMessage.swift | 9 +++++++++ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index b1c0808c3..76398bed0 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -655,6 +655,7 @@ extension AdamantChatsProvider { transactionLocaly = try await sendRichMessageLocaly( richContent: payload.content(), richType: payload.type, + isReply: payload.isReply, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, @@ -739,6 +740,7 @@ extension AdamantChatsProvider { transactionLocaly = try await sendRichMessageLocaly( richContent: payload.content(), richType: payload.type, + isReply: payload.isReply, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, @@ -805,6 +807,7 @@ extension AdamantChatsProvider { private func sendRichMessageLocaly( richContent: [String:String], richType: String, + isReply: Bool, senderId: String, recipientId: String, keypair: Keypair, @@ -823,6 +826,7 @@ extension AdamantChatsProvider { transaction.richContent = richContent transaction.richType = richType + transaction.isReply = isReply transaction.transactionStatus = richProviders[richType] != nil ? .notInitiated : nil diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 6adb8b810..2b198da98 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -495,6 +495,7 @@ private extension ChatViewController { options: [.transitionCrossDissolve], animations: { self.replyView.removeFromSuperview() + self.messageInputBar.topStackViewPadding = .zero }) } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 3f077f5bb..7bfcb8c8f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -31,13 +31,6 @@ extension ChatMessageReplyCell { } extension ChatMessageReplyCell.Model { - func containerHeight(for width: CGFloat) -> CGFloat { - let height = contentHeight(for: width) - - return height - + otherLabelsHeight - } - func contentHeight(for width: CGFloat) -> CGFloat { let maxSize = CGSize(width: width, height: .infinity) @@ -57,4 +50,3 @@ extension ChatMessageReplyCell.Model { private let verticalStackSpacing: CGFloat = 12 private let verticalInsets: CGFloat = 8 private let messageReplyHeight: CGFloat = 20 -private let otherLabelsHeight: CGFloat = 40 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index 792c8d52a..a29deceb0 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -57,9 +57,18 @@ final class ChatTextCellSizeCalculator: TextMessageSizeCalculator { override func sizeForItem(at indexPath: IndexPath) -> CGSize { if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { + let dataSource = messagesLayout.messagesDataSource + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + let contentViewHeight = model.contentHeight(for: messagesFlowLayout.itemWidth) + let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height + let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height + return .init( width: messagesFlowLayout.itemWidth, - height: model.containerHeight(for: messagesFlowLayout.itemWidth) + height: contentViewHeight + + messageBottomLabelHeight + + messageTopLabelHeight ) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 35b78a246..569f10554 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -189,10 +189,10 @@ final class ChatViewModel: NSObject { : .markdownText(text) } - self.replyMessage = nil - guard await validateSendingMessage(message: message) else { return } + replyMessage = nil + do { _ = try await chatsProvider.sendMessage( message, diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index 4e66f0e00..c387d9db1 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -12,6 +12,7 @@ import Foundation protocol RichMessage: Codable { var type: String { get } + var isReply: Bool { get } func content() -> [String:String] func serialized() -> String @@ -37,6 +38,7 @@ struct RichContentKeys { struct RichMessageReply: RichMessage { var type: String + var isReply: Bool var replyto_id: String var message: String var reply_message: String @@ -46,6 +48,7 @@ struct RichMessageReply: RichMessage { self.replyto_id = replyto_id self.message = message self.reply_message = reply_message + self.isReply = true } func content() -> [String : String] { @@ -64,6 +67,7 @@ struct RichMessageTransfer: RichMessage { let amount: Decimal let hash: String let comments: String + var isReply: Bool func content() -> [String:String] { return [ @@ -79,6 +83,7 @@ struct RichMessageTransfer: RichMessage { self.amount = amount self.hash = hash self.comments = comments + self.isReply = false } init?(content: [String:String]) { @@ -116,6 +121,8 @@ struct RichMessageTransfer: RichMessage { } else { self.comments = "" } + + self.isReply = false } } @@ -159,6 +166,8 @@ extension RichMessageTransfer { } else { self.amount = 0 } + + self.isReply = false } } From ca7f561dd017763ffcd3417bbacb39aea1463cd7 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 13 Apr 2023 17:30:56 +0300 Subject: [PATCH 012/136] [trello.com/c/TGBBXBeX] Made load reply message by id --- Adamant.xcodeproj/project.pbxproj | 16 ++ .../xcshareddata/swiftpm/Package.resolved | 4 +- Adamant/AppDelegate.swift | 5 + Adamant/ServiceProtocols/ApiService.swift | 2 + .../RichTransactionReplyService.swift | 13 ++ .../ApiService/AdamantApi+Transactions.swift | 11 +- .../AdamantRichTransactionReplyService.swift | 178 ++++++++++++++++++ .../Chat/ViewModel/ChatMessageFactory.swift | 11 +- Adamant/SwinjectDependencies.swift | 10 + AdamantShared/Models/RichMessage.swift | 25 +-- 10 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 Adamant/ServiceProtocols/RichTransactionReplyService.swift create mode 100644 Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 36563169a..e6c204764 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */; }; 41A1995829D5733D0031AD75 /* ChatMessageCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */; }; 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */; }; + 41C1698C29E7F34900FEB3CB /* RichTransactionReplyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */; }; + 41C1698E29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */; }; 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */; }; 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */; }; 41CEE09629718A10005EF1D2 /* UIImage+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CEE09529718A10005EF1D2 /* UIImage+adamant.swift */; }; @@ -794,6 +796,8 @@ 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageReplyCell+Model.swift"; sourceTree = ""; }; 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageCell+Model.swift"; sourceTree = ""; }; 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsResetTableViewCell.swift; sourceTree = ""; }; + 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionReplyService.swift; sourceTree = ""; }; + 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantRichTransactionReplyService.swift; sourceTree = ""; }; 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3Swift+Adamant.swift"; sourceTree = ""; }; 41CEE09529718A10005EF1D2 /* UIImage+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+adamant.swift"; sourceTree = ""; }; @@ -1422,6 +1426,14 @@ path = ChatBaseMessage; sourceTree = ""; }; + 41C1698A29E7F2EE00FEB3CB /* RichTransactionReplyService */ = { + isa = PBXGroup; + children = ( + 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */, + ); + path = RichTransactionReplyService; + sourceTree = ""; + }; 5551CC8D28A8B72D00B52AD0 /* Stubs */ = { isa = PBXGroup; children = ( @@ -1787,6 +1799,7 @@ 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */, 9324C75D297170600022D7EA /* RichTransactionStatusService.swift */, 41047B73294C61D10039E956 /* VisibleWalletsService.swift */, + 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -1794,6 +1807,7 @@ E913C9061FFFA92E001A83F7 /* Services */ = { isa = PBXGroup; children = ( + 41C1698A29E7F2EE00FEB3CB /* RichTransactionReplyService */, 935F53D429BE8F4800779492 /* RichTransactionStatusService */, 3AA2D5F8280EAF49000ED971 /* SocketService */, E9CAE8D02018AA5000345E76 /* ApiService */, @@ -3032,6 +3046,7 @@ E91947B020002393001362F8 /* AdamantApiService.swift in Sources */, E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */, E9A03FD820DC0ABA007653A1 /* AdamantNodesSource.swift in Sources */, + 41C1698E29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift in Sources */, 649D6BF221C27D5C009E727B /* SearchResultsViewController.swift in Sources */, E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */, E9E7CDB72003994E00DFC4DB /* AdamantUtilities+extended.swift in Sources */, @@ -3280,6 +3295,7 @@ E94008802114EE2000CD2D67 /* AdmWallet.swift in Sources */, 41E142F2289E60EB002EE8D7 /* AdamantSecret.swift in Sources */, 93A91FD1297972B7001DB1F8 /* ChatScrollDownButton.swift in Sources */, + 41C1698C29E7F34900FEB3CB /* RichTransactionReplyService.swift in Sources */, E9A03FDA20DC0B14007653A1 /* NodesSource.swift in Sources */, 935F53D629BE8F7400779492 /* RichTransactionStatusPublisher.swift in Sources */, 9390C5032976B42800270CDF /* ChatDialogManager.swift in Sources */, diff --git a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d07ea46c..f9af189b5 100644 --- a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -105,7 +105,7 @@ "repositoryURL": "https://github.com/rechsteiner/Parchment", "state": { "branch": "main", - "revision": "51eba77aa6761f27cc30631e2823f0304e4eedb4", + "revision": "ee81b508ab5926b7253779d029b0d9af2dd4ee2b", "version": null } }, @@ -174,7 +174,7 @@ }, { "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream.git", + "repositoryURL": "https://github.com/daltoniam/Starscream", "state": { "branch": null, "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 60c688e28..8951fe773 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -208,6 +208,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Task { await service.startObserving() } } + // Setup transactions reply observing + if let service = container.resolve(RichTransactionReplyService.self) { + Task { await service.startObserving() } + } + // Register repeater services if let chatsProvider = container.resolve(ChatsProvider.self) { repeater.registerForegroundCall(label: "chatsProvider", interval: 10, queue: .global(qos: .utility), callback: { diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index 29ff23841..a83ce2a2c 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -97,6 +97,8 @@ protocol ApiService: AnyObject { func getTransaction(id: UInt64) async throws -> Transaction + func getTransaction(id: UInt64, withAsset: Bool) async throws -> Transaction + func getTransactions( forAccount: String, type: TransactionType, diff --git a/Adamant/ServiceProtocols/RichTransactionReplyService.swift b/Adamant/ServiceProtocols/RichTransactionReplyService.swift new file mode 100644 index 000000000..4b1e6b106 --- /dev/null +++ b/Adamant/ServiceProtocols/RichTransactionReplyService.swift @@ -0,0 +1,13 @@ +// +// RichTransactionReplyService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 10.04.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol RichTransactionReplyService: Actor, AnyObject { + func startObserving() +} diff --git a/Adamant/Services/ApiService/AdamantApi+Transactions.swift b/Adamant/Services/ApiService/AdamantApi+Transactions.swift index 02cbfab95..2d99bcab1 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transactions.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transactions.swift @@ -65,10 +65,19 @@ extension AdamantApiService { } func getTransaction(id: UInt64) async throws -> Transaction { + try await getTransaction(id: id, withAsset: false) + } + + func getTransaction(id: UInt64, withAsset: Bool) async throws -> Transaction { + var queryItems = [ + URLQueryItem(name: "id", value: String(id)), + URLQueryItem(name: "returnAsset", value: withAsset ? "1" : "0") + ] + return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in sendRequest( path: ApiCommands.Transactions.getTransaction, - queryItems: [URLQueryItem(name: "id", value: String(id))] + queryItems: queryItems ) { (serverResponse: ApiServiceResult>) in switch serverResponse { case .success(let response): diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift new file mode 100644 index 000000000..f24c06522 --- /dev/null +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -0,0 +1,178 @@ +// +// AdamantRichTransactionReplyService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 10.04.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CoreData +import Combine + +actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService { + private let coreDataStack: CoreDataStack + private let apiService: ApiService + private let adamantCore: AdamantCore + private let accountService: AccountService + + private lazy var controller = getRichTransactionsController() + private let unknownErrorMessage = "UNKNOWN" + + init( + coreDataStack: CoreDataStack, + apiService: ApiService, + adamantCore: AdamantCore, + accountService: AccountService + ) { + self.coreDataStack = coreDataStack + self.apiService = apiService + self.adamantCore = adamantCore + self.accountService = accountService + super.init() + } + + func startObserving() { + controller.delegate = self + try? controller.performFetch() + controller.fetchedObjects?.forEach( update(transaction:) ) + } +} + +extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate { + nonisolated func controller( + _: NSFetchedResultsController, + didChange object: Any, + at _: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath _: IndexPath? + ) { + guard let transaction = object as? RichMessageTransaction, + transaction.isReply + else { + return + } + + Task { await processCoreDataChange(type: type, transaction: transaction) } + } +} + +private extension AdamantRichTransactionReplyService { + func update(transaction: RichMessageTransaction) { + Task { + guard let id = transaction.richContent?[RichContentKeys.reply.replyToId], + transaction.richContent?[RichContentKeys.reply.decodedMessage] == nil + else { return } + + do { + let message = try await getReplyMessage(by: UInt64(id) ?? 0) + print("reply message decoded = \(message)") + + setReplyMessage(for: transaction, message: message) + } catch { + print("error= \(error)") + } + } + } + + func getReplyMessage(by id: UInt64) async throws -> String { + guard let address = accountService.account?.address, + let privateKey = accountService.keypair?.privateKey + else { + throw ApiServiceError.accountNotFound + } + + let transaction = try await apiService.getTransaction(id: id, withAsset: true) + + if let chat = transaction.asset.chat { + let isOut = transaction.senderId == address + + let publicKey: String? = isOut + ? transaction.recipientPublicKey + : transaction.senderPublicKey + + guard let publicKey = publicKey else { return unknownErrorMessage } + + let decodedMessage = adamantCore.decodeMessage( + rawMessage: chat.message, + rawNonce: chat.ownMessage, + senderPublicKey: publicKey, + privateKey: privateKey + )?.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let decodedMessage = decodedMessage else { return unknownErrorMessage } + + var message: String + + switch chat.type { + case .message, .messageOld, .signal, .unknown: + message = decodedMessage + case .richMessage: + if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + let type = richContent[RichContentKeys.type], + let transfer = RichMessageTransfer(content: richContent) { + + message = "\(transfer.type) \(transfer.amount) \(transfer.comments)" + } else if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + let replyMessage = richContent[RichContentKeys.reply.replyMessage] { + + message = replyMessage + } else { + message = decodedMessage + } + } + + return message + } + + let message = "ADM \(transaction.amount)" + + return message + } + + func setReplyMessage( + for transaction: RichMessageTransaction, + message: String + ) { + let privateContext = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + + privateContext.parent = coreDataStack.container.viewContext + + let transaction = privateContext.object(with: transaction.objectID) + as? RichMessageTransaction + transaction?.richContent?[RichContentKeys.reply.decodedMessage] = message + try? privateContext.save() + } + + // MARK: Core Data + + func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { + switch type { + case .insert, .update: + update(transaction: transaction) + case .delete: + break + case .move: + break + @unknown default: + break + } + } + + func getRichTransactionsController() -> NSFetchedResultsController { + let request: NSFetchRequest = NSFetchRequest( + entityName: RichMessageTransaction.entityName + ) + + request.sortDescriptors = [] + return NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: coreDataStack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + } +} diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 433d2a64e..9373170a8 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -126,18 +126,19 @@ private extension ChatMessageFactory { backgroundColor: ChatMessageBackgroundColor ) -> ChatMessage.Content { guard let content = transaction.richContent, - let message = content["message"], - let replyId = content["replyto_id"], - let replyMessage = content["reply_message"] + let replyId = content[RichContentKeys.reply.replyToId], + let replyMessage = content[RichContentKeys.reply.replyMessage] else { return .default } + let decodedMessage = content[RichContentKeys.reply.decodedMessage] ?? "..." + return .reply(.init( id: transaction.txId, replyId: replyId, - message: Self.markdownParser.parse(message), - messageReply: Self.markdownParser.parse(replyMessage), + message: Self.markdownParser.parse(replyMessage), + messageReply: Self.markdownParser.parse(decodedMessage), backgroundColor: backgroundColor )) } diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index d6ee13d81..a540d5410 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -216,5 +216,15 @@ extension Container { richProviders: Dictionary(uniqueKeysWithValues: richProviders) ) }.inObjectScope(.container) + + // MARK: Rich transaction reply service + self.register(RichTransactionReplyService.self) { r in + AdamantRichTransactionReplyService( + coreDataStack: r.resolve(CoreDataStack.self)!, + apiService: r.resolve(ApiService.self)!, + adamantCore: r.resolve(AdamantCore.self)!, + accountService: r.resolve(AccountService.self)! + ) + }.inObjectScope(.container) } } diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index c387d9db1..891c503cc 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -40,22 +40,19 @@ struct RichMessageReply: RichMessage { var type: String var isReply: Bool var replyto_id: String - var message: String var reply_message: String - init(replyto_id: String, reply_message: String, message: String) { + init(replyto_id: String, reply_message: String) { self.type = "reply" self.replyto_id = replyto_id - self.message = message self.reply_message = reply_message self.isReply = true } func content() -> [String : String] { return [ - "replyto_id": replyto_id, - "reply_message": reply_message, - "message": message + RichContentKeys.reply.replyToId: replyto_id, + RichContentKeys.reply.replyMessage: reply_message ] } } @@ -127,12 +124,16 @@ struct RichMessageTransfer: RichMessage { } extension RichContentKeys { - struct transfer { - static let amount = "amount" - static let hash = "hash" - static let comments = "comments" - - private init() {} + enum transfer { + case amount = "amount" + case hash = "hash" + case comments = "comments" + } + + enum reply { + case replyToId = "replyto_id" + case replyMessage = "reply_message" + case decodedMessage = "decodedMessage" } } From 057aaea79deeddf7aed1bef71be6a2663da64c2c Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 13 Apr 2023 18:14:03 +0300 Subject: [PATCH 013/136] [trello.com/c/TGBBXBeX] Fixed transaction reply display --- .../AdamantRichTransactionReplyService.swift | 35 ++++++++++++++++--- .../Chat/ViewModel/ChatViewModel.swift | 3 +- AdamantShared/Models/RichMessage.swift | 12 +++---- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index f24c06522..1f0a7ecd4 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -14,6 +14,7 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService private let apiService: ApiService private let adamantCore: AdamantCore private let accountService: AccountService + private var richMessageProvider: [String: RichMessageProvider] = [:] private lazy var controller = getRichTransactionsController() private let unknownErrorMessage = "UNKNOWN" @@ -29,6 +30,8 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService self.adamantCore = adamantCore self.accountService = accountService super.init() + + self.richMessageProvider = self.makeRichMessageProviders() } func startObserving() { @@ -36,6 +39,15 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService try? controller.performFetch() controller.fetchedObjects?.forEach( update(transaction:) ) } + + func makeRichMessageProviders() -> [String: RichMessageProvider] { + .init( + uniqueKeysWithValues: accountService + .wallets + .compactMap { $0 as? RichMessageProvider } + .map { ($0.dynamicRichMessageType, $0) } + ) + } } extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate { @@ -65,7 +77,7 @@ private extension AdamantRichTransactionReplyService { do { let message = try await getReplyMessage(by: UInt64(id) ?? 0) - print("reply message decoded = \(message)") + print("reply message decoded =\(message); id=\(id)") setReplyMessage(for: transaction, message: message) } catch { @@ -90,6 +102,10 @@ private extension AdamantRichTransactionReplyService { ? transaction.recipientPublicKey : transaction.senderPublicKey + let transactionStatus = isOut + ? String.adamantLocalized.chat.transactionSent + : String.adamantLocalized.chat.transactionReceived + guard let publicKey = publicKey else { return unknownErrorMessage } let decodedMessage = adamantCore.decodeMessage( @@ -105,14 +121,25 @@ private extension AdamantRichTransactionReplyService { switch chat.type { case .message, .messageOld, .signal, .unknown: - message = decodedMessage + let comment = !decodedMessage.isEmpty + ? ": \(decodedMessage)" + : "" + + message = transaction.amount > 0 + ? "\(transactionStatus) \(transaction.amount) \(AdmWalletService.currencySymbol)\(comment)" + : decodedMessage + case .richMessage: if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), let type = richContent[RichContentKeys.type], let transfer = RichMessageTransfer(content: richContent) { + let comment = !transfer.comments.isEmpty + ? ": \(transfer.comments)" + : "" + let humanType = richMessageProvider[transfer.type]?.tokenSymbol ?? transfer.type - message = "\(transfer.type) \(transfer.amount) \(transfer.comments)" + message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" } else if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), let replyMessage = richContent[RichContentKeys.reply.replyMessage] { @@ -126,7 +153,7 @@ private extension AdamantRichTransactionReplyService { return message } - let message = "ADM \(transaction.amount)" + let message = "\(AdmWalletService.currencySymbol) \(transaction.amount)" return message } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 569f10554..25e8eb24c 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -179,8 +179,7 @@ final class ChatViewModel: NSObject { message = .richMessage( payload: RichMessageReply( replyto_id: replyMessage.id, - reply_message: replyMessage.makeReplyContent().string, - message: text + reply_message: text ) ) } else { diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index 891c503cc..e8b0a7a1a 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -125,15 +125,15 @@ struct RichMessageTransfer: RichMessage { extension RichContentKeys { enum transfer { - case amount = "amount" - case hash = "hash" - case comments = "comments" + static let amount = "amount" + static let hash = "hash" + static let comments = "comments" } enum reply { - case replyToId = "replyto_id" - case replyMessage = "reply_message" - case decodedMessage = "decodedMessage" + static let replyToId = "replyto_id" + static let replyMessage = "reply_message" + static let decodedMessage = "decodedMessage" } } From bf0a5d96f452a893819dd4f82b9ea3114a320b1d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 13 Apr 2023 18:31:28 +0300 Subject: [PATCH 014/136] [trello.com/c/TGBBXBeX] Recalculate reply cell height --- .../AdamantRichTransactionReplyService.swift | 103 +++++++++--------- .../ChatTransactionCellSizeCalculator.swift | 7 ++ 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 1f0a7ecd4..b8bf7ee9e 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -95,66 +95,65 @@ private extension AdamantRichTransactionReplyService { let transaction = try await apiService.getTransaction(id: id, withAsset: true) - if let chat = transaction.asset.chat { - let isOut = transaction.senderId == address - - let publicKey: String? = isOut - ? transaction.recipientPublicKey - : transaction.senderPublicKey - - let transactionStatus = isOut - ? String.adamantLocalized.chat.transactionSent - : String.adamantLocalized.chat.transactionReceived - - guard let publicKey = publicKey else { return unknownErrorMessage } - - let decodedMessage = adamantCore.decodeMessage( - rawMessage: chat.message, - rawNonce: chat.ownMessage, - senderPublicKey: publicKey, - privateKey: privateKey - )?.trimmingCharacters(in: .whitespacesAndNewlines) - - guard let decodedMessage = decodedMessage else { return unknownErrorMessage } + guard let chat = transaction.asset.chat else { + let message = "\(AdmWalletService.currencySymbol) \(transaction.amount)" + return message + } + + let isOut = transaction.senderId == address + + let publicKey: String? = isOut + ? transaction.recipientPublicKey + : transaction.senderPublicKey + + let transactionStatus = isOut + ? String.adamantLocalized.chat.transactionSent + : String.adamantLocalized.chat.transactionReceived + + guard let publicKey = publicKey else { return unknownErrorMessage } + + let decodedMessage = adamantCore.decodeMessage( + rawMessage: chat.message, + rawNonce: chat.ownMessage, + senderPublicKey: publicKey, + privateKey: privateKey + )?.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let decodedMessage = decodedMessage else { return unknownErrorMessage } + + var message: String + + switch chat.type { + case .message, .messageOld, .signal, .unknown: + let comment = !decodedMessage.isEmpty + ? ": \(decodedMessage)" + : "" - var message: String + message = transaction.amount > 0 + ? "\(transactionStatus) \(transaction.amount) \(AdmWalletService.currencySymbol)\(comment)" + : decodedMessage - switch chat.type { - case .message, .messageOld, .signal, .unknown: - let comment = !decodedMessage.isEmpty - ? ": \(decodedMessage)" + case .richMessage: + if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + let type = richContent[RichContentKeys.type], + let transfer = RichMessageTransfer(content: richContent) { + let comment = !transfer.comments.isEmpty + ? ": \(transfer.comments)" : "" + let humanType = richMessageProvider[transfer.type]?.tokenSymbol ?? transfer.type - message = transaction.amount > 0 - ? "\(transactionStatus) \(transaction.amount) \(AdmWalletService.currencySymbol)\(comment)" - : decodedMessage + message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" + } else if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + let replyMessage = richContent[RichContentKeys.reply.replyMessage] { - case .richMessage: - if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let type = richContent[RichContentKeys.type], - let transfer = RichMessageTransfer(content: richContent) { - let comment = !transfer.comments.isEmpty - ? ": \(transfer.comments)" - : "" - let humanType = richMessageProvider[transfer.type]?.tokenSymbol ?? transfer.type - - message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" - } else if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let replyMessage = richContent[RichContentKeys.reply.replyMessage] { - - message = replyMessage - } else { - message = decodedMessage - } + message = replyMessage + } else { + message = decodedMessage } - - return message } - let message = "\(AdmWalletService.currencySymbol) \(transaction.amount)" - return message } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index a29deceb0..cde75f7be 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -63,12 +63,19 @@ final class ChatTextCellSizeCalculator: TextMessageSizeCalculator { let contentViewHeight = model.contentHeight(for: messagesFlowLayout.itemWidth) let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height + let messageVerticalPadding = messageContainerPadding(for: message) + let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height + let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height return .init( width: messagesFlowLayout.itemWidth, height: contentViewHeight + messageBottomLabelHeight + messageTopLabelHeight + + messageVerticalPadding.top + + messageVerticalPadding.bottom + + cellBottomLabelHeight + + cellTopLabelHeight ) } From 6d352cb28624e9d7baba5b801dfce63c83183996 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 14 Apr 2023 09:17:58 +0300 Subject: [PATCH 015/136] [trello.com/c/TGBBXBeX] Fixed reply view height --- Adamant/Stories/Chat/View/ChatViewController.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 2b198da98..41dbb8e6f 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -489,14 +489,9 @@ private extension ChatViewController { } func closeReplyView() { - UIView.transition( - with: messageInputBar.topStackView, - duration: 0.25, - options: [.transitionCrossDissolve], - animations: { - self.replyView.removeFromSuperview() - self.messageInputBar.topStackViewPadding = .zero - }) + replyView.removeFromSuperview() + messageInputBar.invalidateIntrinsicContentSize() + messageInputBar.layoutContainerViewIfNeeded() } func didTapTransfer(id: String) { From 5be15cb0a0ccdf2ea9dddbb18280e89df141285c Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 20 Apr 2023 10:20:07 +0300 Subject: [PATCH 016/136] [trello.com/c/TGBBXBeX] Fixed tap on transfer cell --- .../Container/ChatTransactionContainerView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 881b1eba3..00cdf0bdf 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -70,17 +70,17 @@ extension ChatTransactionContainerView: ReusableView { private extension ChatTransactionContainerView { func configure() { + addSubview(swipeView) + swipeView.snp.makeConstraints { make in + make.top.bottom.leading.trailing.equalToSuperview() + } + addSubview(horizontalStack) horizontalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview() $0.leading.trailing.equalToSuperview().inset(12) } - addSubview(swipeView) - swipeView.snp.makeConstraints { make in - make.top.bottom.leading.trailing.equalToSuperview() - } - swipeView.action = { [weak self] message in print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) From 425738e4568a7d69ca2db08af37c188326a5b94d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 20 Apr 2023 13:12:45 +0300 Subject: [PATCH 017/136] [trello.com/c/TGBBXBeX] Scroll to message on tap --- Adamant/ServiceProtocols/ApiService.swift | 3 +- .../DataProviders/ChatsProvider.swift | 5 ++ .../ApiService/AdamantApi+Chats.swift | 16 +++- .../DataProviders/AdamantChatsProvider.swift | 84 ++++++++++++++++++- .../AdamantRichTransactionReplyService.swift | 23 ++--- .../Chat/View/ChatViewController.swift | 7 ++ .../Chat/View/Managers/ChatAction.swift | 1 + .../View/Managers/ChatDataSourceManager.swift | 2 + .../ChatReply/ChatMessageReplyCell.swift | 6 ++ .../Chat/ViewModel/ChatViewModel.swift | 26 ++++++ 10 files changed, 154 insertions(+), 19 deletions(-) diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index bf822b5dc..249cd48d1 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -132,7 +132,8 @@ protocol ApiService: AnyObject { func getChatMessages( address: String, addressRecipient: String, - offset: Int? + offset: Int?, + limit: Int? ) async throws -> ChatRooms // MARK: - Funds diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index 13b84db7e..75d0ac28c 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -197,6 +197,11 @@ protocol ChatsProvider: DataProvider, Actor { func isChatLoading(with addressRecipient: String) -> Bool func isChatLoaded(with addressRecipient: String) -> Bool + func loadTransactionsUntilFind( + _ transactionId: String, + recipient: String + ) async throws + /// Unread messages controller. Sections by chatroom. func getUnreadMessagesController() -> NSFetchedResultsController diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 9252c3d28..95d89b0e4 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -237,11 +237,21 @@ extension AdamantApiService { } } - func getChatMessages(address: String, addressRecipient: String, offset: Int?) async throws -> ChatRooms { + func getChatMessages( + address: String, + addressRecipient: String, + offset: Int?, + limit: Int? + ) async throws -> ChatRooms { // MARK: 1. Prepare params var queryItems: [URLQueryItem] = [] - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } - queryItems.append(URLQueryItem(name: "limit", value: "50")) + if let offset = offset { + queryItems.append(URLQueryItem(name: "offset", value: String(offset))) + } + + if let limit = limit { + queryItems.append(URLQueryItem(name: "limit", value: String(limit))) + } // MARK: 2. Send return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index f21c586cb..0dcdd6694 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -32,6 +32,7 @@ actor AdamantChatsProvider: ChatsProvider { private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? private let apiTransactions = 100 + private let chatTransactionsLimit = 50 private var unconfirmedTransactions: [UInt64:NSManagedObjectID] = [:] private var unconfirmedTransactionsBySignature: [String] = [] @@ -421,7 +422,8 @@ extension AdamantChatsProvider { let chatroom = try? await apiGetChatMessages( address: address, addressRecipient: addressRecipient, - offset: offset + offset: offset, + limit: chatTransactionsLimit ) isChatLoaded[addressRecipient] = true @@ -452,13 +454,15 @@ extension AdamantChatsProvider { func apiGetChatMessages( address: String, addressRecipient: String, - offset: Int? + offset: Int?, + limit: Int? ) async throws -> ChatRooms? { do { let chatrooms = try await apiService.getChatMessages( address: address, addressRecipient: addressRecipient, - offset: offset + offset: offset, + limit: limit ) return chatrooms } catch let error as ApiServiceError { @@ -471,7 +475,8 @@ extension AdamantChatsProvider { return try await apiGetChatMessages( address: address, addressRecipient: addressRecipient, - offset: offset + offset: offset, + limit: limit ) } } @@ -1271,6 +1276,77 @@ extension AdamantChatsProvider { } } + func loadTransactionsUntilFind( + _ transactionId: String, + recipient: String + ) async throws { + guard let address = accountService.account?.address, + let privateKey = accountService.keypair?.privateKey + else { + throw ApiServiceError.accountNotFound + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = self.stack.container.viewContext + + guard getTransfer(id: transactionId, context: context) == nil else { return } + + var transactions: [Transaction] = [] + var offset = chatLoadedMessages[recipient] ?? 0 + var needToRepeat = false + + repeat { + let messages = try await apiGetChatMessages( + address: address, + addressRecipient: recipient, + offset: offset, + limit: chatTransactionsLimit + )?.messages + + guard let messages = messages else { + needToRepeat = false + break + } + + offset += messages.count + transactions.append(contentsOf: messages) + + let findTransactionId = transactions.contains(where: { $0.id == UInt64(transactionId) }) + needToRepeat = messages.count >= chatTransactionsLimit && !findTransactionId + } while needToRepeat + + if transactions.count == 0 { + return + } + + chatLoadedMessages[recipient] = offset + + await process( + messageTransactions: transactions, + senderId: address, + privateKey: privateKey + ) + + // MARK: Get more transactions + + let messages = try await apiGetChatMessages( + address: address, + addressRecipient: recipient, + offset: offset, + limit: chatTransactionsLimit + )?.messages + + guard let messages = messages, messages.count > 0 else { return } + + chatLoadedMessages[recipient] = offset + messages.count + + await process( + messageTransactions: messages, + senderId: address, + privateKey: privateKey + ) + } + /// - New unread messagess ids private func process( messageTransactions: [Transaction], diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index b8bf7ee9e..72794b965 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -75,26 +75,27 @@ private extension AdamantRichTransactionReplyService { transaction.richContent?[RichContentKeys.reply.decodedMessage] == nil else { return } - do { - let message = try await getReplyMessage(by: UInt64(id) ?? 0) - print("reply message decoded =\(message); id=\(id)") - - setReplyMessage(for: transaction, message: message) - } catch { - print("error= \(error)") - } + let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) + let message = try getReplyMessage(by: transactionReply) + + setReplyMessage( + for: transaction, + message: message + ) } } - func getReplyMessage(by id: UInt64) async throws -> String { + func getReplyTransaction(by id: UInt64) async throws -> Transaction { + try await apiService.getTransaction(id: id, withAsset: true) + } + + func getReplyMessage(by transaction: Transaction) throws -> String { guard let address = accountService.account?.address, let privateKey = accountService.keypair?.privateKey else { throw ApiServiceError.accountNotFound } - let transaction = try await apiService.getTransaction(id: id, withAsset: true) - guard let chat = transaction.asset.chat else { let message = "\(AdmWalletService.currencySymbol) \(transaction.amount)" return message diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 15cc58a75..b7fcee358 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -279,6 +279,13 @@ private extension ChatViewController { viewModel.$replyMessage .sink { [weak self] in self?.processSwipeMessage($0) } .store(in: &subscriptions) + + viewModel.$scrollToMessage + .sink { [weak self] in + guard let id = $0 else { return } + self?.setupStartPosition(.messageId(id)) + } + .store(in: &subscriptions) } } diff --git a/Adamant/Stories/Chat/View/Managers/ChatAction.swift b/Adamant/Stories/Chat/View/Managers/ChatAction.swift index c82a42417..e1d0ac85f 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatAction.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatAction.swift @@ -10,4 +10,5 @@ enum ChatAction { case forceUpdateTransactionStatus(id: String) case openTransactionDetails(id: String) case reply(message: MessageModel) + case scrollTo(message: ChatMessageReplyCell.Model) } diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 544812c8b..2b9a17cec 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -129,6 +129,8 @@ private extension ChatDataSourceManager { viewModel.forceUpdateTransactionStatus(id: id) case let .reply(message): viewModel.replyMessage = message + case let .scrollTo(message): + viewModel.scroll(to: message) } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 626463464..8232dff4c 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -152,4 +152,10 @@ class ChatMessageReplyCell: MessageContentCell { override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { messageLabel.handleGesture(touchPoint) } + + override func handleTapGesture(_ gesture: UIGestureRecognizer) { + super.handleTapGesture(gesture) + + actionHandler(.scrollTo(message: model)) + } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index e7cc2a712..1db8f9f63 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -65,6 +65,7 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var partnerName: String? @ObservableValue var inputText = "" @ObservableValue var replyMessage: MessageModel? + @ObservableValue var scrollToMessage: String? var startPosition: ChatStartPosition? { if let messageIdToShow = messageIdToShow { @@ -356,6 +357,31 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } + + func scroll(to message: ChatMessageReplyCell.Model) { + guard let partnerAddress = chatroom?.partner?.address else { return } + + Task { + do { + if !chatTransactions.contains( + where: { $0.transactionId == message.replyId } + ) { + dialog.send(.progress(true)) + try await chatsProvider.loadTransactionsUntilFind( + message.replyId, + recipient: partnerAddress + ) + } + + scrollToMessage = message.replyId + dialog.send(.progress(false)) + } catch { + print(error) + dialog.send(.progress(false)) + dialog.send(.richError(error)) + } + }.stored(in: tasksStorage) + } } extension ChatViewModel: NSFetchedResultsControllerDelegate { From 4a5300f3b1c6afdfc55a0f638ad25786b85c0351 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Mon, 24 Apr 2023 14:11:40 +0300 Subject: [PATCH 018/136] [trello.com/c/TGBBXBeX] Animate scrolled cell --- Adamant.xcodeproj/project.pbxproj | 154 +++++++++--------- Adamant/Helpers/AdamantCellAnimation.swift | 20 +++ .../Chat/View/ChatViewController.swift | 4 +- .../View/Managers/ChatDataSourceManager.swift | 53 +++++- .../ChatMessageCell+Model.swift | 8 +- .../ChatBaseMessage/ChatMessageCell.swift | 12 +- .../ChatMessageReplyCell+Model.swift | 6 +- .../ChatReply/ChatMessageReplyCell.swift | 12 +- .../ChatTransactionContainerView.swift | 4 + .../ChatTransactionContentView+Model.swift | 4 +- .../Content/ChatTransactionContentView.swift | 13 +- .../Chat/ViewModel/ChatMessageFactory.swift | 42 +++-- .../ViewModel/ChatMessagesListFactory.swift | 12 +- .../Chat/ViewModel/ChatViewModel.swift | 15 +- .../Chat/ViewModel/Models/ChatMessage.swift | 4 +- 15 files changed, 252 insertions(+), 111 deletions(-) create mode 100644 Adamant/Helpers/AdamantCellAnimation.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index bdeb21607..d8a27f06c 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B73294C61D10039E956 /* VisibleWalletsService.swift */; }; 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */; }; 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; + 41330F7629F1509400CB587C /* AdamantCellAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */; }; 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; 4150136429B225CC0037F834 /* Double+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4150136329B225CC0037F834 /* Double+adamant.swift */; }; 415441372923AB3700824478 /* BtcProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415441362923AB3700824478 /* BtcProvider.swift */; }; @@ -96,7 +97,7 @@ 55FBAAF728C54B2F0066E629 /* Nodes+Allowance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAF428C54B230066E629 /* Nodes+Allowance.swift */; }; 55FBAAF828C54B300066E629 /* Nodes+Allowance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAF428C54B230066E629 /* Nodes+Allowance.swift */; }; 55FBAAFB28C550920066E629 /* NodesAllowanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */; }; - 6403F5DB2272389800D58779 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 6403F5DB2272389800D58779 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DD22723C6800D58779 /* DashMainnet.swift */; }; 6403F5E022723F6400D58779 /* DashWalletRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DF22723F6400D58779 /* DashWalletRouter.swift */; }; 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E122723F7500D58779 /* DashWallet.swift */; }; @@ -296,7 +297,7 @@ A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B6F262DEDE1009FA43E /* Clibsodium */; }; A5241B77262DEDEF009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B76262DEDEF009FA43E /* Clibsodium */; }; A5241B7E262DEDFE009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B7D262DEDFE009FA43E /* Clibsodium */; }; - A530B0D82842110D003F0210 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + A530B0D82842110D003F0210 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; A544F0D4262C9878001F1A6D /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = A544F0D3262C9878001F1A6D /* Eureka */; }; A57282CA262C94CD00C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282C9262C94CD00C96FA8 /* DateToolsSwift */; }; A57282D1262C94DA00C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282D0262C94DA00C96FA8 /* DateToolsSwift */; }; @@ -782,6 +783,7 @@ 41047B73294C61D10039E956 /* VisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsService.swift; sourceTree = ""; }; 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVisibleWalletsService.swift; sourceTree = ""; }; 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; + 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCellAnimation.swift; sourceTree = ""; }; 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; 4150136329B225CC0037F834 /* Double+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+adamant.swift"; sourceTree = ""; }; 415441362923AB3700824478 /* BtcProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcProvider.swift; sourceTree = ""; }; @@ -1343,7 +1345,7 @@ A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */, A50AEB14262C837900B37C22 /* Alamofire in Frameworks */, 938F7D582955C1DA001915CA /* MessageKit in Frameworks */, - A530B0D82842110D003F0210 /* BuildFile in Frameworks */, + A530B0D82842110D003F0210 /* (null) in Frameworks */, A5DBBADC262C729B004AC028 /* CryptoSwift in Frameworks */, A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */, A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */, @@ -1936,6 +1938,7 @@ 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */, 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */, 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */, + 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */, ); path = Helpers; sourceTree = ""; @@ -2777,25 +2780,25 @@ ); mainGroup = E913C8E51FFFA51D001A83F7; packageReferences = ( - A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */, - A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */, - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */, - A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */, - A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */, - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */, - A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */, - A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */, - A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */, - A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */, - A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */, - A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */, - A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */, - A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */, + A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */, + A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */, + A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */, + A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */, + A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, + A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */, + A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */, + A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */, + A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */, + A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */, + A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */, + A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */, + A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */, A5AC8DFD262E0B030053A7E2 /* XCRemoteSwiftPackageReference "SipHash" */, 3A8875ED27BBF38D00436195 /* XCRemoteSwiftPackageReference "Parchment" */, - 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */, - 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */, - 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */, + 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */, + 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */, + 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */, ); productRefGroup = E913C8EF1FFFA51D001A83F7 /* Products */; projectDirPath = ""; @@ -3179,6 +3182,7 @@ 640EFA9D2558613400E9724B /* DogeProvider.swift in Sources */, E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */, 6416B19F21AD7CBE006089AC /* LskWalletViewController.swift in Sources */, + 41330F7629F1509400CB587C /* AdamantCellAnimation.swift in Sources */, 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */, 6449BA6A235CA0930033B936 /* ERC20Wallet.swift in Sources */, E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, @@ -3198,7 +3202,7 @@ A50A41112822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, - 6403F5DB2272389800D58779 /* BuildFile in Sources */, + 6403F5DB2272389800D58779 /* (null) in Sources */, 6416B1A521AEE157006089AC /* LskWalletService+Transfers.swift in Sources */, 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */, E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, @@ -4172,7 +4176,7 @@ kind = branch; }; }; - 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { + 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/socketio/socket.io-client-swift"; requirement = { @@ -4180,7 +4184,7 @@ kind = branch; }; }; - 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */ = { + 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit.git"; requirement = { @@ -4188,7 +4192,7 @@ minimumVersion = 5.0.0; }; }; - 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */ = { + 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MessageKit/MessageKit.git"; requirement = { @@ -4196,7 +4200,7 @@ minimumVersion = 4.0.0; }; }; - A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */ = { + A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EFPrefix/EFQRCode.git"; requirement = { @@ -4204,7 +4208,7 @@ minimumVersion = 6.1.0; }; }; - A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */ = { + A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/yannickl/QRCodeReader.swift.git"; requirement = { @@ -4212,7 +4216,7 @@ minimumVersion = 10.1.1; }; }; - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */ = { + A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; requirement = { @@ -4220,7 +4224,7 @@ minimumVersion = 5.4.2; }; }; - A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */ = { + A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xmartlabs/Eureka.git"; requirement = { @@ -4228,7 +4232,7 @@ minimumVersion = 5.3.3; }; }; - A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */ = { + A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/maniramezan/DateTools.git"; requirement = { @@ -4236,7 +4240,7 @@ kind = branch; }; }; - A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */ = { + A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4244,7 +4248,7 @@ minimumVersion = 4.2.2; }; }; - A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */ = { + A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RNCryptor/RNCryptor.git"; requirement = { @@ -4252,7 +4256,7 @@ minimumVersion = 5.1.0; }; }; - A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */ = { + A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/skywinder/web3swift.git"; requirement = { @@ -4268,7 +4272,7 @@ minimumVersion = 1.2.2; }; }; - A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */ = { + A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ashleymills/Reachability.swift"; requirement = { @@ -4276,7 +4280,7 @@ minimumVersion = 5.1.0; }; }; - A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */ = { + A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ProcedureKit/ProcedureKit.git"; requirement = { @@ -4284,7 +4288,7 @@ minimumVersion = 5.2.0; }; }; - A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */ = { + A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jedisct1/swift-sodium.git"; requirement = { @@ -4292,7 +4296,7 @@ minimumVersion = 0.9.1; }; }; - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */ = { + A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; requirement = { @@ -4300,7 +4304,7 @@ minimumVersion = 1.5.0; }; }; - A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */ = { + A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Swinject/Swinject.git"; requirement = { @@ -4308,7 +4312,7 @@ minimumVersion = 2.7.1; }; }; - A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */ = { + A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git"; requirement = { @@ -4326,27 +4330,27 @@ }; 416F5EA3290162EB00EF0400 /* SocketIO */ = { isa = XCSwiftPackageProductDependency; - package = 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */; + package = 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */; productName = SocketIO; }; 557AC305287B10D8004699D7 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 55D1D84E287B78F200F94A4E /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 55D1D850287B78FC00F94A4E /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 938F7D572955C1DA001915CA /* MessageKit */ = { isa = XCSwiftPackageProductDependency; - package = 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */; + package = 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */; productName = MessageKit; }; 93FA403529401BFC00D20DB6 /* PopupKit */ = { @@ -4355,102 +4359,102 @@ }; A50AEB03262C815200B37C22 /* EFQRCode */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */; + package = A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */; productName = EFQRCode; }; A50AEB0B262C81E300B37C22 /* QRCodeReader */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */; + package = A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */; productName = QRCodeReader; }; A50AEB13262C837900B37C22 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */; + package = A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; A5241B6F262DEDE1009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5241B76262DEDEF009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5241B7D262DEDFE009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A544F0D3262C9878001F1A6D /* Eureka */ = { isa = XCSwiftPackageProductDependency; - package = A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */; + package = A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */; productName = Eureka; }; A57282C9262C94CD00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D0262C94DA00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D2262C94DF00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D4262C94E500C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57839FA262C95BF00428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A01262C95CA00428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A08262C95D000428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A0A262C95D500428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A0D262C964500428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A14262C965000428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A1B262C965600428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A1D262C965D00428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5785ADE262C63580001BC66 /* web3swift */ = { isa = XCSwiftPackageProductDependency; - package = A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */; + package = A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */; productName = web3swift; }; A5AC8DFE262E0B030053A7E2 /* SipHash */ = { @@ -4460,37 +4464,37 @@ }; A5C99E0D262C9E3A00F7B1B7 /* Reachability */ = { isa = XCSwiftPackageProductDependency; - package = A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */; + package = A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */; productName = Reachability; }; A5D87BA2262CA01D00DC28F0 /* ProcedureKit */ = { isa = XCSwiftPackageProductDependency; - package = A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */; + package = A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */; productName = ProcedureKit; }; A5DBBABC262C7221004AC028 /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5DBBADB262C729B004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE2262C72B0004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE4262C72B7004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE6262C72BD004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAED262C72EF004AC028 /* BitcoinKit */ = { @@ -4503,27 +4507,27 @@ }; A5F0A04A262C9CA90009672A /* Swinject */ = { isa = XCSwiftPackageProductDependency; - package = A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */; + package = A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */; productName = Swinject; }; A5F92993262C855B00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929AE262C857D00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929B5262C858700C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929B7262C858F00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Adamant/Helpers/AdamantCellAnimation.swift b/Adamant/Helpers/AdamantCellAnimation.swift new file mode 100644 index 000000000..d6eb4e195 --- /dev/null +++ b/Adamant/Helpers/AdamantCellAnimation.swift @@ -0,0 +1,20 @@ +// +// UIView+adamant.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 20.04.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +extension UIView { + func startBlinkAnimation() { + let bgColor = backgroundColor + self.backgroundColor = .adamant.active.withAlphaComponent(0.2) + + UIView.animate(withDuration: 1.0) { + self.backgroundColor = bgColor + } + } +} diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index b7fcee358..1d9f7a2a1 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -283,7 +283,7 @@ private extension ChatViewController { viewModel.$scrollToMessage .sink { [weak self] in guard let id = $0 else { return } - self?.setupStartPosition(.messageId(id)) + self?.setupStartPosition(.messageId(id), animated: true) } .store(in: &subscriptions) } @@ -452,7 +452,7 @@ private extension ChatViewController { } @MainActor - func setupStartPosition(_ position: ChatStartPosition) { + func setupStartPosition(_ position: ChatStartPosition, animated: Bool = false) { chatMessagesCollectionView.fixedBottomOffset = nil switch position { diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 2b9a17cec..87edf9466 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -73,11 +73,26 @@ final class ChatDataSourceManager: MessagesDataSource { ChatMessageCell.self, for: indexPath ) - let model = ChatMessageCell.Model(id: model.id, text: model.string) + + let publisher: any Observable = viewModel.$messages.compactMap { + let message = $0[safe: indexPath.section] + guard case let .message(model) = message?.fullModel.content + else { return nil } + + let newModel = ChatMessageCell.Model( + id: model.id, + text: model.string, + animationId: message?.animationId ?? "" + ) + return newModel + } + + let model = ChatMessageCell.Model(id: model.id, text: model.string, animationId: "") cell.model = model cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.actionHandler = { [weak self] in self?.handleAction($0) } + cell.setSubscription(publisher: publisher) return cell } @@ -88,9 +103,19 @@ final class ChatDataSourceManager: MessagesDataSource { for: indexPath ) + let publisher: any Observable = viewModel.$messages.compactMap { + let message = $0[safe: indexPath.section] + guard case var .reply(model) = message?.fullModel.content + else { return nil } + + model.animationId = message?.animationId ?? "" + return model + } + cell.model = model cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.actionHandler = { [weak self] in self?.handleAction($0) } + cell.setSubscription(publisher: publisher) return cell } @@ -103,6 +128,9 @@ final class ChatDataSourceManager: MessagesDataSource { at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> UICollectionViewCell { + guard case let .transaction(model) = message.fullModel.content + else { return UICollectionViewCell() } + let cell = messagesCollectionView.dequeueReusableCell( ChatViewController.TransactionCell.self, for: indexPath @@ -110,10 +138,29 @@ final class ChatDataSourceManager: MessagesDataSource { let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] - guard case let .transaction(model) = message?.fullModel.content else { return nil } - return model.value + guard case let .transaction(model) = message?.fullModel.content + else { return nil } + + var newModel = ChatTransactionContainerView.Model.init( + id: model.value.id, + isFromCurrentSender: model.value.isFromCurrentSender, + content: .init( + id: model.value.content.id, + title: model.value.content.title, + icon: model.value.content.icon, + amount: model.value.content.amount, + currency: model.value.content.currency, + date: model.value.content.date, + comment: model.value.content.comment, + backgroundColor: model.value.content.backgroundColor, + animationId: message?.animationId ?? ""), + status: model.value.status) + return newModel } + cell.wrappedView.model = model.value + cell.wrappedView.configureColor() + cell.wrappedView.actionHandler = { [weak self] in self?.handleAction($0) } cell.wrappedView.setSubscription(publisher: publisher) return cell diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift index 9a5358dae..7789eb7f7 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift @@ -9,13 +9,15 @@ import UIKit extension ChatMessageCell { - struct Model: Equatable, MessageModel { + struct Model: ChatReusableViewModelProtocol, MessageModel { let id: String let text: NSAttributedString - + var animationId: String + static let `default` = Self( id: "", - text: NSAttributedString(string: "") + text: NSAttributedString(string: ""), + animationId: "" ) func makeReplyContent() -> NSAttributedString { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index 2ed137935..ee3fa77be 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -9,8 +9,9 @@ import UIKit import SnapKit import MessageKit +import Combine -class ChatMessageCell: TextMessageCell { +final class ChatMessageCell: TextMessageCell, ChatModelView { private lazy var swipeView: SwipeableView = { let view = SwipeableView(frame: .zero, view: contentView, messagePadding: 8) return view @@ -22,10 +23,19 @@ class ChatMessageCell: TextMessageCell { didSet { guard model != oldValue else { return } swipeView.update(model) + let isSelected = oldValue.animationId != model.animationId + && !model.animationId.isEmpty + && oldValue.id == model.id + && !model.id.isEmpty + + if isSelected { + messageContainerView.startBlinkAnimation() + } } } var actionHandler: (ChatAction) -> Void = { _ in } + var subscription: AnyCancellable? // MARK: - Methods diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 7bfcb8c8f..30e610fb6 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -9,19 +9,21 @@ import UIKit extension ChatMessageReplyCell { - struct Model: Equatable, MessageModel { + struct Model: ChatReusableViewModelProtocol, MessageModel { let id: String let replyId: String let message: NSAttributedString let messageReply: NSAttributedString let backgroundColor: ChatMessageBackgroundColor + var animationId: String static let `default` = Self( id: "", replyId: "", message: NSAttributedString(string: ""), messageReply: NSAttributedString(string: ""), - backgroundColor: .failed + backgroundColor: .failed, + animationId: "" ) func makeReplyContent() -> NSAttributedString { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 8232dff4c..b6c88f907 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -9,8 +9,9 @@ import UIKit import MessageKit import SnapKit +import Combine -class ChatMessageReplyCell: MessageContentCell { +final class ChatMessageReplyCell: MessageContentCell, ChatModelView { /// The labels used to display the message's text. private var messageLabel = MessageLabel() private var replyMessageLabel = MessageLabel() @@ -59,10 +60,19 @@ class ChatMessageReplyCell: MessageContentCell { didSet { guard model != oldValue else { return } swipeView.update(model) + let isSelected = oldValue.animationId != model.animationId + && !model.animationId.isEmpty + && oldValue.id == model.id + && !model.id.isEmpty + + if isSelected { + messageContainerView.startBlinkAnimation() + } } } var actionHandler: (ChatAction) -> Void = { _ in } + var subscription: AnyCancellable? // MARK: - Methods diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 5e2b8a1c7..83308c148 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -57,6 +57,10 @@ final class ChatTransactionContainerView: UIView, ChatModelView { super.init(coder: coder) configure() } + + func configureColor() { + contentView.backgroundColor = model.content.backgroundColor.uiColor + } } extension ChatTransactionContainerView: ReusableView { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift index 519364731..df0e0ee69 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift @@ -18,6 +18,7 @@ extension ChatTransactionContentView { let date: String let comment: String? let backgroundColor: ChatMessageBackgroundColor + var animationId: String static let `default` = Self( id: "", @@ -27,7 +28,8 @@ extension ChatTransactionContentView { currency: "", date: .init(), comment: nil, - backgroundColor: .failed + backgroundColor: .failed, + animationId: "" ) } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index 63e66e5c3..128652f25 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -13,7 +13,11 @@ final class ChatTransactionContentView: UIView { var model: Model = .default { didSet { guard oldValue != model else { return } - update() + let isSelected = oldValue.animationId != model.animationId + && !model.id.isEmpty + && model.id == oldValue.id + + update(isSelected: isSelected) } } @@ -130,8 +134,11 @@ private extension ChatTransactionContentView { } } - func update() { - backgroundColor = model.backgroundColor.uiColor + func update(isSelected: Bool) { + if isSelected { + startBlinkAnimation() + } + titleLabel.text = model.title iconView.image = model.icon amountLabel.text = String(model.amount) diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index f848d562e..37e871e8f 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -44,7 +44,8 @@ struct ChatMessageFactory { expireDate: inout Date?, currentSender: SenderType, dateHeaderOn: Bool, - topSpinnerOn: Bool + topSpinnerOn: Bool, + animationId: String ) -> ChatMessage { let sentDate = transaction.sentDate ?? .now let senderModel = ChatSender(transaction: transaction) @@ -68,7 +69,8 @@ struct ChatMessageFactory { content: makeContent( transaction, isFromCurrentSender: currentSender.senderId == senderModel.senderId, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId ), backgroundColor: backgroundColor, bottomString: makeBottomString( @@ -79,7 +81,8 @@ struct ChatMessageFactory { dateHeader: dateHeaderOn ? makeDateHeader(sentDate: sentDate) : nil, - topSpinnerOn: topSpinnerOn + topSpinnerOn: topSpinnerOn, + animationId: animationId ) } } @@ -89,26 +92,33 @@ private extension ChatMessageFactory { func makeContent( _ transaction: ChatTransaction, isFromCurrentSender: Bool, - backgroundColor: ChatMessageBackgroundColor + backgroundColor: ChatMessageBackgroundColor, + animationId: String ) -> ChatMessage.Content { switch transaction { case let transaction as MessageTransaction: return makeContent(transaction) case let transaction as RichMessageTransaction: if transaction.isReply { - return makeContent(transaction, backgroundColor: backgroundColor) + return makeContent( + transaction, + backgroundColor: backgroundColor, + animationId: animationId + ) } return makeContent( transaction, isFromCurrentSender: isFromCurrentSender, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId ) case let transaction as TransferTransaction: return makeContent( transaction, isFromCurrentSender: isFromCurrentSender, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId ) default: return .default @@ -123,7 +133,8 @@ private extension ChatMessageFactory { func makeContent( _ transaction: RichMessageTransaction, - backgroundColor: ChatMessageBackgroundColor + backgroundColor: ChatMessageBackgroundColor, + animationId: String ) -> ChatMessage.Content { guard let content = transaction.richContent, let replyId = content[RichContentKeys.reply.replyToId], @@ -139,14 +150,16 @@ private extension ChatMessageFactory { replyId: replyId, message: Self.markdownParser.parse(replyMessage), messageReply: Self.markdownParser.parse(decodedMessage), - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId )) } func makeContent( _ transaction: RichMessageTransaction, isFromCurrentSender: Bool, - backgroundColor: ChatMessageBackgroundColor + backgroundColor: ChatMessageBackgroundColor, + animationId: String ) -> ChatMessage.Content { guard let transfer = transaction.transfer else { return .default } let id = transaction.chatMessageId ?? "" @@ -164,7 +177,8 @@ private extension ChatMessageFactory { currency: richMessageProviders[transfer.type]?.tokenSymbol ?? "", date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", comment: transfer.comments, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId ), status: transaction.transactionStatus ?? .notInitiated ))) @@ -173,7 +187,8 @@ private extension ChatMessageFactory { func makeContent( _ transaction: TransferTransaction, isFromCurrentSender: Bool, - backgroundColor: ChatMessageBackgroundColor + backgroundColor: ChatMessageBackgroundColor, + animationId: String ) -> ChatMessage.Content { let id = transaction.chatMessageId ?? "" @@ -192,7 +207,8 @@ private extension ChatMessageFactory { currency: AdmWalletService.currencySymbol, date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", comment: transaction.comment, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + animationId: animationId ), status: transaction.statusEnum.toTransactionStatus() ))) diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift index 3f39ec4e6..3865b9674 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift @@ -21,7 +21,8 @@ actor ChatMessagesListFactory { transactions: [ChatTransaction], sender: ChatSender, isNeedToLoadMoreMessages: Bool, - expirationTimestamp minExpTimestamp: inout TimeInterval? + expirationTimestamp minExpTimestamp: inout TimeInterval?, + animationIds: [String: String] ) -> [ChatMessage] { assert(!Thread.isMainThread, "Do not process messages on main thread") @@ -32,7 +33,8 @@ actor ChatMessagesListFactory { sender: sender, dateHeaderOn: isNeedToDisplayDateHeader(index: index, transactions: transactions), topSpinnerOn: isNeedToLoadMoreMessages && index == .zero, - willExpireAfter: &expTimestamp + willExpireAfter: &expTimestamp, + animationId: animationIds[transaction.transactionId] ?? "" ) if let timestamp = expTimestamp, timestamp < minExpTimestamp ?? .greatestFiniteMagnitude { @@ -50,7 +52,8 @@ private extension ChatMessagesListFactory { sender: SenderType, dateHeaderOn: Bool, topSpinnerOn: Bool, - willExpireAfter: inout TimeInterval? + willExpireAfter: inout TimeInterval?, + animationId: String ) -> ChatMessage { var expireDate: Date? let message = chatMessageFactory.makeMessage( @@ -58,7 +61,8 @@ private extension ChatMessagesListFactory { expireDate: &expireDate, currentSender: sender, dateHeaderOn: dateHeaderOn, - topSpinnerOn: topSpinnerOn + topSpinnerOn: topSpinnerOn, + animationId: animationId ) willExpireAfter = expireDate?.timeIntervalSince1970 diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 1db8f9f63..99b974cba 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -36,6 +36,15 @@ final class ChatViewModel: NSObject { private var timerSubscription: AnyCancellable? private var messageIdToShow: String? private var isLoading = false + private var animationIds: [String: String] = [:] { + didSet { + animationIds.forEach { (key, value) in + guard let index = messages.firstIndex(where: { $0.messageId == key }) + else { return } + messages[index].animationId = value + } + } + } private var isNeedToLoadMoreMessages: Bool { get async { @@ -360,7 +369,6 @@ final class ChatViewModel: NSObject { func scroll(to message: ChatMessageReplyCell.Model) { guard let partnerAddress = chatroom?.partner?.address else { return } - Task { do { if !chatTransactions.contains( @@ -374,6 +382,8 @@ final class ChatViewModel: NSObject { } scrollToMessage = message.replyId + animationIds[message.replyId] = UUID().uuidString + dialog.send(.progress(false)) } catch { print(error) @@ -446,7 +456,8 @@ private extension ChatViewModel { transactions: chatTransactions, sender: sender, isNeedToLoadMoreMessages: isNeedToLoadMoreMessages, - expirationTimestamp: &expirationTimestamp + expirationTimestamp: &expirationTimestamp, + animationIds: animationIds ) await setupNewMessages( diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift index 1d5565221..9b41e44a8 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift @@ -19,6 +19,7 @@ struct ChatMessage: Identifiable, Equatable { let bottomString: ComparableAttributedString? let dateHeader: ComparableAttributedString? let topSpinnerOn: Bool + var animationId: String static let `default` = Self( id: "", @@ -29,7 +30,8 @@ struct ChatMessage: Identifiable, Equatable { backgroundColor: .failed, bottomString: nil, dateHeader: nil, - topSpinnerOn: false + topSpinnerOn: false, + animationId: "" ) } From 3f28670f3a6b919aa78be130175b9bea9200ba35 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 26 Apr 2023 10:01:01 +0300 Subject: [PATCH 019/136] [trello.com/c/TGBBXBeX] Implemented temp scroll position --- .../Chat/View/ChatViewController.swift | 27 ++++++++++++++----- .../Chat/ViewModel/ChatViewModel.swift | 6 +++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 1d9f7a2a1..d8d9513a0 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -282,8 +282,17 @@ private extension ChatViewController { viewModel.$scrollToMessage .sink { [weak self] in - guard let id = $0 else { return } - self?.setupStartPosition(.messageId(id), animated: true) + guard let toId = $0, + let fromId = $1 + else { return } + + if self?.isScrollPositionNearlyTheBottom != true { + if let index = self?.viewModel.tempOffsets.firstIndex(of: fromId) { + self?.viewModel.tempOffsets.remove(at: index) + } + self?.viewModel.tempOffsets.append(fromId) + } + self?.scrollToPosition(.messageId(toId), animated: true) } .store(in: &subscriptions) } @@ -365,7 +374,7 @@ private extension ChatViewController { bottomMessageId = viewModel.messages.last?.messageId guard !messagesLoaded, !viewModel.messages.isEmpty else { return } - viewModel.startPosition.map { setupStartPosition($0) } + viewModel.startPosition.map { scrollToPosition($0) } messagesLoaded = true } @@ -390,8 +399,12 @@ private extension ChatViewController { private extension ChatViewController { func makeScrollDownButton() -> ChatScrollDownButton { let button = ChatScrollDownButton() - button.action = { [weak messagesCollectionView] in - messagesCollectionView?.scrollToBottom(animated: true) + button.action = { [weak self] in + guard let id = self?.viewModel.tempOffsets.popLast() else { + self?.messagesCollectionView.scrollToBottom(animated: true) + return + } + self?.scrollToPosition(.messageId(id), animated: true) } return button @@ -452,7 +465,7 @@ private extension ChatViewController { } @MainActor - func setupStartPosition(_ position: ChatStartPosition, animated: Bool = false) { + func scrollToPosition(_ position: ChatStartPosition, animated: Bool = false) { chatMessagesCollectionView.fixedBottomOffset = nil switch position { @@ -465,7 +478,7 @@ private extension ChatViewController { messagesCollectionView.scrollToItem( at: .init(item: .zero, section: index), at: [.centeredVertically, .centeredHorizontally], - animated: false + animated: animated ) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 99b974cba..751955bfd 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -59,6 +59,8 @@ final class ChatViewModel: NSObject { private(set) var chatroom: Chatroom? private(set) var chatTransactions: [ChatTransaction] = [] + var tempOffsets: [String] = [] + let didTapTransfer = ObservableSender() let dialog = ObservableSender() let didTapAdmChat = ObservableSender<(Chatroom, String?)>() @@ -74,7 +76,7 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var partnerName: String? @ObservableValue var inputText = "" @ObservableValue var replyMessage: MessageModel? - @ObservableValue var scrollToMessage: String? + @ObservableValue var scrollToMessage: (String?, String?) var startPosition: ChatStartPosition? { if let messageIdToShow = messageIdToShow { @@ -381,7 +383,7 @@ final class ChatViewModel: NSObject { ) } - scrollToMessage = message.replyId + scrollToMessage = (message.replyId, message.id) animationIds[message.replyId] = UUID().uuidString dialog.send(.progress(false)) From 92af21e74bb063d819a72ae9f13228b8b6b60ee0 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 26 Apr 2023 12:00:03 +0300 Subject: [PATCH 020/136] [trello.com/c/yt09l4Xt] Start new chat from search --- .../ChatsList/ChatListViewController.swift | 6 +- Adamant/Stories/ChatsList/ChatsRoutes.swift | 10 +- .../ChatsList/NewChatViewController.swift | 2 - .../SearchResultsViewController.swift | 100 ++++++++++++++++-- .../Assets/l18n/de.lproj/Localizable.strings | 3 + .../Assets/l18n/en.lproj/Localizable.strings | 3 + .../Assets/l18n/ru.lproj/Localizable.strings | 3 + 7 files changed, 112 insertions(+), 15 deletions(-) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index 970476e76..7e302c068 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -709,7 +709,6 @@ extension ChatListViewController: NSFetchedResultsControllerDelegate { // MARK: - NewChatViewControllerDelegate extension ChatListViewController: NewChatViewControllerDelegate { func newChatController( - _ controller: NewChatViewController, didSelectAccount account: CoreDataAccount, preMessage: String?, name: String? @@ -1199,6 +1198,11 @@ extension ChatListViewController: UISearchBarDelegate, UISearchResultsUpdating, presenter.presentChatroom(chatroom) } } + + func didSelected(_ account: CoreDataAccount) { + account.chatroom?.isForcedVisible = true + newChatController(didSelectAccount: account, preMessage: nil, name: nil) + } } // MARK: Mac OS HotKeys diff --git a/Adamant/Stories/ChatsList/ChatsRoutes.swift b/Adamant/Stories/ChatsList/ChatsRoutes.swift index e6959bcf6..b9a4e6e25 100644 --- a/Adamant/Stories/ChatsList/ChatsRoutes.swift +++ b/Adamant/Stories/ChatsList/ChatsRoutes.swift @@ -54,10 +54,12 @@ extension AdamantScene { }) static let searchResults = AdamantScene(identifier: "SearchResultsViewController", factory: { r in - let c = SearchResultsViewController(nibName: "SearchResultsViewController", bundle: nil) - c.router = r.resolve(Router.self) - c.avatarService = r.resolve(AvatarService.self) - c.addressBookService = r.resolve(AddressBookService.self) + let c = SearchResultsViewController( + router: r.resolve(Router.self)!, + avatarService: r.resolve(AvatarService.self)!, + addressBookService: r.resolve(AddressBookService.self)!, + accountsProvider: r.resolve(AccountsProvider.self)! + ) return c }) diff --git a/Adamant/Stories/ChatsList/NewChatViewController.swift b/Adamant/Stories/ChatsList/NewChatViewController.swift index 8044a4cc1..7982976a6 100644 --- a/Adamant/Stories/ChatsList/NewChatViewController.swift +++ b/Adamant/Stories/ChatsList/NewChatViewController.swift @@ -35,7 +35,6 @@ extension String.adamantLocalized { // MARK: - Delegate protocol NewChatViewControllerDelegate: AnyObject { func newChatController( - _ controller: NewChatViewController, didSelectAccount account: CoreDataAccount, preMessage: String?, name: String? @@ -279,7 +278,6 @@ class NewChatViewController: FormViewController { account.chatroom?.isForcedVisible = true self.delegate?.newChatController( - self, didSelectAccount: account, preMessage: message, name: name diff --git a/Adamant/Stories/ChatsList/SearchResultsViewController.swift b/Adamant/Stories/ChatsList/SearchResultsViewController.swift index 72596b410..40e01973c 100644 --- a/Adamant/Stories/ChatsList/SearchResultsViewController.swift +++ b/Adamant/Stories/ChatsList/SearchResultsViewController.swift @@ -11,35 +11,58 @@ import Swinject import MarkdownKit extension String.adamantLocalized { - struct search { + enum search { static let contacts = NSLocalizedString("SearchPage.Contacts", comment: "SearchPage: Contacts header") static let messages = NSLocalizedString("SearchPage.Messages", comment: "SearchPage: Messages header") - - private init() {} + static let newContact = NSLocalizedString("SearchPage.Contact.New", comment: "SearchPage: Contacts header") } } protocol SearchResultDelegate: AnyObject { func didSelected(_ message: MessageTransaction) func didSelected(_ chatroom: Chatroom) + func didSelected(_ account: CoreDataAccount) } class SearchResultsViewController: UITableViewController { // MARK: - Dependencies - var router: Router! - var avatarService: AvatarService! - var addressBookService: AddressBookService! + var router: Router + var avatarService: AvatarService + var addressBookService: AddressBookService + var accountsProvider: AccountsProvider // MARK: Properties private var contacts: [Chatroom] = [Chatroom]() private var messages: [MessageTransaction] = [MessageTransaction]() private var searchText: String = "" + private var newAccount: CoreDataAccount? private let markdownParser = MarkdownParser(font: UIFont.systemFont(ofSize: ChatTableViewCell.shortDescriptionTextSize)) weak var delegate: SearchResultDelegate? + // MARK: Init + + init( + router: Router, + avatarService: AvatarService, + addressBookService: AddressBookService, + accountsProvider: AccountsProvider + ) { + self.router = router + self.avatarService = avatarService + self.addressBookService = addressBookService + self.accountsProvider = accountsProvider + super.init(nibName: "SearchResultsViewController", bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Lifecycle + override func viewDidLoad() { super.viewDidLoad() navigationItem.largeTitleDisplayMode = .never @@ -57,6 +80,9 @@ class SearchResultsViewController: UITableViewController { self.contacts = contacts ?? [Chatroom]() self.messages = messages ?? [MessageTransaction]() self.searchText = searchText + self.newAccount = nil + + findAccountIfNeeded() tableView.reloadData() } @@ -71,6 +97,9 @@ class SearchResultsViewController: UITableViewController { if self.messages.count > 0 { sections += 1 } + if newAccount != nil { + sections += 1 + } return sections } @@ -78,6 +107,7 @@ class SearchResultsViewController: UITableViewController { switch defineSection(for: section) { case .contacts: return contacts.count case .messages: return messages.count + case .new: return 1 case .none: return 0 } } @@ -96,6 +126,9 @@ class SearchResultsViewController: UITableViewController { cell.lastMessageLabel.textColor = nil // Managed by NSAttributedText configureCell(cell, for: message) + case .new: + configureCell(cell, for: newAccount) + case .none: break } @@ -168,6 +201,34 @@ class SearchResultsViewController: UITableViewController { } } + private func configureCell(_ cell: ChatTableViewCell, for partner: CoreDataAccount?) { + guard let partner = partner else { return } + + if let avatarName = partner.avatar, let avatar = UIImage.init(named: avatarName) { + cell.avatarImage = avatar + cell.avatarImageView.tintColor = UIColor.adamant.primary + } else { + if let address = partner.publicKey { + let image = self.avatarService.avatar(for: address, size: 200) + + DispatchQueue.onMainAsync { + cell.avatarImage = image + } + + cell.avatarImageView.roundingMode = .round + cell.avatarImageView.clipsToBounds = true + } else { + cell.avatarImage = nil + } + cell.borderWidth = 0 + } + + cell.lastMessageLabel.text = partner.address + cell.accountLabel.text = partner.address + cell.hasUnreadMessages = false + cell.dateLabel.text = nil + } + private func shortDescription(for transaction: ChatTransaction) -> NSAttributedString? { switch transaction { case let message as MessageTransaction: @@ -215,6 +276,10 @@ class SearchResultsViewController: UITableViewController { let message = messages[indexPath.row] delegate.didSelected(message) + case .new: + guard let account = newAccount else { return } + delegate.didSelected(account) + case .none: return } @@ -224,15 +289,15 @@ class SearchResultsViewController: UITableViewController { switch defineSection(for: section) { case .contacts: return String.adamantLocalized.search.contacts case .messages: return String.adamantLocalized.search.messages + case .new: return String.adamantLocalized.search.newContact case .none: return nil } } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { switch defineSection(for: indexPath) { - case .contacts: return 60 + case .contacts, .new, .none: return 60 case .messages: return 80 - case .none: return 60 } } @@ -241,6 +306,7 @@ class SearchResultsViewController: UITableViewController { case contacts case messages case none + case new } private func defineSection(for indexPath: IndexPath) -> Section { @@ -258,8 +324,26 @@ class SearchResultsViewController: UITableViewController { return .contacts } else if self.messages.count > 0 { return .messages + } else if newAccount != nil { + return .new } else { return .none } } + + // MARK: Other + + @MainActor private func findAccountIfNeeded() { + guard case .valid = AdamantUtilities.validateAdamantAddress(address: searchText), + contacts.count == 0, + messages.count == 0 + else { return } + + Task { + let account = try await accountsProvider.getAccount(byAddress: searchText) + newAccount = account + + tableView.reloadData() + } + } } diff --git a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings index 84d7e3671..d7853e09b 100755 --- a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings @@ -232,6 +232,9 @@ /* SearchPage: Contacts header */ "SearchPage.Contacts" = "Kontakte"; +/* SearchPage: Contacts header */ +"SearchPage.Contact.New" = "Neuer Kontakt"; + /* SearchPage: Messages header */ "SearchPage.Messages" = "Mitteilungen"; diff --git a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings index 8e27e8689..ae2659a93 100755 --- a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings @@ -229,6 +229,9 @@ /* SearchPage: Contacts header */ "SearchPage.Contacts" = "Contacts"; +/* SearchPage: Contacts header */ +"SearchPage.Contact.New" = "New contact"; + /* SearchPage: Messages header */ "SearchPage.Messages" = "Messages"; diff --git a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings index ba3ccd5b8..5d473cd2d 100644 --- a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings @@ -229,6 +229,9 @@ /* SearchPage: Contacts header */ "SearchPage.Contacts" = "Контакты"; +/* SearchPage: Contacts header */ +"SearchPage.Contact.New" = "Новый контакт"; + /* SearchPage: Messages header */ "SearchPage.Messages" = "Сообщения"; From 3c809948d1dd6b20f9d02a7246b72719f955f40e Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 26 Apr 2023 15:50:24 +0300 Subject: [PATCH 021/136] [trello.com/c/p7IboXVW] Fixed colors for code markdown --- Adamant/Helpers/UIFont+adamant.swift | 5 ++ .../Chat/ViewModel/ChatMessageFactory.swift | 6 ++- .../ChatsList/ChatListViewController.swift | 6 ++- AdamantShared/Helpers/Markdown+Adamant.swift | 48 +++++++++++++++++++ .../Helpers/UIHelpers/UIColor+adamant.swift | 14 ++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/Adamant/Helpers/UIFont+adamant.swift b/Adamant/Helpers/UIFont+adamant.swift index 88816da52..f7b905572 100644 --- a/Adamant/Helpers/UIFont+adamant.swift +++ b/Adamant/Helpers/UIFont+adamant.swift @@ -13,6 +13,10 @@ extension UIFont { return UIFont(name: "Exo 2", size: size) ?? .systemFont(ofSize: size) } + static func adamantMono(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { + return .monospacedSystemFont(ofSize: size, weight: weight) + } + static func adamantPrimary(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { let name: String @@ -37,4 +41,5 @@ extension UIFont { } static var adamantChatDefault = UIFont.systemFont(ofSize: 17) + static var adamantCodeDefault = UIFont.adamantMono(ofSize: 15, weight: .regular) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index e1081b90b..1a5a9230f 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -72,7 +72,6 @@ private extension ChatMessageFactory { .quote, .bold, .italic, - .code, .strikethrough ], customElements: [ @@ -81,6 +80,11 @@ private extension ChatMessageFactory { MarkdownAdvancedAdm( font: .adamantChatDefault, color: .adamant.active + ), + MarkdownCodeAdamant( + font: .adamantCodeDefault, + textHighlightColor: .adamant.codeBlockText, + textBackgroundColor: .adamant.codeBlock ) ] ) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index 970476e76..433a8f4b9 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -76,7 +76,6 @@ class ChatListViewController: KeyboardObservingViewController { .quote, .bold, .italic, - .code, .strikethrough, .automaticLink ], @@ -86,6 +85,11 @@ class ChatListViewController: KeyboardObservingViewController { MarkdownAdvancedAdm( font: .adamantChatDefault, color: .adamant.active + ), + MarkdownCodeAdamant( + font: .adamantCodeDefault, + textHighlightColor: .adamant.codeBlockText, + textBackgroundColor: .adamant.codeBlock ) ] ) diff --git a/AdamantShared/Helpers/Markdown+Adamant.swift b/AdamantShared/Helpers/Markdown+Adamant.swift index c6acb1f8f..f4caf7f7f 100644 --- a/AdamantShared/Helpers/Markdown+Adamant.swift +++ b/AdamantShared/Helpers/Markdown+Adamant.swift @@ -175,3 +175,51 @@ final class MarkdownLinkAdm: MarkdownLink { attributedString.addAttributes(attributes, range: range) } } + +// MARK: Markdown Code +final class MarkdownCodeAdamant: MarkdownCommonElement { + private static let regex = "\\`[^\\`]*(?:\\`\\`[^\\`]*)*\\`" + private let char = "`" + + var font: MarkdownFont? + var color: MarkdownColor? + var textHighlightColor: MarkdownColor? + var textBackgroundColor: MarkdownColor? + + var regex: String { + return MarkdownCodeAdamant.regex + } + + init(font: MarkdownFont? = MarkdownCode.defaultFont, + color: MarkdownColor? = nil, + textHighlightColor: MarkdownColor? = MarkdownCode.defaultHighlightColor, + textBackgroundColor: MarkdownColor? = MarkdownCode.defaultBackgroundColor) { + self.font = font + self.color = color + self.textHighlightColor = textHighlightColor + self.textBackgroundColor = textBackgroundColor + } + + func addAttributes(_ attributedString: NSMutableAttributedString, range: NSRange) { + var matchString: String = attributedString.attributedSubstring(from: range).string + matchString = matchString.replacingOccurrences(of: char, with: "") + attributedString.replaceCharacters(in: range, with: matchString) + + var codeAttributes = attributes + + textHighlightColor.flatMap { + codeAttributes[NSAttributedString.Key.foregroundColor] = $0 + } + textBackgroundColor.flatMap { + codeAttributes[NSAttributedString.Key.backgroundColor] = $0 + } + font.flatMap { codeAttributes[NSAttributedString.Key.font] = $0 } + + let updatedRange = (attributedString.string as NSString).range(of: matchString) + attributedString.addAttributes(codeAttributes, range: NSRange(location: range.location, length: updatedRange.length)) + } + + func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { + addAttributes(attributedString, range: match.range) + } +} diff --git a/AdamantShared/Helpers/UIHelpers/UIColor+adamant.swift b/AdamantShared/Helpers/UIHelpers/UIColor+adamant.swift index e18c1d15c..aa3a21e35 100644 --- a/AdamantShared/Helpers/UIHelpers/UIColor+adamant.swift +++ b/AdamantShared/Helpers/UIHelpers/UIColor+adamant.swift @@ -86,6 +86,20 @@ extension UIColor { return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } + /// Code block background color + static var codeBlock: UIColor { + let colorWhiteTheme = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 0.1) + let colorDarkTheme = UIColor(hex: "#2a2a2b") + return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) + } + + /// Code block text color + static var codeBlockText: UIColor { + let colorWhiteTheme = UIColor(red: 0.32, green: 0.32, blue: 0.32, alpha: 1) + let colorDarkTheme = UIColor.white.withAlphaComponent(0.8) + return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) + } + /// Main dark gray, ~70% gray static var primary: UIColor { let colorWhiteTheme = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 1) From f484b2c710497ed09163d20b4b3796cb3e9bb05b Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 28 Apr 2023 15:48:38 +0300 Subject: [PATCH 022/136] [trello.com/c/TGBBXBeX] Edited design for reply cell --- Adamant.xcodeproj/project.pbxproj | 4 +++ .../Helpers/NSAttributedText+Adamant.swift | 32 +++++++++++++++++++ Adamant/Helpers/UIFont+adamant.swift | 1 + Adamant/SharedViews/ReplyView.swift | 2 +- .../ChatMessageReplyCell+Model.swift | 3 +- .../ChatReply/ChatMessageReplyCell.swift | 16 ++++++++-- .../Chat/ViewModel/ChatMessageFactory.swift | 25 ++++++++++++++- .../ChatsList/ChatListViewController.swift | 20 ++---------- 8 files changed, 78 insertions(+), 25 deletions(-) create mode 100644 Adamant/Helpers/NSAttributedText+Adamant.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index d8a27f06c..a85e78210 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 418BBB5A2938C45B00CAB719 /* EthereumTokensList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418BBB582938C45B00CAB719 /* EthereumTokensList.swift */; }; 418BBB5B2938C45B00CAB719 /* EthereumTokensList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418BBB582938C45B00CAB719 /* EthereumTokensList.swift */; }; 41935848287841E20083363B /* MacOSDeterminer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41935847287841E20083363B /* MacOSDeterminer.swift */; }; + 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */; }; 4197B9C92952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4197B9C82952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift */; }; 4198D57B28C8B7DA009337F2 /* so-proud-notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */; }; 4198D57D28C8B7FA009337F2 /* relax-message-tone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */; }; @@ -803,6 +804,7 @@ 4186B339294200F4006594A3 /* DashWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+DynamicConstants.swift"; sourceTree = ""; }; 418BBB582938C45B00CAB719 /* EthereumTokensList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTokensList.swift; sourceTree = ""; }; 41935847287841E20083363B /* MacOSDeterminer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacOSDeterminer.swift; sourceTree = ""; }; + 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedText+Adamant.swift"; sourceTree = ""; }; 4197B9C82952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsCheckmarkView.swift; sourceTree = ""; }; 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "so-proud-notification.mp3"; sourceTree = ""; }; 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "relax-message-tone.mp3"; sourceTree = ""; }; @@ -1939,6 +1941,7 @@ 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */, 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */, 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */, + 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */, ); path = Helpers; sourceTree = ""; @@ -3333,6 +3336,7 @@ E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, 640EFAA52558613D00E9724B /* ERC20Provider.swift in Sources */, + 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */, 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */, 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, diff --git a/Adamant/Helpers/NSAttributedText+Adamant.swift b/Adamant/Helpers/NSAttributedText+Adamant.swift new file mode 100644 index 000000000..0d29c6e3c --- /dev/null +++ b/Adamant/Helpers/NSAttributedText+Adamant.swift @@ -0,0 +1,32 @@ +// +// NSAttributedText+Adamant.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.04.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +extension NSAttributedString { + func resolveLinkColor(_ color: UIColor = UIColor.adamant.active) -> NSMutableAttributedString { + let mutableText = NSMutableAttributedString(attributedString: self) + + mutableText.enumerateAttribute( + .link, + in: NSRange(location: 0, length: self.length), + options: [] + ) { (value, range, _) in + guard value != nil else { return } + + mutableText.removeAttribute(.link, range: range) + mutableText.addAttribute( + .foregroundColor, + value: color, + range: range + ) + } + + return mutableText + } +} diff --git a/Adamant/Helpers/UIFont+adamant.swift b/Adamant/Helpers/UIFont+adamant.swift index 88816da52..d755458b7 100644 --- a/Adamant/Helpers/UIFont+adamant.swift +++ b/Adamant/Helpers/UIFont+adamant.swift @@ -37,4 +37,5 @@ extension UIFont { } static var adamantChatDefault = UIFont.systemFont(ofSize: 17) + static var adamantChatReplyDefault = UIFont.systemFont(ofSize: 15) } diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift index b53ee893d..0e3140d4b 100644 --- a/Adamant/SharedViews/ReplyView.swift +++ b/Adamant/SharedViews/ReplyView.swift @@ -100,7 +100,7 @@ final class ReplyView: UIView { extension ReplyView { func update(with model: MessageModel) { backgroundColor = .clear - messageLabel.attributedText = model.makeReplyContent() + messageLabel.attributedText = model.makeReplyContent().resolveLinkColor() } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 30e610fb6..84343a191 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -45,10 +45,9 @@ extension ChatMessageReplyCell.Model { return verticalInsets * 2 + verticalStackSpacing + messageHeight - + messageReplyHeight + + ChatMessageReplyCell.replyViewHeight } } private let verticalStackSpacing: CGFloat = 12 private let verticalInsets: CGFloat = 8 -private let messageReplyHeight: CGFloat = 20 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index b6c88f907..62e2a19e6 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -14,16 +14,22 @@ import Combine final class ChatMessageReplyCell: MessageContentCell, ChatModelView { /// The labels used to display the message's text. private var messageLabel = MessageLabel() - private var replyMessageLabel = MessageLabel() + private var replyMessageLabel = UILabel() private lazy var swipeView: SwipeableView = { let view = SwipeableView(frame: .zero, view: contentView, messagePadding: 8) return view }() + static let replyViewHeight: CGFloat = 25 + private lazy var replyView: UIView = { let view = UIView() + view.backgroundColor = .lightGray.withAlphaComponent(0.15) + view.layer.cornerRadius = 5 + let colorView = UIView() + colorView.layer.cornerRadius = 2 colorView.backgroundColor = .adamant.active view.addSubview(colorView) @@ -34,9 +40,13 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { $0.width.equalTo(2) } replyMessageLabel.snp.makeConstraints { - $0.top.bottom.trailing.equalToSuperview() + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().offset(-5) $0.leading.equalTo(colorView.snp.trailing).offset(3) } + view.snp.makeConstraints { make in + make.height.equalTo(Self.replyViewHeight) + } return view }() @@ -108,7 +118,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { replyMessageLabel.numberOfLines = 1 verticalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(8) - $0.leading.trailing.equalToSuperview().inset(12) + $0.leading.trailing.equalToSuperview().inset(8) } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 37e871e8f..46b78ec51 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -35,6 +35,28 @@ struct ChatMessageFactory { ] ) + static let markdownReplyParser = MarkdownParser( + font: .adamantChatReplyDefault, + color: .adamant.primary, + enabledElements: [ + .header, + .list, + .quote, + .bold, + .italic, + .code, + .strikethrough + ], + customElements: [ + MarkdownSimpleAdm(), + MarkdownLinkAdm(), + MarkdownAdvancedAdm( + font: .adamantChatDefault, + color: .adamant.active + ) + ] + ) + init(richMessageProviders: [String: RichMessageProvider]) { self.richMessageProviders = richMessageProviders } @@ -144,12 +166,13 @@ private extension ChatMessageFactory { } let decodedMessage = content[RichContentKeys.reply.decodedMessage] ?? "..." + let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() return .reply(.init( id: transaction.txId, replyId: replyId, message: Self.markdownParser.parse(replyMessage), - messageReply: Self.markdownParser.parse(decodedMessage), + messageReply: decodedMessageMarkDown, backgroundColor: backgroundColor, animationId: animationId )) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index 970476e76..f83ac20f1 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -856,25 +856,9 @@ extension ChatListViewController { raw = text } - let attributesText = markdownParser.parse(raw) - let mutableText = NSMutableAttributedString(attributedString: attributesText) + let attributesText = markdownParser.parse(raw).resolveLinkColor() - mutableText.enumerateAttribute( - .link, - in: NSRange(location: 0, length: attributesText.length), - options: [] - ) { (value, range, _) in - guard value != nil else { return } - - mutableText.removeAttribute(.link, range: range) - mutableText.addAttribute( - .foregroundColor, - value: UIColor.adamant.active, - range: range - ) - } - - return mutableText + return attributesText case let transfer as TransferTransaction: if let admService = richMessageProviders[AdmWalletService.richMessageType] as? AdmWalletService { From ebe0af1869ff3abf179003537a80a25453ced683 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 28 Apr 2023 16:21:18 +0300 Subject: [PATCH 023/136] [trello.com/c/TGBBXBeX] Present short reply text in chats list --- .../ChatsList/ChatListViewController.swift | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index f83ac20f1..ef4c3ce5c 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -868,17 +868,38 @@ extension ChatListViewController { } case let richMessage as RichMessageTransaction: - let description: NSAttributedString + if let type = richMessage.richType, + let provider = richMessageProviders[type] { + return provider.shortDescription(for: richMessage) + } - if let type = richMessage.richType, let provider = richMessageProviders[type] { - description = provider.shortDescription(for: richMessage) - } else if let serialized = richMessage.serializedMessage() { - description = NSAttributedString(string: serialized) - } else { - return nil + if richMessage.isReply, + let content = richMessage.richContent, + let text = content[RichContentKeys.reply.replyMessage] { + + let prefix = richMessage.isOutgoing + ? "\(String.adamantLocalized.chatList.sentMessagePrefix)" + : text + + let replyImageAttachment = NSTextAttachment() + replyImageAttachment.image = UIImage(named: "reply") + replyImageAttachment.bounds = CGRect(x: .zero, y: -3, width: 20, height: 20) + let imageString = NSAttributedString(attachment: replyImageAttachment) + + let markDownText = markdownParser.parse(" \(text)").resolveLinkColor() + + let fullString = NSMutableAttributedString(string: prefix) + fullString.append(imageString) + fullString.append(markDownText) + + return fullString } - return description + if let serialized = richMessage.serializedMessage() { + return NSAttributedString(string: serialized) + } + + return nil /* if richMessage.isOutgoing { From e5dc6a00d666268e7a6c709677a2a883efb2d45d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 4 May 2023 12:34:55 +0300 Subject: [PATCH 024/136] [trello.com/c/TGBBXBeX] fix: bug when scrolling & swiping at the same time --- Adamant/Helpers/SwipePanGestureRecognizer.swift | 12 ++++++++++++ Adamant/SharedViews/SwipeableView.swift | 14 ++++++++++++++ .../Stories/Chat/View/ChatViewController.swift | 16 ++++++++++++++++ .../Stories/Chat/View/Managers/ChatAction.swift | 1 + .../View/Managers/ChatDataSourceManager.swift | 2 ++ .../ChatBaseMessage/ChatMessageCell.swift | 4 ++++ .../ChatReply/ChatMessageReplyCell.swift | 4 ++++ .../Container/ChatTransactionContainerView.swift | 4 ++++ .../Stories/Chat/ViewModel/ChatViewModel.swift | 1 + 9 files changed, 58 insertions(+) diff --git a/Adamant/Helpers/SwipePanGestureRecognizer.swift b/Adamant/Helpers/SwipePanGestureRecognizer.swift index b32f89049..ed74088e3 100644 --- a/Adamant/Helpers/SwipePanGestureRecognizer.swift +++ b/Adamant/Helpers/SwipePanGestureRecognizer.swift @@ -35,6 +35,18 @@ class SwipePanGestureRecognizer: UIPanGestureRecognizer, UIGestureRecognizerDele } } + func gestureRecognizerShouldBegin( + _ gestureRecognizer: UIGestureRecognizer + ) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + + let velocity = panGesture.velocity(in: self.view) + let isHorizontal = abs(velocity.x) > abs(velocity.y) + return isHorizontal + } + func gestureRecognizer( _ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer diff --git a/Adamant/SharedViews/SwipeableView.swift b/Adamant/SharedViews/SwipeableView.swift index 35ce2edb8..f93b6f12e 100644 --- a/Adamant/SharedViews/SwipeableView.swift +++ b/Adamant/SharedViews/SwipeableView.swift @@ -22,6 +22,7 @@ class SwipeableView: UIView { private var oldContentOffset: CGPoint? var action: ((MessageModel) -> Void)? + var swipeStateAction: ((SwipeableView.State) -> Void)? // MARK: Init @@ -77,6 +78,10 @@ private extension SwipeableView { return } + if recognizer.state == .began { + swipeStateAction?(.began) + } + let isOnStartPosition = movingView.frame.origin.x == 0 || movingView.frame.origin.x == messagePadding if isOnStartPosition && translation.x > 0 { return } @@ -100,6 +105,7 @@ private extension SwipeableView { } if recognizer.state == .ended { + swipeStateAction?(.ended) canReplyVibrate = true if replyAction { @@ -117,3 +123,11 @@ private extension SwipeableView { } } } + +// MARK: State +extension SwipeableView { + enum State { + case began + case ended + } +} diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index d8d9513a0..225654ab4 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -159,6 +159,18 @@ extension ChatViewController { let velocity = panGesture.velocity(in: messagesCollectionView) return abs(velocity.x) > abs(velocity.y) } + + private func swipeStateAction(_ state: SwipeableView.State) { + if state == .began { + messagesCollectionView.setContentOffset(messagesCollectionView.contentOffset, animated: false) + messagesCollectionView.isScrollEnabled = false + } + + if state == .ended { + messagesCollectionView.isScrollEnabled = true + messagesCollectionView.keyboardDismissMode = .interactive + } + } } // MARK: Delegate Protocols @@ -295,6 +307,10 @@ private extension ChatViewController { self?.scrollToPosition(.messageId(toId), animated: true) } .store(in: &subscriptions) + + viewModel.$swipeState + .sink { [weak self] in self?.swipeStateAction($0) } + .store(in: &subscriptions) } } diff --git a/Adamant/Stories/Chat/View/Managers/ChatAction.swift b/Adamant/Stories/Chat/View/Managers/ChatAction.swift index e1d0ac85f..1d3aac721 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatAction.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatAction.swift @@ -11,4 +11,5 @@ enum ChatAction { case openTransactionDetails(id: String) case reply(message: MessageModel) case scrollTo(message: ChatMessageReplyCell.Model) + case swipeState(state: SwipeableView.State) } diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 87edf9466..aca891c57 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -178,6 +178,8 @@ private extension ChatDataSourceManager { viewModel.replyMessage = message case let .scrollTo(message): viewModel.scroll(to: message) + case let .swipeState(state): + viewModel.swipeState = state } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index ee3fa77be..7b348ff9b 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -51,5 +51,9 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) } + + swipeView.swipeStateAction = { [weak self] state in + self?.actionHandler(.swipeState(state: state)) + } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 62e2a19e6..78ff85b09 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -113,6 +113,10 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { self?.actionHandler(.reply(message: message)) } + swipeView.swipeStateAction = { [weak self] state in + self?.actionHandler(.swipeState(state: state)) + } + messageContainerView.addSubview(verticalStack) messageLabel.numberOfLines = 0 replyMessageLabel.numberOfLines = 1 diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 83308c148..af63e1380 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -87,6 +87,10 @@ private extension ChatTransactionContainerView { print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) } + + swipeView.swipeStateAction = { [weak self] state in + self?.actionHandler(.swipeState(state: state)) + } } func update() { diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 751955bfd..0439f4ec9 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -74,6 +74,7 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var isSendingAvailable = false @ObservableValue private(set) var fee = "" @ObservableValue private(set) var partnerName: String? + @ObservableValue var swipeState: SwipeableView.State = .ended @ObservableValue var inputText = "" @ObservableValue var replyMessage: MessageModel? @ObservableValue var scrollToMessage: (String?, String?) From e731e4a26fc222b3672f7bb168abdd27c47d89c6 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 4 May 2023 16:33:25 +0300 Subject: [PATCH 025/136] [trello.com/c/TGBBXBeX] feat: send transfer as reply --- .../Adamant.xcdatamodel/contents | 3 +- ...RichMessageTransaction+CoreDataClass.swift | 6 +- ...essageTransaction+CoreDataProperties.swift | 21 +++++- .../AdamantChatTransactionService.swift | 43 ++++++++---- .../AdamantChatsProvider+search.swift | 2 +- .../DataProviders/AdamantChatsProvider.swift | 6 +- .../AdamantRichTransactionReplyService.swift | 8 +-- .../View/Managers/ChatDataSourceManager.swift | 7 +- .../ChatTransactionContentView+Model.swift | 8 ++- .../Content/ChatTransactionContentView.swift | 70 ++++++++++++++++++- .../Chat/ViewModel/ChatMessageFactory.swift | 24 +++++-- .../ChatsList/ChatListViewController.swift | 2 +- .../ComplexTransferViewController.swift | 2 + .../Bitcoin/BtcTransferViewController.swift | 21 +++++- ...BtcWalletService+RichMessageProvider.swift | 16 +++-- ...e+RichMessageProviderWithStatusCheck.swift | 6 +- .../Dash/DashTransferViewController.swift | 21 +++++- ...ashWalletService+RichMessageProvider.swift | 18 ++--- ...e+RichMessageProviderWithStatusCheck.swift | 6 +- .../Doge/DogeTransferViewController.swift | 21 +++++- ...ogeWalletService+RichMessageProvider.swift | 18 ++--- ...e+RichMessageProviderWithStatusCheck.swift | 5 +- .../ERC20/ERC20TransferViewController.swift | 21 +++++- ...C20WalletService+RichMessageProvider.swift | 16 +++-- ...e+RichMessageProviderWithStatusCheck.swift | 5 +- .../Ethereum/EthTransferViewController.swift | 21 +++++- ...EthWalletService+RichMessageProvider.swift | 15 ++-- ...e+RichMessageProviderWithStatusCheck.swift | 5 +- .../Lisk/LskTransferViewController.swift | 23 +++++- ...LskWalletService+RichMessageProvider.swift | 18 +++-- ...e+RichMessageProviderWithStatusCheck.swift | 6 +- .../Wallets/TransferViewControllerBase.swift | 1 + AdamantShared/Helpers/RichMessageTools.swift | 8 ++- AdamantShared/Models/RichMessage.swift | 63 +++++++++++++++-- .../RichMessageNotificationProvider.swift | 2 +- .../TransferBaseProvider.swift | 13 +++- .../NotificationService.swift | 2 +- .../NotificationViewController.swift | 8 ++- 38 files changed, 435 insertions(+), 126 deletions(-) diff --git a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents index 347e14e20..c64dd5263 100644 --- a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents +++ b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents @@ -60,7 +60,8 @@ - + + diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift b/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift index a767c53e8..85b239e54 100644 --- a/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift +++ b/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift @@ -15,11 +15,7 @@ public class RichMessageTransaction: ChatTransaction { static let entityName = "RichMessageTransaction" override func serializedMessage() -> String? { - if let richContent = richContent, let data = try? JSONEncoder().encode(richContent), let raw = String(data: data, encoding: String.Encoding.utf8) { - return raw - } else { - return nil - } + return richContentSerialized } override var transactionStatus: TransactionStatus? { diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift index fdc044f3c..51b9d8078 100644 --- a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift +++ b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift @@ -16,9 +16,26 @@ extension RichMessageTransaction { return NSFetchRequest(entityName: "RichMessageTransaction") } - @NSManaged public var richContent: [String:String]? + @NSManaged public var richContentSerialized: String? + @NSManaged public var richContent: [String: Any]? @NSManaged public var richType: String? @NSManaged public var isReply: Bool @NSManaged public var transferStatusRaw: NSNumber? - + + func isTransferReply() -> Bool { + return richContent?[RichContentKeys.reply.replyMessage] is [String: String] + } + + func getRichValue(for key: String) -> String? { + if let value = richContent?[key] as? String { + return value + } + + if let content = richContent?[RichContentKeys.reply.replyMessage] as? [String: String], + let value = content[key] { + return value + } + + return nil + } } diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index ff0dd90cd..c35be0416 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -108,28 +108,45 @@ actor AdamantChatTransactionService: ChatTransactionService { // MARK: Rich message case .richMessage: if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let type = richContent[RichContentKeys.type] { - let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) + let richContent = RichMessageTools.richContent(from: data), + let type = richContent[RichContentKeys.type] as? String, + type != RichContentKeys.reply.reply { + let trs = RichMessageTransaction( + entity: RichMessageTransaction.entity(), + insertInto: context + ) + trs.richContent = richContent trs.richType = type trs.isReply = false trs.transactionStatus = richProviders[type] != nil ? .notInitiated : nil messageTransaction = trs - } else if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent["replyto_id"] != nil { - let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) + + break + } + + if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.reply.replyToId] != nil { + let trs = RichMessageTransaction( + entity: RichMessageTransaction.entity(), + insertInto: context + ) + let transferContent = richContent[RichContentKeys.reply.replyMessage] as? [String: String] + let type = (transferContent?[RichContentKeys.type] as? String) ?? RichContentKeys.reply.reply + trs.richContent = richContent - trs.richType = "reply" + trs.richType = type trs.isReply = true - trs.transactionStatus = nil - messageTransaction = trs - } else { - let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) - trs.message = decodedMessage + trs.transactionStatus = richProviders[type] != nil ? .notInitiated : nil messageTransaction = trs + + break } + + let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) + trs.message = decodedMessage + messageTransaction = trs } } else { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift index 8bf20b1a7..a71abfb2e 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift @@ -42,7 +42,7 @@ extension AdamantChatsProvider { func isTransactionUnique(_ transaction: RichMessageTransaction) -> Bool { guard let type = transaction.richType, - let hash = transaction.richContent?[RichContentKeys.transfer.hash] + let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return false } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 4fe6089b6..92bbf1189 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -678,6 +678,7 @@ extension AdamantChatsProvider { case .richMessage(let payload): transactionLocaly = try await sendRichMessageLocaly( richContent: payload.content(), + richContentSerialized: payload.serialized(), richType: payload.type, isReply: payload.isReply, senderId: loggedAccount.address, @@ -763,6 +764,7 @@ extension AdamantChatsProvider { case .richMessage(let payload): transactionLocaly = try await sendRichMessageLocaly( richContent: payload.content(), + richContentSerialized: payload.serialized(), richType: payload.type, isReply: payload.isReply, senderId: loggedAccount.address, @@ -830,7 +832,8 @@ extension AdamantChatsProvider { } private func sendRichMessageLocaly( - richContent: [String:String], + richContent: [String: Any], + richContentSerialized: String, richType: String, isReply: Bool, senderId: String, @@ -852,6 +855,7 @@ extension AdamantChatsProvider { transaction.richContent = richContent transaction.richType = richType transaction.isReply = isReply + transaction.richContentSerialized = richContentSerialized transaction.transactionStatus = richProviders[richType] != nil ? .notInitiated : nil diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 72794b965..1dc4ad05c 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -71,8 +71,8 @@ extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate private extension AdamantRichTransactionReplyService { func update(transaction: RichMessageTransaction) { Task { - guard let id = transaction.richContent?[RichContentKeys.reply.replyToId], - transaction.richContent?[RichContentKeys.reply.decodedMessage] == nil + guard let id = transaction.getRichValue(for: RichContentKeys.reply.replyToId), + transaction.getRichValue(for:RichContentKeys.reply.decodedMessage) == nil else { return } let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) @@ -137,7 +137,7 @@ private extension AdamantRichTransactionReplyService { case .richMessage: if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let type = richContent[RichContentKeys.type], + let type = richContent[RichContentKeys.type] as? String, let transfer = RichMessageTransfer(content: richContent) { let comment = !transfer.comments.isEmpty ? ": \(transfer.comments)" @@ -147,7 +147,7 @@ private extension AdamantRichTransactionReplyService { message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" } else if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let replyMessage = richContent[RichContentKeys.reply.replyMessage] { + let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? String { message = replyMessage } else { diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index aca891c57..6e53dd9a6 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -141,7 +141,7 @@ final class ChatDataSourceManager: MessagesDataSource { guard case let .transaction(model) = message?.fullModel.content else { return nil } - var newModel = ChatTransactionContainerView.Model.init( + let newModel = ChatTransactionContainerView.Model.init( id: model.value.id, isFromCurrentSender: model.value.isFromCurrentSender, content: .init( @@ -153,7 +153,10 @@ final class ChatDataSourceManager: MessagesDataSource { date: model.value.content.date, comment: model.value.content.comment, backgroundColor: model.value.content.backgroundColor, - animationId: message?.animationId ?? ""), + animationId: message?.animationId ?? "", + isReply: model.value.content.isReply, + replyMessage: model.value.content.replyMessage, + replyId: model.value.content.replyId), status: model.value.status) return newModel } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift index df0e0ee69..4bc10bd52 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift @@ -19,6 +19,9 @@ extension ChatTransactionContentView { let comment: String? let backgroundColor: ChatMessageBackgroundColor var animationId: String + var isReply: Bool + var replyMessage: NSAttributedString + var replyId: String static let `default` = Self( id: "", @@ -29,7 +32,10 @@ extension ChatTransactionContentView { date: .init(), comment: nil, backgroundColor: .failed, - animationId: "" + animationId: "", + isReply: false, + replyMessage: NSAttributedString(string: ""), + replyId: "" ) } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index 128652f25..af528a004 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -34,6 +34,41 @@ final class ChatTransactionContentView: UIView { numberOfLines: .zero ) + var replyViewDynamicHeight: CGFloat { + model.isReply ? replyViewHeight : 0 + } + + private var replyMessageLabel = UILabel() + + private lazy var replyView: UIView = { + let view = UIView() + view.backgroundColor = .lightGray.withAlphaComponent(0.15) + view.layer.cornerRadius = 5 + + let colorView = UIView() + colorView.layer.cornerRadius = 2 + colorView.backgroundColor = .adamant.active + + view.addSubview(colorView) + view.addSubview(replyMessageLabel) + + replyMessageLabel.numberOfLines = 1 + + colorView.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.width.equalTo(2) + } + replyMessageLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().offset(-5) + $0.leading.equalTo(colorView.snp.trailing).offset(3) + } + view.snp.makeConstraints { make in + make.height.equalTo(replyViewDynamicHeight) + } + return view + }() + private let iconView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFit @@ -65,7 +100,7 @@ final class ChatTransactionContentView: UIView { }() private lazy var verticalStack: UIStackView = { - let stack = UIStackView(arrangedSubviews: [titleLabel, moneyInfoView, dateLabel, commentLabel]) + let stack = UIStackView(arrangedSubviews: [replyView, titleLabel, moneyInfoView, dateLabel, commentLabel]) stack.axis = .vertical stack.spacing = verticalStackSpacing return stack @@ -109,12 +144,16 @@ extension ChatTransactionContentView.Model { context: nil ).height ?? .zero + let replyViewDynamicHeight: CGFloat = isReply ? replyViewHeight : 0 + let stackSpacingCount: CGFloat = isReply ? 4 : 3 + return verticalInsets * 2 - + verticalStackSpacing * 3 + + verticalStackSpacing * stackSpacingCount + iconSize + titleHeight + dateHeight + commentHeight + + replyViewDynamicHeight } } @@ -146,9 +185,33 @@ private extension ChatTransactionContentView { dateLabel.text = model.date commentLabel.text = model.comment commentLabel.isHidden = model.comment == nil + + if model.isReply { + replyMessageLabel.attributedText = model.replyMessage + } else { + replyMessageLabel.attributedText = nil + } + + replyView.snp.updateConstraints { make in + make.height.equalTo(replyViewDynamicHeight) + } } - @objc func didTap() { + @objc func didTap(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + if replyView.frame.contains(touchLocation) { + actionHandler(.scrollTo(message: .init( + id: model.id, + replyId: model.replyId, + message: NSAttributedString(string: ""), + messageReply: NSAttributedString(string: ""), + backgroundColor: .failed, + animationId: "" + ))) + return + } + actionHandler(.openTransactionDetails(id: model.id)) } } @@ -159,3 +222,4 @@ private let commentFont = UIFont.systemFont(ofSize: 14) private let iconSize: CGFloat = 55 private let verticalStackSpacing: CGFloat = 6 private let verticalInsets: CGFloat = 8 +private let replyViewHeight: CGFloat = 25 diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 46b78ec51..dc92bd91c 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -121,7 +121,8 @@ private extension ChatMessageFactory { case let transaction as MessageTransaction: return makeContent(transaction) case let transaction as RichMessageTransaction: - if transaction.isReply { + if transaction.isReply, + !transaction.isTransferReply() { return makeContent( transaction, backgroundColor: backgroundColor, @@ -158,14 +159,13 @@ private extension ChatMessageFactory { backgroundColor: ChatMessageBackgroundColor, animationId: String ) -> ChatMessage.Content { - guard let content = transaction.richContent, - let replyId = content[RichContentKeys.reply.replyToId], - let replyMessage = content[RichContentKeys.reply.replyMessage] + guard let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId), + let replyMessage = transaction.getRichValue(for: RichContentKeys.reply.replyMessage) else { return .default } - let decodedMessage = content[RichContentKeys.reply.decodedMessage] ?? "..." + let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedMessage) ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() return .reply(.init( @@ -187,6 +187,10 @@ private extension ChatMessageFactory { guard let transfer = transaction.transfer else { return .default } let id = transaction.chatMessageId ?? "" + let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedMessage) ?? "..." + let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() + let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId) ?? "" + return .transaction(.init(value: .init( id: id, isFromCurrentSender: isFromCurrentSender, @@ -201,7 +205,10 @@ private extension ChatMessageFactory { date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", comment: transfer.comments, backgroundColor: backgroundColor, - animationId: animationId + animationId: animationId, + isReply: transaction.isTransferReply(), + replyMessage: decodedMessageMarkDown, + replyId: replyId ), status: transaction.transactionStatus ?? .notInitiated ))) @@ -231,7 +238,10 @@ private extension ChatMessageFactory { date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", comment: transaction.comment, backgroundColor: backgroundColor, - animationId: animationId + animationId: animationId, + isReply: false, + replyMessage: NSAttributedString(string: ""), + replyId: "" ), status: transaction.statusEnum.toTransactionStatus() ))) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index ec574d77e..7240cf28f 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -875,7 +875,7 @@ extension ChatListViewController { if richMessage.isReply, let content = richMessage.richContent, - let text = content[RichContentKeys.reply.replyMessage] { + let text = content[RichContentKeys.reply.replyMessage] as? String { let prefix = richMessage.isOutgoing ? "\(String.adamantLocalized.chatList.sentMessagePrefix)" diff --git a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift b/Adamant/Stories/ChatsList/ComplexTransferViewController.swift index 44eaf1a77..b9e1c730c 100644 --- a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Stories/ChatsList/ComplexTransferViewController.swift @@ -32,6 +32,7 @@ class ComplexTransferViewController: UIViewController { navigationItem.title = partner?.chatroom?.getName(addressBookService: addressBookService) } } + var replyToMessageId: String? = "1199913720300790117" override func viewDidLoad() { super.viewDidLoad() @@ -104,6 +105,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { let name = partner?.chatroom?.getName(addressBookService: addressBookService) + v.replyToMessageId = replyToMessageId v.admReportRecipient = address v.recipientIsReadonly = true v.commentsEnabled = service.commentsEnabledForRichMessages && partner?.isDummy != true diff --git a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift index e3ef90943..6de306a85 100644 --- a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift +++ b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift @@ -241,9 +241,26 @@ final class BtcTransferViewController: TransferViewControllerBase { comments: String, hash: String ) async throws { - let payload = RichMessageTransfer(type: BtcWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage - let message = AdamantMessage.richMessage(payload: payload) + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: BtcWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: BtcWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) } diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift b/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift index fc61c7d47..d313ed990 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift @@ -40,16 +40,16 @@ extension BtcWalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService else { - return + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService + else { + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -131,7 +131,8 @@ extension BtcWalletService: RichMessageProvider { } let amount: Decimal - if let amountRaw = richTransaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { amount = decimal } else { amount = 0 @@ -163,7 +164,8 @@ extension BtcWalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(BtcWalletService.currencySymbol)") } diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift index 8d86fe904..7c35b2ab2 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,7 +10,8 @@ import Foundation extension BtcWalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.richContent?[RichContentKeys.transfer.hash] else { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return .init(sentDate: nil, status: .inconsistent) } @@ -52,7 +53,8 @@ private extension BtcWalletService { else { return .inconsistent } // MARK: Check amount - if let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { guard reported == btcTransaction.amountValue else { return .inconsistent } diff --git a/Adamant/Wallets/Dash/DashTransferViewController.swift b/Adamant/Wallets/Dash/DashTransferViewController.swift index 00502c5de..afcd7f689 100644 --- a/Adamant/Wallets/Dash/DashTransferViewController.swift +++ b/Adamant/Wallets/Dash/DashTransferViewController.swift @@ -233,9 +233,26 @@ final class DashTransferViewController: TransferViewControllerBase { comments: String, hash: String ) async throws { - let payload = RichMessageTransfer(type: DashWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage - let message = AdamantMessage.richMessage(payload: payload) + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: DashWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: DashWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } chatsProvider.removeChatPositon(for: admAddress) _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) diff --git a/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift b/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift index 46a4c5ad6..7f145c84e 100644 --- a/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift @@ -40,17 +40,17 @@ extension DashWalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService, - let address = wallet?.address else { - return + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService, + let address = wallet?.address + else { + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -138,7 +138,8 @@ extension DashWalletService: RichMessageProvider { } let amount: Decimal - if let amountRaw = richTransaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { amount = decimal } else { amount = 0 @@ -174,7 +175,8 @@ extension DashWalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(DashWalletService.currencySymbol)") } diff --git a/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift index e8abf21c6..a1fce2ba4 100644 --- a/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,7 +10,8 @@ import Foundation extension DashWalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.richContent?[RichContentKeys.transfer.hash] else { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return .init(sentDate: nil, status: .inconsistent) } @@ -46,7 +47,8 @@ private extension DashWalletService { } // MARK: Check amount & address - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return .inconsistent } diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index e92a92d64..796bfb7cb 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -225,9 +225,26 @@ final class DogeTransferViewController: TransferViewControllerBase { comments: String, hash: String ) async throws { - let payload = RichMessageTransfer(type: DogeWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage - let message = AdamantMessage.richMessage(payload: payload) + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: DogeWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: DogeWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } chatsProvider.removeChatPositon(for: admAddress) _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index ab647ec53..23ec66700 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -40,17 +40,17 @@ extension DogeWalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService, - let address = wallet?.address else { - return + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService, + let address = wallet?.address + else { + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -105,7 +105,8 @@ extension DogeWalletService: RichMessageProvider { } catch { let amount: Decimal - if let amountRaw = transaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + if let amountRaw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { amount = decimal } else { amount = 0 @@ -137,7 +138,8 @@ extension DogeWalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(DogeWalletService.currencySymbol)") } diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift index 9c788f807..00e2a275d 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,7 +10,8 @@ import Foundation extension DogeWalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.richContent?[RichContentKeys.transfer.hash] else { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return .init(sentDate: nil, status: .inconsistent) } @@ -52,7 +53,7 @@ private extension DogeWalletService { // MARK: Check amount & address guard - let raw = transaction.richContent?[RichContentKeys.transfer.amount], + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return .inconsistent diff --git a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift index 2b07c97c4..717328705 100644 --- a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift +++ b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift @@ -287,9 +287,26 @@ final class ERC20TransferViewController: TransferViewControllerBase { guard let type = (self.service as? RichMessageProvider)?.dynamicRichMessageType else { return } - let payload = RichMessageTransfer(type: type, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage - let message = AdamantMessage.richMessage(payload: payload) + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: type, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: type, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) } diff --git a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift b/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift index b2c623537..3149a38fb 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift @@ -36,16 +36,16 @@ extension ERC20WalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService else { - return + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService + else { + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -97,7 +97,8 @@ extension ERC20WalletService: RichMessageProvider { vc.transaction = ethTransaction } catch { let amount: Decimal - if let amountRaw = transaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + if let amountRaw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { amount = decimal } else { amount = 0 @@ -130,7 +131,8 @@ extension ERC20WalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(self.tokenSymbol)") } diff --git a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift index bd43f09e6..44e1ab528 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift @@ -12,7 +12,8 @@ import struct BigInt.BigUInt extension ERC20WalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.richContent?[RichContentKeys.transfer.hash] else { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return .init(sentDate: nil, status: .inconsistent) } @@ -66,7 +67,7 @@ private extension ERC20WalletService { // MARK: Compare amounts guard - let raw = transaction.richContent?[RichContentKeys.transfer.amount], + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return .inconsistent diff --git a/Adamant/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Wallets/Ethereum/EthTransferViewController.swift index d62d7f8df..fa82aa600 100644 --- a/Adamant/Wallets/Ethereum/EthTransferViewController.swift +++ b/Adamant/Wallets/Ethereum/EthTransferViewController.swift @@ -278,9 +278,26 @@ final class EthTransferViewController: TransferViewControllerBase { comments: String, hash: String ) async throws { - let payload = RichMessageTransfer(type: EthWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage - let message = AdamantMessage.richMessage(payload: payload) + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: EthWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: EthWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } chatsProvider.removeChatPositon(for: admAddress) _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift index fcd6217b2..d9cd3a1be 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift @@ -40,16 +40,16 @@ extension EthWalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService else { - return + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService + else { + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -104,7 +104,7 @@ extension EthWalletService: RichMessageProvider { var amount: Decimal = .zero if - let amountRaw = transaction.richContent?[RichContentKeys.transfer.amount], + let amountRaw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let decimal = Decimal(string: amountRaw) { amount = decimal @@ -137,7 +137,8 @@ extension EthWalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(EthWalletService.currencySymbol)") } diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift index 1ee76da49..ceaa13375 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift @@ -14,7 +14,7 @@ extension EthWalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { guard let web3 = await web3, - let hash = transaction.richContent?[RichContentKeys.transfer.hash] + let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return .init(sentDate: nil, status: .inconsistent) } @@ -112,7 +112,8 @@ private extension EthWalletService { // MARK: Compare amounts let realAmount = eth.value.asDecimal(exponent: EthWalletService.currencyExponent) - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reported = AdamantBalanceFormat.deserializeBalance(from: raw) else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reported = AdamantBalanceFormat.deserializeBalance(from: raw) else { return .inconsistent } let min = reported - reported*0.005 diff --git a/Adamant/Wallets/Lisk/LskTransferViewController.swift b/Adamant/Wallets/Lisk/LskTransferViewController.swift index d618303b3..bb3b0a23b 100644 --- a/Adamant/Wallets/Lisk/LskTransferViewController.swift +++ b/Adamant/Wallets/Lisk/LskTransferViewController.swift @@ -241,10 +241,27 @@ final class LskTransferViewController: TransferViewControllerBase { comments: String, hash: String ) async throws { - let payload = RichMessageTransfer(type: LskWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message: AdamantMessage + + if let replyToMessageId = replyToMessageId { + let payload = RichTransferReply( + replyto_id: replyToMessageId, + type: LskWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } else { + let payload = RichMessageTransfer( + type: LskWalletService.richMessageType, + amount: amount, + hash: hash, + comments: comments + ) + message = AdamantMessage.richMessage(payload: payload) + } - let message = AdamantMessage.richMessage(payload: payload) - chatsProvider.removeChatPositon(for: admAddress) _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) } diff --git a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift b/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift index 7d4472956..5d0a6e7df 100644 --- a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift @@ -41,16 +41,18 @@ extension LskWalletService: RichMessageProvider { @MainActor func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { // MARK: 0. Prepare - guard let richContent = transaction.richContent, - let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService else { - return + print("tapped") + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let dialogService = dialogService + else { + print("tapped error") + return } dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let comment: String? - if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil @@ -152,7 +154,8 @@ extension LskWalletService: RichMessageProvider { vc.comment = comment let amount: Decimal - if let amountRaw = richTransaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { amount = decimal } else { amount = 0 @@ -179,7 +182,8 @@ extension LskWalletService: RichMessageProvider { func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { return NSAttributedString(string: "⬅️ \(LskWalletService.currencySymbol)") } diff --git a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift index d9056a305..df9bfcb2b 100644 --- a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift @@ -11,7 +11,8 @@ import LiskKit extension LskWalletService: RichMessageProviderWithStatusCheck { func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.richContent?[RichContentKeys.transfer.hash] else { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return .init(sentDate: nil, status: .inconsistent) } @@ -67,7 +68,8 @@ private extension LskWalletService { } // MARK: Check amount - if let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { let min = reported - reported*0.005 let max = reported + reported*0.005 diff --git a/Adamant/Wallets/TransferViewControllerBase.swift b/Adamant/Wallets/TransferViewControllerBase.swift index 94b4b3654..c1d5b66ec 100644 --- a/Adamant/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Wallets/TransferViewControllerBase.swift @@ -139,6 +139,7 @@ class TransferViewControllerBase: FormViewController { var commentsEnabled: Bool = false var rootCoinBalance: Decimal? var isNeedAddFeeToTotal: Bool { true } + var replyToMessageId: String? var service: WalletServiceWithSend? { didSet { diff --git a/AdamantShared/Helpers/RichMessageTools.swift b/AdamantShared/Helpers/RichMessageTools.swift index 984f5329a..cc8969237 100644 --- a/AdamantShared/Helpers/RichMessageTools.swift +++ b/AdamantShared/Helpers/RichMessageTools.swift @@ -9,7 +9,7 @@ import Foundation struct RichMessageTools { - static func richContent(from data: Data) -> [String:String]? { + static func richContent(from data: Data) -> [String: Any]? { guard let jsonRaw = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil } @@ -29,7 +29,7 @@ struct RichMessageTools { json[RichContentKeys.type] = key.lowercased() } - var fixedJson = [String:String]() + var fixedJson: [String: Any] = [:] let formatter = AdamantBalanceFormat.rawNumberDotFormatter formatter.decimalSeparator = "." @@ -39,8 +39,10 @@ struct RichMessageTools { fixedJson[key] = value } else if let value = raw as? NSNumber, let amount = formatter.string(from: value) { fixedJson[key] = amount + } else if let value = raw as? [String: String] { + fixedJson[key] = value } else { - fixedJson[key] = String(describing: raw) + fixedJson[key] = raw } } diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index e8b0a7a1a..7cd6f4098 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -10,11 +10,11 @@ import Foundation // MARK: - RichMessage -protocol RichMessage: Codable { +protocol RichMessage: Encodable { var type: String { get } var isReply: Bool { get } - func content() -> [String:String] + func content() -> [String: Any] func serialized() -> String } @@ -42,14 +42,54 @@ struct RichMessageReply: RichMessage { var replyto_id: String var reply_message: String + enum CodingKeys: String, CodingKey { + case replyto_id, reply_message + } + init(replyto_id: String, reply_message: String) { - self.type = "reply" + self.type = RichContentKeys.reply.reply self.replyto_id = replyto_id self.reply_message = reply_message self.isReply = true } - func content() -> [String : String] { + func content() -> [String: Any] { + return [ + RichContentKeys.reply.replyToId: replyto_id, + RichContentKeys.reply.replyMessage: reply_message + ] + } +} + +struct RichTransferReply: RichMessage { + var type: String + var isReply: Bool + var replyto_id: String + var reply_message: [String: String] + + enum CodingKeys: String, CodingKey { + case replyto_id, reply_message + } + + init( + replyto_id: String, + type: String, + amount: Decimal, + hash: String, + comments: String + ) { + self.type = type + self.replyto_id = replyto_id + self.reply_message = [ + RichMessageTransfer.CodingKeys.type.stringValue: type, + RichMessageTransfer.CodingKeys.amount.stringValue: RichMessageTransfer.serialize(balance: amount), + RichMessageTransfer.CodingKeys.hash.stringValue: hash, + RichMessageTransfer.CodingKeys.comments.stringValue: comments + ] + self.isReply = true + } + + func content() -> [String: Any] { return [ RichContentKeys.reply.replyToId: replyto_id, RichContentKeys.reply.replyMessage: reply_message @@ -66,7 +106,7 @@ struct RichMessageTransfer: RichMessage { let comments: String var isReply: Bool - func content() -> [String:String] { + func content() -> [String: Any] { return [ CodingKeys.type.stringValue: type, CodingKeys.amount.stringValue: RichMessageTransfer.serialize(balance: amount), @@ -83,6 +123,18 @@ struct RichMessageTransfer: RichMessage { self.isReply = false } + init?(content: [String: Any]) { + if let content = content[RichContentKeys.reply.replyMessage] as? [String: String] { + self.init(content: content) + } else { + guard let content = content as? [String: String] else { + return nil + } + + self.init(content: content) + } + } + init?(content: [String:String]) { guard let type = content[CodingKeys.type.stringValue] else { return nil @@ -131,6 +183,7 @@ extension RichContentKeys { } enum reply { + static let reply = "reply" static let replyToId = "replyto_id" static let replyMessage = "reply_message" static let decodedMessage = "decodedMessage" diff --git a/AdamantShared/ProvidersCore/RichMessageNotificationProvider.swift b/AdamantShared/ProvidersCore/RichMessageNotificationProvider.swift index d699877a6..73c174512 100644 --- a/AdamantShared/ProvidersCore/RichMessageNotificationProvider.swift +++ b/AdamantShared/ProvidersCore/RichMessageNotificationProvider.swift @@ -11,7 +11,7 @@ import UIKit protocol RichMessageNotificationProvider { static var richMessageType: String { get } - func notificationContent(for transaction: Transaction, partnerAddress: String, partnerName: String?, richContent: [String:String]) -> NotificationContent? + func notificationContent(for transaction: Transaction, partnerAddress: String, partnerName: String?, richContent: [String: Any]) -> NotificationContent? } protocol TransferNotificationContentProvider: RichMessageNotificationProvider { diff --git a/AdamantShared/RichMessageProviders/TransferBaseProvider.swift b/AdamantShared/RichMessageProviders/TransferBaseProvider.swift index 331ab67b6..02ed0c773 100644 --- a/AdamantShared/RichMessageProviders/TransferBaseProvider.swift +++ b/AdamantShared/RichMessageProviders/TransferBaseProvider.swift @@ -13,13 +13,20 @@ import MarkdownKit class TransferBaseProvider: TransferNotificationContentProvider { /// Create notification content for Rich messages - func notificationContent(for transaction: Transaction, partnerAddress: String, partnerName: String?, richContent: [String:String]) -> NotificationContent? { - guard let amountRaw = richContent[RichContentKeys.transfer.amount], let amount = Decimal(string: amountRaw) else { + func notificationContent( + for transaction: Transaction, + partnerAddress: String, + partnerName: String?, + richContent: [String: Any] + ) -> NotificationContent? { + guard let amountRaw = richContent[RichContentKeys.transfer.amount] as? String, + let amount = Decimal(string: amountRaw) else { return nil } let comment: String? - if let raw = richContent[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = richContent[RichContentKeys.transfer.comments] as? String, + raw.count > 0 { comment = raw } else { comment = nil diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index cb435167d..724ec062f 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -146,7 +146,7 @@ class NotificationService: UNNotificationServiceExtension { case .richMessage: guard let data = message.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let key = richContent[RichContentKeys.type]?.lowercased(), + let key = (richContent[RichContentKeys.type] as? String)?.lowercased(), let provider = richMessageProviders[key], let content = provider.notificationContent(for: transaction, partnerAddress: partnerAddress, partnerName: partnerName, richContent: richContent) else { diff --git a/TransferNotificationContentExtension/NotificationViewController.swift b/TransferNotificationContentExtension/NotificationViewController.swift index 84414ca27..92b8b0934 100644 --- a/TransferNotificationContentExtension/NotificationViewController.swift +++ b/TransferNotificationContentExtension/NotificationViewController.swift @@ -189,7 +189,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi case .richMessage: guard let data = message.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let key = richContent[RichContentKeys.type]?.lowercased(), + let key = (richContent[RichContentKeys.type] as? String)?.lowercased(), let p = richMessageProviders[key] else { showError() return @@ -197,13 +197,15 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi provider = p - if let raw = richContent[RichContentKeys.transfer.comments], raw.count > 0 { + if let raw = richContent[RichContentKeys.transfer.comments] as? String, + raw.count > 0 { comments = raw } else { comments = nil } - if let raw = richContent[RichContentKeys.transfer.amount], let decimal = Decimal(string: raw) { + if let raw = richContent[RichContentKeys.transfer.amount] as? String, + let decimal = Decimal(string: raw) { amount = decimal } else { amount = 0 From 44474d80bffbd6991acf6beb96b06199239b9673 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Mon, 8 May 2023 12:28:13 +0300 Subject: [PATCH 026/136] [trello.com/c/TGBBXBeX] fix: transfer reply message id between vc --- Adamant/Stories/Chat/ChatFactory.swift | 3 ++- Adamant/Stories/Chat/View/ChatViewController.swift | 7 +++++-- .../Stories/ChatsList/ComplexTransferViewController.swift | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Adamant/Stories/Chat/ChatFactory.swift b/Adamant/Stories/Chat/ChatFactory.swift index fb4496b02..20e983dd8 100644 --- a/Adamant/Stories/Chat/ChatFactory.swift +++ b/Adamant/Stories/Chat/ChatFactory.swift @@ -104,12 +104,13 @@ private extension ChatFactory { func makeSendTransactionAction( viewModel: ChatViewModel ) -> ChatViewController.SendTransaction { - { [router, viewModel] parentVC in + { [router, viewModel] parentVC, messageId in guard let vc = router.get(scene: AdamantScene.Chats.complexTransfer) as? ComplexTransferViewController else { return } vc.partner = viewModel.chatroom?.partner vc.transferDelegate = parentVC + vc.replyToMessageId = messageId let navigator = UINavigationController(rootViewController: vc) navigator.modalPresentationStyle = .overFullScreen diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index 225654ab4..f98a79b89 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -16,7 +16,7 @@ import SnapKit final class ChatViewController: MessagesViewController { typealias SpinnerCell = MessageCellWrapper typealias TransactionCell = CollectionCellWrapper - typealias SendTransaction = (UIViewController & ComplexTransferViewControllerDelegate) -> Void + typealias SendTransaction = ( _ parentVC: UIViewController & ComplexTransferViewControllerDelegate, _ replyToMessageId: String?) -> Void // MARK: Dependencies @@ -70,7 +70,10 @@ final class ChatViewController: MessagesViewController { self.richMessageProviders = richMessageProviders self.admService = admService super.init(nibName: nil, bundle: nil) - inputBar.onAttachmentButtonTap = { [weak self] in self.map { sendTransaction($0) } } + inputBar.onAttachmentButtonTap = { [weak self] in + self.map { sendTransaction($0, viewModel.replyMessage?.id) } + self?.processSwipeMessage(nil) + } } required init?(coder: NSCoder) { diff --git a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift b/Adamant/Stories/ChatsList/ComplexTransferViewController.swift index b9e1c730c..bac634e2d 100644 --- a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Stories/ChatsList/ComplexTransferViewController.swift @@ -32,7 +32,7 @@ class ComplexTransferViewController: UIViewController { navigationItem.title = partner?.chatroom?.getName(addressBookService: addressBookService) } } - var replyToMessageId: String? = "1199913720300790117" + var replyToMessageId: String? override func viewDidLoad() { super.viewDidLoad() From d03bb5490e3dbb830c73e995edadc2a1ddf9d470 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Mon, 8 May 2023 14:00:34 +0300 Subject: [PATCH 027/136] [trello.com/c/TGBBXBeX] fix: reply cell design --- Adamant/Helpers/UIFont+adamant.swift | 2 +- .../ChatReply/ChatMessageReplyCell.swift | 32 +++++++++++-------- .../Content/ChatTransactionContentView.swift | 14 +++++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/Adamant/Helpers/UIFont+adamant.swift b/Adamant/Helpers/UIFont+adamant.swift index d755458b7..f8d5ed6db 100644 --- a/Adamant/Helpers/UIFont+adamant.swift +++ b/Adamant/Helpers/UIFont+adamant.swift @@ -37,5 +37,5 @@ extension UIFont { } static var adamantChatDefault = UIFont.systemFont(ofSize: 17) - static var adamantChatReplyDefault = UIFont.systemFont(ofSize: 15) + static var adamantChatReplyDefault = UIFont.systemFont(ofSize: 14) } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 78ff85b09..ee874e4a1 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -23,14 +23,18 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { static let replyViewHeight: CGFloat = 25 + private lazy var colorView: UIView = { + let view = UIView() + view.clipsToBounds = true + view.backgroundColor = .adamant.active + return view + }() + private lazy var replyView: UIView = { let view = UIView() view.backgroundColor = .lightGray.withAlphaComponent(0.15) - view.layer.cornerRadius = 5 - - let colorView = UIView() - colorView.layer.cornerRadius = 2 - colorView.backgroundColor = .adamant.active + view.layer.cornerRadius = 8 + view.clipsToBounds = true view.addSubview(colorView) view.addSubview(replyMessageLabel) @@ -42,7 +46,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { replyMessageLabel.snp.makeConstraints { $0.centerY.equalToSuperview() $0.trailing.equalToSuperview().offset(-5) - $0.leading.equalTo(colorView.snp.trailing).offset(3) + $0.leading.equalTo(colorView.snp.trailing).offset(6) } view.snp.makeConstraints { make in make.height.equalTo(Self.replyViewHeight) @@ -53,7 +57,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { private lazy var verticalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [replyView, messageLabel]) stack.axis = .vertical - stack.spacing = 10 + stack.spacing = 6 return stack }() @@ -84,6 +88,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { var actionHandler: (ChatAction) -> Void = { _ in } var subscription: AnyCancellable? + private var trailingReplyViewOffset: CGFloat = 4 + // MARK: - Methods override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { @@ -122,7 +128,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { replyMessageLabel.numberOfLines = 1 verticalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(8) - $0.leading.trailing.equalToSuperview().inset(8) + $0.leading.equalToSuperview().inset(8) + $0.trailing.equalToSuperview().offset(-14) } } @@ -157,11 +164,10 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { func updateFrames() { let size = messageContainerView.frame.size messageContainerView.frame = CGRect( - origin: messageContainerView.frame.origin, - size: CGSize( - width: size.width, - height: model.contentHeight(for: size.width) - ) + x: messageContainerView.frame.origin.x - trailingReplyViewOffset, + y: messageContainerView.frame.origin.y, + width: size.width + trailingReplyViewOffset, + height: model.contentHeight(for: size.width) ) let origin = CGPoint( diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index af528a004..3ccd3712f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -40,14 +40,18 @@ final class ChatTransactionContentView: UIView { private var replyMessageLabel = UILabel() + private lazy var colorView: UIView = { + let view = UIView() + view.clipsToBounds = true + view.backgroundColor = .adamant.active + return view + }() + private lazy var replyView: UIView = { let view = UIView() view.backgroundColor = .lightGray.withAlphaComponent(0.15) view.layer.cornerRadius = 5 - - let colorView = UIView() - colorView.layer.cornerRadius = 2 - colorView.backgroundColor = .adamant.active + view.clipsToBounds = true view.addSubview(colorView) view.addSubview(replyMessageLabel) @@ -61,7 +65,7 @@ final class ChatTransactionContentView: UIView { replyMessageLabel.snp.makeConstraints { $0.centerY.equalToSuperview() $0.trailing.equalToSuperview().offset(-5) - $0.leading.equalTo(colorView.snp.trailing).offset(3) + $0.leading.equalTo(colorView.snp.trailing).offset(6) } view.snp.makeConstraints { make in make.height.equalTo(replyViewDynamicHeight) From f2bec2ff1023c8e7e5518f296e349951f4d5e71b Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 9 May 2023 18:15:12 +0300 Subject: [PATCH 028/136] [trello.com/c/TGBBXBeX] feat: send ADM transfer as reply --- .../Adamant.xcdatamodel/contents | 2 + ...ansferTransaction+CoreDataProperties.swift | 3 +- .../DataProviders/TransfersProvider.swift | 3 +- .../AdamantChatTransactionService.swift | 33 ++++- .../AdamantTransfersProvider.swift | 30 ++++- .../AdamantRichTransactionReplyService.swift | 121 +++++++++++++++--- .../Chat/ViewModel/ChatMessageFactory.swift | 14 +- .../Adamant/AdmTransferViewController.swift | 14 +- .../Adamant/AdmWalletService+Send.swift | 6 +- Adamant/Wallets/WalletService.swift | 3 +- AdamantShared/Models/RichMessage.swift | 2 +- 11 files changed, 188 insertions(+), 43 deletions(-) diff --git a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents index c64dd5263..34ea726e7 100644 --- a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents +++ b/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents @@ -67,5 +67,7 @@ + + \ No newline at end of file diff --git a/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift b/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift index f130c41cf..9270b2e88 100644 --- a/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift +++ b/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift @@ -17,5 +17,6 @@ extension TransferTransaction { } @NSManaged public var comment: String? - + @NSManaged public var replyToId: String? + @NSManaged public var decodedReplyMessage: String? } diff --git a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift index fb47e2715..d9ce18114 100644 --- a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift @@ -155,7 +155,8 @@ protocol TransfersProvider: DataProvider, Actor { func transferFunds( toAddress recipient: String, amount: Decimal, - comment: String? + comment: String?, + replyToMessageId: String? ) async throws -> TransactionDetails // MARK: - Transactions diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index c35be0416..be4299485 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -110,7 +110,8 @@ actor AdamantChatTransactionService: ChatTransactionService { if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), let type = richContent[RichContentKeys.type] as? String, - type != RichContentKeys.reply.reply { + type != RichContentKeys.reply.reply, + richContent[RichContentKeys.reply.replyToId] == nil { let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context @@ -127,7 +128,35 @@ actor AdamantChatTransactionService: ChatTransactionService { if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.reply.replyToId] != nil { + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount > 0 { + + let trs: TransferTransaction + + if let trsDB = getTransfer( + id: String(transaction.id), + context: context + ) { + trs = trsDB + } else { + trs = TransferTransaction( + entity: TransferTransaction.entity(), + insertInto: context + ) + } + + trs.comment = richContent[RichContentKeys.reply.replyMessage] as? String + trs.replyToId = richContent[RichContentKeys.reply.replyToId] as? String + + messageTransaction = trs + print("find adm rich, id=\(trs.replyToId), com = \(trs.comment)") + break + } + + if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount <= 0 { let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index 12dc2c88f..c92d74748 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -375,14 +375,16 @@ extension AdamantTransfersProvider { func transferFunds( toAddress recipient: String, amount: Decimal, - comment: String? + comment: String?, + replyToMessageId: String? ) async throws -> TransactionDetails { - if let comment = comment, - comment.count > 0 { + let comment = comment ?? "" + if !comment.isEmpty || replyToMessageId != nil { return try await transferFundsInternal( toAddress: recipient, amount: amount, - comment: comment + comment: comment, + replyToMessageId: replyToMessageId ) } @@ -395,7 +397,8 @@ extension AdamantTransfersProvider { private func transferFundsInternal( toAddress recipient: String, amount: Decimal, - comment: String + comment: String, + replyToMessageId: String? ) async throws -> TransactionDetails { // MARK: 0. Prepare guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { @@ -455,6 +458,7 @@ extension AdamantTransfersProvider { transaction.fee = Self.transferFee as NSDecimalNumber transaction.partner = partner transaction.transactionId = UUID().uuidString + transaction.replyToId = replyToMessageId chatroom.addToTransactions(transaction) @@ -478,8 +482,15 @@ extension AdamantTransfersProvider { } // MARK: 6. Encode + + let asset = replyToMessageId == nil + ? comment + : RichMessageReply( + replyto_id: replyToMessageId ?? "", + reply_message: comment).serialized() + guard let encodedMessage = adamantCore.encodeMessage( - comment, + asset, recipientPublicKey: recipientPublicKey, privateKey: keypair.privateKey) else { @@ -487,12 +498,17 @@ extension AdamantTransfersProvider { } // MARK: 7. Send + + let type: ChatType = replyToMessageId == nil + ? .message + : .richMessage + let signedTransaction = apiService.createSendTransaction( senderId: loggedAccount.address, recipientId: recipient, keypair: keypair, message: encodedMessage.message, - type: ChatType.message, + type: type, nonce: encodedMessage.nonce, amount: amount ) diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 1dc4ad05c..4ea52cc8e 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -16,7 +16,8 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService private let accountService: AccountService private var richMessageProvider: [String: RichMessageProvider] = [:] - private lazy var controller = getRichTransactionsController() + private lazy var richController = getRichTransactionsController() + private lazy var transferController = getTransferController() private let unknownErrorMessage = "UNKNOWN" init( @@ -35,9 +36,13 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService } func startObserving() { - controller.delegate = self - try? controller.performFetch() - controller.fetchedObjects?.forEach( update(transaction:) ) + richController.delegate = self + try? richController.performFetch() + richController.fetchedObjects?.forEach( update(transaction:) ) + + transferController.delegate = self + try? transferController.performFetch() + transferController.fetchedObjects?.forEach( update(transaction:) ) } func makeRichMessageProviders() -> [String: RichMessageProvider] { @@ -55,24 +60,42 @@ extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate _: NSFetchedResultsController, didChange object: Any, at _: IndexPath?, - for type: NSFetchedResultsChangeType, + for type_: NSFetchedResultsChangeType, newIndexPath _: IndexPath? ) { - guard let transaction = object as? RichMessageTransaction, - transaction.isReply - else { - return + if let transaction = object as? RichMessageTransaction, + transaction.isReply { + Task { await processCoreDataChange(type: type_, transaction: transaction) } } - Task { await processCoreDataChange(type: type, transaction: transaction) } + if let transaction = object as? TransferTransaction, + transaction.replyToId != nil { + Task { await processCoreDataChange(type: type_, transaction: transaction) } + } } } private extension AdamantRichTransactionReplyService { + func update(transaction: TransferTransaction) { + Task { + guard let id = transaction.replyToId, + transaction.decodedReplyMessage == nil + else { return } + + let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) + let message = try getReplyMessage(by: transactionReply) + + setReplyMessage( + for: transaction, + message: message + ) + } + } + func update(transaction: RichMessageTransaction) { Task { guard let id = transaction.getRichValue(for: RichContentKeys.reply.replyToId), - transaction.getRichValue(for:RichContentKeys.reply.decodedMessage) == nil + transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil else { return } let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) @@ -96,11 +119,6 @@ private extension AdamantRichTransactionReplyService { throw ApiServiceError.accountNotFound } - guard let chat = transaction.asset.chat else { - let message = "\(AdmWalletService.currencySymbol) \(transaction.amount)" - return message - } - let isOut = transaction.senderId == address let publicKey: String? = isOut @@ -111,6 +129,11 @@ private extension AdamantRichTransactionReplyService { ? String.adamantLocalized.chat.transactionSent : String.adamantLocalized.chat.transactionReceived + guard let chat = transaction.asset.chat else { + let message = "\(transactionStatus) \(AdmWalletService.currencySymbol) \(transaction.amount)" + return message + } + guard let publicKey = publicKey else { return unknownErrorMessage } let decodedMessage = adamantCore.decodeMessage( @@ -137,7 +160,16 @@ private extension AdamantRichTransactionReplyService { case .richMessage: if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let type = richContent[RichContentKeys.type] as? String, + transaction.amount > 0 { + let comment = richContent[RichContentKeys.reply.replyMessage] as? String + let humanType = AdmWalletService.currencySymbol + + message = "\(transactionStatus) \(transaction.amount) \(humanType)\(comment ?? "")" + break + } + + if let data = decodedMessage.data(using: String.Encoding.utf8), + let richContent = RichMessageTools.richContent(from: data), let transfer = RichMessageTransfer(content: richContent) { let comment = !transfer.comments.isEmpty ? ": \(transfer.comments)" @@ -145,14 +177,18 @@ private extension AdamantRichTransactionReplyService { let humanType = richMessageProvider[transfer.type]?.tokenSymbol ?? transfer.type message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" - } else if let data = decodedMessage.data(using: String.Encoding.utf8), + break + } + + if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? String { message = replyMessage - } else { - message = decodedMessage + break } + + message = decodedMessage } return message @@ -170,12 +206,41 @@ private extension AdamantRichTransactionReplyService { let transaction = privateContext.object(with: transaction.objectID) as? RichMessageTransaction - transaction?.richContent?[RichContentKeys.reply.decodedMessage] = message + transaction?.richContent?[RichContentKeys.reply.decodedReplyMessage] = message + try? privateContext.save() + } + + func setReplyMessage( + for transaction: TransferTransaction, + message: String + ) { + let privateContext = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + + privateContext.parent = coreDataStack.container.viewContext + + let transaction = privateContext.object(with: transaction.objectID) + as? TransferTransaction + transaction?.decodedReplyMessage = message try? privateContext.save() } // MARK: Core Data + func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: TransferTransaction) { + switch type { + case .insert, .update: + update(transaction: transaction) + case .delete: + break + case .move: + break + @unknown default: + break + } + } + func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { switch type { case .insert, .update: @@ -202,4 +267,18 @@ private extension AdamantRichTransactionReplyService { cacheName: nil ) } + + func getTransferController() -> NSFetchedResultsController { + let request: NSFetchRequest = NSFetchRequest( + entityName: TransferTransaction.entityName + ) + + request.sortDescriptors = [] + return NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: coreDataStack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + } } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index dc92bd91c..afdb02151 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -165,7 +165,7 @@ private extension ChatMessageFactory { return .default } - let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedMessage) ?? "..." + let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() return .reply(.init( @@ -187,7 +187,7 @@ private extension ChatMessageFactory { guard let transfer = transaction.transfer else { return .default } let id = transaction.chatMessageId ?? "" - let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedMessage) ?? "..." + let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId) ?? "" @@ -222,6 +222,10 @@ private extension ChatMessageFactory { ) -> ChatMessage.Content { let id = transaction.chatMessageId ?? "" + let decodedMessage = transaction.decodedReplyMessage ?? "..." + let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() + let replyId = transaction.replyToId ?? "" + return .transaction(.init(value: .init( id: id, isFromCurrentSender: isFromCurrentSender, @@ -239,9 +243,9 @@ private extension ChatMessageFactory { comment: transaction.comment, backgroundColor: backgroundColor, animationId: animationId, - isReply: false, - replyMessage: NSAttributedString(string: ""), - replyId: "" + isReply: !replyId.isEmpty, + replyMessage: decodedMessageMarkDown, + replyId: replyId ), status: transaction.statusEnum.toTransactionStatus() ))) diff --git a/Adamant/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Wallets/Adamant/AdmTransferViewController.swift index 842981ea9..576f64114 100644 --- a/Adamant/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Wallets/Adamant/AdmTransferViewController.swift @@ -94,10 +94,20 @@ final class AdmTransferViewController: TransferViewControllerBase { } @MainActor - private func sendFundsInternal(service: AdmWalletService, recipient: String, amount: Decimal, comments: String) { + private func sendFundsInternal( + service: AdmWalletService, + recipient: String, + amount: Decimal, + comments: String + ) { Task { do { - let result = try await service.sendMoney(recipient: recipient, amount: amount, comments: comments) + let result = try await service.sendMoney( + recipient: recipient, + amount: amount, + comments: comments, + replyToMessageId: replyToMessageId + ) service.update() dialogService.dismissProgress() diff --git a/Adamant/Wallets/Adamant/AdmWalletService+Send.swift b/Adamant/Wallets/Adamant/AdmWalletService+Send.swift index 52d17962f..6bb08844d 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService+Send.swift +++ b/Adamant/Wallets/Adamant/AdmWalletService+Send.swift @@ -24,13 +24,15 @@ extension AdmWalletService: WalletServiceSimpleSend { func sendMoney( recipient: String, amount: Decimal, - comments: String + comments: String, + replyToMessageId: String? ) async throws -> TransactionDetails { do { let transaction = try await transfersProvider.transferFunds( toAddress: recipient, amount: amount, - comment: comments + comment: comments, + replyToMessageId: replyToMessageId ) return transaction diff --git a/Adamant/Wallets/WalletService.swift b/Adamant/Wallets/WalletService.swift index 4f9773fae..0ecf6f69a 100644 --- a/Adamant/Wallets/WalletService.swift +++ b/Adamant/Wallets/WalletService.swift @@ -294,7 +294,8 @@ protocol WalletServiceSimpleSend: WalletServiceWithSend { func sendMoney( recipient: String, amount: Decimal, - comments: String + comments: String, + replyToMessageId: String? ) async throws -> TransactionDetails } diff --git a/AdamantShared/Models/RichMessage.swift b/AdamantShared/Models/RichMessage.swift index 7cd6f4098..764e7c0ba 100644 --- a/AdamantShared/Models/RichMessage.swift +++ b/AdamantShared/Models/RichMessage.swift @@ -186,7 +186,7 @@ extension RichContentKeys { static let reply = "reply" static let replyToId = "replyto_id" static let replyMessage = "reply_message" - static let decodedMessage = "decodedMessage" + static let decodedReplyMessage = "decodedMessage" } } From e81478f52b40ab6a9fd6c656f62a287e26518d0d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 10 May 2023 16:49:02 +0300 Subject: [PATCH 029/136] [trello.com/c/TGBBXBeX] fix: wait for new messages to be processed (scroll to message) --- .../Chat/ViewModel/ChatViewModel.swift | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 8f06ef0e1..feed34fbd 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -58,7 +58,8 @@ final class ChatViewModel: NSObject { private(set) var sender = ChatSender.default private(set) var chatroom: Chatroom? private(set) var chatTransactions: [ChatTransaction] = [] - + private var tempCancellables = Set() + var tempOffsets: [String] = [] let didTapTransfer = ObservableSender() @@ -389,8 +390,10 @@ final class ChatViewModel: NSObject { ) } + await waitForMessage(withId: message.replyId) + scrollToMessage = (message.replyId, message.id) - animationIds[message.replyId] = UUID().uuidString + // animationIds[message.replyId] = UUID().uuidString dialog.send(.progress(false)) } catch { @@ -400,6 +403,21 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } + + func waitForMessage(withId messageId: String) async { + guard !messages.contains(where: { $0.messageId == messageId }) else { + return + } + + await withUnsafeContinuation { continuation in + $messages + .filter { $0.contains(where: { $0.messageId == messageId }) } + .sink { [weak self] _ in + self?.tempCancellables.removeAll() + continuation.resume() + }.store(in: &tempCancellables) + } + } } extension ChatViewModel: NSFetchedResultsControllerDelegate { From 3fe403160d15dfb6509bc93f2fa2c3151d9bfc2d Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 11 May 2023 13:38:52 +0300 Subject: [PATCH 030/136] [trello.com/c/TGBBXBeX] fix: animate the cell to which you scrolled --- .../Chat/View/ChatViewController.swift | 20 +++++++ .../View/Managers/ChatDataSourceManager.swift | 37 +++---------- .../ChatBaseMessage/ChatMessageCell.swift | 10 ++++ .../ChatReply/ChatMessageReplyCell.swift | 9 ++++ .../ChatTransactionCellSizeCalculator.swift | 2 +- .../ChatTransactionContainerView+Model.swift | 2 +- .../ChatTransactionContainerView.swift | 6 +++ .../Content/ChatTransactionContentView.swift | 16 ++++-- .../Chat/ViewModel/ChatMessageFactory.swift | 17 +++--- .../Chat/ViewModel/ChatViewModel.swift | 9 ++-- .../Chat/ViewModel/Models/ChatMessage.swift | 54 ++++++++++++++----- .../UIHelpers/CollectionCellWrapper.swift | 11 ++++ 12 files changed, 132 insertions(+), 61 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index f98a79b89..bb6099548 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -499,6 +499,8 @@ private extension ChatViewController { at: [.centeredVertically, .centeredHorizontally], animated: animated ) + + viewModel.needToAnimateCellIndex = index } guard !viewAppeared else { return } @@ -657,6 +659,24 @@ private extension ChatViewController { } } +// MARK: Animate cell + +extension ChatViewController { + internal override func scrollViewDidEndScrollingAnimation(_: UIScrollView) { + guard let index = viewModel.needToAnimateCellIndex else { return } + + let cell = messagesCollectionView.cellForItem(at: .init(item: .zero, section: index)) + cell?.isSelected = true + + Task { + await Task.sleep(interval: 1.0) + cell?.isSelected = false + } + + viewModel.needToAnimateCellIndex = nil + } +} + private let scrollDownButtonInset: CGFloat = 20 private let messagePadding: CGFloat = 12 private var replyAction: Bool = false diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 6e53dd9a6..24c29fe5f 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -79,17 +79,10 @@ final class ChatDataSourceManager: MessagesDataSource { guard case let .message(model) = message?.fullModel.content else { return nil } - let newModel = ChatMessageCell.Model( - id: model.id, - text: model.string, - animationId: message?.animationId ?? "" - ) - return newModel + return model.value } - let model = ChatMessageCell.Model(id: model.id, text: model.string, animationId: "") - - cell.model = model + cell.model = model.value cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.setSubscription(publisher: publisher) @@ -105,14 +98,13 @@ final class ChatDataSourceManager: MessagesDataSource { let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] - guard case var .reply(model) = message?.fullModel.content + guard case let .reply(model) = message?.fullModel.content else { return nil } - model.animationId = message?.animationId ?? "" - return model + return model.value } - cell.model = model + cell.model = model.value cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.setSubscription(publisher: publisher) @@ -141,24 +133,7 @@ final class ChatDataSourceManager: MessagesDataSource { guard case let .transaction(model) = message?.fullModel.content else { return nil } - let newModel = ChatTransactionContainerView.Model.init( - id: model.value.id, - isFromCurrentSender: model.value.isFromCurrentSender, - content: .init( - id: model.value.content.id, - title: model.value.content.title, - icon: model.value.content.icon, - amount: model.value.content.amount, - currency: model.value.content.currency, - date: model.value.content.date, - comment: model.value.content.comment, - backgroundColor: model.value.content.backgroundColor, - animationId: message?.animationId ?? "", - isReply: model.value.content.isReply, - replyMessage: model.value.content.replyMessage, - replyId: model.value.content.replyId), - status: model.value.status) - return newModel + return model.value } cell.wrappedView.model = model.value diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index 7b348ff9b..f9f03c69f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -23,10 +23,12 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { didSet { guard model != oldValue else { return } swipeView.update(model) + let isSelected = oldValue.animationId != model.animationId && !model.animationId.isEmpty && oldValue.id == model.id && !model.id.isEmpty + && !oldValue.id.isEmpty if isSelected { messageContainerView.startBlinkAnimation() @@ -34,6 +36,14 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { } } + override var isSelected: Bool { + didSet { + if isSelected { + messageContainerView.startBlinkAnimation() + } + } + } + var actionHandler: (ChatAction) -> Void = { _ in } var subscription: AnyCancellable? diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index ee874e4a1..a8321b41b 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -78,6 +78,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { && !model.animationId.isEmpty && oldValue.id == model.id && !model.id.isEmpty + && !oldValue.id.isEmpty if isSelected { messageContainerView.startBlinkAnimation() @@ -85,6 +86,14 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { } } + override var isSelected: Bool { + didSet { + if isSelected { + messageContainerView.startBlinkAnimation() + } + } + } + var actionHandler: (ChatAction) -> Void = { _ in } var subscription: AnyCancellable? diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift index a37b6d955..96252325b 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift @@ -60,7 +60,7 @@ final class ChatTextCellSizeCalculator: TextMessageSizeCalculator { let dataSource = messagesLayout.messagesDataSource let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - let contentViewHeight = model.contentHeight(for: messagesFlowLayout.itemWidth) + let contentViewHeight = model.value.contentHeight(for: messagesFlowLayout.itemWidth) let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height let messageVerticalPadding = messageContainerPadding(for: message) diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift index cd80c0517..f05b07c59 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift @@ -12,7 +12,7 @@ extension ChatTransactionContainerView { struct Model: ChatReusableViewModelProtocol, MessageModel { let id: String let isFromCurrentSender: Bool - let content: ChatTransactionContentView.Model + var content: ChatTransactionContentView.Model let status: TransactionStatus static let `default` = Self( diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index af63e1380..d429f724f 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -48,6 +48,12 @@ final class ChatTransactionContainerView: UIView, ChatModelView { return view }() + var isSelected: Bool = false { + didSet { + contentView.isSelected = isSelected + } + } + override init(frame: CGRect) { super.init(frame: frame) configure() diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index 3ccd3712f..ecd551ad5 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -14,13 +14,23 @@ final class ChatTransactionContentView: UIView { didSet { guard oldValue != model else { return } let isSelected = oldValue.animationId != model.animationId + && !model.animationId.isEmpty + && oldValue.id == model.id && !model.id.isEmpty - && model.id == oldValue.id + && !oldValue.id.isEmpty update(isSelected: isSelected) } } + var isSelected: Bool = false { + didSet { + if isSelected { + startBlinkAnimation() + } + } + } + var actionHandler: (ChatAction) -> Void = { _ in } private let titleLabel = UILabel(font: titleFont, textColor: .adamant.textColor) @@ -178,10 +188,6 @@ private extension ChatTransactionContentView { } func update(isSelected: Bool) { - if isSelected { - startBlinkAnimation() - } - titleLabel.text = model.title iconView.image = model.icon amountLabel.text = String(model.amount) diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index afdb02151..2648e1ab2 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -103,8 +103,7 @@ struct ChatMessageFactory { dateHeader: dateHeaderOn ? makeDateHeader(sentDate: sentDate) : nil, - topSpinnerOn: topSpinnerOn, - animationId: animationId + topSpinnerOn: topSpinnerOn ) } } @@ -119,7 +118,7 @@ private extension ChatMessageFactory { ) -> ChatMessage.Content { switch transaction { case let transaction as MessageTransaction: - return makeContent(transaction) + return makeContent(transaction, animationId: animationId) case let transaction as RichMessageTransaction: if transaction.isReply, !transaction.isTransferReply() { @@ -148,9 +147,14 @@ private extension ChatMessageFactory { } } - func makeContent(_ transaction: MessageTransaction) -> ChatMessage.Content { + func makeContent(_ transaction: MessageTransaction, animationId: String) -> ChatMessage.Content { transaction.message.map { - .message(.init(string: Self.markdownParser.parse($0), id: transaction.txId)) + .message(.init( + value: .init( + id: transaction.txId, + text: Self.markdownParser.parse($0), + animationId: animationId) + )) } ?? .default } @@ -169,12 +173,13 @@ private extension ChatMessageFactory { let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() return .reply(.init( + value: .init( id: transaction.txId, replyId: replyId, message: Self.markdownParser.parse(replyMessage), messageReply: decodedMessageMarkDown, backgroundColor: backgroundColor, - animationId: animationId + animationId: animationId) )) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index feed34fbd..23a80f63a 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -38,10 +38,8 @@ final class ChatViewModel: NSObject { private var isLoading = false private var animationIds: [String: String] = [:] { didSet { - animationIds.forEach { (key, value) in - guard let index = messages.firstIndex(where: { $0.messageId == key }) - else { return } - messages[index].animationId = value + messages.indices.forEach { + messages[$0].animationId = animationIds[messages[$0].id] ?? "" } } } @@ -61,6 +59,7 @@ final class ChatViewModel: NSObject { private var tempCancellables = Set() var tempOffsets: [String] = [] + var needToAnimateCellIndex: Int? let didTapTransfer = ObservableSender() let dialog = ObservableSender() @@ -393,7 +392,7 @@ final class ChatViewModel: NSObject { await waitForMessage(withId: message.replyId) scrollToMessage = (message.replyId, message.id) - // animationIds[message.replyId] = UUID().uuidString + // animationIds[message.replyId] = UUID().uuidString dialog.send(.progress(false)) } catch { diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift index 9b41e44a8..9cee1084d 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift @@ -14,12 +14,11 @@ struct ChatMessage: Identifiable, Equatable { let sentDate: Date let senderModel: ChatSender let status: Status - let content: Content + var content: Content let backgroundColor: ChatMessageBackgroundColor let bottomString: ComparableAttributedString? let dateHeader: ComparableAttributedString? let topSpinnerOn: Bool - var animationId: String static let `default` = Self( id: "", @@ -30,8 +29,7 @@ struct ChatMessage: Identifiable, Equatable { backgroundColor: .failed, bottomString: nil, dateHeader: nil, - topSpinnerOn: false, - animationId: "" + topSpinnerOn: false ) } @@ -49,11 +47,11 @@ extension ChatMessage { } enum Content: Equatable { - case message(ComparableAttributedString) + case message(EqualWrapper) case transaction(EqualWrapper) - case reply(ChatMessageReplyCell.Model) + case reply(EqualWrapper) - static let `default` = Self.message(.init(string: .init())) + static let `default` = Self.message(.init(value: .default)) } } @@ -63,14 +61,14 @@ extension ChatMessage: MessageType { var kind: MessageKind { switch content { - case let .message(text): - return .attributedText(text.string) + case let .message(model): + return .attributedText(model.value.text) case let .transaction(model): return .custom(model) case let .reply(model): - let message = model.message.string.count > model.messageReply.string.count - ? model.message - : model.messageReply + let message = model.value.message.string.count > model.value.messageReply.string.count + ? model.value.message + : model.value.messageReply return .attributedText(message) } } @@ -87,3 +85,35 @@ extension MessageType { } } } + +extension ChatMessage { + var animationId: String { + get { + switch content { + case let .message(model): + return model.value.animationId + case let .reply(model): + return model.value.animationId + case let .transaction(model): + return model.value.content.animationId + } + } + + set { + switch content { + case let .message(model): + var model = model.value + model.animationId = newValue + content = .message(.init(value: model)) + case let .reply(model): + var model = model.value + model.animationId = newValue + content = .reply(.init(value: model)) + case let .transaction(model): + var model = model.value + model.content.animationId = newValue + content = .transaction(.init(value: model)) + } + } + } +} diff --git a/AdamantShared/Helpers/UIHelpers/CollectionCellWrapper.swift b/AdamantShared/Helpers/UIHelpers/CollectionCellWrapper.swift index 72339e5b8..2c092a8d6 100644 --- a/AdamantShared/Helpers/UIHelpers/CollectionCellWrapper.swift +++ b/AdamantShared/Helpers/UIHelpers/CollectionCellWrapper.swift @@ -25,6 +25,17 @@ final class CollectionCellWrapper: UICollectionViewCell { override func prepareForReuse() { wrappedView.prepareForReuse() } + + override var isSelected: Bool { + didSet { + guard let view = wrappedView as? ChatTransactionContainerView else { + wrappedView.startBlinkAnimation() + return + } + + view.isSelected = isSelected + } + } } private extension CollectionCellWrapper { From 41475db1563d20ac3755436866e786ef2684f6e5 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 11 May 2023 14:36:56 +0300 Subject: [PATCH 031/136] [trello.com/c/TGBBXBeX] fix: recalculate scroll to bottom offset --- .../Chat/View/ChatViewController.swift | 8 ++--- .../Chat/ViewModel/ChatViewModel.swift | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index bb6099548..da20f13f3 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -302,10 +302,7 @@ private extension ChatViewController { else { return } if self?.isScrollPositionNearlyTheBottom != true { - if let index = self?.viewModel.tempOffsets.firstIndex(of: fromId) { - self?.viewModel.tempOffsets.remove(at: index) - } - self?.viewModel.tempOffsets.append(fromId) + self?.viewModel.appendTempOffset(fromId, toId: toId) } self?.scrollToPosition(.messageId(toId), animated: true) } @@ -419,7 +416,8 @@ private extension ChatViewController { func makeScrollDownButton() -> ChatScrollDownButton { let button = ChatScrollDownButton() button.action = { [weak self] in - guard let id = self?.viewModel.tempOffsets.popLast() else { + guard let id = self?.viewModel.getTempOffset(visibleIndex: self?.messagesCollectionView.indexPathsForVisibleItems.last?.section) + else { self?.messagesCollectionView.scrollToBottom(animated: true) return } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 23a80f63a..f30c58447 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -57,6 +57,7 @@ final class ChatViewModel: NSObject { private(set) var chatroom: Chatroom? private(set) var chatTransactions: [ChatTransaction] = [] private var tempCancellables = Set() + private var minDiffCountForOffset = 5 var tempOffsets: [String] = [] var needToAnimateCellIndex: Int? @@ -419,6 +420,35 @@ final class ChatViewModel: NSObject { } } +extension ChatViewModel { + func getTempOffset(visibleIndex: Int?) -> String? { + let lastId = tempOffsets.popLast() + + guard let visibleIndex = visibleIndex, + let index = messages.firstIndex(where: { $0.messageId == lastId }) + else { + return lastId + } + + return index > visibleIndex ? lastId : nil + } + + func appendTempOffset(_ id: String, toId: String) { + guard let indexFrom = messages.firstIndex(where: { $0.messageId == id }), + let indexTo = messages.firstIndex(where: { $0.messageId == toId }), + (indexFrom - indexTo) >= minDiffCountForOffset + else { + return + } + + if let index = tempOffsets.firstIndex(of: id) { + tempOffsets.remove(at: index) + } + + tempOffsets.append(id) + } +} + extension ChatViewModel: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_: NSFetchedResultsController) { updateTransactions(performFetch: false) From b9485517315c8c216caf676b45f03e94641bca9c Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 09:59:13 +0300 Subject: [PATCH 032/136] [trello.com/c/NPxRWu5G] Replaced azbit & twitter logo --- .../Row/azbit_logo.imageset/Contents.json | 9 ++++++--- .../azbit_logo.imageset/azbit-logo-3_1_42x42.png | Bin 1093 -> 0 bytes .../azbit_logo.imageset/azbit-logo-3_21x21.png | Bin 659 -> 0 bytes .../azbit_logo.imageset/azbit-logo-3_2_63x63.png | Bin 1443 -> 0 bytes .../Row/azbit_logo.imageset/azbit-logo.png | Bin 0 -> 442 bytes .../Row/azbit_logo.imageset/azbit-logo@2x.png | Bin 0 -> 798 bytes .../Row/azbit_logo.imageset/azbit-logo@3x.png | Bin 0 -> 1372 bytes .../Row/row_twitter.imageset/Contents.json | 8 ++++---- .../Row/row_twitter.imageset/twitter-16.png | Bin 1328 -> 0 bytes .../Row/row_twitter.imageset/twitter-17.png | Bin 1721 -> 0 bytes .../Row/row_twitter.imageset/twitter-18.png | Bin 3258 -> 0 bytes .../Row/row_twitter.imageset/twitter.png | Bin 0 -> 727 bytes .../Row/row_twitter.imageset/twitter@2x.png | Bin 0 -> 1295 bytes .../Row/row_twitter.imageset/twitter@3x.png | Bin 0 -> 1913 bytes 14 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_1_42x42.png delete mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_21x21.png delete mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_2_63x63.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo@3x.png delete mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter-16.png delete mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter-17.png delete mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter-18.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@3x.png diff --git a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/Contents.json index 4200d4b36..0f6614ca9 100644 --- a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/Contents.json +++ b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "azbit-logo-3_21x21.png", + "filename" : "azbit-logo.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "azbit-logo-3_1_42x42.png", + "filename" : "azbit-logo@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "azbit-logo-3_2_63x63.png", + "filename" : "azbit-logo@3x.png", "idiom" : "universal", "scale" : "3x" } @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_1_42x42.png b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_1_42x42.png deleted file mode 100644 index da2d3ed9686381e2ba6f5bf16c743f0cd832c37d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1093 zcmeAS@N?(olHy`uVBq!ia0vp^S|H590wnWKF@Bw^J?Zb-B0oW3YEz_(L`$RZ#b!H~APFzYWvVOJel^%!{{F?fb3W;E&+ZhT zt9xJm&i478-CTi38E%|To8@?Xt%jkZ>AK0jg39j_ug;if<8|X)VT1X9{wxNSAb+6% z->{zZ9}jGqQOl4Lw76CM6JHj?nnisqrwekz$~{fkE-Swgtg>3(S$w>dafRYRm$`>* zUqpWnJ;@jCvudZrxt+Hh{xjt~da`K6hiyCAx6TNeyj4SMqSd51#j!gAZ>H^vKH}_j ze)$pR;KMRkCn?tGzZBTzCY$NE^VZCF&sJPW-*HCO&T}8vbRXWyz4J{TzFaz6G0$V- z)-}b)Leplcuf6Th^oJ=*Qe>M<@!sj}p{@~1Usbq&JI`Y@IAdyjx#7jkjVi&G;>S4+ zvR}UC;iy(K_1p1l(H4On$F&#s3ohZDZ@TKqH>Wd4gIi{v`uew3OvJ?GQDB4Lua^AW zo^Oj5Mf%%tb}>#km%Q@c@yfF{3|z@uJ~kEQZ9Md;Zh6nX6N1SzCh$IqUb^Vh!SvcG zcbqP1OmeH7^0`CKYi*%c>7KM>6a3fBHQ8|Ml5@#QmwZLRn-!EUzc$B zi|<`fG|lvk&d)QGPBZ@6sa&TeUZU7~$j-6h{sM{0jG(hF;!_jZZn~Mj);(=gw~k4( zWRm`)C<|8(5oYnCiQG>0#~s})95-w91a?U>$j*N!aGoJ!anssqitS!eAGkeME_f`H zbjeTDXTiq!mUGnr3XD;tNKe6rsR|xyWZnKHSH-v*p(Hp^|*kacd~^%dAqwX{BQ3+vmeM~FY)wsWq;1X!l}XXV%kGP z1_s7*PZ!4!jq|0K_GSksim-jSU)>qXoaCZqHRXcl5&oVaaryI%_q0!cKIe16IpHsY zhiB|f%g}H-A#i%$&Gb9{Nkvmm9-mWwsfDrQ(mc`aZv=cKU;gKLQ*|=4;KtT1HvYYd z`!iKL_wQI3rjhXcSoPIHofipL&Mw-g*cKRTY8zg2-So}s!uOx6Z(OK~7jlMuJ!AE-f0^p~(6zfV zHa>BFc<_Dp?Pq=dt6o%0x6w_~4{F1z*Jfd!OE=a7g%crEs3>b?pz4 z(N2qJ+-kTVb?w6muX`>3AFrKmSG6ZnhV5gL#o~2)GiTSgY)HAcZ%0cJFjiDcTq8nC}Q!>*kacih|37iAe ipaHj`Br`X)xFj*R0Jk2EzyfEW9tKZWKbLh*2~7aqHUkR) diff --git a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_2_63x63.png b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo-3_2_63x63.png deleted file mode 100644 index 30e92d92cfcf03148c302249f93b05ec4314a168..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmZ{kYc$k(7{`BqreR#xCX8DdT7!m6;~LG$-HgkSgi%S1amlPXGm~P_EH;OAE6R+u zgVt~yigl?KxfLU{?ja?~HY&z#5^c28o84EP^E}V@JkN7J=lkxP=}U4~R?t=e04Ng) zE`Cxy{z$~*dg8ISVV7EIS~woFxH9SFFgVPmO*rJ^bdO2!;OmyIHfB*KGc{2uU0zi zLOxPV()6Ewg$fM4Vv1#UhZ0k>T6zNT?e+KE;vS(A-)=;qb=0%0w7uLtYPo*FyubG% z9(Seg^)emVjq(&7IQN{nBF2lJw+co4*7mq~{)WA1o4>+b!I!dcmMbAR!str*xk3wp zSHys2U6|rb5M`w03>ySTMeg0d%b*T~0RMtWs6JD;nw|3 z?QRamf3m8T%rq~w420|kv7!1-E6GHEVRo+j%laFT;xd~^D!Ew zI^cRMeEUK;TDWs` zHO?$mJbpq5_`sokhx{?` zJKjqZh6Vjk+wOdv0mMoAN0o!X$!&LCumh9RJumyYXln;U#r(}~&vGYfwu-TdTur#{ zXuIgtJB+-~;bCjXH_jQ3fQ=i4M=F=EW!`Kjd}o$)sB!X!9fX zKBa5Rfn!z68%SGmZhrB?G1+V=kDi`UIMO=ZU-%Yg3dB?$mDYRQ6+C}Ue@v(tVYcOd z|ETiPoSA^Z3uw=2ihGTk@OVXA`D;>tpCF=bAgOE;pk5;gU_5_v(5K!8)2YOZ!n@k9 z)<6!ouYs8GpGTQ#21N~8SVuI8@#_%~#ZQq@1s~TF7qw(Hsoysk21Rka+Md(i7_ggc z%0y(EW(S`t5NWq=36od7jCj0pB7S(t#-l?91^K!ME^b3xCFW3rrCSMQzB&E7xh%-R zha^il9A=Tz&&8;1spvMe5uNvJ&a%aWAZ9_GCXOs&=^VY2hU8R~IGXExctz%2Md=ApOs;z}HKbL?)4vAmWWd!ymwl`pV=27R zPb)(G;Oy+>NR<+D)t3KzCe)N_eS7qw{O;4z;aP|~pPX`1L1nZ@cYWM=N3^9`Mog*o z(0-sZr`r6iGT2?&9CLbG&3@~UydZt@z`}<*k+ElCcg5WiKEmb4eHmz4+bVHU3_FR0D>>Tg(TfmY)vd`C76=zaW#QTAey{=!7^`Mk) zd~4nrZOE1zG(yQcqMu0U*$=PI0*YVbrOxELUwjo9K6E8!Y*ogJ75IQvZGI5kdB!`#XK!B|O-ZyBx~H5T9nRE@nGSg-VK~MoMP5b{EOyMe@O%hoGGosCqC2PL)>jd0 z-k95%QG@PFzkzwLgBdw5W_~%SQ8lBUu-^s4ImnWxlp)JCh($Tiim(f3L`VhTus91d ztfjR4u}AGJv359X3>IsL#U6lleNDgr0a_d-nws!!AdZ~iCIyWDb6`c&BABf6H2Sw0 TTdyFVlmSFnlFJ>Z(A0kcy0&sL diff --git a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo.png b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d033cf2d3f27dc5209d6924cd131597133e8c5f7 GIT binary patch literal 442 zcmV;r0Y(0aP)WKorenl6}Amf}NH7 z2Z}#1AK(wzrLqow!PpDdR>5Z5Efg%{2PoFsWGmZ+?Zj-xd*HfUhbVC~;J{@j^Eh+n z%$b`g`iJsNu^SRFr*ITkp#Z^d87uX|zy~2f!EUuPnVc!5-l$#n1q3TMNYiToa}Bnp zD}ZY0E9uz#aU8$a>-7oMPv~r#W{;}rHY|-``E^P1JKqMh(ps0q?osGiK-Wa;TU_2> zEtY$%4u!_cG)ZRAn8J3|6uVcn@Sb!u&bfE`zTvS21E16Q-A7%YvbGBk%eGo*JQ`UX zT?Vtpf(jC_#pd4n4D)!*Z7hTiACN&;tfeoHkq-s(Fldo5=mi})Li|!55G2nrY(^Ht z>5h0ajR~}V46MGEv+|*Ixx}A}w3rj^s!vAyX0hR5c*(4@zWKpQw}ePI8()jmXhP-O ki~)<#znghm0{;|X0AT}#b!8hKga7~l07*qoM6N<$f=5rqx&QzG literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo@2x.png b/Adamant/Assets/Assets.xcassets/Row/azbit_logo.imageset/azbit-logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcdc9b184448b73ce2112fa00e30cc50e6e24d3 GIT binary patch literal 798 zcmV+(1L6FMP)wv3L} zeFs%lJ*2)Xo0FFBWI@+IEG;h9xu|Z*$qsHyZt;V-@yT;hG z7&kIR&7LxL54vB$9?;9Y1OnOY6bgmI(|&)KZr@2j7EZ>3ocIFI?LmcB!^QE(*12e| zft?zL@%qQ$X9bXS3=g#93}kUOnNHU!n!mta(xk1)WO9G?>PllU9F%D?m{jQWFd{d+ z957AunWk#}(Rj2$|2+Zw6#iKI8}rM&thg%iiHMSkO2qCza+G%@3$B_XE3ve=s`Akf z6+XGuwsF;!lVbbL?Q0PV{wI=6@{ZrxWXlu>F^>FSE6y0j{0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU${z*hZRCwC#n>|q5NECo&3$PtyFo8>A zeiAd8q4o@2DqV9G+|VXzd<~gY#|_t`RM&(+t~zLvG@&GOq`3;0DZ)5yC}IK$A?*W2vDVNenk%@-e(eM~ijp#x<8oy4B z2aUFq;Zw+PjZQ?{NQyBYRBD$Xrx|Ff)eVQpo$>P9(%noaJ7F0Ep#`B zv9em;2>0}C9UUJls;b^WeRD+L@ap*mjILlXI1it@|9+VSqxJiTM@JR!4@V*^1bus` z|C}4p=fv}Xw!f+ah+<;sM&!lu$w?@a%l=7j`SfR+rVXAHPxc7*HcRF5rlKg5^pT(H zKRjn`8VPcfz;zPomyC|LaM&5VenWyb1buIr=UiodHCY=N3`W5OFh*r8w&N=6!&zHi z_b8m|3_ObO<@FUp(D#zZtgj|(BUqEqImSx(eupToL3@}mCh>87)>o6Y5wyJrLn`~2 zT$>~?-y}TH$Jgb1%v089XasnlF+yhgMQbLF1U!Axc**(Z!jUVjU+-5)Nho2I(1&mNk^+_v#KeE z1Xj0`+$chsXnk3m4V9XNKFQ+ytUE=U=5Pfx1AR%W&hQwZO`q)o@yO3mmvg%Ic6L8c zE2Yvjy7e2;MYO^sRQ$XT1<&ozpJq$t@?F&DN1_RfUm3^rA%S!rsz?W!%vd&`dx_dT zC;E%`yn_@)o`*MKU-W6MRQIEw;soVGQS8I0ZTV6YM}zogUB>FMb> zY6o=MI+MRm&}f6Rz9=5wWSkT`POQm>vyrwD=v%c&s5pDbk-pP;dXetGFpbbreXmBY zUB3tMhHSefX337SOP0QSbu!7Z`Sq>g5gDq<o5!L13#UU$rO2%vmQzuZxUNV4P9(L7lKbJi5XJ=s1hE zEB?T|RkXQP?4ZvP+OUW&GajOUK;PH+cYVd8vR5jXexmLCxKnSa=l3gtzyTbmRaKRk zI2Tj&nV`*b5kz#c@mti0N-YlmnaknP zq0istdrdO8EkK)6Qt+Am5v?scE?~L|eTD^3UFs=(iY_<)O!VoMA_i>_c|)5c^tBS& zY@@H$&}IvLt%f#R=o2{pt1hQvRBM!lHjyZk(Y5S9F`Ol`(B$9dx)FUzp-p8(^4`i^H zc>21sKW7$Ymsb&fvOt%Cf%(6ui(`n#@w*|pKGC5Pb?5d@kN$Pk-B5Z1&uN9n%BKwu z&UEPLE)JeF({S}#7UMe_g%Oh_W6sQ(C2-N@qH6TEmez)l0O89HV!mn?hb9~~IDUeu zSwfvvlj~Eccjo(B_xk1PO;QwO75;zuS$ux>=Y8iVf4=iv=D~ixO;Z=PvGuAw+i>=n z*3zXG`34UTzs&rpzB{&T+qQXYCaJVHr%vCaCLCS)+l-OHdhM#D`I?%XdkUVMIH~0s z)xT$@mGR`QU%yHH5)zU-c_kw$sUd)4M(Urshc{1ps>JPfGb} zWN0WDJ$aJQuSdVWx_*0cF?jC60FzJMN3}xlthk(Mvn|>E(dU%2FYnf$+VsinaK*ie zsr4tHsBGQ3)zEZG;*^O`o~+qxBY%Hief^#2=H~fVmo9S?zOX+kZeD@DqN0=~gUZr^ zIUz0`6I?j>Qc?`IZ83RPE5v8KQ!u(`|H`ZV-_lZ-PX1NZu55evVZiEep$+$EpKY_7 zn`g@%yrxCvpOWXD7i**Um1}D$iS1-#Gpaj({`zZqlYd*o1$L}j9p1X;+qai&dV?aen(Al`a{Emd{esv4`H>-TAl5_OOc-Z~4}! z8#>cwG#pi`)#KPF?ix7nS>-0jcPWw%r!P!c>L9|{usdQ+Q0w05`s~`AJIxikEd4+H zImNhD%A?wMHS3nBB{?#QJDY--ZpKRw|(}}N7WAv!f)K&8)M9{AZPu`$Q_S9p0O4Gn7f*Tjq%o-k9&&B zv$Cc(eC;{!KHq3&%v>4sKg+Ja-s$vmzhc>L<@+xk=lf?`m*+i|+rEc`zd+`*>Qc{M zD_Q%^fnjnEfeiEdZN%f&YfL&mYgX7h3pJneD^P{_A)Bzf-SEcPr0xV_tgT43B{|_nTX+K6Clmnni7G zr{3SX<=FaUH-fK-#yFUO)&53 zK0TRL@m8kguaaQl7*^Ids-8w7r;1{FqGaXy4R{tg1u`5I|a z0umuuBZu6UmLm5dc0`H32NDX!7m=e};l+R6%=@E%?0$E5_A@)Pv$F->o-T*v)#L#H zhX}4XU#V(-HAPveFPKYm_?kqx`g#CxMIV67900bZrOZVDE};Ne3Io8d3;>msd+k17 zdm-U&E;s;q3>^z;hDdSsr%F}YB{gxp^O!V%(+D1TxCr_UTmwn5RV@JEKt2I?hIp}W z#V^pE=%OlFxF-;qnm`UH%OgpOf{>YC!c8xTk1^OEKGv}0%?DaYC+b=H%D8&{b85yR zTt;)yL*#V3E6ErUQqHgMhvOUNWQ_(I6O3HR&nuVL{*`~GXeEi8{DW7FWGkjSS+XUn z!t}azf{aZ;Rp@bbynQ){lZj^b9ee{8ef3oLRbLE4agTqta2hsr>7IRI*>h${HSk z!7{H=bmPXIjp!yAPQN^4`GiK(0sJ}E2%WwLwU*ho-mZ6I6*Fg{_1z77V$z<-b z{!BQX^cEfJRhcY5VHMDIHEnXK>nfu9)$uI^1s&f^B2{0`{k(q3j7XC4hb;~25;C$y zFK=vIwyRQvfhYP7?*_!3J>R05?~u!o9F9RtP+M5@mG5ou5(7Cbi_ewZT@4I)Vme;p zQSa{04(7+Lg*yRGC%wZgjL#kavQ44Wvp6xBq@V5G*nb5xnQ3Sv0)w%>{xRLJ)g;3} z>pS+-hRe5nai89n692iR3*E>*FI97eT;7Hk)CtrPYT%3`&}FQW(GUtpgTDEcXrN)NQnf zWu*siA3ZF0JL^+Y*oW+IOkg6jV4ed@w@^vePv^cNplt8B%-Dht+J)-XL)-^>Y#D2j z-pkH%Q5DwPP5~A+sSEs67k)Br)f@u!t}gmRkFl)luP;nt&O@PD2nH;>_PKp?e9@?} z{76&dNgbyOilE>QPfcAR=ZACoJMq**dwRU{JQL%7IYnk*pVi>wl76uUtdl$L$#zc~aeFpL^k}J19UhRRbsPNPYdwz8eCfJ;;sZ{W> z;;d=Uo8gW=;!Qq2H3r>)oc~xT48iD_Ec;7_Ro%`8z4H#DQr9trRGTBXqu+~xSUi4^ z-6!*m?p%*9D{G6RdNJJ~Xx68sK^$&C<>pO|49~caYCZ`A)z6y7FzJ3O-ocG|j;8J^ zU+Q#+{xY_I%e`}lVKW+6bntb5KUYY>n~vo*M!xxMcCzDSY&`UOc2*|X8~%Myldx2$ zr{}Sdlj9hB3m`(AZFoUevZygFq>^ zoy%J^HC_!$`l%tXjyHOYu(~omjgyoA*{0;*_v;MySaWTGta)nroPMeKElKyK#QnQW z!y6COdsXV|34upW(W2Bl0v&Jrm4#nz2rnD=)|?M!)Aeo_#mwf91?+`p^Jc6N zMTI1yw~PC&nzm=JtluzOdnr-=E@WrOa@D;CRRU>oC?XE7;EK% zvuAsIcghzsO-%&pr~DGpgQ9s`H*{$#0NpDbY{kKl<{Ot?@V9$JV|96Zd*tXh2GB|6 zAA^#UYNuP1b+%TcFb_I2rWO{W(+XytrQfef$qF?-{l2j@1->uSFY&1uZNXMZ-?SbL ze~}hRrbXFBq((^vSfDH{ktj=~g*g#rV`ph@XJu`ILfN5E=D$U!{U4B+92pm#{`Y`W lxo}7d82)bpEiN&NN+Tzx{5_+>+*g(|fPnYJ@two2{};~Q?vDTf diff --git a/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter-18.png b/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter-18.png deleted file mode 100644 index aa4468ac6fdd9ea11eacf62e259c0dffe4a67f58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3258 zcmZ`+RalgZ7X4>n=oslnDM4yLx~028q+@^~h8jvj0Uar+GjJ57k&q5)hE9p0Q@|r7 z2Fw7Wl9%&xzwbWWhrQRX_3g*KcA}}V4mAZE1polldb*nCH>mcnL5Xj?Dc+sw25!6Q zni~N?ga81;m zP&arpbz^uRb=*xr8mwoeMY>1CNXjimV^OaM0F;S(nhz|)SAW<(wzOPgy zKN;+Bfii#WFRLcv8K3A{Q&rjMVrBUoG+eq9Hl?Vv(Rny|-hGbz^~<4ipq1I9YUe2_O(Q01q+3yYRbbtFK75OJn-QnYM)8AGIzc!xLY4bXwP(<%-)k?vc`f5-BLykZI-rhjd$}Z%P zfnjv&dHhIy92l^fWK8(N>7KsIN48d`>TtwM7!CncSRDwc9$BR%-J*e&QQz?|k$0G3_VnC7GZGeXtq*jD@`~}r) zE2Ahpj{o}JtCmj1%^c46*}57)wCJRT8ixbURBeYU&dV@88+2SX;)`j=U5YfuN00vA z>;5B9f;igdjEs(|W=ij#nWJy=_D<);K!|~jjb1dRC>yoEcRx$BGy8X|>*L>_894x{#7IN=iOw~iXZCF6+P(f7OrHL|HC^l9OQ{dkWa482C zvxH!~zJz}QjnUPgG~uZDtE>a3B1BP$)EfISOt3=}FOwKQm)Wih5vFmla4d<%GP2Kfe4fEL3E=j{6cv-`~&h)uMdX-}v=(iFhst zTas#LJ7~KEOKnCnESS||4WraEd`T-Gy;=#}A?z9vxW1nj>V{0dPyb=HZint z|A`z35F{_u-YVe?z+AwvKpvPY@$^Sultt&`CY5<-z_+P)_b7-k?^pl3kBW?@Ot)$d zm6Juql^DMsR&E}4ICbCZp)4|6I{DJ=D(bSXsj=fIP$`?r>7{I~nh|xt z{$H8kl_BKX$uCjIiP!KgYszp!aG->MW0#=HvirsEil_S%0zvoa*F$z!x6UJ7k23qz z=xx)QzvZw=jgoZvqOxx?u;C3)G+c{o@G&V60N`#86IW~q_Hy9Nrb1UtNdyZzOM#z= zqtSVlxf{RlIXW~W3+$;(q&DS08nM1$^k4a@^; zn*FK&{(afR+?*q&;aEjO&lDQjS`RJR4ys{lyS_TO4o8XxTyc zneBh0$W=BtnArQV$$hs#Vqk`)(_GH4ccIEACj*quZfPBQJ%a6uxub#LFSlIX+|pCX zvk6|Q*^OYe-rVGIZ(FSI=slHkavDl}%O}zv1bHGY%~G@A*slJdveWCsW&ur%q(QXu zCDKozZ_;DGTljYS!*h6|73k>2@9>MHd-reUVpvH@iBwdS&snlQG~e31RlV3eiWrm} zaZ_-&mm?K(I(S>%Md>2BQVo0)bMW@hwE~G)(aj7zyC-F5LmnSVY-BVmf1Wzq$*b!% zeL2BOn8&3h<-H(@$hd9#GfccMv>$HLS_gPZ1b-eRZ%HpMozdO#>UqM~mX@X|^-;Kf z_;NifOZ+UX58`U$#(O<}bs0m{NZ@)deV|%^nd>-1RRBQx0+d^jY_M*<3YP8bsy0Kb z8Z5PF43c2)zUC^Y;UbP#g zbqCyek*Bnu33j!40OI~RYXA(z#)03&1kv`w{}Oo>|6^l)B|N@C+3z})^R>EV^9oh2 zqRu3#k4qr)K#Mcx@O#(0xjW35E43)4tGvJBaEWzU<3jx2&9&}99h47%qoYxZ<+6eJ zpL+_P)vJ>Sx%Y=jjI;Z}h603~A@x4O#p9q9Mf7LaPs1Y8D1uB^Ou@E(|pI5=|k47{NgVABP2SmLy0GA zV_A@J<-67Fg#2LD)h77`>;A@(8tl#0oAq*;A2x6e6wVECK7F;ODoD0c{`h z>gXpi6YGymP68AnOjjfFsNEs?g4{fP*ZmO<4GGsh<2B^ngM<0-;ILP=bILn3#IXpu znfF84%EaNyafnJu#tV|MmgDQZuF{&&P%;1>7MCubLoW7@alU6=2l`aXxWbC8jH zjSGd6Oz8!z**aF0z8CH`pc-5(l>IY$>o{Y28bax7z_T~~V=C7-Zj4W9wntUw_xTp8 zqihC>KB&e6S}3#d_T1Gq5NI|!8U37jGXGNOZn?|wOV;GjFsJZdJxZHN_r^PKxSnz( zB!P>IeSaO_oVRBrAw5!leganyYRY~spF-K#W0RSYZHa{+?SJCt%9O8zaKacG)v(AM z^LMte(CTz$FS@l;{D7PsN?_Bwr(j_1jmP*pw!Gjo3nODDNsU8((u5(d=yp(*i%WGYtL z=R{{SqN)>wD*{U@>Fe9#DtSkD=JE61UW*P3ORY8)bx*fmOjegOFps}H-W#zOSnQB3 zf~q8>dTi|P3sO@wVvEepo@th3&^#22p7f|Zt95EE-(E>m46UcH>!KUTU2U0hhXJD# z!KkV?O}?xa*i$wWjLb;I!XWVHcbmu05qUz`&JO%*Xr|qfi7KJhx;i0wVk#8fpm3C}^_*Ay)!#=wj0|eg2OQiErnfgE zPAbmlw?=q*?Ax_IKqwHjisKiTkllT+GxaJ6@aXNzg!a+`Ic{;@MtgBV)J9NZ{9hq_ zHXYwdb`6R77A4cGX8W&B=MUDh35L4_yDPW_x!(XF36s1p3cD{VDPakdRk$yqAT22j zgDJpZavy9w|A&C|hkJQE{{IBMSD)5y2!j9J5bT9?M+Lhe1O7jUE3WU~p9Ij;GS;kB HcaHlH>;eVi diff --git a/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter.png b/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa41e3c55786958aad7b9fa7b4b1440f00522b0 GIT binary patch literal 727 zcmV;|0x127P)u+^+cNJVTcw2KQ83*rx`m{^J^Y-FKKotVPJ()16glsOWrLq(cK4RM+j z=T~FCS3RRNajZ;;lTPoR-+Le5ySsOR!2cw-7ZN%&>a`~D4T`{<;u~jRj(J?olc{9dTO@Y?QdaKy~7xzTa=+Xa}Ra8-3*3xc(ge1*r6Wby3m?9l5LyJ7e8i>uzBt<4_k|f1TN9C*+OqPR5p8bFe%|jDN!r||h{dV|3wN@)|b8no7q9~;} z=UfpyK^9rG__d-CW3C(zN)b!1&eqhZrciTNj{Qis!FE z5Uy#F$kHT0>7AUCSFYun;yf{ zXVK~Dv?#Iyin}`Q+KMc{fcc!)`Y4ox^8kN@IS=>)#>DDLqcM{88}aJ~uKkKU1aNaC zaYG$Q=QXRN_;bg1W0*d__^xD1IH? zDCxF30Q2KwEev?M%)MOn*k^Ls=i&H`g^p>)q?VsY|5W}6FaX*U_SzM~Ma=*J002ov JPDHLkV1lbtU6%j= literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@2x.png b/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3c84eb880e5f38251dd40a0b487dd92352b15025 GIT binary patch literal 1295 zcmV+q1@QWbP)658Z2oCEb7f#Sb5Uw4EwYXzRV> zefk`x)0s{oCUTS4&U^DabKkw^ejJV?`A=&Ln9f{Ln=Iu&0_s;+YU^4s-!MFjV zS93ZG;{lAxTA<s*Q;#}JU_<8H!Og!N$_=rj7_9xGd7 z01{s`!{&SC^1@DOXE9$Wq;W3!15uC16L-5@130D2CJ95Fdlcfy3fvcLGTVXCZwD+q zF9LtBNRvvgkRScJxw)iwZJQ)@huTATML~$_oE}CkBR-K9c-z6_Wo8Nu%8VJ1D0xL$ zUH`N(iW=9PN}@>s4t&;}ptntWc4JYS%XqxCy`5a$Sf96W@5>tY+b?nxRg_|J6#qyg zGjQ`9nTA=p!X9M>Bub@HN$Y_`2j2%241Vp)x3AYL)#?)WVl;&blYr0COP8$L%l98Y zQhFe9A`&GnGO$4@IDmGu{BGr1X?K_1hyceqz!8J@Maaw3R)My*w#GPqJ;zg}$X)js ze_Gt`1bQMKzhC{B0_|Sn44nu33dSui((syaI7KmP!x^(kN&JFK<7K$bGSUTTa}=CG zLG8kr0PrcfO&)PMoqfc6y^JjYETgp?)2JW_QZpl*$UK$4U`$;EPU}A2^|YhCJ<8L_ z%7^z8;2@XKn@P)xhSuT?Xt@C!H4>)LApnc9cnZcWg)a%B#|UuX9sUN?^47NM&GU?~ zy`}_pLshOk)*evhD2^gcEf5T6gOSs8$r^ zdogvGDIjPxRtpcKr!3P*wOU<6(ucKvqtMrE>FVs{*&|a+qs;5kTTg(^4IXwv-QsSE z)uN`qfD-AeaUCdVpy%YNX~w6AA(Za@=5wt$`N?KJ_ar3CNH4vh*>0cLmymJfZHof69EF<0%QR8FlhG0fTk=^#y-N@hee7ikgGDB4eFdXVgU=7A; zF=N&y_W`9&M12g#SF3&!s3yHnc;(&p)W?DXBaO04HqqM=5NMYnoFImY-KBD+tnfK%XGb{3%3Lf#oWH`XmTzYXWY!Mv3OaHA2CGKxl)KdToV5$k4B<06 zV)iQ60fzSXoMLv}z?o+BQjg?F4CJQ(0|0>ABKWgTO9TJ_002ovPDHLk FV1ir=V)Ot2 literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@3x.png b/Adamant/Assets/Assets.xcassets/Row/row_twitter.imageset/twitter@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c0cb8f56a6474f670345e77426e887357da0e91f GIT binary patch literal 1913 zcmV-<2Zs2GP)j{0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU(8%ab#RCwC#T02nORuq-AKYlXH$NX%} zGMOPQ9@3-=bjb|Zcw8loaf7RjA z$lm^b4+4#u~3wj*}m zQLCzL^>y`+6k3$*WazflhuAWfNG7Isc6VRr@-7UI)wMq#V7M3ne~wCq-`j;VhV~%l z1;yw^CnqNx=zg4HRym^KhTLX<^X7~xA6&-d zv5&`GIYs|IUP*>N5aTI;KHjc5#y(C0lu$+eySnzR7S_Mw5S-`Wv>xEyJ z>Z+O%3?^rVHQWXxg zMBFz^Q(lj!zw<_iWgXt!+6rT3fRN}r{P|8Pc~t) zY?gS2H^at$b!F9rK@632ymMqLY#Th)cy`WC4PL38y(|h~n=v#Z->iu&FVV#RJg0zX zNEz8irxeAT%`SzjI3A;a?(W5&*9U_kw%kFD?@?=N?p{&VPEZ1ty`6MAoh%jLNU!}q z|6S%^qktposIlRA{-ngsr?b@nr@C^TaCBQb+i%P=-C-0wxKl`m1kDD5ziE;<)GIaW zSSbK+YG~}?+Tx#WF+qXlgINEmCA`Vyf}-`|=M!DkFvnEqbK&;9tO{mog@kl_A|LBn&pz<|?OYwLf_6%+i`hQ?uz)QmdW zvEi}038mr)N?rmkzh+&1FTVf%=KHPRik;-Z@;|a{4uFq3qQas9pn%`GnjfeLRNSqp zu9i}%)azo@dR&#Qs}0WLw=nMh4ScJkeLA+crI>9VNM|xHK?xC1!g}U76EAe#$2f0k zYM$jLEYRI&oW9G^o*V(`Im-tS7Gs4{0yjB{lJgz{#mQg+>G`BhNI=*O6#{t2_4Y{) zxV{A?xAxcR0}xl6a6E-PfNS1IbcFZ`6`$7|IsEux!!1|8*3$YD&LVTuQ(xw@AJ3cX zgEaG_Q8*xY%n7|06ypf^PQDuvm3%*rc5tn*z%lNfZY?n@XKl?bzZd}TvznVLxaFA* zeIcWSm+yyhOHOQz_qnn4G?#22q;mJrAkI%$UbeR1Pl>jViL4DAV$xvk4<{$?y9L}9 zcv^Qyf`h9$gJ$LJ;P7xVA6G}v*qxbliaFJBA$VLyCUCk&^)47Ohk+n6c03tIh;JKCiC}EZc^2>VkFr7`4$eZpOBw zn5RtsP%__`=(cDCdg&rS+e&yuUNFhsD}W*p@KXXDEk;}Hq^k=Ok Date: Fri, 12 May 2023 11:16:40 +0300 Subject: [PATCH 033/136] [trello.com/c/2w5z2xi2] fix: Process payment links --- Adamant.xcodeproj/project.pbxproj | 154 +++++++++--------- Adamant/Utilities/AdamantCoinTools.swift | 64 ++++++++ .../Wallets/Adamant/AdmWalletService.swift | 6 +- .../Bitcoin/BtcTransferViewController.swift | 27 --- .../Wallets/Bitcoin/BtcWalletService.swift | 4 + .../Dash/DashTransferViewController.swift | 27 --- Adamant/Wallets/Dash/DashWalletService.swift | 4 + .../Doge/DogeTransferViewController.swift | 27 --- Adamant/Wallets/Doge/DogeWalletService.swift | 4 + .../ERC20/ERC20TransferViewController.swift | 27 --- .../Wallets/ERC20/ERC20WalletService.swift | 4 + .../Ethereum/EthTransferViewController.swift | 27 --- .../Wallets/Ethereum/EthWalletService.swift | 4 + .../Lisk/LskTransferViewController.swift | 27 --- Adamant/Wallets/Lisk/LskWalletService.swift | 4 + .../Wallets/TransferViewControllerBase.swift | 43 ++++- Adamant/Wallets/WalletService.swift | 1 + 17 files changed, 207 insertions(+), 247 deletions(-) create mode 100644 Adamant/Utilities/AdamantCoinTools.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index e7fbfa2a7..be3beb249 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 41E142F3289E60EB002EE8D7 /* AdamantSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E142F1289E60EB002EE8D7 /* AdamantSecret.swift */; }; 41E142F4289E60EB002EE8D7 /* AdamantSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E142F1289E60EB002EE8D7 /* AdamantSecret.swift */; }; 41E142F5289E60EB002EE8D7 /* AdamantSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E142F1289E60EB002EE8D7 /* AdamantSecret.swift */; }; + 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */; }; 4E9EE86F28CE793D008359F7 /* SafeDecimalRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */; }; 550066C5284D65DB0044C0B1 /* HealthCheckService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550066C4284D65DB0044C0B1 /* HealthCheckService.swift */; }; 550066C7284D682D0044C0B1 /* AdamantHealthCheckService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550066C6284D682D0044C0B1 /* AdamantHealthCheckService.swift */; }; @@ -86,7 +87,7 @@ 55FBAAF728C54B2F0066E629 /* Nodes+Allowance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAF428C54B230066E629 /* Nodes+Allowance.swift */; }; 55FBAAF828C54B300066E629 /* Nodes+Allowance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAF428C54B230066E629 /* Nodes+Allowance.swift */; }; 55FBAAFB28C550920066E629 /* NodesAllowanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */; }; - 6403F5DB2272389800D58779 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 6403F5DB2272389800D58779 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DD22723C6800D58779 /* DashMainnet.swift */; }; 6403F5E022723F6400D58779 /* DashWalletRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DF22723F6400D58779 /* DashWalletRouter.swift */; }; 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E122723F7500D58779 /* DashWallet.swift */; }; @@ -287,7 +288,7 @@ A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B6F262DEDE1009FA43E /* Clibsodium */; }; A5241B77262DEDEF009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B76262DEDEF009FA43E /* Clibsodium */; }; A5241B7E262DEDFE009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B7D262DEDFE009FA43E /* Clibsodium */; }; - A530B0D82842110D003F0210 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + A530B0D82842110D003F0210 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; A544F0D4262C9878001F1A6D /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = A544F0D3262C9878001F1A6D /* Eureka */; }; A57282CA262C94CD00C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282C9262C94CD00C96FA8 /* DateToolsSwift */; }; A57282D1262C94DA00C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282D0262C94DA00C96FA8 /* DateToolsSwift */; }; @@ -802,6 +803,7 @@ 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3Swift+Adamant.swift"; sourceTree = ""; }; 41CEE09529718A10005EF1D2 /* UIImage+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+adamant.swift"; sourceTree = ""; }; 41E142F1289E60EB002EE8D7 /* AdamantSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantSecret.swift; sourceTree = ""; }; + 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoinTools.swift; sourceTree = ""; }; 4A4D67BD3DC89C07D1351248 /* Pods-AdmCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AdmCore.release.xcconfig"; path = "Target Support Files/Pods-AdmCore/Pods-AdmCore.release.xcconfig"; sourceTree = ""; }; 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDecimalRow.swift; sourceTree = ""; }; 550066C4284D65DB0044C0B1 /* HealthCheckService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthCheckService.swift; sourceTree = ""; }; @@ -1325,7 +1327,7 @@ A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */, A50AEB14262C837900B37C22 /* Alamofire in Frameworks */, 938F7D582955C1DA001915CA /* MessageKit in Frameworks */, - A530B0D82842110D003F0210 /* BuildFile in Frameworks */, + A530B0D82842110D003F0210 /* (null) in Frameworks */, A5DBBADC262C729B004AC028 /* CryptoSwift in Frameworks */, A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */, A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */, @@ -2077,6 +2079,7 @@ A5BBD810262C657300B5C40C /* ByteBackpacker.swift */, E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, + 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */, 6406D74C21CE3AC100196713 /* KeyboardManager.swift */, 649E9A142111B3C200686B01 /* Mnemonic+extended.swift */, @@ -2726,25 +2729,25 @@ ); mainGroup = E913C8E51FFFA51D001A83F7; packageReferences = ( - A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */, - A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */, - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */, - A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */, - A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */, - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */, - A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */, - A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */, - A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */, - A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */, - A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */, - A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */, - A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */, - A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */, + A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */, + A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */, + A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */, + A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */, + A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, + A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */, + A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */, + A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */, + A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */, + A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */, + A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */, + A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */, + A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */, A5AC8DFD262E0B030053A7E2 /* XCRemoteSwiftPackageReference "SipHash" */, 3A8875ED27BBF38D00436195 /* XCRemoteSwiftPackageReference "Parchment" */, - 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */, - 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */, - 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */, + 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */, + 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */, + 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */, ); productRefGroup = E913C8EF1FFFA51D001A83F7 /* Products */; projectDirPath = ""; @@ -3090,6 +3093,7 @@ 93684A2A29EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, 640EFAA42558613A00E9724B /* EthProvider.swift in Sources */, + 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */, 9371130F2996EDA900F64CF9 /* ChatRefreshMock.swift in Sources */, 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */, 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */, @@ -3143,7 +3147,7 @@ A50A41112822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, - 6403F5DB2272389800D58779 /* BuildFile in Sources */, + 6403F5DB2272389800D58779 /* (null) in Sources */, 6416B1A521AEE157006089AC /* LskWalletService+Transfers.swift in Sources */, 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */, E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, @@ -4112,7 +4116,7 @@ kind = branch; }; }; - 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { + 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/socketio/socket.io-client-swift"; requirement = { @@ -4120,7 +4124,7 @@ kind = branch; }; }; - 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */ = { + 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit.git"; requirement = { @@ -4128,7 +4132,7 @@ minimumVersion = 5.0.0; }; }; - 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */ = { + 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MessageKit/MessageKit.git"; requirement = { @@ -4136,7 +4140,7 @@ minimumVersion = 4.0.0; }; }; - A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */ = { + A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EFPrefix/EFQRCode.git"; requirement = { @@ -4144,7 +4148,7 @@ minimumVersion = 6.1.0; }; }; - A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */ = { + A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/yannickl/QRCodeReader.swift.git"; requirement = { @@ -4152,7 +4156,7 @@ minimumVersion = 10.1.1; }; }; - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */ = { + A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; requirement = { @@ -4160,7 +4164,7 @@ minimumVersion = 5.4.2; }; }; - A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */ = { + A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xmartlabs/Eureka.git"; requirement = { @@ -4168,7 +4172,7 @@ minimumVersion = 5.3.3; }; }; - A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */ = { + A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/maniramezan/DateTools.git"; requirement = { @@ -4176,7 +4180,7 @@ kind = branch; }; }; - A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */ = { + A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4184,7 +4188,7 @@ minimumVersion = 4.2.2; }; }; - A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */ = { + A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RNCryptor/RNCryptor.git"; requirement = { @@ -4192,7 +4196,7 @@ minimumVersion = 5.1.0; }; }; - A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */ = { + A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/skywinder/web3swift.git"; requirement = { @@ -4208,7 +4212,7 @@ minimumVersion = 1.2.2; }; }; - A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */ = { + A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ashleymills/Reachability.swift"; requirement = { @@ -4216,7 +4220,7 @@ minimumVersion = 5.1.0; }; }; - A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */ = { + A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ProcedureKit/ProcedureKit.git"; requirement = { @@ -4224,7 +4228,7 @@ minimumVersion = 5.2.0; }; }; - A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */ = { + A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jedisct1/swift-sodium.git"; requirement = { @@ -4232,7 +4236,7 @@ minimumVersion = 0.9.1; }; }; - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */ = { + A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; requirement = { @@ -4240,7 +4244,7 @@ minimumVersion = 1.5.0; }; }; - A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */ = { + A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Swinject/Swinject.git"; requirement = { @@ -4248,7 +4252,7 @@ minimumVersion = 2.7.1; }; }; - A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */ = { + A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git"; requirement = { @@ -4266,27 +4270,27 @@ }; 416F5EA3290162EB00EF0400 /* SocketIO */ = { isa = XCSwiftPackageProductDependency; - package = 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */; + package = 416F5EA2290162EB00EF0400 /* XCRemoteSwiftPackageReference "socket" */; productName = SocketIO; }; 557AC305287B10D8004699D7 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 55D1D84E287B78F200F94A4E /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 55D1D850287B78FC00F94A4E /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit.git" */; + package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; 938F7D572955C1DA001915CA /* MessageKit */ = { isa = XCSwiftPackageProductDependency; - package = 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit.git" */; + package = 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */; productName = MessageKit; }; 93FA403529401BFC00D20DB6 /* PopupKit */ = { @@ -4295,102 +4299,102 @@ }; A50AEB03262C815200B37C22 /* EFQRCode */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode.git" */; + package = A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */; productName = EFQRCode; }; A50AEB0B262C81E300B37C22 /* QRCodeReader */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift.git" */; + package = A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */; productName = QRCodeReader; }; A50AEB13262C837900B37C22 /* Alamofire */ = { isa = XCSwiftPackageProductDependency; - package = A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire.git" */; + package = A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; A5241B6F262DEDE1009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5241B76262DEDEF009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5241B7D262DEDFE009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A544F0D3262C9878001F1A6D /* Eureka */ = { isa = XCSwiftPackageProductDependency; - package = A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka.git" */; + package = A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */; productName = Eureka; }; A57282C9262C94CD00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D0262C94DA00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D2262C94DF00C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57282D4262C94E500C96FA8 /* DateToolsSwift */ = { isa = XCSwiftPackageProductDependency; - package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools.git" */; + package = A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */; productName = DateToolsSwift; }; A57839FA262C95BF00428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A01262C95CA00428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A08262C95D000428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A0A262C95D500428183 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess.git" */; + package = A57839F9262C95BF00428183 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; A5783A0D262C964500428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A14262C965000428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A1B262C965600428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5783A1D262C965D00428183 /* RNCryptor */ = { isa = XCSwiftPackageProductDependency; - package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor.git" */; + package = A5783A0C262C964500428183 /* XCRemoteSwiftPackageReference "RNCryptor" */; productName = RNCryptor; }; A5785ADE262C63580001BC66 /* web3swift */ = { isa = XCSwiftPackageProductDependency; - package = A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift.git" */; + package = A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */; productName = web3swift; }; A5AC8DFE262E0B030053A7E2 /* SipHash */ = { @@ -4400,37 +4404,37 @@ }; A5C99E0D262C9E3A00F7B1B7 /* Reachability */ = { isa = XCSwiftPackageProductDependency; - package = A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability.swift" */; + package = A5C99E0C262C9E3A00F7B1B7 /* XCRemoteSwiftPackageReference "Reachability" */; productName = Reachability; }; A5D87BA2262CA01D00DC28F0 /* ProcedureKit */ = { isa = XCSwiftPackageProductDependency; - package = A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit.git" */; + package = A5D87BA1262CA01D00DC28F0 /* XCRemoteSwiftPackageReference "ProcedureKit" */; productName = ProcedureKit; }; A5DBBABC262C7221004AC028 /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium.git" */; + package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; A5DBBADB262C729B004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE2262C72B0004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE4262C72B7004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAE6262C72BD004AC028 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; A5DBBAED262C72EF004AC028 /* BitcoinKit */ = { @@ -4443,27 +4447,27 @@ }; A5F0A04A262C9CA90009672A /* Swinject */ = { isa = XCSwiftPackageProductDependency; - package = A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject.git" */; + package = A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */; productName = Swinject; }; A5F92993262C855B00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929AE262C857D00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929B5262C858700C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; A5F929B7262C858F00C3E60A /* MarkdownKit */ = { isa = XCSwiftPackageProductDependency; - package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit.git" */; + package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Adamant/Utilities/AdamantCoinTools.swift b/Adamant/Utilities/AdamantCoinTools.swift new file mode 100644 index 000000000..7fd684064 --- /dev/null +++ b/Adamant/Utilities/AdamantCoinTools.swift @@ -0,0 +1,64 @@ +// +// AdamantCoinTools.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 12.05.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct QQAddressInformation { + let address: String + let params: [QQAddressParams]? +} + +enum QQAddressParams { + case amount(String) + + init?(raw: String) { + let keyValue = raw.split(separator: "=") + + guard keyValue.count == 2 else { return nil } + + let key = keyValue[0] + let value = String(keyValue[1]) + + switch keyValue[0] { + case "amount": + self = .amount(value) + default: + return nil + } + } +} + +class AdamantCoinTools { + static func decode(uri: String, qqPrefix: String) -> QQAddressInformation? { + if uri.isEmpty { + return nil + } + + let request = uri.split(separator: ":") + if request.count > 2 || request[0] != qqPrefix { + return nil + } + + let addressAndParams = request[1].split(separator: "?") + guard let addressRaw = addressAndParams.first else { + return nil + } + + var params: [QQAddressParams]? = nil + if addressAndParams.count > 1 { + let p = addressAndParams[1].split(separator: "&").compactMap { + QQAddressParams(raw: String($0)) + } + + params = p.count > 0 ? p : nil + } + + let address = QQAddressInformation(address: String(addressRaw), params: params) + return address + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletService.swift b/Adamant/Wallets/Adamant/AdmWalletService.swift index 4a919edaf..03c5781ae 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService.swift +++ b/Adamant/Wallets/Adamant/AdmWalletService.swift @@ -32,7 +32,7 @@ class AdmWalletService: NSObject, WalletService { } var tokenNetworkSymbol: String { - return "ADM" + return Self.currencySymbol } var tokenContract: String { @@ -43,6 +43,10 @@ class AdmWalletService: NSObject, WalletService { return tokenNetworkSymbol + tokenSymbol } + var qqPrefix: String { + return Self.qqPrefix + } + // MARK: - Dependencies weak var accountService: AccountService? var apiService: ApiService! diff --git a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift index e3ef90943..cae81bc18 100644 --- a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift +++ b/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift @@ -208,33 +208,6 @@ final class BtcTransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("bitcoin:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - func reportTransferTo( admAddress: String, amount: Decimal, diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Wallets/Bitcoin/BtcWalletService.swift index afc8f5c0c..b2dfef9ee 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService.swift @@ -74,6 +74,10 @@ class BtcWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var qqPrefix: String { + return Self.qqPrefix + } + var wallet: WalletAccount? { return btcWallet } var walletViewController: WalletViewController { diff --git a/Adamant/Wallets/Dash/DashTransferViewController.swift b/Adamant/Wallets/Dash/DashTransferViewController.swift index 00502c5de..9bcb8da65 100644 --- a/Adamant/Wallets/Dash/DashTransferViewController.swift +++ b/Adamant/Wallets/Dash/DashTransferViewController.swift @@ -200,33 +200,6 @@ final class DashTransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("dash:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - func reportTransferTo( admAddress: String, amount: Decimal, diff --git a/Adamant/Wallets/Dash/DashWalletService.swift b/Adamant/Wallets/Dash/DashWalletService.swift index 38425fe6e..38a822e6a 100644 --- a/Adamant/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Wallets/Dash/DashWalletService.swift @@ -33,6 +33,10 @@ class DashWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var qqPrefix: String { + return Self.qqPrefix + } + var wallet: WalletAccount? { return dashWallet } var walletViewController: WalletViewController { diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index e92a92d64..00e3461e5 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -191,33 +191,6 @@ final class DogeTransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("doge:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - @MainActor func reportTransferTo( admAddress: String, diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index f9db2edd2..29df0f0a6 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -87,6 +87,10 @@ class DogeWalletService: WalletService { return DogeWalletService.fixedFee } + var qqPrefix: String { + return Self.qqPrefix + } + static let kvsAddress = "doge:address" private (set) var isWarningGasPrice = false diff --git a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift index 2b07c97c4..82eb8d433 100644 --- a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift +++ b/Adamant/Wallets/ERC20/ERC20TransferViewController.swift @@ -251,33 +251,6 @@ final class ERC20TransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("ethereum:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - func reportTransferTo( admAddress: String, amount: Decimal, diff --git a/Adamant/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Wallets/ERC20/ERC20WalletService.swift index 65c94768d..2c00dd0ff 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Wallets/ERC20/ERC20WalletService.swift @@ -61,6 +61,10 @@ class ERC20WalletService: WalletService { return token?.defaultOrdinalLevel } + var qqPrefix: String { + return EthWalletService.qqPrefix + } + private (set) var blockchainSymbol: String = "ETH" private (set) var isDynamicFee: Bool = true private (set) var transactionFee: Decimal = 0.0 diff --git a/Adamant/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Wallets/Ethereum/EthTransferViewController.swift index d62d7f8df..f205a280a 100644 --- a/Adamant/Wallets/Ethereum/EthTransferViewController.swift +++ b/Adamant/Wallets/Ethereum/EthTransferViewController.swift @@ -245,33 +245,6 @@ final class EthTransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("ethereum:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - func reportTransferTo( admAddress: String, amount: Decimal, diff --git a/Adamant/Wallets/Ethereum/EthWalletService.swift b/Adamant/Wallets/Ethereum/EthWalletService.swift index 6897be2ae..a522b7b13 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Wallets/Ethereum/EthWalletService.swift @@ -89,6 +89,10 @@ class EthWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var qqPrefix: String { + return Self.qqPrefix + } + private (set) var isDynamicFee: Bool = true private (set) var transactionFee: Decimal = 0.0 private (set) var gasPrice: BigUInt = 0 diff --git a/Adamant/Wallets/Lisk/LskTransferViewController.swift b/Adamant/Wallets/Lisk/LskTransferViewController.swift index d618303b3..6c4d1f623 100644 --- a/Adamant/Wallets/Lisk/LskTransferViewController.swift +++ b/Adamant/Wallets/Lisk/LskTransferViewController.swift @@ -208,33 +208,6 @@ final class LskTransferViewController: TransferViewControllerBase { return row } - override func handleRawAddress(_ address: String) -> Bool { - guard let service = service else { - return false - } - - let parsedAddress: String - if address.hasPrefix("lisk:") || address.hasPrefix("lsk:"), let firstIndex = address.firstIndex(of: ":") { - let index = address.index(firstIndex, offsetBy: 1) - parsedAddress = String(address[index...]) - } else { - parsedAddress = address - } - - switch service.validate(address: parsedAddress) { - case .valid: - if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { - row.value = parsedAddress - row.updateCell() - } - - return true - - default: - return false - } - } - func reportTransferTo( admAddress: String, amount: Decimal, diff --git a/Adamant/Wallets/Lisk/LskWalletService.swift b/Adamant/Wallets/Lisk/LskWalletService.swift index e1319d8e2..c69c5089d 100644 --- a/Adamant/Wallets/Lisk/LskWalletService.swift +++ b/Adamant/Wallets/Lisk/LskWalletService.swift @@ -78,6 +78,10 @@ class LskWalletService: WalletService { return tokenNetworkSymbol + tokenSymbol } + var qqPrefix: String { + return Self.qqPrefix + } + // MARK: - Properties let transferAvailable: Bool = true private var initialBalanceCheck = false diff --git a/Adamant/Wallets/TransferViewControllerBase.swift b/Adamant/Wallets/TransferViewControllerBase.swift index 94b4b3654..ea9a01a88 100644 --- a/Adamant/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Wallets/TransferViewControllerBase.swift @@ -777,6 +777,40 @@ class TransferViewControllerBase: FormViewController { return WalletViewControllerBase.BaseRows.send.localized } + /// User loaded address from QR (camera or library) + /// + /// - Parameter address: raw readed address + /// - Returns: string was successfully handled + func handleRawAddress(_ address: String) -> Bool { + //fatalError("You must implement raw address handling") + guard let service = service else { + return false + } + + let parsedAddress = AdamantCoinTools.decode( + uri: address, + qqPrefix: service.qqPrefix + ) + + guard let parsedAddress = parsedAddress, + case .valid = service.validate(address: parsedAddress.address) + else { return false } + + form.rowBy(tag: BaseRows.address.tag)?.value = parsedAddress.address + form.rowBy(tag: BaseRows.address.tag)?.updateCell() + + parsedAddress.params?.forEach { param in + switch param { + case .amount(let amount): + let row: SafeDecimalRow? = form.rowBy(tag: BaseRows.amount.tag) + row?.value = Double(amount) + row?.updateCell() + } + } + + return true + } + // MARK: - Abstract /// Send funds to recipient after validations @@ -786,15 +820,6 @@ class TransferViewControllerBase: FormViewController { fatalError("You must implement sending logic") } - /// User loaded address from QR (camera or library) - /// You must override this method - /// - /// - Parameter address: raw readed address - /// - Returns: string was successfully handled - func handleRawAddress(_ address: String) -> Bool { - fatalError("You must implement raw address handling") - } - /// Build recipient address row /// You must override this method func recipientRow() -> BaseRow { diff --git a/Adamant/Wallets/WalletService.swift b/Adamant/Wallets/WalletService.swift index 4f9773fae..22dee3986 100644 --- a/Adamant/Wallets/WalletService.swift +++ b/Adamant/Wallets/WalletService.swift @@ -262,6 +262,7 @@ protocol WalletServiceWithTransfers: WalletService { protocol WalletServiceWithSend: WalletService { var transactionFeeUpdated: Notification.Name { get } + var qqPrefix: String { get } var blockchainSymbol: String { get } var isDynamicFee : Bool { get } var diplayTransactionFee : Decimal { get } From 2ef0d31c5d267e787ee7cc9679c601e125257089 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 11:54:38 +0300 Subject: [PATCH 034/136] [trello.com/c/yVLYpudV] Dec fee note offset --- Adamant/SharedViews/DoubleDetailsTableViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/SharedViews/DoubleDetailsTableViewCell.swift b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift index a121f6746..9bc6e655c 100644 --- a/Adamant/SharedViews/DoubleDetailsTableViewCell.swift +++ b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift @@ -46,7 +46,7 @@ public final class DoubleDetailsTableViewCell: Cell, CellType { detailsLabel.text = value.first secondDetailsLabel.text = value.second - stackView.spacing = value.second == nil ? 0 : 6 + stackView.spacing = value.second == nil ? 0 : 1 } else { detailsLabel.text = nil secondDetailsLabel.text = nil From ed210068cfb32c9b3d561d088699128741234e49 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 15:43:40 +0300 Subject: [PATCH 035/136] [trello.com/c/TGBBXBeX] fix: short message in the chat list --- Adamant/Stories/ChatsList/ChatListViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index 7240cf28f..41b65b2c4 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -879,7 +879,7 @@ extension ChatListViewController { let prefix = richMessage.isOutgoing ? "\(String.adamantLocalized.chatList.sentMessagePrefix)" - : text + : "" let replyImageAttachment = NSTextAttachment() replyImageAttachment.image = UIImage(named: "reply") From fb1b4fce3561920d0d2e9ccee933c7d3397ae1f6 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 16:34:44 +0300 Subject: [PATCH 036/136] [trello.com/c/TGBBXBeX] fix: ADM transfer with comment in chat doesn't show comment --- .../AdamantChatTransactionService.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index be4299485..d13322d83 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -88,13 +88,22 @@ actor AdamantChatTransactionService: ChatTransactionService { // MARK: Text message case .message, .messageOld, .signal, .unknown: if transaction.amount > 0 { - if let trs = getTransfer(id: String(transaction.id), context: context) { - messageTransaction = trs + let trs: TransferTransaction + + if let trsDB = getTransfer( + id: String(transaction.id), + context: context + ) { + trs = trsDB } else { - let trs = TransferTransaction(entity: TransferTransaction.entity(), insertInto: context) - trs.comment = decodedMessage - messageTransaction = trs + trs = TransferTransaction( + entity: TransferTransaction.entity(), + insertInto: context + ) } + + trs.comment = decodedMessage + messageTransaction = trs } else { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = decodedMessage @@ -149,7 +158,6 @@ actor AdamantChatTransactionService: ChatTransactionService { trs.replyToId = richContent[RichContentKeys.reply.replyToId] as? String messageTransaction = trs - print("find adm rich, id=\(trs.replyToId), com = \(trs.comment)") break } From c4e793e8314332f010e9cfe060e1205acf3d8516 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 16:59:20 +0300 Subject: [PATCH 037/136] [trello.com/c/TGBBXBeX] feat: process unknown reply id --- Adamant/Helpers/String+localized.swift | 5 ++ .../DataProviders/AdamantChatsProvider.swift | 13 +++- .../AdamantRichTransactionReplyService.swift | 60 ++++++++++++------- .../Assets/l18n/de.lproj/Localizable.strings | 6 ++ .../Assets/l18n/en.lproj/Localizable.strings | 6 ++ .../Assets/l18n/ru.lproj/Localizable.strings | 6 ++ 6 files changed, 70 insertions(+), 26 deletions(-) diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index e6971d485..0e9431d0a 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -72,4 +72,9 @@ extension String.adamantLocalized { return String.localizedStringWithFormat(NSLocalizedString("Error.RemoteServerErrorFormat", comment: "Shared error: Remote error format, %@ for message"), message) } } + + enum reply { + static let shortUnknownMessageError = NSLocalizedString("Reply.ShortUnknownMessageError", comment: "Short unknown message error") + static let longUnknownMessageError = NSLocalizedString("Reply.LongUnknownMessageError", comment: "Long unknown message error") + } } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 92bbf1189..d7cb69142 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -1305,6 +1305,7 @@ extension AdamantChatsProvider { var transactions: [Transaction] = [] var offset = chatLoadedMessages[recipient] ?? 0 var needToRepeat = false + var isFind = false repeat { let messages = try await apiGetChatMessages( @@ -1321,15 +1322,21 @@ extension AdamantChatsProvider { offset += messages.count transactions.append(contentsOf: messages) - - let findTransactionId = transactions.contains(where: { $0.id == UInt64(transactionId) }) - needToRepeat = messages.count >= chatTransactionsLimit && !findTransactionId + isFind = transactions.contains(where: { $0.id == UInt64(transactionId) }) + needToRepeat = messages.count >= chatTransactionsLimit && !isFind } while needToRepeat if transactions.count == 0 { return } + guard isFind else { + throw ApiServiceError.internalError( + message: String.adamantLocalized.reply.longUnknownMessageError, + error: nil + ) + } + chatLoadedMessages[recipient] = offset await process( diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 4ea52cc8e..85e8d86db 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -18,7 +18,7 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService private lazy var richController = getRichTransactionsController() private lazy var transferController = getTransferController() - private let unknownErrorMessage = "UNKNOWN" + private let unknownErrorMessage = String.adamantLocalized.reply.shortUnknownMessageError init( coreDataStack: CoreDataStack, @@ -78,33 +78,47 @@ extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate private extension AdamantRichTransactionReplyService { func update(transaction: TransferTransaction) { Task { - guard let id = transaction.replyToId, - transaction.decodedReplyMessage == nil - else { return } - - let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) - let message = try getReplyMessage(by: transactionReply) - - setReplyMessage( - for: transaction, - message: message - ) + do { + guard let id = transaction.replyToId, + transaction.decodedReplyMessage == nil + else { return } + + let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) + let message = try getReplyMessage(by: transactionReply) + + setReplyMessage( + for: transaction, + message: message + ) + } catch { + setReplyMessage( + for: transaction, + message: unknownErrorMessage + ) + } } } func update(transaction: RichMessageTransaction) { Task { - guard let id = transaction.getRichValue(for: RichContentKeys.reply.replyToId), - transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil - else { return } - - let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) - let message = try getReplyMessage(by: transactionReply) - - setReplyMessage( - for: transaction, - message: message - ) + do { + guard let id = transaction.getRichValue(for: RichContentKeys.reply.replyToId), + transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil + else { return } + + let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) + let message = try getReplyMessage(by: transactionReply) + + setReplyMessage( + for: transaction, + message: message + ) + } catch { + setReplyMessage( + for: transaction, + message: unknownErrorMessage + ) + } } } diff --git a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings index 84d7e3671..45951fa81 100755 --- a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings @@ -1056,3 +1056,9 @@ "EULA.Accept" = "Akzeptieren"; "EULA.Decline" = "Ablehnen"; "EULA.Text" = "ADAMANT Messenger basiert auf dezentralisierter Technologie und der Blockchain, infolgedessen:\n\n✓ ADAMANT Messenger ist ursprünglich auf die Sicherheit der Kommunikation und die Privatsphäre der Nutzer ausgerichtet.\n✓ Der Source Code vom ADAMANT Messenger ist im vollen Umfang unter der GPL-3.0-Lizenz verfügbar und ist auf GitHub verfügbar: github.com/adamant-im\n✓ ADAMANT Messenger wird \"as is\" ausgeliefert, ohne Gewährleistung.\n✓ Die Entwickler können nur der Entwicklung vom ADAMANT Messenger beitragen.\n✓ Die Entwickler haben keine Möglichkeit, die Handlungen von Nutzer zu kontrollieren.\n✓ Die Entwickler haben keine Privilegien in der Nutzung vom ADAMANT Messenger.\n✓ Die Entwickler haben keine Möglichkeit, Profile im ADAMANT Messenger einzuschränken oder zu blockieren.\n✓ Die Entwickler haben keine Möglichkeit, die Nachrichten zu entschlüsseln, sie zu lesen, oder auf andere Informationen von Nutzern vom ADAMANT Messenger zuzugreifen.\n✓ Entwickler können den Zugriff auf ein Benutzerkonto bei einem Verlust nicht wiederherstellen. Nur die Benutzer selbst sind für die Sicherheit und den Schutz der Daten ihrer Konten verantwortlich.\n✓ Die Entwickler tragen keine Haftung für den Inhalt, der von Nutzern erstellt wird, ihre Nachrichten, Medien oder Intentionen von Nutzern vom ADAMANT Messenger.\n✓ Die Entwickler haben keine Möglichkeit, die Information zu speichern, die zwischen den Nutzern ausgetauscht wird und tragen keine Haftung für die Aufbewahrung dieser auf der Blockchain.\n✓ Die Entwickler tragen keine Haftung für jegliche Vermögenswerte (einschließlich, aber nicht nur auf Kryptowährungen eingeschränkt), die in Verbindung mit oder mit Hilfe vom ADAMANT Messenger gesendet werden, und haben keinen Zugriff darauf.\n✓ ADAMANT Messenger speichert Metadaten wie Zeitstempel der gesendeten Nachrichten, die öffentlich auf der Blockchain verfügbar sind.\n✓ Die Entwickler kooperieren nicht mit Dritten oder anderen Services, die Daten verarbeiten oder speichern.\n✓ Die Infrastruktur vom ADAMANT Messenger (Blockchain Nodes) gehört den Nutzern und wird von ihnen betrieben.\n✓ Die Entwickler haben keine Möglichkeit, den ADAMANT Messenger zu deaktivieren, zu stoppen oder zu pausieren. Die Entwickler garantieren keinen stabilen Betrieb vom ADAMANT Messenger.\n✓ Die Entwickler tragen keine Haftung für mögliche Risiken, Kosten oder Probleme, die mit der Nutzung vom ADAMANT Messenger auftreten können.\n✓ Ab sofort tragen die Nutzer vom ADAMANT Messenger die volle Verantwortung für dessen Nutzung, einschließlich, aber nicht beschränkt auf: Legitimität der blockchain und anderer Technologien, die im ADAMANT Messenger verwendet werden, Nutzung der anonymen Nachrichtenservices, und andere Gesetze, die mit der Verwendung von solchen und ähnlichen Services verbunden sind.\n\nIndem Sie ADAMANT verwenden, nehmen sie die aktuellen Geschäftsbedingungen an und sind weiterhin mit diesen einverstanden."; + +/* Reply: Short unknown message error */ +"Reply.ShortUnknownMessageError" = "UNBEKANNTE NACHRICHT"; + +/* Reply: Long unknown message error */ +"Reply.LongUnknownMessageError" = "Wir haben diese Nachricht nicht gefunden"; diff --git a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings index 8e27e8689..17a021e54 100755 --- a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings @@ -1035,3 +1035,9 @@ "EULA.Accept" = "Accept"; "EULA.Decline" = "Decline"; "EULA.Text" = "ADAMANT Messenger is based on decentralized Blockchain technologies, and therefore:\n\n✓ ADAMANT Messenger has no tolerance for objectionable content or abusive users\n✓ ADAMANT Messenger is initially focused on the security of correspondence, and the privacy of its users.\n✓ ADAMANT Messenger displays completely open-source code at all times, license GPL-3.0. The source code is available on GitHub: github.com/adamant-im.\n✓ ADAMANT Messenger is offered “AS IS”, with no warranties.\n✓ Developers are only contributors and users of the ADAMANT Messenger.\n✓ Developers do not control any actions on the part of users.\n✓ Developers do not have privileges in the ADAMANT Messenger.\n✓ Developers have no possibility to disable or block any accounts in the ADAMANT Messenger.\n✓ Developers have no possibility to obtain encryption keys or read correspondence or other information of any ADAMANT Messenger user.\n✓ Developers have no possibility to restore access to any user’s account if lost. Users are responsible for the safety and protection of their own accounts.\n✓ Developers are not responsible for user content, messages, media, and goals and intentions of using the ADAMANT Messenger.\n✓ Developers do not store any information transmitted between users (messages, multimedia) and are not responsible for its storage on/in the Blockchain.\n✓ Developers have no possibility to access, nor are responsible for any assets (including but not limited to: cryptocurrencies), connected to or sent via the ADAMANT Messenger.\n✓ ADAMANT Messenger utilizes metadata, as facts of communications between accounts, available publicly in its Blockchain.\n✓ Developers do not cooperate with any third party or external services in terms of providing any usage data.\n✓ ADAMANT Messenger infrastructure (Blockchain nodes) belong to its users and are run by users.\n✓ Developers have no possibility to deactivate, stop or pause the ADAMANT Messenger. Developers offer no warranty on running the ADAMANT Messenger.\n✓ Developers do not bear responsibility for the possible risks, costs, and faults associated with the ADAMANT Messenger.\n✓ Users of the ADAMANT Messenger henceforth take all responsibility for its use, including but not limited to: legitimacy of the blockchain and other technologies applied within the Messenger, using anonymous messaging services, and other jurisdiction laws.\n\nBy using the ADAMANT Messenger, you henceforth accept these Terms of Service."; + +/* Reply: Short unknown message error */ +"Reply.ShortUnknownMessageError" = "UNKNOWN MESSAGE"; + +/* Reply: Long unknown message error */ +"Reply.LongUnknownMessageError" = "We did not find this message"; diff --git a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings index ba3ccd5b8..079072720 100644 --- a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings @@ -1032,3 +1032,9 @@ "EULA.Accept" = "Принять"; "EULA.Decline" = "Отказаться"; "EULA.Text" = "ADAMANT Messenger основан на децентрализованных технологиях и блокчейне, вследствие чего:\n\n✓ ADAMANT Messenger не допускает незаконный контент или оскорбляющих пользователей\n✓ ADAMANT Messenger изначально сосредоточен на безопасности обмена сообщениями и конфиденциальности своих пользователей.\n✓ Исходный код ADAMANT Messenger всегда в полном объеме доступен под лицензией GPL-3.0. Исходный код доступен в GitHub: github.com/adamant-im\n✓ Использование ADAMANT Messenger предполагается без гарантии, по модели «как есть».\n✓ Разработчики лишь вносят вклад в развитие ADAMANT Messenger.\n✓ Разработчики не имеют возможности контролировать действия пользователей ADAMANT Messenger.\n✓ Разработчики не имеют никаких привилегий в использовании ADAMANT Messenger.\n✓ Разработчики не имеют возможности ограничивать, отключать или блокировать какие-либо учетные записи в ADAMANT Messenger.\n✓ Разработчики не имеют возможности получить ключи шифрования или читать сообщения, или другую информацию пользователей ADAMANT Messenger.\n✓ Разработчики не имеют возможности восстановить доступ к любой учетной записи пользователя в случае ее утери. Только сами пользователи несут ответственность за безопасность и защиту данных своих учетных записей.\n✓ Разработчики не несут ответственности за контент, вносимый пользователем, его сообщения, данные мультимедиа, а также за цели и намерения пользователей при использовании ADAMANT Messenger.\n✓ Разработчики не имеют возможности хранить информацию, передаваемую между пользователями (сообщения, мультимедиа) и не несут ответственности за ее хранение в блокчейне.\n✓ Разработчики не несут ответственности за какие-либо ценные активы (включая, но не ограничиваясь криптовалютами), связанные с или отправленные посредством ADAMANT Messenger, и не имеют возможности доступа к ним.\n✓ ADAMANT Messenger сохраняет метаданные, как факты отправки сообщений, записи о которых доступны публично в блокчейне.\n✓ Разработчики не сотрудничают ни с третьими сторонами, ни с внешними сервисами и компаниями с точки зрения предоставления данных об использовании ADAMANT Messenger.\n✓ Инфраструктура ADAMANT Messenger (узлы блокчейна) принадлежит пользователям и управляется пользователями.\n✓ Разработчики не имеют возможности деактивировать, остановить или приостановить работу ADAMANT Messenger. Разработчики не предоставляют никаких гарантий на безошибочную работу и запуск ADAMANT Messenger и не несут ответственность за стабильную работу ADAMANT Messenger.\n✓ Разработчики не несут ответственности за возможные риски, издержки и проблемы, связанные с ADAMANT Messenger.\n✓ Пользователи ADAMANT Messenger впредь берут на себя всю ответственность за его использование, включая, но не ограничиваясь перечисленным: легитимность блокчейна и других технологий, применяемых в ADAMANT Messenger, использование анонимных сервисов обмена сообщениями, и другие законы, связанные с использованием подобных сервисов.\n\nИспользуя ADAMANT Messenger, вы принимаете настоящие Условия использования и согласны с ними в дальнейшем."; + +/* Reply: Short unknown message error */ +"Reply.ShortUnknownMessageError" = "СООБЩЕНИЕ НЕ НАЙДЕНО"; + +/* Reply: Long unknown message error */ +"Reply.LongUnknownMessageError" = "Мы не нашли это сообщение"; From 44589066842dd189a506d09682b03a670209b1f2 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Fri, 12 May 2023 17:05:42 +0300 Subject: [PATCH 038/136] [trello.com/c/TGBBXBeX] refactor: calculate size for transfer & reply cell --- Adamant.xcodeproj/project.pbxproj | 4 - .../View/Managers/ChatLayoutManager.swift | 10 ++- .../FixedTextMessageSizeCalculator.swift | 28 ++++++- .../ChatMessageReplyCell+Model.swift | 4 +- .../ChatReply/ChatMessageReplyCell.swift | 33 ++++---- .../ChatTransactionCellSizeCalculator.swift | 84 ------------------- .../Content/ChatTransactionContentView.swift | 3 +- .../Chat/ViewModel/ChatMessageFactory.swift | 9 +- 8 files changed, 59 insertions(+), 116 deletions(-) delete mode 100644 Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 49e94cb7d..e518c4b98 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -235,7 +235,6 @@ 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9345769428FD0C34004E6C7A /* UIViewController+email.swift */; }; 934B9BE5296C58210027A8D0 /* CollectionCellWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934B9BE4296C58210027A8D0 /* CollectionCellWrapper.swift */; }; 934B9BE7296C620B0027A8D0 /* UILabel+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934B9BE6296C620B0027A8D0 /* UILabel+adamant.swift */; }; - 934B9BE9296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934B9BE8296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift */; }; 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93547BC929E2262D00B0914B /* WelcomeViewController.swift */; }; 935F53D629BE8F7400779492 /* RichTransactionStatusPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935F53D529BE8F7400779492 /* RichTransactionStatusPublisher.swift */; }; 935F53D829BEA7CA00779492 /* TimeInterval+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935F53D729BEA7CA00779492 /* TimeInterval+Extension.swift */; }; @@ -958,7 +957,6 @@ 9345769428FD0C34004E6C7A /* UIViewController+email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+email.swift"; sourceTree = ""; }; 934B9BE4296C58210027A8D0 /* CollectionCellWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCellWrapper.swift; sourceTree = ""; }; 934B9BE6296C620B0027A8D0 /* UILabel+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+adamant.swift"; sourceTree = ""; }; - 934B9BE8296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionCellSizeCalculator.swift; sourceTree = ""; }; 93547BC929E2262D00B0914B /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 935F53D529BE8F7400779492 /* RichTransactionStatusPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionStatusPublisher.swift; sourceTree = ""; }; 935F53D729BEA7CA00779492 /* TimeInterval+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extension.swift"; sourceTree = ""; }; @@ -1606,7 +1604,6 @@ children = ( 93CC8DC5296F00C4003772BF /* Container */, 93CC8DC4296F00B7003772BF /* Content */, - 934B9BE8296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift */, ); path = ChatTransaction; sourceTree = ""; @@ -3344,7 +3341,6 @@ 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */, 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, - 934B9BE9296C680D0027A8D0 /* ChatTransactionCellSizeCalculator.swift in Sources */, E96D64B72295BED700CA5587 /* DelegateVote.swift in Sources */, 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.swift in Sources */, E993302221354BC300CD5200 /* EthWalletRoutes.swift in Sources */, diff --git a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift index f9b817c2d..f4d9e97a5 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift @@ -81,7 +81,11 @@ final class ChatLayoutManager: MessagesLayoutDelegate { at _: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> CellSizeCalculator? { - FixedTextMessageSizeCalculator(layout: messagesCollectionView.messagesCollectionViewFlowLayout) + FixedTextMessageSizeCalculator( + layout: messagesCollectionView.messagesCollectionViewFlowLayout, + getCurrentSender: { [sender = viewModel.sender] in sender }, + getMessages: { [messages = viewModel.messages] in messages } + ) } func customCellSizeCalculator( @@ -89,7 +93,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { at _: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> CellSizeCalculator { - ChatCellSizeCalculator( + FixedTextMessageSizeCalculator( layout: messagesCollectionView.messagesCollectionViewFlowLayout, getCurrentSender: { [sender = viewModel.sender] in sender }, getMessages: { [messages = viewModel.messages] in messages } @@ -110,7 +114,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> CellSizeCalculator? { - ChatTextCellSizeCalculator( + FixedTextMessageSizeCalculator( layout: messagesCollectionView.messagesCollectionViewFlowLayout, getCurrentSender: { [sender = viewModel.sender] in sender }, getMessages: { [messages = viewModel.messages] in messages } diff --git a/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift b/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift index ce87f3444..4c8c78cd9 100644 --- a/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift @@ -10,6 +10,22 @@ import MessageKit final class FixedTextMessageSizeCalculator: MessageSizeCalculator { + private let getCurrentSender: () -> SenderType + private let getMessages: () -> [ChatMessage] + private let messagesFlowLayout: MessagesCollectionViewFlowLayout + + init( + layout: MessagesCollectionViewFlowLayout, + getCurrentSender: @escaping () -> SenderType, + getMessages: @escaping () -> [ChatMessage] + ) { + self.getMessages = getMessages + self.getCurrentSender = getCurrentSender + self.messagesFlowLayout = layout + super.init() + self.layout = layout + } + override func messageContainerMaxWidth( for message: MessageType, at indexPath: IndexPath @@ -41,7 +57,17 @@ let messageInsets = messageLabelInsets(for: message) messageContainerSize.width += messageInsets.horizontal messageContainerSize.height += messageInsets.vertical - + + if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { + let contentViewHeight = model.value.contentHeight(for: messageContainerSize.width) + messageContainerSize.height = contentViewHeight + } + + if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { + let contentViewHeight = model.value.height(for: messagesFlowLayout.itemWidth) + messageContainerSize.height = contentViewHeight + } + return messageContainerSize } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 84343a191..7548429be 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -16,6 +16,7 @@ extension ChatMessageReplyCell { let messageReply: NSAttributedString let backgroundColor: ChatMessageBackgroundColor var animationId: String + let isFromCurrentSender: Bool static let `default` = Self( id: "", @@ -23,7 +24,8 @@ extension ChatMessageReplyCell { message: NSAttributedString(string: ""), messageReply: NSAttributedString(string: ""), backgroundColor: .failed, - animationId: "" + animationId: "", + isFromCurrentSender: false ) func makeReplyContent() -> NSAttributedString { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index a8321b41b..1503772df 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -83,6 +83,13 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { if isSelected { messageContainerView.startBlinkAnimation() } + + let leading = model.isFromCurrentSender ? smallHInset : longHInset + let trailing = model.isFromCurrentSender ? longHInset : smallHInset + verticalStack.snp.updateConstraints { + $0.leading.equalToSuperview().inset(leading) + $0.trailing.equalToSuperview().inset(trailing) + } } } @@ -98,6 +105,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { var subscription: AnyCancellable? private var trailingReplyViewOffset: CGFloat = 4 + private let smallHInset: CGFloat = 8 + private let longHInset: CGFloat = 14 // MARK: - Methods @@ -135,10 +144,13 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { messageContainerView.addSubview(verticalStack) messageLabel.numberOfLines = 0 replyMessageLabel.numberOfLines = 1 + + let leading = model.isFromCurrentSender ? smallHInset : longHInset + let trailing = model.isFromCurrentSender ? longHInset : smallHInset verticalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(8) - $0.leading.equalToSuperview().inset(8) - $0.trailing.equalToSuperview().offset(-14) + $0.leading.equalToSuperview().inset(leading) + $0.trailing.equalToSuperview().inset(trailing) } } @@ -166,25 +178,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { } replyMessageLabel.attributedText = model.messageReply - - updateFrames() } - func updateFrames() { - let size = messageContainerView.frame.size - messageContainerView.frame = CGRect( - x: messageContainerView.frame.origin.x - trailingReplyViewOffset, - y: messageContainerView.frame.origin.y, - width: size.width + trailingReplyViewOffset, - height: model.contentHeight(for: size.width) - ) - - let origin = CGPoint( - x: 0, - y: messageContainerView.frame.maxY - ) - messageBottomLabel.frame = CGRect(origin: origin, size: messageBottomLabel.frame.size) - } /// Used to handle the cell's contentView's tap gesture. /// Return false when the contentView does not need to handle the gesture. diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift deleted file mode 100644 index 96252325b..000000000 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCellSizeCalculator.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// ChatCellSizeCalculator.swift -// Adamant -// -// Created by Andrey Golubenko on 10.01.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import MessageKit -import UIKit - -final class ChatCellSizeCalculator: CellSizeCalculator { - private let getCurrentSender: () -> SenderType - private let getMessages: () -> [ChatMessage] - private let messagesFlowLayout: MessagesCollectionViewFlowLayout - - init( - layout: MessagesCollectionViewFlowLayout, - getCurrentSender: @escaping () -> SenderType, - getMessages: @escaping () -> [ChatMessage] - ) { - self.getMessages = getMessages - self.getCurrentSender = getCurrentSender - self.messagesFlowLayout = layout - super.init() - self.layout = layout - } - - override func sizeForItem(at indexPath: IndexPath) -> CGSize { - if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { - return .init( - width: messagesFlowLayout.itemWidth, - height: model.value.height(for: messagesFlowLayout.itemWidth) - ) - } - - return .zero - } -} - -final class ChatTextCellSizeCalculator: TextMessageSizeCalculator { - private let getCurrentSender: () -> SenderType - private let getMessages: () -> [ChatMessage] - private let messagesFlowLayout: MessagesCollectionViewFlowLayout - - init( - layout: MessagesCollectionViewFlowLayout, - getCurrentSender: @escaping () -> SenderType, - getMessages: @escaping () -> [ChatMessage] - ) { - self.getMessages = getMessages - self.getCurrentSender = getCurrentSender - self.messagesFlowLayout = layout - super.init() - self.layout = layout - } - - override func sizeForItem(at indexPath: IndexPath) -> CGSize { - if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { - let dataSource = messagesLayout.messagesDataSource - let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - - let contentViewHeight = model.value.contentHeight(for: messagesFlowLayout.itemWidth) - let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height - let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height - let messageVerticalPadding = messageContainerPadding(for: message) - let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height - let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height - - return .init( - width: messagesFlowLayout.itemWidth, - height: contentViewHeight - + messageBottomLabelHeight - + messageTopLabelHeight - + messageVerticalPadding.top - + messageVerticalPadding.bottom - + cellBottomLabelHeight - + cellTopLabelHeight - ) - } - - return super.sizeForItem(at: indexPath) - } -} diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index ecd551ad5..117039e3b 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -217,7 +217,8 @@ private extension ChatTransactionContentView { message: NSAttributedString(string: ""), messageReply: NSAttributedString(string: ""), backgroundColor: .failed, - animationId: "" + animationId: "", + isFromCurrentSender: true ))) return } diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift index 2648e1ab2..ac49ca532 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift @@ -122,8 +122,9 @@ private extension ChatMessageFactory { case let transaction as RichMessageTransaction: if transaction.isReply, !transaction.isTransferReply() { - return makeContent( + return makeReplyContent( transaction, + isFromCurrentSender: isFromCurrentSender, backgroundColor: backgroundColor, animationId: animationId ) @@ -158,8 +159,9 @@ private extension ChatMessageFactory { } ?? .default } - func makeContent( + func makeReplyContent( _ transaction: RichMessageTransaction, + isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor, animationId: String ) -> ChatMessage.Content { @@ -179,7 +181,8 @@ private extension ChatMessageFactory { message: Self.markdownParser.parse(replyMessage), messageReply: decodedMessageMarkDown, backgroundColor: backgroundColor, - animationId: animationId) + animationId: animationId, + isFromCurrentSender: isFromCurrentSender) )) } From 4fb3e68f50856b6c573e6387c2bd6510d8290425 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 16 May 2023 12:13:46 +0300 Subject: [PATCH 039/136] [trello.com/c/TGBBXBeX] feat: made restrictions for failed and pending message --- Adamant/Helpers/String+localized.swift | 2 ++ .../AdamantChatTransactionService.swift | 1 + .../DataProviders/AdamantChatsProvider.swift | 5 +++-- .../View/Managers/ChatDataSourceManager.swift | 2 +- .../ChatBaseMessage/ChatMessageCell.swift | 1 - .../Subviews/ChatReply/ChatMessageReplyCell.swift | 2 -- .../Container/ChatTransactionContainerView.swift | 1 - .../Stories/Chat/ViewModel/ChatViewModel.swift | 15 +++++++++++++++ .../Assets/l18n/de.lproj/Localizable.strings | 6 ++++++ .../Assets/l18n/en.lproj/Localizable.strings | 6 ++++++ .../Assets/l18n/ru.lproj/Localizable.strings | 6 ++++++ 11 files changed, 40 insertions(+), 7 deletions(-) diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index 0e9431d0a..fbedd2ce6 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -76,5 +76,7 @@ extension String.adamantLocalized { enum reply { static let shortUnknownMessageError = NSLocalizedString("Reply.ShortUnknownMessageError", comment: "Short unknown message error") static let longUnknownMessageError = NSLocalizedString("Reply.LongUnknownMessageError", comment: "Long unknown message error") + static let failedMessageError = NSLocalizedString("Reply.failedMessageError", comment: "Failed message reply error") + static let pendingMessageError = NSLocalizedString("Reply.pendingMessageError", comment: "Pending message reply error") } } diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index d13322d83..1cc3d131a 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -254,6 +254,7 @@ actor AdamantChatTransactionService: ChatTransactionService { transfer.partner = partner } + transfer.chatMessageId = String(transaction.id) transfer.isOutgoing = isOut transfer.partner = partner return transfer diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index d7cb69142..6a880c4b1 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -799,14 +799,15 @@ extension AdamantChatsProvider { from chatroom: Chatroom? = nil ) async throws -> ChatTransaction { let transaction = MessageTransaction(context: context) + let id = UUID().uuidString transaction.date = Date() as NSDate transaction.recipientId = recipientId transaction.senderId = senderId transaction.type = Int16(type.rawValue) transaction.isOutgoing = true - transaction.chatMessageId = UUID().uuidString + transaction.chatMessageId = id transaction.isMarkdown = isMarkdown - transaction.transactionId = UUID().uuidString + transaction.transactionId = id transaction.message = text diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift index 24c29fe5f..781641195 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift @@ -153,7 +153,7 @@ private extension ChatDataSourceManager { case let .forceUpdateTransactionStatus(id): viewModel.forceUpdateTransactionStatus(id: id) case let .reply(message): - viewModel.replyMessage = message + viewModel.replyMessageIfNeeded(message) case let .scrollTo(message): viewModel.scroll(to: message) case let .swipeState(state): diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index f9f03c69f..0016b846a 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -58,7 +58,6 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { } swipeView.action = { [weak self] message in - print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 1503772df..c7fd2bf79 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -133,7 +133,6 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { } swipeView.action = { [weak self] message in - print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) } @@ -180,7 +179,6 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { replyMessageLabel.attributedText = model.messageReply } - /// Used to handle the cell's contentView's tap gesture. /// Return false when the contentView does not need to handle the gesture. override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index d429f724f..08fd7a5a0 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -90,7 +90,6 @@ private extension ChatTransactionContainerView { } swipeView.action = { [weak self] message in - print("message id \(message.id), text = \(message.makeReplyContent().string)") self?.actionHandler(.reply(message: message)) } diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index f30c58447..79943797b 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -418,6 +418,21 @@ final class ChatViewModel: NSObject { }.store(in: &tempCancellables) } } + + func replyMessageIfNeeded(_ messageModel: MessageModel?) { + let message = messages.first(where: { $0.messageId == messageModel?.id }) + guard message?.status != .failed else { + dialog.send(.warning(String.adamantLocalized.reply.failedMessageError)) + return + } + + guard message?.status != .pending else { + dialog.send(.warning(String.adamantLocalized.reply.pendingMessageError)) + return + } + + replyMessage = messageModel + } } extension ChatViewModel { diff --git a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings index 45951fa81..c65a47e90 100755 --- a/AdamantShared/Assets/l18n/de.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/de.lproj/Localizable.strings @@ -1062,3 +1062,9 @@ /* Reply: Long unknown message error */ "Reply.LongUnknownMessageError" = "Wir haben diese Nachricht nicht gefunden"; + +/* Reply: Failed message reply error */ +"Reply.failedMessageError" = "Sie können nicht auf eine fehlgeschlagene Nachricht antworten"; + +/* Pending message reply error */ +"Reply.pendingMessageError" = "Sie können nicht auf eine ausstehende Nachricht antworten. Auf Bestätigungen warten (schätzungsweise 1–2 Sekunden)"; diff --git a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings index 17a021e54..49e48ef33 100755 --- a/AdamantShared/Assets/l18n/en.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/en.lproj/Localizable.strings @@ -1041,3 +1041,9 @@ /* Reply: Long unknown message error */ "Reply.LongUnknownMessageError" = "We did not find this message"; + +/* Reply: Failed message reply error */ +"Reply.failedMessageError" = "You can't reply to a failed message"; + +/* Pending message reply error */ +"Reply.pendingMessageError" = "You can't reply to a pending message. Wait for confirmations (estimate 1-2 seconds)"; diff --git a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings index 079072720..449b3b0a1 100644 --- a/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings +++ b/AdamantShared/Assets/l18n/ru.lproj/Localizable.strings @@ -1038,3 +1038,9 @@ /* Reply: Long unknown message error */ "Reply.LongUnknownMessageError" = "Мы не нашли это сообщение"; + +/* Reply: Failed message reply error */ +"Reply.failedMessageError" = "Вы не можете ответить на ошибочное сообщение"; + +/* Pending message reply error */ +"Reply.pendingMessageError" = "Вы не можете ответить на ожидающее сообщение. Дождитесь подтверждения (оценка 1-2 секунды)"; From 23091cbb9b9c805f73426380823ad0c7342a36b1 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Tue, 16 May 2023 12:29:17 +0300 Subject: [PATCH 040/136] [trello.com/c/TGBBXBeX] fix: process different gesture states --- Adamant/SharedViews/SwipeableView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Adamant/SharedViews/SwipeableView.swift b/Adamant/SharedViews/SwipeableView.swift index f93b6f12e..fb10aec8d 100644 --- a/Adamant/SharedViews/SwipeableView.swift +++ b/Adamant/SharedViews/SwipeableView.swift @@ -84,7 +84,10 @@ private extension SwipeableView { let isOnStartPosition = movingView.frame.origin.x == 0 || movingView.frame.origin.x == messagePadding - if isOnStartPosition && translation.x > 0 { return } + if isOnStartPosition && translation.x > 0 { + swipeStateAction?(.ended) + return + } if movingView.frame.origin.x <= messagePadding { movingView.center = CGPoint( @@ -121,6 +124,10 @@ private extension SwipeableView { ) } } + + if recognizer.state == .cancelled || recognizer.state == .failed { + swipeStateAction?(.ended) + } } } From 5bcf65d19ae2293af1f8e197440d2236e44e11d0 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 17 May 2023 12:17:57 +0300 Subject: [PATCH 041/136] [trello.com/c/TGBBXBeX] fix: scrollable inputContainerView when swiping a message --- Adamant.xcodeproj/project.pbxproj | 8 +- .../Chat/View/ChatViewController.swift | 15 +- .../View/Managers/ChatKeyboardManager.swift | 41 ++ Adamant/Utilities/KeyboardManager.swift | 595 ------------------ 4 files changed, 57 insertions(+), 602 deletions(-) create mode 100644 Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift delete mode 100644 Adamant/Utilities/KeyboardManager.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index e518c4b98..3550e3760 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 41047B72294B5F210039E956 /* VisibleWalletsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */; }; 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B73294C61D10039E956 /* VisibleWalletsService.swift */; }; 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */; }; + 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */; }; 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; 41330F7629F1509400CB587C /* AdamantCellAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */; }; 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; @@ -105,7 +106,6 @@ 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E322723F8C00D58779 /* DashWalletService.swift */; }; 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */; }; 6406D74A21C7F06000196713 /* SearchResultsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6406D74821C7F06000196713 /* SearchResultsViewController.xib */; }; - 6406D74D21CE3AC100196713 /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6406D74C21CE3AC100196713 /* KeyboardManager.swift */; }; 640EFA902558612100E9724B /* RichMessageNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E957E0F1229999DA0019732A /* RichMessageNotificationProvider.swift */; }; 640EFA962558612400E9724B /* NotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E957E127229B0CF10019732A /* NotificationContent.swift */; }; 640EFA9C2558613200E9724B /* AdamantProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E957E0FA229AB9310019732A /* AdamantProvider.swift */; }; @@ -783,6 +783,7 @@ 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsTableViewCell.swift; sourceTree = ""; }; 41047B73294C61D10039E956 /* VisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsService.swift; sourceTree = ""; }; 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVisibleWalletsService.swift; sourceTree = ""; }; + 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatKeyboardManager.swift; sourceTree = ""; }; 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCellAnimation.swift; sourceTree = ""; }; 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; @@ -847,7 +848,6 @@ 6403F5E322723F8C00D58779 /* DashWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletService.swift; sourceTree = ""; }; 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletViewController.swift; sourceTree = ""; }; 6406D74821C7F06000196713 /* SearchResultsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchResultsViewController.xib; sourceTree = ""; }; - 6406D74C21CE3AC100196713 /* KeyboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; }; 6414C18D217DF43100373FA6 /* String+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+adamant.swift"; sourceTree = ""; }; 6416B19C21AD7B92006089AC /* LskWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletRoutes.swift; sourceTree = ""; }; 6416B19E21AD7CBE006089AC /* LskWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletViewController.swift; sourceTree = ""; }; @@ -1628,6 +1628,7 @@ 938F7D602955C92B001915CA /* ChatDataSourceManager.swift */, 9390C5022976B42800270CDF /* ChatDialogManager.swift */, 932F77582989F999006D8801 /* ChatCellManager.swift */, + 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */, 9340077F29AC341000A20622 /* ChatAction.swift */, 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */, ); @@ -2133,7 +2134,6 @@ E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */, - 6406D74C21CE3AC100196713 /* KeyboardManager.swift */, 649E9A142111B3C200686B01 /* Mnemonic+extended.swift */, E96D64C92295C4A800CA5587 /* WordList.swift */, ); @@ -3265,6 +3265,7 @@ 55FBAAF528C54B230066E629 /* Nodes+Allowance.swift in Sources */, E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, 93E5D4DB293000BE00439298 /* UnregisteredTransaction.swift in Sources */, + 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */, 6449BA68235CA0930033B936 /* ERC20WalletService.swift in Sources */, 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */, 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */, @@ -3391,7 +3392,6 @@ A5E0422B282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift in Sources */, E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */, E9FEECA421413659007DD7C8 /* RichMessageProvider.swift in Sources */, - 6406D74D21CE3AC100196713 /* KeyboardManager.swift in Sources */, 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */, 6416B1A321AD7EA1006089AC /* LskTransactionDetailsViewController.swift in Sources */, E9A03FD620DBC8E2007653A1 /* AdamantApi+Peers.swift in Sources */, diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index da20f13f3..fca73c688 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -58,6 +58,11 @@ final class ChatViewController: MessagesViewController { return view }() + private lazy var chatKeyboardManager: ChatKeyboardManager = { + let data = ChatKeyboardManager(scrollView: messagesCollectionView) + return data + }() + init( viewModel: ChatViewModel, richMessageProviders: [String: RichMessageProvider], @@ -92,7 +97,7 @@ final class ChatViewController: MessagesViewController { configureHeader() configureLayout() configureReplyView() - configureGesture() + configureGestures() setupObservers() viewModel.loadFirstMessagesIfNeeded() } @@ -171,7 +176,6 @@ extension ChatViewController { if state == .ended { messagesCollectionView.isScrollEnabled = true - messagesCollectionView.keyboardDismissMode = .interactive } } } @@ -364,7 +368,12 @@ private extension ChatViewController { } } - func configureGesture() { + func configureGestures() { + if let gesture = messagesCollectionView.gestureRecognizers?[13] as? UIPanGestureRecognizer { + gesture.delegate = chatKeyboardManager + chatKeyboardManager.panGesture = gesture + } + let panGesture = UIPanGestureRecognizer() panGesture.delegate = self messagesCollectionView.addGestureRecognizer(panGesture) diff --git a/Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift b/Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift new file mode 100644 index 000000000..6becde0b6 --- /dev/null +++ b/Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift @@ -0,0 +1,41 @@ +// +// ChatKeyboardManager.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 17.05.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +final class ChatKeyboardManager: NSObject, UIGestureRecognizerDelegate { + private let scrollView: UIScrollView + var panGesture: UIPanGestureRecognizer? + + init(scrollView: UIScrollView) { + self.scrollView = scrollView + super.init() + } + + /// Only receive a `UITouch` event when the `scrollView`'s keyboard dismiss mode is interactive + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return scrollView.keyboardDismissMode == .interactive + } + + /// Only recognice gestures when is vertical velocity + func gestureRecognizerShouldBegin( + _ gestureRecognizer: UIGestureRecognizer + ) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return true + } + + let velocity = panGesture.velocity(in: scrollView) + return abs(velocity.x) < abs(velocity.y) + } + + /// Only recognice simultaneous gestures when its the `panGesture` + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return gestureRecognizer === panGesture + } +} diff --git a/Adamant/Utilities/KeyboardManager.swift b/Adamant/Utilities/KeyboardManager.swift deleted file mode 100644 index fde0c5700..000000000 --- a/Adamant/Utilities/KeyboardManager.swift +++ /dev/null @@ -1,595 +0,0 @@ -// -// KeyboardManager.swift -// InputBarAccessoryView -// -// Copyright © 2017-2018 Nathan Tannar. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Nathan Tannar on 8/18/17. -// - -import UIKit - -/// An object that observes keyboard notifications such that event callbacks can be set for each notification -open class KeyboardManager: NSObject, UIGestureRecognizerDelegate { - - /// A callback that passes a `KeyboardNotification` as an input - public typealias EventCallback = (KeyboardNotification) -> Void - - // MARK: - Properties [Public] - - /// A weak reference to a view bounded to the top of the keyboard to act as an `InputAccessoryView` - /// but kept within the bounds of the `UIViewController`s view - open weak var inputAccessoryView: UIView? - - /// A flag that indicates if a portion of the keyboard is visible on the screen - private(set) public var isKeyboardHidden: Bool = true - - // MARK: - Properties [Private] - - /// The `NSLayoutConstraintSet` that holds the `inputAccessoryView` to the bottom if its superview - private var constraints: NSLayoutConstraintSet? - - /// A weak reference to a `UIScrollView` that has been attached for interactive keyboard dismissal - private weak var scrollView: UIScrollView? - - /// The `EventCallback` actions for each `KeyboardEvent`. Default value is EMPTY - private var callbacks: [KeyboardEvent: EventCallback] = [:] - - /// The pan gesture that handles dragging on the `scrollView` - private var panGesture: UIPanGestureRecognizer? - - /// A cached notification used as a starting point when a user dragging the `scrollView` down - /// to interactively dismiss the keyboard - private var cachedNotification: KeyboardNotification? - - // MARK: - Initialization - - /// Creates a `KeyboardManager` object an binds the view as fake `InputAccessoryView` - /// - /// - Parameter inputAccessoryView: The view to bind to the top of the keyboard but within its superview - public convenience init(inputAccessoryView: UIView) { - self.init() - self.bind(inputAccessoryView: inputAccessoryView) - } - - /// Creates a `KeyboardManager` object that observes the state of the keyboard - public override init() { - super.init() - addObservers() - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - De-Initialization - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - Keyboard Observer - - /// Add an observer for each keyboard notification - private func addObservers() { - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow(notification:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardDidShow(notification:)), - name: UIResponder.keyboardDidShowNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillHide(notification:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardDidHide(notification:)), - name: UIResponder.keyboardDidHideNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillChangeFrame(notification:)), - name: UIResponder.keyboardWillChangeFrameNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardDidChangeFrame(notification:)), - name: UIResponder.keyboardDidChangeFrameNotification, - object: nil) - } - - // MARK: - Mutate Callback Dictionary - - /// Sets the `EventCallback` for a `KeyboardEvent` - /// - /// - Parameters: - /// - event: KeyboardEvent - /// - callback: EventCallback - /// - Returns: Self - @discardableResult - open func on(event: KeyboardEvent, do callback: EventCallback?) -> Self { - callbacks[event] = callback - return self - } - - /// Constrains the `inputAccessoryView` to the bottom of its superview and sets the - /// `.willChangeFrame` and `.willHide` event callbacks such that it mimics an `InputAccessoryView` - /// that is bound to the top of the keyboard - /// - /// - Parameter inputAccessoryView: The view to bind to the top of the keyboard but within its superview - /// - Returns: Self - @discardableResult - open func bind(inputAccessoryView: UIView, usingTabBar tabBar: UITabBar? = nil) -> Self { - guard let superview = inputAccessoryView.superview else { - fatalError("`inputAccessoryView` must have a superview") - } - let tabBarHeight = isMacOS ? 0 : tabBar?.bounds.size.height ?? 0 - self.inputAccessoryView = inputAccessoryView - inputAccessoryView.translatesAutoresizingMaskIntoConstraints = false - constraints = NSLayoutConstraintSet( - bottom: inputAccessoryView.bottomAnchor.constraint(equalTo: superview.bottomAnchor), - left: inputAccessoryView.leftAnchor.constraint(equalTo: superview.leftAnchor), - right: inputAccessoryView.rightAnchor.constraint(equalTo: superview.rightAnchor) - ).activate() - - callbacks[.willShow] = { [weak self] (notification) in - let keyboardHeight = notification.endFrame.height - guard - self?.isKeyboardHidden == false, - self?.constraints?.bottom?.constant == 0, - notification.isForCurrentApp else { return } - self?.animateAlongside(notification) { - self?.constraints?.bottom?.constant = -keyboardHeight + tabBarHeight - self?.inputAccessoryView?.superview?.layoutIfNeeded() - } - } - callbacks[.willChangeFrame] = { [weak self] (notification) in - let keyboardHeight = notification.endFrame.height - guard - self?.isKeyboardHidden == false, - notification.isForCurrentApp else { return } - self?.animateAlongside(notification) { - self?.constraints?.bottom?.constant = -keyboardHeight + tabBarHeight - self?.inputAccessoryView?.superview?.layoutIfNeeded() - } - } - callbacks[.willHide] = { [weak self] (notification) in - guard notification.isForCurrentApp else { return } - self?.animateAlongside(notification) { [weak self] in - self?.constraints?.bottom?.constant = 0 - self?.inputAccessoryView?.superview?.layoutIfNeeded() - } - } - return self - } - - /// Adds a `UIPanGestureRecognizer` to the `scrollView` to enable interactive dismissal` - /// - /// - Parameter scrollView: UIScrollView - /// - Returns: Self - @discardableResult - open func bind(to scrollView: UIScrollView) -> Self { - self.scrollView = scrollView - self.scrollView?.keyboardDismissMode = .interactive // allows dismissing keyboard interactively - let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureRecognizer)) - recognizer.delegate = self - self.panGesture = recognizer - self.scrollView?.addGestureRecognizer(recognizer) - return self - } - - // MARK: - Keyboard Notifications - - /// An observer method called last in the lifecycle of a keyboard becoming visible - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardDidShow(notification: NSNotification) { - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.didShow]?(keyboardNotification) - } - - /// An observer method called last in the lifecycle of a keyboard becoming hidden - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardDidHide(notification: NSNotification) { - isKeyboardHidden = true - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.didHide]?(keyboardNotification) - } - - /// An observer method called third in the lifecycle of a keyboard becoming visible/hidden - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardDidChangeFrame(notification: NSNotification) { - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.didChangeFrame]?(keyboardNotification) - cachedNotification = keyboardNotification - } - - /// An observer method called first in the lifecycle of a keyboard becoming visible/hidden - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardWillChangeFrame(notification: NSNotification) { - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.willChangeFrame]?(keyboardNotification) - cachedNotification = keyboardNotification - } - - /// An observer method called second in the lifecycle of a keyboard becoming visible - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardWillShow(notification: NSNotification) { - isKeyboardHidden = false - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.willShow]?(keyboardNotification) - } - - /// An observer method called second in the lifecycle of a keyboard becoming hidden - /// - /// - Parameter notification: NSNotification - @objc - open func keyboardWillHide(notification: NSNotification) { - guard let keyboardNotification = KeyboardNotification(from: notification) else { return } - callbacks[.willHide]?(keyboardNotification) - } - - // MARK: - Helper Methods - - private func animateAlongside(_ notification: KeyboardNotification, animations: @escaping () -> Void) { - UIView.animate(withDuration: notification.timeInterval, delay: 0, options: [notification.animationOptions, .allowAnimatedContent, .beginFromCurrentState], animations: animations, completion: nil) - } - - // MARK: - UIGestureRecognizerDelegate - - /// Starts with the cached `KeyboardNotification` and calculates a new `endFrame` based - /// on the `UIPanGestureRecognizer` then calls the `.willChangeFrame` `EventCallback` action - /// - /// - Parameter recognizer: UIPanGestureRecognizer - @objc - open func handlePanGestureRecognizer(recognizer: UIPanGestureRecognizer) { - guard - !isKeyboardHidden, - var keyboardNotification = cachedNotification, - case .changed = recognizer.state, - let view = recognizer.view, - let window = UIApplication.shared.windows.first - else { return } - - let location = recognizer.location(in: view) - let absoluteLocation = view.convert(location, to: window) - var frame = keyboardNotification.endFrame - frame.origin.y = max(absoluteLocation.y, window.bounds.height - frame.height) - frame.size.height = window.bounds.height - frame.origin.y - keyboardNotification.endFrame = frame - callbacks[.willChangeFrame]?(keyboardNotification) - } - - /// Only receive a `UITouch` event when the `scrollView`'s keyboard dismiss mode is interactive - open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - return scrollView?.keyboardDismissMode == .interactive - } - - /// Only recognice simultaneous gestures when its the `panGesture` - open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return gestureRecognizer === panGesture - } - -} - -// -// KeyboardEvent.swift -// InputBarAccessoryView -// -// Copyright © 2017-2018 Nathan Tannar. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Nathan Tannar on 8/18/17. -// -import Foundation - -/// Keyboard events that can happen. Translates directly to `UIKeyboard` notifications from UIKit. -public enum KeyboardEvent { - - /// Event raised by UIKit's `.UIKeyboardWillShow`. - case willShow - - /// Event raised by UIKit's `.UIKeyboardDidShow`. - case didShow - - /// Event raised by UIKit's `.UIKeyboardWillShow`. - case willHide - - /// Event raised by UIKit's `.UIKeyboardDidHide`. - case didHide - - /// Event raised by UIKit's `.UIKeyboardWillChangeFrame`. - case willChangeFrame - - /// Event raised by UIKit's `.UIKeyboardDidChangeFrame`. - case didChangeFrame - - /// Non-keyboard based event raised by UIKit - case unknown - -} - -// -// KeyboardNotification.swift -// InputBarAccessoryView -// -// Copyright © 2017-2018 Nathan Tannar. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Nathan Tannar on 8/18/17. -// - -import UIKit - -/// An object containing the key animation properties from NSNotification -public struct KeyboardNotification { - - // MARK: - Properties - - /// The event that triggered the transition - public let event: KeyboardEvent - - /// The animation length the keyboards transition - public let timeInterval: TimeInterval - - /// The animation properties of the keyboards transition - public let animationOptions: UIView.AnimationOptions - - /// iPad supports split-screen apps, this indicates if the notification was for the current app - public let isForCurrentApp: Bool - - /// The keyboards frame at the start of its transition - public var startFrame: CGRect - - /// The keyboards frame at the beginning of its transition - public var endFrame: CGRect - - /// Requires that the `NSNotification` is based on a `UIKeyboard...` event - /// - /// - Parameter notification: `KeyboardNotification` - public init?(from notification: NSNotification) { - guard notification.event != .unknown else { return nil } - self.event = notification.event - self.timeInterval = notification.timeInterval ?? 0.25 - self.animationOptions = notification.animationOptions - self.isForCurrentApp = notification.isForCurrentApp ?? true - self.startFrame = notification.startFrame ?? .zero - self.endFrame = notification.endFrame ?? .zero - } - -} - -// -// NSConstraintLayoutSet.swift -// InputBarAccessoryView -// -// Copyright © 2017-2018 Nathan Tannar. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Nathan Tannar on 8/25/17. -// - -import Foundation -import UIKit - -class NSLayoutConstraintSet { - - var top: NSLayoutConstraint? - var bottom: NSLayoutConstraint? - var left: NSLayoutConstraint? - var right: NSLayoutConstraint? - var centerX: NSLayoutConstraint? - var centerY: NSLayoutConstraint? - var width: NSLayoutConstraint? - var height: NSLayoutConstraint? - - public init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, - left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, - centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, - width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { - self.top = top - self.bottom = bottom - self.left = left - self.right = right - self.centerX = centerX - self.centerY = centerY - self.width = width - self.height = height - } - - /// All of the currently configured constraints - private var availableConstraints: [NSLayoutConstraint] { - #if swift(>=4.1) - return [top, bottom, left, right, centerX, centerY, width, height].compactMap {$0} - #else - return [top, bottom, left, right, centerX, centerY, width, height].flatMap {$0} - #endif - } - - /// Activates all of the non-nil constraints - /// - /// - Returns: Self - @discardableResult - func activate() -> Self { - NSLayoutConstraint.activate(availableConstraints) - return self - } - - /// Deactivates all of the non-nil constraints - /// - /// - Returns: Self - @discardableResult - func deactivate() -> Self { - NSLayoutConstraint.deactivate(availableConstraints) - return self - } -} - -// -// NSNotification+Extensions.swift -// InputBarAccessoryView -// -// Copyright © 2017-2018 Nathan Tannar. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Nathan Tannar on 8/25/17. -// - -import UIKit - -internal extension NSNotification { - - var event: KeyboardEvent { - switch self.name { - case UIResponder.keyboardWillShowNotification: - return .willShow - case UIResponder.keyboardDidShowNotification: - return .didShow - case UIResponder.keyboardWillHideNotification: - return .willHide - case UIResponder.keyboardDidHideNotification: - return .didHide - case UIResponder.keyboardWillChangeFrameNotification: - return .willChangeFrame - case UIResponder.keyboardDidChangeFrameNotification: - return .didChangeFrame - default: - return .unknown - } - } - - var timeInterval: TimeInterval? { - guard let value = userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { return nil } - return TimeInterval(truncating: value) - } - - var animationCurve: UIView.AnimationCurve? { - guard let index = (userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue else { return nil } - guard index >= 0 && index <= 3 else { return .linear } - return UIView.AnimationCurve.init(rawValue: index) ?? .linear - } - - var animationOptions: UIView.AnimationOptions { - guard let curve = animationCurve else { return [] } - switch curve { - case .easeIn: - return .curveEaseIn - case .easeOut: - return .curveEaseOut - case .easeInOut: - return .curveEaseInOut - case .linear: - return .curveLinear - @unknown default: - return [] - } - } - - var startFrame: CGRect? { - return (userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue - } - - var endFrame: CGRect? { - return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue - } - - var isForCurrentApp: Bool? { - return (userInfo?[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber)?.boolValue - } - -} From 6a3eee0a147d34c9a81f6ae6faa51d6daaada317 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 17 May 2023 12:37:04 +0300 Subject: [PATCH 042/136] [trello.com/c/TGBBXBeX] feat: support email for internal error is optional --- Adamant/ServiceProtocols/DialogService.swift | 2 ++ Adamant/Services/AdamantDialogService.swift | 19 +++++++++++++++---- .../View/Managers/ChatDialogManager.swift | 4 ++-- .../Chat/ViewModel/ChatViewModel.swift | 8 ++++---- .../Chat/ViewModel/Models/ChatDialog.swift | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index 7701de2c9..cdb3c181e 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -132,7 +132,9 @@ protocol DialogService: AnyObject { func showWarning(withMessage: String) func showError(withMessage: String, supportEmail: Bool, error: Error?) func showRichError(error: RichError) + func showRichError(error: RichError, supportEmail: Bool?) func showRichError(error: Error) + func showRichError(error: Error, supportEmail: Bool?) func showNoConnectionNotification() func dissmisNoConnectionNotification() diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdamantDialogService.swift index d758a4756..871fc730c 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -135,29 +135,40 @@ extension AdamantDialogService { } func showRichError(error: RichError) { + showRichError(error: error, supportEmail: nil) + } + + func showRichError(error: RichError, supportEmail: Bool?) { switch error.level { case .warning: showWarning(withMessage: error.message) case .error: showError( withMessage: error.message, - supportEmail: false, + supportEmail: supportEmail ?? false, error: error.internalError ) case .internalError: showError( withMessage: error.message, - supportEmail: true, + supportEmail: supportEmail ?? true, error: error.internalError ) } } func showRichError(error: Error) { + showRichError(error: error, supportEmail: nil) + } + + func showRichError(error: Error, supportEmail: Bool?) { if let error = error as? RichError { - showRichError(error: error) + showRichError(error: error, supportEmail: supportEmail) } else { - showError(withMessage: error.localizedDescription, supportEmail: true, error: error) + showError( + withMessage: error.localizedDescription, + supportEmail: supportEmail ?? true, + error: error) } } diff --git a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift index 03d2dedf0..ce4983cb7 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift @@ -52,8 +52,8 @@ private extension ChatDialogManager { ) case let .warning(message): dialogService.showWarning(withMessage: message) - case let .richError(error): - dialogService.showRichError(error: error) + case let .richError(error, supportEmail): + dialogService.showRichError(error: error, supportEmail: supportEmail) case let .menu(sender): showMenu(sender: sender) case .freeTokenAlert: diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift index 79943797b..f49c89323 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift @@ -352,7 +352,7 @@ final class ChatViewModel: NSObject { case .invalidTransactionStatus: dialog.send(.warning(.adamantLocalized.chat.cancelError)) default: - dialog.send(.richError(error)) + dialog.send(.richError(error, supportEmail: true)) } } }.stored(in: tasksStorage) @@ -370,7 +370,7 @@ final class ChatViewModel: NSObject { case .invalidTransactionStatus: break default: - dialog.send(.richError(error)) + dialog.send(.richError(error, supportEmail: true)) } } }.stored(in: tasksStorage) @@ -399,7 +399,7 @@ final class ChatViewModel: NSObject { } catch { print(error) dialog.send(.progress(false)) - dialog.send(.richError(error)) + dialog.send(.richError(error, supportEmail: false)) } }.stored(in: tasksStorage) } @@ -594,7 +594,7 @@ private extension ChatViewModel { break } - dialog.send(.richError(error)) + dialog.send(.richError(error, supportEmail: true)) } func inputTextUpdated() { diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift b/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift index 49f20ef2c..8974272bd 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift +++ b/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift @@ -13,7 +13,7 @@ enum ChatDialog { case alert(String) case error(String, supportEmail: Bool) case warning(String) - case richError(Error) + case richError(Error, supportEmail: Bool) case freeTokenAlert case removeMessageAlert(id: String) case reportMessageAlert(id: String) From ed1d3640d9757f92256e3e69cadcffdd3023f6dc Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 17 May 2023 13:33:59 +0300 Subject: [PATCH 043/136] [trello.com/c/TGBBXBeX] refactor: message size calculator --- .../FixedTextMessageSizeCalculator.swift | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift b/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift index 4c8c78cd9..af52674df 100644 --- a/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift +++ b/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift @@ -38,33 +38,39 @@ override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - var messageContainerSize: CGSize - let attributedText: NSAttributedString + var messageContainerSize: CGSize = .zero + let messageInsets = messageLabelInsets(for: message) - let textMessageKind = message.kind.textMessageKind - switch textMessageKind { - case .attributedText(let text): - attributedText = text - case .text(let text), .emoji(let text): - attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) - default: - assertionFailure("messageContainerSize received unhandled MessageDataType: \(message.kind)") - return .zero + if case let .message(model) = getMessages()[indexPath.section].fullModel.content { + messageContainerSize = labelSize(for: model.value.text, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical } - - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - - let messageInsets = messageLabelInsets(for: message) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { let contentViewHeight = model.value.contentHeight(for: messageContainerSize.width) + + let attributedText: NSAttributedString + + let textMessageKind = message.kind.textMessageKind + switch textMessageKind { + case .attributedText(let text): + attributedText = text + case .text(let text), .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + default: + assertionFailure("messageContainerSize received unhandled MessageDataType: \(message.kind)") + return .zero + } + + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal messageContainerSize.height = contentViewHeight } if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { let contentViewHeight = model.value.height(for: messagesFlowLayout.itemWidth) + messageContainerSize.width += messageInsets.horizontal messageContainerSize.height = contentViewHeight } From 52fb784e89840b8f6151fe81516fd1d123d94675 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Wed, 17 May 2023 16:33:36 +0300 Subject: [PATCH 044/136] [trello.com/c/TGBBXBeX] feat: check db for reply message --- .../DataProviders/AdamantChatsProvider.swift | 19 +++- .../AdamantRichTransactionReplyService.swift | 104 ++++++++++++++++-- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 6a880c4b1..a205d78ee 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -1228,6 +1228,23 @@ extension AdamantChatsProvider { return nil } } + + /// Search transaction in local storage + /// + /// - Parameter id: Transacton ID + /// - Returns: Transaction, if found + func getBaseTransactionFromDB(id: String, context: NSManagedObjectContext) -> BaseTransaction? { + let request = NSFetchRequest(entityName: "BaseTransaction") + request.predicate = NSPredicate(format: "transactionId == %@", String(id)) + request.fetchLimit = 1 + + do { + let result = try context.fetch(request) + return result.first + } catch { + return nil + } + } } // MARK: - Processing @@ -1301,7 +1318,7 @@ extension AdamantChatsProvider { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = self.stack.container.viewContext - guard getTransfer(id: transactionId, context: context) == nil else { return } + guard getBaseTransactionFromDB(id: transactionId, context: context) == nil else { return } var transactions: [Transaction] = [] var offset = chatLoadedMessages[recipient] ?? 0 diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 85e8d86db..8805cb6be 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -83,8 +83,7 @@ private extension AdamantRichTransactionReplyService { transaction.decodedReplyMessage == nil else { return } - let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) - let message = try getReplyMessage(by: transactionReply) + let message = try await getReplyMessage(from: id) setReplyMessage( for: transaction, @@ -106,8 +105,7 @@ private extension AdamantRichTransactionReplyService { transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil else { return } - let transactionReply = try await getReplyTransaction(by: UInt64(id) ?? 0) - let message = try getReplyMessage(by: transactionReply) + let message = try await getReplyMessage(from: id) setReplyMessage( for: transaction, @@ -122,11 +120,20 @@ private extension AdamantRichTransactionReplyService { } } - func getReplyTransaction(by id: UInt64) async throws -> Transaction { + func getReplyMessage(from id: String) async throws -> String { + if let baseTransaction = getTransactionFromDB(id: id) { + return try getReplyMessage(from: baseTransaction) + } + + let transactionReply = try await getTransactionFromAPI(by: UInt64(id) ?? 0) + return try getReplyMessage(from: transactionReply) + } + + func getTransactionFromAPI(by id: UInt64) async throws -> Transaction { try await apiService.getTransaction(id: id, withAsset: true) } - func getReplyMessage(by transaction: Transaction) throws -> String { + func getReplyMessage(from transaction: Transaction) throws -> String { guard let address = accountService.account?.address, let privateKey = accountService.keypair?.privateKey else { @@ -175,10 +182,14 @@ private extension AdamantRichTransactionReplyService { if let data = decodedMessage.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), transaction.amount > 0 { - let comment = richContent[RichContentKeys.reply.replyMessage] as? String + let commentContent = (richContent[RichContentKeys.reply.replyMessage] as? String) ?? "" + let comment = !commentContent.isEmpty + ? ": \(commentContent)" + : "" + let humanType = AdmWalletService.currencySymbol - message = "\(transactionStatus) \(transaction.amount) \(humanType)\(comment ?? "")" + message = "\(transactionStatus) \(transaction.amount) \(humanType)\(comment)" break } @@ -207,7 +218,55 @@ private extension AdamantRichTransactionReplyService { return message } - + + func getReplyMessage(from transaction: BaseTransaction) throws -> String { + guard let address = accountService.account?.address else { + throw ApiServiceError.accountNotFound + } + + let isOut = transaction.senderId == address + + let transactionStatus = isOut + ? String.adamantLocalized.chat.transactionSent + : String.adamantLocalized.chat.transactionReceived + + var message: String + + switch transaction { + case let trs as MessageTransaction: + message = trs.message ?? "" + case let trs as TransferTransaction: + let trsComment = trs.comment ?? "" + let comment = !trsComment.isEmpty + ? ": \(trsComment)" + : "" + + message = "\(transactionStatus) \(trs.amount ?? 0.0) \(AdmWalletService.currencySymbol)\(comment)" + case let trs as RichMessageTransaction: + if let replyMessage = trs.getRichValue(for: RichContentKeys.reply.replyMessage) { + message = replyMessage + break + } + + if let richContent = trs.richContent, + let transfer = RichMessageTransfer(content: richContent) { + let comment = !transfer.comments.isEmpty + ? ": \(transfer.comments)" + : "" + let humanType = richMessageProvider[transfer.type]?.tokenSymbol ?? transfer.type + + message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" + break + } + + message = unknownErrorMessage + default: + message = unknownErrorMessage + } + + return message + } + func setReplyMessage( for transaction: RichMessageTransaction, message: String @@ -239,9 +298,34 @@ private extension AdamantRichTransactionReplyService { transaction?.decodedReplyMessage = message try? privateContext.save() } +} - // MARK: Core Data +// MARK: Core Data + +private extension AdamantRichTransactionReplyService { + /// Search transaction in local storage + /// + /// - Parameter id: Transacton ID + /// - Returns: Transaction, if found + func getTransactionFromDB(id: String) -> BaseTransaction? { + let privateContext = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + privateContext.parent = coreDataStack.container.viewContext + + let request = NSFetchRequest(entityName: "BaseTransaction") + request.predicate = NSPredicate(format: "transactionId == %@", String(id)) + request.fetchLimit = 1 + + do { + let result = try privateContext.fetch(request) + return result.first + } catch { + return nil + } + } + func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: TransferTransaction) { switch type { case .insert, .update: From 3fc9753728222e24ce790bd073d1ba92d07e6865 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 18 May 2023 11:16:14 +0300 Subject: [PATCH 045/136] [trello.com/c/TGBBXBeX] fix: animate a cell with long distance --- .../Chat/View/ChatViewController.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Stories/Chat/View/ChatViewController.swift index fca73c688..6bee17bd6 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Stories/Chat/View/ChatViewController.swift @@ -145,6 +145,13 @@ final class ChatViewController: MessagesViewController { willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath ) { + if let index = viewModel.needToAnimateCellIndex, + indexPath.section == index { + cell.isSelected = true + cell.isSelected = false + viewModel.needToAnimateCellIndex = nil + } + super.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath) guard indexPath.section < 4 else { return } viewModel.loadMoreMessagesIfNeeded() @@ -672,13 +679,15 @@ extension ChatViewController { internal override func scrollViewDidEndScrollingAnimation(_: UIScrollView) { guard let index = viewModel.needToAnimateCellIndex else { return } + let isVisible = messagesCollectionView.indexPathsForVisibleItems.contains { + $0.section == index + } + + guard isVisible else { return } + let cell = messagesCollectionView.cellForItem(at: .init(item: .zero, section: index)) cell?.isSelected = true - - Task { - await Task.sleep(interval: 1.0) - cell?.isSelected = false - } + cell?.isSelected = false viewModel.needToAnimateCellIndex = nil } From 694f480614a42cecf3a9b7e2980764ca2fc72e78 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 18 May 2023 12:05:38 +0300 Subject: [PATCH 046/136] [trello.com/c/fgOg4Ftk] Select all text in name field --- .../Chat/View/Managers/ChatDialogManager.swift | 4 +++- .../Stories/ChatsList/ChatListViewController.swift | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift index 03d2dedf0..500deaf55 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift @@ -224,7 +224,9 @@ private extension ChatDialogManager { style: .default ) { [weak self] _ in guard let alert = self?.makeRenameAlert() else { return } - self?.dialogService.present(alert, animated: true, completion: nil) + self?.dialogService.present(alert, animated: true, completion: { + alert.textFields?.forEach { $0.selectAll(nil) } + }) } } diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Stories/ChatsList/ChatListViewController.swift index 0fbe7349f..8e8012ffa 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Stories/ChatsList/ChatListViewController.swift @@ -958,13 +958,10 @@ extension ChatListViewController { let rename = UIAlertAction(title: String.adamantLocalized.chat.rename, style: .default) { [weak self] _ in let alert = UIAlertController(title: String(format: String.adamantLocalized.chat.actionsBody, address), message: nil, preferredStyle: .alert) - alert.addTextField { (textField) in + alert.addTextField { [weak self] textField in textField.placeholder = String.adamantLocalized.chat.name textField.autocapitalizationType = .words - - if let name = partner.name { - textField.text = name - } + textField.text = self?.addressBook.getName(for: address) } alert.addAction(UIAlertAction(title: String.adamantLocalized.chat.rename, style: .default) { [weak alert] (_) in @@ -978,7 +975,9 @@ extension ChatListViewController { alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen - self?.present(alert, animated: true, completion: nil) + self?.present(alert, animated: true, completion: { + alert.textFields?.forEach { $0.selectAll(nil) } + }) } let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil) From 0f0b433927fd85022442ce70f491a0016255df28 Mon Sep 17 00:00:00 2001 From: StanislavDevIOS Date: Thu, 18 May 2023 12:20:57 +0300 Subject: [PATCH 047/136] [trello.com/c/A2gfk2q1] Inc top constraint for wallets --- Adamant/Stories/Account/AccountViewController.swift | 2 +- Adamant/Stories/Account/WalletCollectionViewCell.xib | 10 +++++----- .../ChatsList/ComplexTransferViewController.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Stories/Account/AccountViewController.swift index 281a2fb8c..3163862d7 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Stories/Account/AccountViewController.swift @@ -764,7 +764,7 @@ class AccountViewController: FormViewController { private func updatePagingItemHeight() { if walletViewControllers.count > 0 { - pagingViewController.menuItemSize = .fixed(width: 110, height: 110) + pagingViewController.menuItemSize = .fixed(width: 110, height: 114) } else { pagingViewController.menuItemSize = .fixed(width: 110, height: 0) } diff --git a/Adamant/Stories/Account/WalletCollectionViewCell.xib b/Adamant/Stories/Account/WalletCollectionViewCell.xib index 834b30cf9..56515e828 100644 --- a/Adamant/Stories/Account/WalletCollectionViewCell.xib +++ b/Adamant/Stories/Account/WalletCollectionViewCell.xib @@ -18,23 +18,23 @@ - + - +