diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index f1e5c2fe06d45ccab6ea77b5422a5df83cec9262..c4da099e02c599f90cf55f6c49f28ca3676c90a4 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -329,26 +329,23 @@ extension GroupChatController: UICollectionViewDataSource { let canReply: () -> Bool = { item.status == .sent || item.status == .received } let performReply: () -> Void = { [weak self] in - //self?.viewModel.didRequestReply(item) + self?.viewModel.didRequestReply(item) } let name: (Data) -> String = viewModel.getName(from:) - let text: (Data) -> String = viewModel.getText(from:) let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) + let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) if item.status == .received { if item.replyMessageId != nil { let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildReplyGroup( -// bubble: cell.leftView, -// with: item, -// reply: .init( -// text: text(item.payload.reply!.messageId), -// sender: name(item.payload.reply!.senderId) -// ), -// sender: name(item.sender) -// ) + Bubbler.buildReplyGroup( + bubble: cell.leftView, + with: item, + reply: replyContent(item.replyMessageId!), + sender: name(item.senderId) + ) cell.canReply = canReply() cell.performReply = performReply @@ -357,7 +354,12 @@ extension GroupChatController: UICollectionViewDataSource { return cell } else { let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildGroup(bubble: cell.leftView, with: item, with: name(item.sender)) + Bubbler.buildGroup( + bubble: cell.leftView, + with: item, + with: name(item.senderId) + ) + cell.canReply = canReply() cell.performReply = performReply cell.leftView.didTapShowRound = { showRound(item.roundURL) } @@ -368,15 +370,12 @@ extension GroupChatController: UICollectionViewDataSource { if item.replyMessageId != nil { let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildReplyGroup( -// bubble: cell.rightView, -// with: item, -// reply: .init( -// text: text(item.payload.reply!.messageId), -// sender: name(item.payload.reply!.senderId) -// ), -// sender: name(item.sender) -// ) + Bubbler.buildReplyGroup( + bubble: cell.rightView, + with: item, + reply: replyContent(item.replyMessageId!), + sender: name(item.senderId) + ) cell.canReply = canReply() cell.performReply = performReply @@ -385,7 +384,12 @@ extension GroupChatController: UICollectionViewDataSource { } else { let cell: OutgoingFailedGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildGroup(bubble: cell.rightView, with: item, with: name(item.sender)) + Bubbler.buildGroup( + bubble: cell.rightView, + with: item, + with: name(item.senderId) + ) + cell.canReply = canReply() cell.performReply = performReply @@ -395,15 +399,12 @@ extension GroupChatController: UICollectionViewDataSource { if item.replyMessageId != nil { let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildReplyGroup( -// bubble: cell.rightView, -// with: item, -// reply: .init( -// text: text(item.payload.reply!.messageId), -// sender: name(item.payload.reply!.senderId) -// ), -// sender: name(item.sender) -// ) + Bubbler.buildReplyGroup( + bubble: cell.rightView, + with: item, + reply: replyContent(item.replyMessageId!), + sender: name(item.senderId) + ) cell.canReply = canReply() cell.performReply = performReply @@ -413,7 +414,12 @@ extension GroupChatController: UICollectionViewDataSource { } else { let cell: OutgoingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// Bubbler.buildGroup(bubble: cell.rightView, with: item, with: name(item.sender)) + Bubbler.buildGroup( + bubble: cell.rightView, + with: item, + with: name(item.senderId) + ) + cell.canReply = canReply() cell.performReply = performReply cell.rightView.didTapShowRound = { showRound(item.roundURL) } @@ -537,7 +543,7 @@ extension GroupChatController: UICollectionViewDelegate { } let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in - //self?.viewModel.didRequestReply(item) + self?.viewModel.didRequestReply(item) } let delete = UIAction(title: Localized.Chat.BubbleMenu.delete, state: .off) { [weak self] _ in diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 7f611e182aa3dfdf95a7cc92567e93485afe9398..cf4d559592955cee58b705a04cc00f212b913b61 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -489,10 +489,9 @@ extension SingleChatController: UICollectionViewDataSource { cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let name: (Data) -> String = viewModel.getName(from:) - let text: (Data) -> String = viewModel.getText(from:) let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) let item = sections[indexPath.section].elements[indexPath.item] + let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:) let performReply: () -> Void = { [weak self] in self?.viewModel.didRequestReply(item) } let factory = CellFactory.combined(factories: [ @@ -503,9 +502,9 @@ extension SingleChatController: UICollectionViewDataSource { .incomingText(performReply: performReply, showRound: showRound), .outgoingText(performReply: performReply, showRound: showRound), .outgoingFailedText(performReply: performReply), - .incomingReply(performReply: performReply, name: name, text: text, showRound: showRound), - .outgoingReply(performReply: performReply, name: name, text: text, showRound: showRound), - .outgoingFailedReply(performReply: performReply, name: name, text: text) + .incomingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), + .outgoingReply(performReply: performReply, replyContent: replyContent, showRound: showRound), + .outgoingFailedReply(performReply: performReply, replyContent: replyContent) ]) return factory(item: item, collectionView: collectionView, indexPath: indexPath) diff --git a/Sources/ChatFeature/Helpers/BubbleBuilder.swift b/Sources/ChatFeature/Helpers/BubbleBuilder.swift index dadbaef0496edcb6a98442a0b07e99419d2249fe..81aa5c03c686d64f76008d3cef2169aba0a4f3d2 100644 --- a/Sources/ChatFeature/Helpers/BubbleBuilder.swift +++ b/Sources/ChatFeature/Helpers/BubbleBuilder.swift @@ -1,10 +1,11 @@ import UIKit import Shared +import XXModels final class Bubbler { static func build( audioBubble: AudioMessageView, - with item: ChatItem + with item: Message ) { audioBubble.dateLabel.text = item.date.asHoursAndMinutes() @@ -41,7 +42,7 @@ final class Bubbler { static func build( imageBubble: ImageMessageView, - with item: ChatItem + with item: Message ) { // let progress = item.payload.attachment!.progress // imageBubble.progressLabel.text = String(format: "%.1f%%", progress * 100) @@ -80,9 +81,9 @@ final class Bubbler { static func build( bubble: StackMessageView, - with item: ChatItem + with item: Message ) { - bubble.textView.text = item.payload.text + bubble.textView.text = item.text bubble.senderLabel.removeFromSuperview() bubble.dateLabel.text = item.date.asHoursAndMinutes() @@ -135,14 +136,14 @@ final class Bubbler { static func buildReply( bubble: ReplyStackMessageView, - with item: ChatItem, - reply: ReplyModel + with item: Message, + reply: (contactTitle: String, messageText: String) ) { bubble.dateLabel.text = item.date.asHoursAndMinutes() - bubble.textView.text = item.payload.text + bubble.textView.text = item.text - bubble.replyView.message.text = reply.text - bubble.replyView.title.text = reply.sender + bubble.replyView.message.text = reply.messageText + bubble.replyView.title.text = reply.contactTitle let roundButtonColor: UIColor @@ -195,4 +196,112 @@ final class Bubbler { ) bubble.roundButton.setAttributedTitle(attrString, for: .normal) } + + static func buildReplyGroup( + bubble: ReplyStackMessageView, + with item: Message, + reply: (contactTitle: String, messageText: String), + sender: String + ) { + bubble.dateLabel.text = item.date.asHoursAndMinutes() + bubble.textView.text = item.text + + bubble.replyView.message.text = reply.messageText + bubble.replyView.title.text = reply.contactTitle + + let roundButtonColor: UIColor + + switch item.status { + case .received, .receiving: + bubble.senderLabel.text = sender + bubble.backgroundColor = Asset.neutralWhite.color + bubble.textView.textColor = Asset.neutralActive.color + bubble.dateLabel.textColor = Asset.neutralDisabled.color + roundButtonColor = Asset.neutralDisabled.color + bubble.replyView.container.backgroundColor = Asset.brandDefault.color + bubble.replyView.space.backgroundColor = Asset.brandPrimary.color + bubble.lockerImageView.removeFromSuperview() + bubble.revertBottomStackOrder() + case .sendingFailed, .sendingTimedOut: + bubble.senderLabel.removeFromSuperview() + bubble.backgroundColor = Asset.accentDanger.color + bubble.textView.textColor = Asset.neutralWhite.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + bubble.replyView.space.backgroundColor = Asset.neutralWhite.color + bubble.replyView.container.backgroundColor = Asset.brandLight.color + case .sent, .sending: + bubble.senderLabel.removeFromSuperview() + bubble.textView.textColor = Asset.neutralWhite.color + bubble.backgroundColor = Asset.brandBubble.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + bubble.replyView.space.backgroundColor = Asset.neutralWhite.color + bubble.replyView.container.backgroundColor = Asset.brandLight.color + case .receivingFailed: + fatalError() + } + + let attrString = NSAttributedString( + string: "show mix", + attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: roundButtonColor, + .foregroundColor: roundButtonColor, + .font: Fonts.Mulish.regular.font(size: 12.0) as Any + ] + ) + + bubble.roundButton.setAttributedTitle(attrString, for: .normal) + } + + static func buildGroup( + bubble: StackMessageView, + with item: Message, + with senderName: String + ) { + bubble.textView.text = item.text + bubble.dateLabel.text = item.date.asHoursAndMinutes() + + let roundButtonColor: UIColor + + switch item.status { + case .received, .receiving: + bubble.senderLabel.text = senderName + bubble.backgroundColor = Asset.neutralWhite.color + bubble.textView.textColor = Asset.neutralActive.color + bubble.dateLabel.textColor = Asset.neutralDisabled.color + roundButtonColor = Asset.neutralDisabled.color + bubble.lockerImageView.removeFromSuperview() + bubble.revertBottomStackOrder() + case .sendingFailed, .sendingTimedOut: + bubble.senderLabel.removeFromSuperview() + bubble.backgroundColor = Asset.accentDanger.color + bubble.textView.textColor = Asset.neutralWhite.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + case .sent, .sending: + bubble.senderLabel.removeFromSuperview() + bubble.backgroundColor = Asset.brandBubble.color + bubble.textView.textColor = Asset.neutralWhite.color + bubble.dateLabel.textColor = Asset.neutralWhite.color + roundButtonColor = Asset.neutralWhite.color + case .receivingFailed: + fatalError() + } + + let attrString = NSAttributedString( + string: "show mix", + attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: roundButtonColor, + .foregroundColor: roundButtonColor, + .font: Fonts.Mulish.regular.font(size: 12.0) as Any + ] + ) + + bubble.roundButton.setAttributedTitle(attrString, for: .normal) + } + + } diff --git a/Sources/ChatFeature/Helpers/CellConfigurator.swift b/Sources/ChatFeature/Helpers/CellConfigurator.swift index 01c5f6c026cf156fc81f298ec43e6b551882517d..63ab95fda306b9356872ebf0886ff3d4748c2219 100644 --- a/Sources/ChatFeature/Helpers/CellConfigurator.swift +++ b/Sources/ChatFeature/Helpers/CellConfigurator.swift @@ -1,16 +1,17 @@ import UIKit import Shared import Combine +import XXModels import Voxophone import AVFoundation struct CellFactory { - var canBuild: (ChatItem) -> Bool + var canBuild: (Message) -> Bool - var build: (ChatItem, UICollectionView, IndexPath) -> UICollectionViewCell + var build: (Message, UICollectionView, IndexPath) -> UICollectionViewCell func callAsFunction( - item: ChatItem, + item: Message, collectionView: UICollectionView, indexPath: IndexPath ) -> UICollectionViewCell { @@ -236,14 +237,13 @@ extension CellFactory { extension CellFactory { static func outgoingReply( performReply: @escaping () -> Void, - name: @escaping (Data) -> String, - text: @escaping (Data) -> String, + replyContent: @escaping (Data) -> (String, String), showRound: @escaping (String?) -> Void ) -> Self { .init( canBuild: { item in (item.status == .sent || item.status == .sending) - && item.payload.reply != nil + && item.replyMessageId != nil }, build: { item, collectionView, indexPath in let cell: OutgoingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -251,10 +251,7 @@ extension CellFactory { Bubbler.buildReply( bubble: cell.rightView, with: item, - reply: .init( - text: text(item.payload.reply!.messageId), - sender: name(item.payload.reply!.senderId) - ) + reply: replyContent(item.replyMessageId!) ) cell.canReply = item.status == .sent @@ -267,14 +264,13 @@ extension CellFactory { static func incomingReply( performReply: @escaping () -> Void, - name: @escaping (Data) -> String, - text: @escaping (Data) -> String, + replyContent: @escaping (Data) -> (String, String), showRound: @escaping (String?) -> Void ) -> Self { .init( canBuild: { item in item.status == .received - && item.payload.reply != nil + && item.replyMessageId != nil }, build: { item, collectionView, indexPath in let cell: IncomingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -282,10 +278,7 @@ extension CellFactory { Bubbler.buildReply( bubble: cell.leftView, with: item, - reply: .init( - text: text(item.payload.reply!.messageId), - sender: name(item.payload.reply!.senderId) - ) + reply: replyContent(item.replyMessageId!) ) cell.canReply = item.status == .received cell.performReply = performReply @@ -298,13 +291,12 @@ extension CellFactory { static func outgoingFailedReply( performReply: @escaping () -> Void, - name: @escaping (Data) -> String, - text: @escaping (Data) -> String + replyContent: @escaping (Data) -> (String, String) ) -> Self { .init( canBuild: { item in (item.status == .sendingFailed || item.status == .sendingTimedOut) - && item.payload.reply != nil + && item.replyMessageId != nil }, build: { item, collectionView, indexPath in let cell: OutgoingFailedReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -312,10 +304,7 @@ extension CellFactory { Bubbler.buildReply( bubble: cell.rightView, with: item, - reply: .init( - text: text(item.payload.reply!.messageId), - sender: name(item.payload.reply!.senderId) - ) + reply: replyContent(item.replyMessageId!) ) cell.canReply = false @@ -334,7 +323,7 @@ extension CellFactory { .init( canBuild: { item in item.status == .received - && item.payload.reply == nil + && item.replyMessageId == nil }, build: { item, collectionView, indexPath in let cell: IncomingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -356,7 +345,7 @@ extension CellFactory { .init( canBuild: { item in (item.status == .sending || item.status == .sent) - && item.payload.reply == nil + && item.replyMessageId == nil }, build: { item, collectionView, indexPath in let cell: OutgoingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -375,7 +364,7 @@ extension CellFactory { .init( canBuild: { item in (item.status == .sendingFailed || item.status == .sendingTimedOut) - && item.payload.reply == nil + && item.replyMessageId == nil }, build: { item, collectionView, indexPath in let cell: OutgoingFailedTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) @@ -412,9 +401,9 @@ struct ActionFactory { } static func build( - from item: ChatItem, + from item: Message, action: Action, - closure: @escaping (ChatItem) -> Void + closure: @escaping (Message) -> Void ) -> UIAction? { switch action { diff --git a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift index 7d611491a6824f1c0301a542a7d9e71e2810b7a8..39db1157f8c2e61a7506bba14f4a4b15df5500d0 100644 --- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift @@ -88,25 +88,43 @@ final class GroupChatViewModel { stagedReply = nil } - func getName(from senderId: Data) -> String { - guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else { - return "You" + func getReplyContent(for messageId: Data) -> (String, String) { + guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { + return ("[DELETED]", "[DELETED]") } - return (contact.nickname ?? contact.username) ?? "Fetching username..." + guard let contact = try? session.dbManager.fetchContacts(.init(id: [message.senderId])).first else { + return ("You", message.text) + } + + let contactTitle = (contact.nickname ?? contact.username) ?? "Fetching username..." + return (contactTitle, message.text) } - func getText(from messageId: Data) -> String { - guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { - return "[DELETED]" + func getName(from senderId: Data) -> String { + guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else { + return "You" } - return message.text + return (contact.nickname ?? contact.username) ?? "Fetching username..." } func didRequestReply(_ message: Message) { guard let networkId = message.networkId else { return } + + let senderTitle: String = { + if message.senderId == session.myId { + return "You" + } else { + guard let contact = try? session.dbManager.fetchContacts(.init(id: [message.senderId])).first else { + return "Error" + } + + return (contact.nickname ?? contact.username) ?? "Fetching username..." + } + }() + stagedReply = Reply(messageId: networkId, senderId: message.senderId) - replySubject.send((getName(from: message.senderId), message.text)) + replySubject.send((senderTitle, message.text)) } } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 8690ca1ccc9c850631e6f15fd798a41ab8c54c08..c68096778ff636b588ae07426c1c5838bc7cdbff 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -181,6 +181,8 @@ final class SingleChatViewModel { } func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + let senderTitle: String = { if message.senderId == session.myId { return "You" @@ -190,15 +192,20 @@ final class SingleChatViewModel { }() replySubject.send((senderTitle, message.text)) - stagedReply = Reply(messageId: message.networkId!, senderId: message.senderId) + stagedReply = Reply(messageId: networkId, senderId: message.senderId) } - func getText(from messageId: Data) -> String { + func getReplyContent(for messageId: Data) -> (String, String) { guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { - return "[DELETED]" + return ("[DELETED]", "[DELETED]") + } + + guard let contact = try? session.dbManager.fetchContacts(.init(id: [message.senderId])).first else { + return ("You", message.text) } - return message.text + let contactTitle = (contact.nickname ?? contact.username) ?? "Fetching username..." + return (contactTitle, message.text) } func showRoundFrom(_ roundURL: String?) { @@ -217,10 +224,6 @@ final class SingleChatViewModel { sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) } - func getName(from senderId: Data) -> String { - senderId == session.myId ? "You" : contact.nickname ?? contact.username! - } - func itemAt(indexPath: IndexPath) -> Message? { guard sectionsRelay.value.count > indexPath.section else { return nil }