diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b0f77c254..ae0c87b79 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -140,12 +140,16 @@ 7B251C3627D82D9E001A6284 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; + 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; + 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; 7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; + 7B8D5FC728336093008324D9 /* ReactMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC628336093008324D9 /* ReactMessage.swift */; }; + 7B8D5FC9283369CC008324D9 /* ReactMessage+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; @@ -1131,12 +1135,16 @@ 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; + 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; + 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; + 7B8D5FC628336093008324D9 /* ReactMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactMessage.swift; sourceTree = ""; }; + 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReactMessage+Conversion.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; @@ -2083,6 +2091,15 @@ path = "Views & Modals"; sourceTree = ""; }; + 7B8D5FC528336071008324D9 /* Emoji Reacts */ = { + isa = PBXGroup; + children = ( + 7B8D5FC628336093008324D9 /* ReactMessage.swift */, + 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */, + ); + path = "Emoji Reacts"; + sourceTree = ""; + }; 7B93D06827CF173D00811CB6 /* Message Requests */ = { isa = PBXGroup; children = ( @@ -2222,6 +2239,8 @@ B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */, 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */, 7B7CB188270430D20079FF93 /* CallMessageView.swift */, + 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */, + 7B7037442834BCC0000DCF35 /* ReactionView.swift */, ); path = "Content Views"; sourceTree = ""; @@ -2559,6 +2578,7 @@ C300A5F02554B08500555489 /* Sending & Receiving */ = { isa = PBXGroup; children = ( + 7B8D5FC528336071008324D9 /* Emoji Reacts */, C3D9E3B52567685D0040E4F3 /* Attachments */, B8F5F61925EDE4B0003BF8D4 /* Data Extraction */, C32C5B01256DC054003C73A2 /* Expiration */, @@ -4715,6 +4735,7 @@ C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */, + 7B8D5FC9283369CC008324D9 /* ReactMessage+Conversion.swift in Sources */, 7BD477AA27F15F24004E2822 /* OpenGroupServerIdLookup.swift in Sources */, C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */, C352A3932557883D00338F3E /* JobDelegate.swift in Sources */, @@ -4803,6 +4824,7 @@ C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */, C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */, B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */, + 7B8D5FC728336093008324D9 /* ReactMessage.swift in Sources */, C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */, C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, @@ -4929,6 +4951,7 @@ B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */, B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, + 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, @@ -4963,6 +4986,7 @@ 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, + 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index eb1208b39..1a1614631 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -821,8 +821,23 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc func react(_ viewItem: ConversationViewItem, with emoji: String) { print("Ryan Test: \(emoji)") - // TODO: send emoji react message UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) + guard let message = viewItem.interaction as? TSMessage else { return } + var authorId = getUserHexEncodedPublicKey() + if let incomingMessage = message as? TSIncomingMessage { authorId = incomingMessage.authorId } + let reactMessage = ReactMessage(timestamp: message.timestamp, authorId: authorId, emoji: emoji) + reactMessage.sender = getUserHexEncodedPublicKey() + let thread = self.thread + let sentTimestamp: UInt64 = NSDate.millisecondTimestamp() + let visibleMessage = VisibleMessage() + visibleMessage.sentTimestamp = sentTimestamp + visibleMessage.reaction = .from(reactMessage) + visibleMessage.reaction?.kind = .react + Storage.write { transaction in + message.update(withReaction: reactMessage, transaction: transaction) + // TODO: send emoji react message +// MessageSender.send(<#T##message: Message##Message#>, in: thread, using: <#T##YapDatabaseReadWriteTransaction#>) + } } func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift new file mode 100644 index 000000000..f603174fb --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -0,0 +1,46 @@ + +final class ReactionContainerView : UIView { + private lazy var containerView: UIStackView = { + let result = UIStackView() + result.axis = .vertical + result.spacing = Values.smallSpacing + return result + }() + + private var showingAllReactions = false + + // MARK: Lifecycle + init() { + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + addSubview(containerView) + containerView.pin(to: self) + } + + public func update(_ reactions: [(String, Int)]) { + for subview in containerView.arrangedSubviews { + containerView.removeArrangedSubview(subview) + } + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = Values.smallSpacing + for reaction in reactions { + let reactionView = ReactionView(emoji: reaction.0, number: reaction.1) + stackView.addArrangedSubview(reactionView) + } + containerView.addArrangedSubview(stackView) + } +} + + diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift new file mode 100644 index 000000000..4f25b7925 --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -0,0 +1,49 @@ +import UIKit + +final class ReactionView : UIView { + private let emoji: String + private let number: Int + + // MARK: Settings + private static let height: CGFloat = 22 + + // MARK: Lifecycle + init(emoji: String, number: Int) { + self.emoji = emoji + self.number = number + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + let emojiLabel = UILabel() + emojiLabel.text = emoji + emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + + let numberLabel = UILabel() + numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.2f", Float(number) / 1000) + "k" + numberLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + numberLabel.textColor = Colors.text + + let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ]) + stackView.axis = .horizontal + stackView.spacing = Values.verySmallSpacing + stackView.alignment = .center + stackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing) + stackView.isLayoutMarginsRelativeArrangement = true + addSubview(stackView) + stackView.pin(to: self) + + set(.height, to: ReactionView.height) + backgroundColor = Colors.receivedMessageBackground + layer.cornerRadius = ReactionView.height / 2 + } +} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f247fcb47..f2caaeb6e 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -9,11 +9,16 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) private lazy var profilePictureViewLeftConstraint = profilePictureView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var profilePictureViewWidthConstraint = profilePictureView.set(.width, to: Values.verySmallProfilePictureSize) + private lazy var bubbleViewLeftConstraint1 = bubbleView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var bubbleViewLeftConstraint2 = bubbleView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize) private lazy var bubbleViewTopConstraint = bubbleView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) private lazy var bubbleViewRightConstraint1 = bubbleView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) private lazy var bubbleViewRightConstraint2 = bubbleView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize) + + private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: bubbleView) + private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: bubbleView) + private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: bubbleView, withInset: 0) private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) private lazy var messageStatusImageViewHeightConstraint = messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) @@ -81,6 +86,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private lazy var snContentView = UIView() + private lazy var reactionContainerView = ReactionContainerView() + internal lazy var messageStatusImageView: UIImageView = { let result = UIImageView() result.contentMode = .scaleAspectFit @@ -163,7 +170,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { addSubview(profilePictureView) profilePictureViewLeftConstraint.isActive = true profilePictureViewWidthConstraint.isActive = true - profilePictureView.pin(.bottom, to: .bottom, of: self, withInset: -1) // Moderator icon image view moderatorIconImageView.set(.width, to: 20) moderatorIconImageView.set(.height, to: 20) @@ -182,6 +188,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { // Content view bubbleView.addSubview(snContentView) snContentView.pin(to: bubbleView) + // Reaction view + addSubview(reactionContainerView) + reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.smallSpacing) + reactionContainerViewLeftConstraint.isActive = true // Message status image view addSubview(messageStatusImageView) messageStatusImageViewTopConstraint.isActive = true @@ -244,6 +254,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { updateBubbleViewCorners() // Content view populateContentView(for: viewItem, message: message) + // Reaction view + reactionContainerViewLeftConstraint.isActive = (direction == .incoming) + reactionContainerViewRightConstraint.isActive = (direction == .outgoing) + populateReaction(for: viewItem, message: message) // Date break headerViewTopConstraint.constant = shouldInsetHeader ? Values.mediumSpacing : 1 headerView.subviews.forEach { $0.removeFromSuperview() } @@ -438,6 +452,21 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } + private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) { + let reactions: OrderedDictionary = OrderedDictionary() + for reaction in message.reactions { + if let reactMessage = reaction as? ReactMessage, let emoji = reactMessage.emoji { + if let number = reactions.value(forKey: emoji) { + reactions.replace(key: emoji, value: number + 1) + } else { + reactions.append(key: emoji, value: 1) + } + } + } + print("Ryan Test: \(reactions.orderedKeys)") + reactionContainerView.update(reactions.orderedItems) + } + override func layoutSubviews() { super.layoutSubviews() updateBubbleViewCorners() diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.h b/SessionMessagingKit/Messages/Signal/TSMessage.h index 8739181c0..dc58ab759 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSMessage.h @@ -20,6 +20,7 @@ typedef NS_ENUM(NSUInteger, TSMessageDirection) { @class TSAttachment; @class TSAttachmentStream; @class TSQuotedMessage; +@class SNReactMessage; @class YapDatabaseReadWriteTransaction; extern const NSUInteger kOversizeTextMessageSizeThreshold; @@ -41,6 +42,7 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; @property (nonatomic, nullable) NSString *serverHash; @property (nonatomic) BOOL isDeleted; @property (nonatomic) BOOL isCallMessage; +@property (nonatomic, readonly) NSMutableArray *reactions; - (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; @@ -88,6 +90,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; - (void)updateCallMessageWithNewBody:(NSString *)newBody transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index 0584aaf74..db112d305 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -29,6 +29,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; @property (nonatomic, nullable) NSString *body; @property (nonatomic) uint32_t expiresInSeconds; @property (nonatomic) uint64_t expireStartedAt; +@property (nonatomic) NSMutableArray *reactions; /** * The version of the model class's schema last used to serialize this model. Use this to manage data migrations during @@ -88,6 +89,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; _serverHash = serverHash; _isDeleted = false; _isCallMessage = false; + _reactions = [NSMutableArray new]; return self; } @@ -137,6 +139,10 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; if (!_attachmentIds) { _attachmentIds = [NSMutableArray new]; } + + if (!_reactions) { + _reactions = [NSMutableArray new]; + } _schemaVersion = OWSMessageSchemaVersion; @@ -451,6 +457,18 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; }]; } +- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if ([self isKindOfClass:[TSIncomingMessage class]] || [self isKindOfClass:[TSOutgoingMessage class]]) { + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + if (![message.reactions containsObject:reaction]) { + [message.reactions addObject:reaction]; + } + }]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 21c1a41be..b35186428 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -13,6 +13,7 @@ public final class VisibleMessage : Message { @objc public var contact: Contact? @objc public var profile: Profile? @objc public var openGroupInvitation: OpenGroupInvitation? + @objc public var reaction: Reaction? public override var isSelfSendValid: Bool { true } @@ -24,6 +25,7 @@ public final class VisibleMessage : Message { guard super.isValid else { return false } if !attachmentIDs.isEmpty { return true } if openGroupInvitation != nil { return true } + if reaction != nil { return true } if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true } return false } @@ -39,6 +41,7 @@ public final class VisibleMessage : Message { // TODO: Contact if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile } if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation } + if let reaction = coder.decodeObject(forKey: "reaction") as! Reaction? { self.reaction = reaction } } public override func encode(with coder: NSCoder) { @@ -51,6 +54,7 @@ public final class VisibleMessage : Message { // TODO: Contact coder.encode(profile, forKey: "profile") coder.encode(openGroupInvitation, forKey: "openGroupInvitation") + coder.encode(reaction, forKey: "reaction") } // MARK: Proto Conversion @@ -65,6 +69,8 @@ public final class VisibleMessage : Message { if let profile = Profile.fromProto(dataMessage) { result.profile = profile } if let openGroupInvitationProto = dataMessage.openGroupInvitation, let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation } + if let reactionProto = dataMessage.reaction, + let reaction = Reaction.fromProto(reactionProto) { result.reaction = reaction } result.syncTarget = dataMessage.syncTarget return result } @@ -103,6 +109,10 @@ public final class VisibleMessage : Message { // TODO: Contact // Open group invitation if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) } + // Emoji react + if let reaction = reaction, let reactionProto = reaction.toProto() { + dataMessage.setReaction(reactionProto) + } // Group context do { try setGroupContextIfNeeded(on: dataMessage, using: transaction) @@ -133,8 +143,9 @@ public final class VisibleMessage : Message { quote: \(quote?.description ?? "null"), linkPreview: \(linkPreview?.description ?? "null"), contact: \(contact?.description ?? "null"), - profile: \(profile?.description ?? "null") - "openGroupInvitation": \(openGroupInvitation?.description ?? "null") + profile: \(profile?.description ?? "null"), + reaction: \(reaction?.description ?? "null"), + openGroupInvitation: \(openGroupInvitation?.description ?? "null") ) """ } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 442d87c2e..a575ba288 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -2434,6 +2434,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr builder.setQuote(_value) } builder.setPreview(preview) + if let _value = reaction { + builder.setReaction(_value) + } if let _value = profile { builder.setProfile(_value) } @@ -2503,6 +2506,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr proto.preview = wrappedItems.map { $0.proto } } + @objc public func setReaction(_ valueParam: SNProtoDataMessageReaction) { + proto.reaction = valueParam.proto + } + @objc public func setProfile(_ valueParam: SNProtoDataMessageLokiProfile) { proto.profile = valueParam.proto } @@ -2538,6 +2545,8 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr @objc public let preview: [SNProtoDataMessagePreview] + @objc public let reaction: SNProtoDataMessageReaction? + @objc public let profile: SNProtoDataMessageLokiProfile? @objc public let openGroupInvitation: SNProtoDataMessageOpenGroupInvitation? @@ -2600,6 +2609,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr group: SNProtoGroupContext?, quote: SNProtoDataMessageQuote?, preview: [SNProtoDataMessagePreview], + reaction: SNProtoDataMessageReaction?, profile: SNProtoDataMessageLokiProfile?, openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?, closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) { @@ -2608,6 +2618,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr self.group = group self.quote = quote self.preview = preview + self.reaction = reaction self.profile = profile self.openGroupInvitation = openGroupInvitation self.closedGroupControlMessage = closedGroupControlMessage @@ -2640,6 +2651,11 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr var preview: [SNProtoDataMessagePreview] = [] preview = try proto.preview.map { try SNProtoDataMessagePreview.parseProto($0) } + var reaction: SNProtoDataMessageReaction? = nil + if proto.hasReaction { + reaction = try SNProtoDataMessageReaction.parseProto(proto.reaction) + } + var profile: SNProtoDataMessageLokiProfile? = nil if proto.hasProfile { profile = try SNProtoDataMessageLokiProfile.parseProto(proto.profile) @@ -2664,6 +2680,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr group: group, quote: quote, preview: preview, + reaction: reaction, profile: profile, openGroupInvitation: openGroupInvitation, closedGroupControlMessage: closedGroupControlMessage) diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index cc1bf5c28..77cbc650f 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -599,6 +599,15 @@ struct SessionProtos_DataMessage { set {_uniqueStorage()._preview = newValue} } + var reaction: SessionProtos_DataMessage.Reaction { + get {return _storage._reaction ?? SessionProtos_DataMessage.Reaction()} + set {_uniqueStorage()._reaction = newValue} + } + /// Returns true if `reaction` has been explicitly set. + var hasReaction: Bool {return _storage._reaction != nil} + /// Clears the value of `reaction`. Subsequent reads from it will return its default value. + mutating func clearReaction() {_uniqueStorage()._reaction = nil} + var profile: SessionProtos_DataMessage.LokiProfile { get {return _storage._profile ?? SessionProtos_DataMessage.LokiProfile()} set {_uniqueStorage()._profile = newValue} @@ -2178,6 +2187,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 7: .same(proto: "timestamp"), 8: .same(proto: "quote"), 10: .same(proto: "preview"), + 11: .same(proto: "reaction"), 101: .same(proto: "profile"), 102: .same(proto: "openGroupInvitation"), 104: .same(proto: "closedGroupControlMessage"), @@ -2194,6 +2204,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _timestamp: UInt64? = nil var _quote: SessionProtos_DataMessage.Quote? = nil var _preview: [SessionProtos_DataMessage.Preview] = [] + var _reaction: SessionProtos_DataMessage.Reaction? = nil var _profile: SessionProtos_DataMessage.LokiProfile? = nil var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil @@ -2213,6 +2224,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa _timestamp = source._timestamp _quote = source._quote _preview = source._preview + _reaction = source._reaction _profile = source._profile _openGroupInvitation = source._openGroupInvitation _closedGroupControlMessage = source._closedGroupControlMessage @@ -2233,6 +2245,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if let v = _storage._group, !v.isInitialized {return false} if let v = _storage._quote, !v.isInitialized {return false} if !SwiftProtobuf.Internal.areAllInitialized(_storage._preview) {return false} + if let v = _storage._reaction, !v.isInitialized {return false} if let v = _storage._openGroupInvitation, !v.isInitialized {return false} if let v = _storage._closedGroupControlMessage, !v.isInitialized {return false} return true @@ -2256,6 +2269,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa case 7: try { try decoder.decodeSingularUInt64Field(value: &_storage._timestamp) }() case 8: try { try decoder.decodeSingularMessageField(value: &_storage._quote) }() case 10: try { try decoder.decodeRepeatedMessageField(value: &_storage._preview) }() + case 11: try { try decoder.decodeSingularMessageField(value: &_storage._reaction) }() case 101: try { try decoder.decodeSingularMessageField(value: &_storage._profile) }() case 102: try { try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation) }() case 104: try { try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage) }() @@ -2295,6 +2309,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if !_storage._preview.isEmpty { try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10) } + if let v = _storage._reaction { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } if let v = _storage._profile { try visitor.visitSingularMessageField(value: v, fieldNumber: 101) } @@ -2325,6 +2342,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._timestamp != rhs_storage._timestamp {return false} if _storage._quote != rhs_storage._quote {return false} if _storage._preview != rhs_storage._preview {return false} + if _storage._reaction != rhs_storage._reaction {return false} if _storage._profile != rhs_storage._profile {return false} if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false} if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false} diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 6df37aef9..88f1fc115 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -198,6 +198,7 @@ message DataMessage { optional uint64 timestamp = 7; optional Quote quote = 8; repeated Preview preview = 10; + optional Reaction reaction = 11; optional LokiProfile profile = 101; optional OpenGroupInvitation openGroupInvitation = 102; optional ClosedGroupControlMessage closedGroupControlMessage = 104; diff --git a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift new file mode 100644 index 000000000..34cc1bfdf --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift @@ -0,0 +1,24 @@ + +extension ReactMessage { + + /// To be used for outgoing messages only. + public static func from(_ reaction: VisibleMessage.Reaction?) -> ReactMessage? { + guard let reaction = reaction else { return nil } + return ReactMessage( + timestamp: reaction.timestamp!, + authorId: reaction.publicKey!, + emoji: reaction.emoji) + } +} + +extension VisibleMessage.Reaction { + + public static func from(_ reaction: ReactMessage?) -> VisibleMessage.Reaction? { + guard let reaction = reaction else { return nil } + let result = VisibleMessage.Reaction() + result.timestamp = reaction.timestamp + result.publicKey = reaction.authorId + result.emoji = reaction.emoji + return result + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift new file mode 100644 index 000000000..40b6c2f13 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift @@ -0,0 +1,39 @@ + +@objc(SNReactMessage) +public final class ReactMessage : MTLModel { + + public var timestamp: UInt64? + public var authorId: String? + + @objc + public var emoji: String? + + @objc + public var sender: String? + + @objc + public var messageId: String? + + @objc + public init(timestamp: UInt64, authorId: String, emoji: String?) { + self.timestamp = timestamp + self.authorId = authorId + self.emoji = emoji + super.init() + } + + @objc + public override init() { + super.init() + } + + @objc + public required init!(coder: NSCoder) { + super.init(coder: coder) + } + + @objc + public required init(dictionary dictionaryValue: [String: Any]!) throws { + try super.init(dictionary: dictionaryValue) + } +} diff --git a/SignalUtilitiesKit/Utilities/OrderedDictionary.swift b/SignalUtilitiesKit/Utilities/OrderedDictionary.swift index 5cdd820c1..5e38f9e4d 100644 --- a/SignalUtilitiesKit/Utilities/OrderedDictionary.swift +++ b/SignalUtilitiesKit/Utilities/OrderedDictionary.swift @@ -102,4 +102,16 @@ public class OrderedDictionary { } return values } + + public var orderedItems: [(KeyType, ValueType)] { + var items = [(KeyType, ValueType)]() + for key in orderedKeys { + guard let value = self.keyValueMap[key] else { + owsFailDebug("Missing value") + continue + } + items.append((key, value)) + } + return items + } }