WIP: reactions list

This commit is contained in:
ryanzhao 2022-06-01 17:06:02 +10:00
parent 3af692ed01
commit b5c7902833
7 changed files with 146 additions and 16 deletions

View File

@ -157,6 +157,7 @@
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; };
7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */; };
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; };
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */; };
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; };
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; };
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; };
@ -1153,6 +1154,7 @@
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
7B9F71C828470667006DFE7B /* ReactionListSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListSheet.swift; sourceTree = "<group>"; };
7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = "<group>"; };
7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = "<group>"; };
7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = "<group>"; };
@ -2278,6 +2280,7 @@
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
7B1581E3271FC59C00848B49 /* CallModal.swift */,
7BFFB33B27D02F5800BEA04E /* CallPermissionRequestModal.swift */,
7B9F71C828470667006DFE7B /* ReactionListSheet.swift */,
);
path = "Views & Modals";
sourceTree = "<group>";
@ -4990,6 +4993,7 @@
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */,
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */,
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */,
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */,

View File

@ -819,6 +819,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
presentAlert(alert)
}
func showReactionList(_ viewItem: ConversationViewItem) {
guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return }
let reactionListSheet = ReactionListSheet(for: message.reactions as! [ReactMessage])
reactionListSheet.modalPresentationStyle = .overFullScreen
present(reactionListSheet, animated: true, completion: nil)
}
func react(_ viewItem: ConversationViewItem, with emoji: String) {
UserDefaults.standard.addNewRecentlyUsedEmoji(emoji)
react(viewItem, with: emoji, cancel: false)
@ -856,6 +863,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) {
// TODO: to be implemented
}
func contextMenuDismissed() {

View File

@ -21,7 +21,7 @@ final class ReactionContainerView : UIView {
private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6
var reactions: [(String, (Int, Bool))] = []
var reactionViews: [ReactionView] = []
var reactionViews: [ReactionButton] = []
var expandButton: ExpandingReactionButton?
var collapseButton: UIStackView = {
let arrow = UIImageView(image: UIImage(named: "ic_chevron_up")?.resizedImage(to: CGSize(width: 15, height: 13))?.withRenderingMode(.alwaysTemplate))
@ -93,7 +93,7 @@ final class ReactionContainerView : UIView {
}
for reaction in displayedReactions {
let reactionView = ReactionView(emoji: reaction.0, value: reaction.1)
let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.0, showBorder: reaction.1.1)
stackView.addArrangedSubview(reactionView)
reactionViews.append(reactionView)
}

View File

@ -1,18 +1,29 @@
import UIKit
final class ReactionView : UIView {
final class ReactionButton : UIView {
let emoji: String
let number: Int
let hasCurrentUser: Bool
let showBorder: Bool
let largeSize: Bool
// MARK: Settings
private static let height: CGFloat = 22
private var height: CGFloat {
return largeSize ? 32 : 22
}
private var fontSize: CGFloat {
return largeSize ? Values.mediumFontSize : Values.verySmallFontSize
}
private var spacing: CGFloat {
return largeSize ? Values.mediumSpacing : Values.verySmallSpacing
}
// MARK: Lifecycle
init(emoji: String, value: (Int, Bool)) {
init(emoji: String, value: Int, showBorder: Bool = false, largeSize: Bool = false) {
self.emoji = emoji
self.number = value.0
self.hasCurrentUser = value.1
self.number = value
self.showBorder = showBorder
self.largeSize = largeSize
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
@ -28,27 +39,27 @@ final class ReactionView : UIView {
private func setUpViewHierarchy() {
let emojiLabel = UILabel()
emojiLabel.text = emoji
emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
emojiLabel.font = .systemFont(ofSize: fontSize)
let numberLabel = UILabel()
numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k"
numberLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
numberLabel.font = .systemFont(ofSize: fontSize)
numberLabel.textColor = Colors.text
let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ])
stackView.axis = .horizontal
stackView.spacing = Values.verySmallSpacing
stackView.spacing = spacing
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)
set(.height, to: self.height)
backgroundColor = Colors.receivedMessageBackground
layer.cornerRadius = ReactionView.height / 2
layer.cornerRadius = self.height / 2
if hasCurrentUser {
if showBorder {
layer.borderWidth = 1
layer.borderColor = Colors.accent.cgColor
}

View File

@ -80,6 +80,7 @@ protocol MessageCellDelegate : AnyObject {
func openURL(_ url: URL)
func handleReplyButtonTapped(for viewItem: ConversationViewItem)
func showUserDetails(for sessionID: String)
func showReactionList(_ viewItem: ConversationViewItem)
func quickReact(_ viewItem: ConversationViewItem, with emoji: String)
func cancelReact(_ viewItem: ConversationViewItem, for emoji: String)
func needsLayout()

View File

@ -539,7 +539,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
let convertedLocation = reactionContainerView.convert(location, from: self)
for reactionView in reactionContainerView.reactionViews {
if reactionView.frame.contains(convertedLocation) {
// TODO: Show react list
delegate?.showReactionList(viewItem)
break
}
}
@ -562,7 +562,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
let convertedLocation = reactionContainerView.convert(location, from: self)
for reactionView in reactionContainerView.reactionViews {
if reactionView.frame.contains(convertedLocation) {
if reactionView.hasCurrentUser {
if reactionView.showBorder {
delegate?.cancelReact(viewItem, for: reactionView.emoji)
} else {
delegate?.quickReact(viewItem, with: reactionView.emoji)

View File

@ -0,0 +1,106 @@
import UIKit
final class ReactionListSheet : BaseVC {
private let reactions: [ReactMessage]
private var reactionMap: OrderedDictionary<String, [ReactMessage]> = OrderedDictionary()
// MARK: Components
lazy var contentView: UIView = {
let result = UIView()
result.layer.borderWidth = 0.5
result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor
result.backgroundColor = Colors.modalBackground
return result
}()
lazy var reactionContainer: UIStackView = {
let result = UIStackView()
let spacing = Values.smallSpacing
result.spacing = spacing
result.layoutMargins = UIEdgeInsets(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
result.isLayoutMarginsRelativeArrangement = true
return result
}()
// MARK: Lifecycle
init(for reactions: [ReactMessage]) {
self.reactions = reactions
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(for:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(for:) instead.")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close))
swipeGestureRecognizer.direction = .down
view.addGestureRecognizer(swipeGestureRecognizer)
populateData()
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
view.addSubview(contentView)
contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view)
contentView.set(.height, to: 440)
populateContentView()
}
private func populateContentView() {
// Reactions container
let scrollableContainer = UIScrollView(wrapping: reactionContainer, withInsets: .zero)
scrollableContainer.showsVerticalScrollIndicator = false
scrollableContainer.showsHorizontalScrollIndicator = false
scrollableContainer.set(.height, to: 48)
for reaction in reactionMap.orderedItems {
let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.count, largeSize: true)
reactionContainer.addArrangedSubview(reactionView)
}
contentView.addSubview(scrollableContainer)
scrollableContainer.pin([ UIView.VerticalEdge.top, UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView)
// Line
let lineView = UIView()
lineView.backgroundColor = Colors.border.withAlphaComponent(0.5)
lineView.set(.height, to: 0.5)
contentView.addSubview(lineView)
lineView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView)
lineView.pin(.top, to: .bottom, of: scrollableContainer)
}
private func populateData() {
for reaction in reactions {
if let emoji = reaction.emoji {
if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) }
var value = reactionMap.value(forKey: emoji)!
value.append(reaction)
reactionMap.replace(key: emoji, value: value)
}
}
}
// MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: view)
if contentView.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
close()
}
}
@objc func close() {
dismiss(animated: true, completion: nil)
}
}