Edit Send UI

This commit is contained in:
Pete Walters 2023-07-07 13:26:15 -05:00 committed by GitHub
parent 0c5916fb3d
commit 1a02a0eff6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 300 additions and 46 deletions

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "send-blue-28.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,95 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.172549 0.419608 0.929412 scn
28.000000 14.000000 m
28.000000 6.268013 21.731987 0.000000 14.000000 0.000000 c
6.268013 0.000000 0.000000 6.268013 0.000000 14.000000 c
0.000000 21.731987 6.268013 28.000000 14.000000 28.000000 c
21.731987 28.000000 28.000000 21.731987 28.000000 14.000000 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 7.125000 5.979156 cm
1.000000 1.000000 1.000000 scn
6.874999 15.583286 m
7.118114 15.583286 7.351272 15.486710 7.523180 15.314801 c
13.481515 9.356466 l
13.839495 8.998486 13.839495 8.418084 13.481515 8.060104 c
13.123533 7.702124 12.543133 7.702124 12.185152 8.060104 c
7.679847 12.565409 l
7.791668 10.999953 l
7.791668 0.916669 l
7.791668 0.410408 7.381263 0.000003 6.875002 0.000003 c
6.368741 0.000003 5.958335 0.410409 5.958335 0.916669 c
5.958335 10.999953 l
6.070151 12.565408 l
1.564848 8.060102 l
1.206868 7.702122 0.626466 7.702122 0.268486 8.060102 c
-0.089495 8.418082 -0.089495 8.998484 0.268485 9.356464 c
6.226818 15.314800 l
6.398726 15.486710 6.631884 15.583286 6.874999 15.583286 c
h
f
n
Q
endstream
endobj
3 0 obj
1135
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 28.000000 28.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001225 00000 n
0000001248 00000 n
0000001421 00000 n
0000001495 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1554
%%EOF

View File

@ -13,6 +13,7 @@ protocol MessageActionsDelegate: AnyObject {
func messageActionsDeleteItem(_ itemViewModel: CVItemViewModelImpl)
func messageActionsSpeakItem(_ itemViewModel: CVItemViewModelImpl)
func messageActionsStopSpeakingItem(_ itemViewModel: CVItemViewModelImpl)
func messageActionsEditItem(_ itemViewModel: CVItemViewModelImpl)
}
// MARK: -
@ -96,6 +97,18 @@ struct MessageActionBuilder {
})
}
static func editMessage(itemViewModel: CVItemViewModelImpl, delegate: MessageActionsDelegate) -> MessageAction {
return MessageAction(
.edit,
accessibilityLabel: NSLocalizedString("MESSAGE_ACTION_EDIT_MESSAGE", comment: "Action sheet edit message accessibility label"),
accessibilityIdentifier: UIView.accessibilityIdentifier(containerName: "message_action", name: "edit_message"),
contextMenuTitle: NSLocalizedString("CONTEXT_MENU_EDIT_MESSAGE", comment: "Context menu edit button title"),
contextMenuAttributes: [],
block: { [weak delegate] (_) in
delegate?.messageActionsEditItem(itemViewModel)
})
}
static func speakMessage(itemViewModel: CVItemViewModelImpl, delegate: MessageActionsDelegate) -> MessageAction {
MessageAction(
.speak,
@ -151,6 +164,11 @@ class MessageActions: NSObject {
let selectAction = MessageActionBuilder.selectMessage(itemViewModel: itemViewModel, delegate: delegate)
actions.append(selectAction)
if itemViewModel.canEditMessage {
let editAction = MessageActionBuilder.editMessage(itemViewModel: itemViewModel, delegate: delegate)
actions.append(editAction)
}
if itemViewModel.canCopyOrShareOrSpeakText {
// If the user started speaking a message and then turns of the "speak selection" OS setting,
// we still want to let them turn it off.

View File

@ -155,6 +155,10 @@ public class CVItemViewModelImpl: CVComponentStateWrapper {
extension CVItemViewModelImpl {
var canEditMessage: Bool {
return FeatureFlags.editMessageSend
}
var canCopyOrShareOrSpeakText: Bool {
guard !isViewOnce else {
return false

View File

@ -172,8 +172,8 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
return inputTextView
}()
private lazy var attachmentButton: AttachmentButton = {
let button = AttachmentButton()
private lazy var addOrCancelButton: AddOrCancelButton = {
let button = AddOrCancelButton()
button.accessibilityLabel = OWSLocalizedString(
"ATTACHMENT_LABEL",
comment: "Accessibility label for attaching photos"
@ -183,7 +183,7 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
comment: "Accessibility hint describing what you can do with the attachment button"
)
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "attachmentButton")
button.addTarget(self, action: #selector(attachmentButtonPressed), for: .touchUpInside)
button.addTarget(self, action: #selector(addOrCancelButtonPressed), for: .touchUpInside)
button.autoSetDimensions(to: CGSize(square: LayoutMetrics.minToolbarItemHeight))
button.setContentHuggingHorizontalHigh()
button.setCompressionResistanceHorizontalHigh()
@ -222,6 +222,35 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
return button
}()
private lazy var editMessageLabelWrapper: UIView = {
let view = UIView.container()
let editIconView = UIImageView(image: UIImage(named: Theme.iconName(.contextMenuEdit)))
editIconView.contentMode = .scaleAspectFit
editIconView.autoSetDimension(.height, toSize: 16.0)
editIconView.setContentHuggingHigh()
let editLabel = UILabel()
editLabel.text = OWSLocalizedString(
"INPUT_TOOLBAR_EDIT_MESSAGE_LABEL",
comment: "Label at the top of the input text when editing a message"
)
editLabel.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
editLabel.textColor = Theme.primaryTextColor
let stack = UIStackView(arrangedSubviews: [editIconView, editLabel])
stack.axis = .horizontal
stack.alignment = .fill
view.addSubview(stack)
stack.autoPinEdgesToSuperviewEdges(with: .init(hMargin: 10, vMargin: 8))
view.setContentHuggingHorizontalLow()
view.setCompressionResistanceHorizontalLow()
view.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "editMessageWrapper")
return view
}()
private lazy var quotedReplyWrapper: UIView = {
let view = UIView.container()
view.setContentHuggingHorizontalLow()
@ -322,11 +351,18 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
OWSLogger.info("")
}
editMessageLabelWrapper.isHidden = !shouldShowEditUI
quotedReplyWrapper.isHidden = quotedReply == nil
self.quotedReply = quotedReply
// Vertical stack of message component views in the center: Link Preview, Reply Quote, Text Input View.
let messageContentVStack = UIStackView(arrangedSubviews: [ quotedReplyWrapper, linkPreviewWrapper, inputTextView ])
let messageContentVStack = UIStackView(arrangedSubviews: [
editMessageLabelWrapper,
quotedReplyWrapper,
linkPreviewWrapper,
inputTextView
])
messageContentVStack.axis = .vertical
messageContentVStack.alignment = .fill
messageContentVStack.setContentHuggingHorizontalLow()
@ -362,9 +398,9 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
// Horizontal Stack: Attachment button, message components, Camera|VoiceNote|Send button.
//
// + Attachment button: pinned to the bottom left corner.
mainPanelView.addSubview(attachmentButton)
attachmentButton.autoPinEdge(toSuperviewMargin: .left)
attachmentButton.autoPinEdge(toSuperviewEdge: .bottom)
mainPanelView.addSubview(addOrCancelButton)
addOrCancelButton.autoPinEdge(toSuperviewMargin: .left)
addOrCancelButton.autoPinEdge(toSuperviewEdge: .bottom)
// Camera | Voice Message | Send: pinned to the bottom right corner.
mainPanelView.addSubview(rightEdgeControlsView)
@ -472,7 +508,7 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
voiceMemoContentView.setIsHidden(true, animated: isAnimated)
// Show Send button instead of Camera and Voice Message buttons only when text input isn't empty.
let hasNonWhitespaceTextInput = !inputTextView.trimmedText.isEmpty
let hasNonWhitespaceTextInput = !inputTextView.trimmedText.isEmpty || shouldShowEditUI
rightEdgeControlsState = hasNonWhitespaceTextInput ? .sendButton : .default
}
@ -485,17 +521,23 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
// Attachment Button
let hideAttachmentButton = isShowingVoiceMemoUI
if setAttachmentButtonHidden(hideAttachmentButton, usingAnimator: animator) {
if setAddOrCancelButtonHidden(hideAttachmentButton, usingAnimator: animator) {
hasLayoutChanged = true
}
// Attachment button has more complex animations and cannot be grouped with the rest.
let attachmentButtonAppearance: AttachmentButton.Appearance = desiredKeyboardType == .attachment ? .close : .add
attachmentButton.setAppearance(attachmentButtonAppearance, usingAnimator: animator)
let addOrCancelButtonAppearance: AddOrCancelButton.Appearance = {
if shouldShowEditUI {
return .close
} else {
return desiredKeyboardType == .attachment ? .close : .add
}
}()
addOrCancelButton.setAppearance(addOrCancelButtonAppearance, usingAnimator: animator)
// Show / hide Sticker or Keyboard buttons inside of the text input field.
// Either buttons are only visible if there's no any text input, including whitespace-only.
let hideStickerOrKeyboardButton = !inputTextView.untrimmedText.isEmpty || isShowingVoiceMemoUI || quotedReply != nil
let hideStickerOrKeyboardButton = shouldShowEditUI || !inputTextView.untrimmedText.isEmpty || isShowingVoiceMemoUI || quotedReply != nil
let hideStickerButton = hideStickerOrKeyboardButton || desiredKeyboardType == .sticker
let hideKeyboardButton = hideStickerOrKeyboardButton || !hideStickerButton
ConversationInputToolbar.setView(stickerButton, hidden: hideStickerButton, usingAnimator: animator)
@ -557,14 +599,14 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
constant: 16
)
} else {
constraint = messageContentView.leftAnchor.constraint(equalTo: attachmentButton.rightAnchor)
constraint = messageContentView.leftAnchor.constraint(equalTo: addOrCancelButton.rightAnchor)
}
addConstraint(constraint)
messageContentViewLeftEdgeConstraint = constraint
}
private func setAttachmentButtonHidden(_ isHidden: Bool, usingAnimator animator: UIViewPropertyAnimator?) -> Bool {
guard ConversationInputToolbar.setView(attachmentButton, hidden: isHidden, usingAnimator: animator) else { return false }
private func setAddOrCancelButtonHidden(_ isHidden: Bool, usingAnimator animator: UIViewPropertyAnimator?) -> Bool {
guard ConversationInputToolbar.setView(addOrCancelButton, hidden: isHidden, usingAnimator: animator) else { return false }
updateMessageContentViewLeftEdgeConstraint(isViewHidden: isHidden)
return true
}
@ -617,7 +659,7 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
let button = UIButton(type: .system)
button.accessibilityLabel = MessageStrings.sendButton
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "sendButton")
button.setImage(UIImage(imageLiteralResourceName: "send-blue-32"), for: .normal)
button.setImage(UIImage(imageLiteralResourceName: "send-blue-28"), for: .normal)
button.bounds.size = CGSize(width: 48, height: LayoutMetrics.minToolbarItemHeight)
return button
}()
@ -731,15 +773,15 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
}
}
// MARK: Attachment Button
// MARK: Add/Cancel Button
private class AttachmentButton: UIButton {
private class AddOrCancelButton: UIButton {
private let roundedCornersBackground: UIView = {
let view = UIView()
view.backgroundColor = .init(rgbHex: 0x3B3B3B)
view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.cornerRadius = 14
view.isUserInteractionEnabled = false
return view
}()
@ -888,10 +930,44 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
}
func clearTextMessage(animated: Bool) {
editTarget = nil
setMessageBody(nil, animated: animated)
inputTextView.undoManager?.removeAllActions()
}
// MARK: Edit Message
var shouldShowEditUI: Bool { editTarget != nil }
var editTarget: TSOutgoingMessage? {
didSet {
let animateChanges = window != nil
// Show the 'editing' tag
if let editTarget = editTarget {
let body = editTarget.body ?? ""
let ranges = editTarget.bodyRanges ?? .empty
let messageBody = MessageBody(text: body, ranges: ranges)
self.setMessageBody(messageBody, animated: true)
showEditMessageView(animated: animateChanges)
} else {
quotedReply = nil
hideEditMessageView(animated: animateChanges)
}
}
}
private func showEditMessageView(animated: Bool) {
toggleMessageComponentVisibility(hide: false, component: editMessageLabelWrapper, animated: animated)
}
private func hideEditMessageView(animated: Bool) {
owsAssertDebug(editTarget == nil)
toggleMessageComponentVisibility(hide: true, component: editMessageLabelWrapper, animated: animated)
}
// MARK: Quoted Reply
var quotedReply: QuotedReplyModel? {
@ -932,42 +1008,38 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
updateInputLinkPreview()
if animated, quotedReplyWrapper.isHidden {
isAnimatingHeightChange = true
UIView.animate(
withDuration: ConversationInputToolbar.heightChangeAnimationDuration,
animations: {
self.quotedReplyWrapper.isHidden = false
},
completion: { _ in
self.isAnimatingHeightChange = false
}
)
} else {
quotedReplyWrapper.isHidden = false
}
toggleMessageComponentVisibility(hide: false, component: quotedReplyWrapper, animated: animated)
}
private func hideQuotedReplyView(animated: Bool) {
owsAssertDebug(quotedReply == nil)
toggleMessageComponentVisibility(hide: true, component: quotedReplyWrapper, animated: animated) { _ in
self.quotedReplyWrapper.removeAllSubviews()
}
}
if animated {
private func toggleMessageComponentVisibility(
hide: Bool,
component: UIView,
animated: Bool,
completion: ((Bool) -> Void)? = nil
) {
if animated, component.isHidden != hide {
isAnimatingHeightChange = true
UIView.animate(
withDuration: ConversationInputToolbar.heightChangeAnimationDuration,
animations: {
self.quotedReplyWrapper.isHidden = true
component.isHidden = hide
},
completion: { _ in
completion: { completed in
self.isAnimatingHeightChange = false
self.quotedReplyWrapper.removeAllSubviews()
completion?(completed)
}
)
} else {
quotedReplyWrapper.isHidden = true
quotedReplyWrapper.removeAllSubviews()
component.isHidden = hide
completion?(true)
}
}
@ -1845,7 +1917,7 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
UIView.setAnimationsEnabled(false)
_ = inputTextView.becomeFirstResponder()
inputTextView.resignFirstResponder()
_ = inputTextView.resignFirstResponder()
inputTextView.reloadMentionState()
@ -1876,7 +1948,7 @@ public class ConversationInputToolbar: UIView, LinkPreviewViewDraftDelegate, Quo
}
func endEditingMessage() {
inputTextView.resignFirstResponder()
_ = inputTextView.resignFirstResponder()
_ = stickerKeyboardIfLoaded?.resignFirstResponder()
_ = attachmentKeyboardIfLoaded?.resignFirstResponder()
}
@ -1926,10 +1998,15 @@ extension ConversationInputToolbar {
}
@objc
private func attachmentButtonPressed() {
private func addOrCancelButtonPressed() {
Logger.verbose("")
ImpactHapticFeedback.impactOccurred(style: .light)
toggleKeyboardType(.attachment, animated: true)
if shouldShowEditUI {
editTarget = nil
clearTextMessage(animated: true)
} else {
toggleKeyboardType(.attachment, animated: true)
}
}
@objc

View File

@ -106,7 +106,7 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
thread: self.thread,
quotedReplyModel: inputToolbar.quotedReply,
linkPreviewDraft: inputToolbar.linkPreviewDraft,
editTarget: nil,
editTarget: inputToolbar.editTarget,
persistenceCompletionHandler: {
AssertIsOnMainThread()
self.loadCoordinator.enqueueReload()
@ -358,7 +358,7 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
mediaAttachments: attachments,
thread: self.thread,
quotedReplyModel: inputToolbar.quotedReply,
editTarget: nil,
editTarget: inputToolbar.editTarget,
persistenceCompletionHandler: {
AssertIsOnMainThread()
self.loadCoordinator.enqueueReload()

View File

@ -115,6 +115,7 @@ extension ConversationViewController: ContextMenuInteractionDelegate {
let actionOrder: [MessageAction.MessageActionType] = [
.reply,
.forward,
.edit,
.copy,
.share,
.select,

View File

@ -8,6 +8,36 @@ import SignalServiceKit
import SignalUI
extension ConversationViewController: MessageActionsDelegate {
func messageActionsEditItem(_ itemViewModel: CVItemViewModelImpl) {
populateMessageEdit(itemViewModel)
}
func populateMessageEdit(_ itemViewModel: CVItemViewModelImpl) {
guard let message = itemViewModel.interaction as? TSOutgoingMessage else {
return owsFailDebug("Invalid interaction.")
}
// TODO: validate message can still be edited.
if message.quotedMessage != nil {
let load = {
Self.databaseStorage.read { transaction in
QuotedReplyModel(message: message, transaction: transaction)
}
}
guard let quotedReply = load() else {
owsFailDebug("Could not build quoted reply.")
return
}
inputToolbar?.quotedReply = quotedReply
}
inputToolbar?.editTarget = message
inputToolbar?.beginEditingMessage()
}
func messageActionsShowDetailsForItem(_ itemViewModel: CVItemViewModelImpl) {
showDetailView(itemViewModel)
}

View File

@ -23,6 +23,7 @@ public class MessageAction: NSObject {
case select
case speak
case stopSpeaking
case edit
}
let actionType: MessageActionType
@ -63,6 +64,8 @@ public class MessageAction: NSObject {
return .contextMenuSpeak
case .stopSpeaking:
return .contextMenuStopSpeaking
case .edit:
return .contextMenuEdit
}
}()
return Theme.iconImage(icon)

View File

@ -1189,6 +1189,9 @@
/* Context menu button title */
"CONTEXT_MENU_DETAILS" = "Info";
/* Context menu edit button title */
"CONTEXT_MENU_EDIT_MESSAGE" = "Edit";
/* Context menu button title */
"CONTEXT_MENU_FORWARD_MESSAGE" = "Forward";
@ -3313,6 +3316,9 @@
/* Shown in inbox and conversation when a user joins Signal, embeds the new user's {{contact name}} */
"INFO_MESSAGE_USER_JOINED_SIGNAL_BODY_FORMAT" = "%@ is on Signal!";
/* Label at the top of the input text when editing a message */
"INPUT_TOOLBAR_EDIT_MESSAGE_LABEL" = "Edit message";
/* accessibility label for the button which shows the regular keyboard instead of sticker picker */
"INPUT_TOOLBAR_KEYBOARD_BUTTON_ACCESSIBILITY_LABEL" = "Keyboard";
@ -3598,6 +3604,9 @@
/* Action sheet button title */
"MESSAGE_ACTION_DETAILS" = "More Info";
/* Action sheet edit message accessibility label */
"MESSAGE_ACTION_EDIT_MESSAGE" = "Edit Message";
/* Label for button to compose a new email. */
"MESSAGE_ACTION_EMAIL_NEW_MAIL_MESSAGE" = "New Email Message";

View File

@ -128,6 +128,8 @@ public class FeatureFlags: BaseFlags {
/// If true, _only_ aci safety numbers will be displayed, and e164 safety numbers will not
/// be displayed.
public static let onlyAciSafetyNumbers = false
public static let editMessageSend = build.includes(.dev)
}
// MARK: -