Switch to expanding attachment buttons

This commit is contained in:
Niels Andriesse 2021-02-22 10:49:35 +11:00
parent d30fd2fb07
commit a6ae026541
6 changed files with 172 additions and 31 deletions

View File

@ -282,6 +282,7 @@
B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84EA225DF745A005A043E /* LinkPreviewState.swift */; };
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */; };
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
@ -1272,6 +1273,7 @@
B8CCF6422397711F0091D419 /* SettingsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVC.swift; sourceTree = "<group>"; };
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConversationViewAction.h; sourceTree = "<group>"; };
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = "<group>"; };
B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = "<group>"; };
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = "<group>"; };
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Jobs.swift"; sourceTree = "<group>"; };
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OpenGroups.swift"; sourceTree = "<group>"; };
@ -2229,6 +2231,7 @@
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */,
C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */,
C302093D25DCBF07001F572D /* MentionSelectionView.swift */,
B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */,
);
path = "Input View";
sourceTree = "<group>";
@ -4950,6 +4953,7 @@
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */,
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */,
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,

View File

@ -1,6 +1,5 @@
// TODO
// Brendan no likey buttons above text field
// Slight paging glitch
// Image detail VC transition glitch
// Photo rounding

View File

@ -0,0 +1,121 @@
final class ExpandingAttachmentsButton : UIView, InputViewButtonDelegate {
private let delegate: ExpandingAttachmentsButtonDelegate
private var isExpanded = false { didSet { expandOrCollapse() } }
// MARK: Constraints
private lazy var gifButtonContainerBottomConstraint = gifButtonContainer.pin(.bottom, to: .bottom, of: self)
private lazy var documentButtonContainerBottomConstraint = documentButtonContainer.pin(.bottom, to: .bottom, of: self)
private lazy var libraryButtonContainerBottomConstraint = libraryButtonContainer.pin(.bottom, to: .bottom, of: self)
private lazy var cameraButtonContainerBottomConstraint = cameraButtonContainer.pin(.bottom, to: .bottom, of: self)
// MARK: UI Components
lazy var gifButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_gif_black"), delegate: self, hasOpaqueBackground: true)
lazy var gifButtonContainer = container(for: gifButton)
lazy var documentButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_document_black"), delegate: self, hasOpaqueBackground: true)
lazy var documentButtonContainer = container(for: documentButton)
lazy var libraryButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_roll_black"), delegate: self, hasOpaqueBackground: true)
lazy var libraryButtonContainer = container(for: libraryButton)
lazy var cameraButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_black"), delegate: self, hasOpaqueBackground: true)
lazy var cameraButtonContainer = container(for: cameraButton)
lazy var mainButton = InputViewButton(icon: #imageLiteral(resourceName: "ic_plus_24"), delegate: self)
lazy var mainButtonContainer = container(for: mainButton)
// MARK: Lifecycle
init(delegate: ExpandingAttachmentsButtonDelegate) {
self.delegate = delegate
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(delegate:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(delegate:) instead.")
}
private func setUpViewHierarchy() {
backgroundColor = .clear
// GIF button
addSubview(gifButtonContainer)
gifButtonContainer.alpha = 0
// Document button
addSubview(documentButtonContainer)
documentButtonContainer.alpha = 0
// Library button
addSubview(libraryButtonContainer)
libraryButtonContainer.alpha = 0
// Camera button
addSubview(cameraButtonContainer)
cameraButtonContainer.alpha = 0
// Main button
addSubview(mainButtonContainer)
// Constraints
mainButtonContainer.pin(to: self)
gifButtonContainer.center(.horizontal, in: self)
documentButtonContainer.center(.horizontal, in: self)
libraryButtonContainer.center(.horizontal, in: self)
cameraButtonContainer.center(.horizontal, in: self)
[ gifButtonContainerBottomConstraint, documentButtonContainerBottomConstraint, libraryButtonContainerBottomConstraint, cameraButtonContainerBottomConstraint ].forEach {
$0.isActive = true
}
}
// MARK: Animation
private func expandOrCollapse() {
if isExpanded {
let expandedButtonSize = InputViewButton.expandedSize
let spacing: CGFloat = 4
cameraButtonContainerBottomConstraint.constant = -1 * (expandedButtonSize + spacing)
libraryButtonContainerBottomConstraint.constant = -2 * (expandedButtonSize + spacing)
documentButtonContainerBottomConstraint.constant = -3 * (expandedButtonSize + spacing)
gifButtonContainerBottomConstraint.constant = -4 * (expandedButtonSize + spacing)
UIView.animate(withDuration: 0.25) {
[ self.gifButtonContainer, self.documentButtonContainer, self.libraryButtonContainer, self.cameraButtonContainer ].forEach {
$0.alpha = 1
}
self.layoutIfNeeded()
}
} else {
[ gifButtonContainerBottomConstraint, documentButtonContainerBottomConstraint, libraryButtonContainerBottomConstraint, cameraButtonContainerBottomConstraint ].forEach {
$0.constant = 0
}
UIView.animate(withDuration: 0.25) {
[ self.gifButtonContainer, self.documentButtonContainer, self.libraryButtonContainer, self.cameraButtonContainer ].forEach {
$0.alpha = 0
}
self.layoutIfNeeded()
}
}
}
// MARK: Interaction
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) {
if inputViewButton == gifButton { delegate.handleGIFButtonTapped(); isExpanded = false }
if inputViewButton == documentButton { delegate.handleDocumentButtonTapped(); isExpanded = false }
if inputViewButton == libraryButton { delegate.handleLibraryButtonTapped(); isExpanded = false }
if inputViewButton == cameraButton { delegate.handleCameraButtonTapped(); isExpanded = false }
if inputViewButton == mainButton { isExpanded = !isExpanded }
}
// MARK: Convenience
private func container(for button: InputViewButton) -> UIView {
let result = UIView()
result.addSubview(button)
result.set(.width, to: InputViewButton.expandedSize)
result.set(.height, to: InputViewButton.expandedSize)
button.center(in: result)
return result
}
}
// MARK: Delegate
protocol ExpandingAttachmentsButtonDelegate {
func handleGIFButtonTapped()
func handleDocumentButtonTapped()
func handleLibraryButtonTapped()
func handleCameraButtonTapped()
}

View File

@ -20,11 +20,10 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
var lastSearchedText: String? { nil }
// MARK: UI Components
private lazy var cameraButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_black"), delegate: self)
private lazy var libraryButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_roll_black"), delegate: self)
private lazy var gifButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_gif_black"), delegate: self)
private lazy var documentButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_document_black"), delegate: self)
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
private lazy var voiceMessageButton = InputViewButton(icon: #imageLiteral(resourceName: "Microphone"), delegate: self)
private lazy var sendButton: InputViewButton = {
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
result.isHidden = true
@ -94,18 +93,13 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
separator.set(.height, to: 1 / UIScreen.main.scale)
addSubview(separator)
separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.top, UIView.HorizontalEdge.trailing ], to: self)
// Buttons
let (cameraButtonContainer, libraryButtonContainer, gifButtonContainer, documentButtonContainer) = (container(for: cameraButton), container(for: libraryButton), container(for: gifButton), container(for: documentButton))
let buttonStackView = UIStackView(arrangedSubviews: [ cameraButtonContainer, libraryButtonContainer, gifButtonContainer, documentButtonContainer, UIView.hStretchingSpacer() ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.smallSpacing
// Bottom stack view
let bottomStackView = UIStackView(arrangedSubviews: [ inputTextView, container(for: sendButton) ])
let bottomStackView = UIStackView(arrangedSubviews: [ attachmentsButton, inputTextView, container(for: sendButton) ])
bottomStackView.axis = .horizontal
bottomStackView.spacing = Values.smallSpacing
bottomStackView.alignment = .center
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, additionalContentContainer, bottomStackView ])
let mainStackView = UIStackView(arrangedSubviews: [ additionalContentContainer, bottomStackView ])
mainStackView.axis = .vertical
mainStackView.isLayoutMarginsRelativeArrangement = true
let adjustment = (InputViewButton.expandedSize - InputViewButton.size) / 2
@ -204,19 +198,31 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
}
// MARK: Interaction
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let buttonContainers = [ attachmentsButton.mainButton, attachmentsButton.cameraButton,
attachmentsButton.libraryButton, attachmentsButton.documentButton, attachmentsButton.gifButton ]
let buttonContainer = buttonContainers.first { $0.superview!.convert($0.frame, to: self).contains(point) }
if let buttonContainer = buttonContainer {
return buttonContainer
} else {
return super.hitTest(point, with: event)
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if mentionsViewContainer.frame.contains(point) {
let buttonContainers = [ attachmentsButton.gifButtonContainer, attachmentsButton.documentButtonContainer,
attachmentsButton.libraryButtonContainer, attachmentsButton.cameraButtonContainer, attachmentsButton.mainButtonContainer ]
let isPointInsideAttachmentsButton = buttonContainers.contains { $0.superview!.convert($0.frame, to: self).contains(point) }
if isPointInsideAttachmentsButton {
return true
} else if mentionsViewContainer.frame.contains(point) {
return true
} else {
return super.point(inside: point, with: event)
}
}
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) {
if inputViewButton == cameraButton { delegate.handleCameraButtonTapped() }
if inputViewButton == libraryButton { delegate.handleLibraryButtonTapped() }
if inputViewButton == gifButton { delegate.handleGIFButtonTapped() }
if inputViewButton == documentButton { delegate.handleDocumentButtonTapped() }
if inputViewButton == sendButton { delegate.handleSendButtonTapped() }
}
@ -264,14 +270,14 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
voiceMessageRecordingView.pin(to: self)
self.voiceMessageRecordingView = voiceMessageRecordingView
voiceMessageRecordingView.animate()
let allOtherViews = [ cameraButton, libraryButton, gifButton, documentButton, sendButton, inputTextView, additionalContentContainer ]
let allOtherViews = [ attachmentsButton, sendButton, inputTextView, additionalContentContainer ]
UIView.animate(withDuration: 0.25) {
allOtherViews.forEach { $0.alpha = 0 }
}
}
func hideVoiceMessageUI() {
let allOtherViews = [ cameraButton, libraryButton, gifButton, documentButton, sendButton, inputTextView, additionalContentContainer ]
let allOtherViews = [ attachmentsButton, sendButton, inputTextView, additionalContentContainer ]
UIView.animate(withDuration: 0.25, animations: {
allOtherViews.forEach { $0.alpha = 1 }
self.voiceMessageRecordingView?.alpha = 0
@ -320,13 +326,9 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
}
// MARK: Delegate
protocol InputViewDelegate : VoiceMessageRecordingViewDelegate {
protocol InputViewDelegate : ExpandingAttachmentsButtonDelegate, VoiceMessageRecordingViewDelegate {
func showLinkPreviewSuggestionModal()
func handleCameraButtonTapped()
func handleLibraryButtonTapped()
func handleGIFButtonTapped()
func handleDocumentButtonTapped()
func handleSendButtonTapped()
func handleQuoteViewCancelButtonTapped()
func inputTextViewDidChangeContent(_ inputTextView: InputTextView)

View File

@ -3,21 +3,26 @@ final class InputViewButton : UIView {
private let icon: UIImage
private let isSendButton: Bool
private let delegate: InputViewButtonDelegate
private let hasOpaqueBackground: Bool
private lazy var widthConstraint = set(.width, to: InputViewButton.size)
private lazy var heightConstraint = set(.height, to: InputViewButton.size)
private var longPressTimer: Timer?
private var isLongPress = false
// MARK: UI Components
private lazy var backgroundView = UIView()
// MARK: Settings
static let size = CGFloat(40)
static let expandedSize = CGFloat(48)
static let iconSize: CGFloat = 20
// MARK: Lifecycle
init(icon: UIImage, isSendButton: Bool = false, delegate: InputViewButtonDelegate) {
init(icon: UIImage, isSendButton: Bool = false, delegate: InputViewButtonDelegate, hasOpaqueBackground: Bool = false) {
self.icon = icon
self.isSendButton = isSendButton
self.delegate = delegate
self.hasOpaqueBackground = hasOpaqueBackground
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
@ -31,7 +36,10 @@ final class InputViewButton : UIView {
}
private func setUpViewHierarchy() {
backgroundColor = isSendButton ? Colors.accent : Colors.text.withAlphaComponent(0.05)
backgroundColor = .clear
backgroundView.backgroundColor = isSendButton ? Colors.accent : Colors.text.withAlphaComponent(0.05)
addSubview(backgroundView)
backgroundView.pin(to: self)
layer.cornerRadius = InputViewButton.size / 2
layer.masksToBounds = true
isUserInteractionEnabled = true
@ -58,7 +66,7 @@ final class InputViewButton : UIView {
self.layer.cornerRadius = size / 2
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
self.setCircularGlow(with: glowConfiguration)
self.backgroundColor = backgroundColor
self.backgroundView.backgroundColor = backgroundColor
}
}
@ -118,3 +126,10 @@ protocol InputViewButtonDelegate {
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch)
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch)
}
extension InputViewButtonDelegate {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton) { }
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch) { }
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch) { }
}

View File

@ -378,7 +378,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
override func prepareForReuse() {
super.prepareForReuse()
unloadContent?()
let viewsToMove = [ bubbleView, profilePictureView, replyButton ]
let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ]
viewsToMove.forEach { $0.transform = .identity }
replyButton.alpha = 0
timerView.prepareForReuse()
@ -431,7 +431,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
let viewsToMove = [ bubbleView, profilePictureView, replyButton ]
let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ]
let translationX = gestureRecognizer.translation(in: self).x.clamp(-CGFloat.greatestFiniteMagnitude, 0)
switch gestureRecognizer.state {
case .changed:
@ -460,7 +460,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
private func resetReply() {
let viewsToMove = [ bubbleView, profilePictureView, replyButton ]
let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ]
UIView.animate(withDuration: 0.25) {
viewsToMove.forEach { $0.transform = .identity }
self.replyButton.alpha = 0