WIP: reactions list
This commit is contained in:
parent
3af692ed01
commit
b5c7902833
|
@ -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 */,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue