Separate gestures for text/vs media

// FREEBIE
This commit is contained in:
sdkjfhsdkjhfsdlkjhfsdf 2017-12-19 12:35:39 -06:00
parent 92477c78b7
commit 0e9c9a9bb3
4 changed files with 321 additions and 106 deletions

View File

@ -256,13 +256,21 @@ NS_ASSUME_NONNULL_BEGIN
[self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.footerView autoPinWidthToSuperview];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self addGestureRecognizer:tap];
UITapGestureRecognizer *mediaTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)];
[self.mediaMaskingView addGestureRecognizer:mediaTap];
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self addGestureRecognizer:longPress];
UITapGestureRecognizer *textTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextTapGesture:)];
[self.textBubbleImageView addGestureRecognizer:textTap];
UILongPressGestureRecognizer *mediaLongPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaLongPressGesture:)];
[self.mediaMaskingView addGestureRecognizer:mediaLongPress];
UILongPressGestureRecognizer *textLongPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextLongPressGesture:)];
[self.textBubbleImageView addGestureRecognizer:textLongPress];
PanDirectionGestureRecognizer *panGesture =
[[PanDirectionGestureRecognizer alloc] initWithDirection:PanDirectionHorizontal
@ -337,6 +345,14 @@ NS_ASSUME_NONNULL_BEGIN
return self.viewItem.messageCellType;
}
- (BOOL)hasText
{
// This should always be valid for the appropriate cell types.
OWSAssert(self.viewItem);
return self.viewItem.hasText;
}
- (nullable DisplayableText *)displayableText
{
// This should always be valid for the appropriate cell types.
@ -830,7 +846,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)addCaptionIfNecessary
{
if (self.viewItem.hasText) {
if (self.hasText) {
[self loadForTextDisplay];
} else {
[self.contentConstraints addObject:[self.textBubbleImageView autoSetDimension:ALDimensionHeight toSize:0]];
@ -949,7 +965,7 @@ NS_ASSUME_NONNULL_BEGIN
self.mediaMaskingView.isOutgoing = self.isOutgoing;
// Hide tail on attachments followed by a caption
self.mediaMaskingView.hideTail = self.viewItem.hasText;
self.mediaMaskingView.hideTail = self.hasText;
self.mediaMaskingView.maskedSubview = view;
[self.mediaMaskingView updateMask];
}
@ -1002,7 +1018,7 @@ NS_ASSUME_NONNULL_BEGIN
CGSize mediaContentSize = CGSizeZero;
CGSize textContentSize = CGSizeZero;
if (self.viewItem.hasText) {
if (self.hasText) {
textContentSize = [self textBubbleSizeForContentWidth:contentWidth];
}
switch (self.cellType) {
@ -1208,72 +1224,110 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Gesture recognizers
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
- (void)handleTextTapGesture:(UITapGestureRecognizer *)sender
{
OWSAssert(self.delegate);
if (sender.state == UIGestureRecognizerStateRecognized) {
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
[self.delegate didTapFailedOutgoingMessage:outgoingMessage];
return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
// Ignore taps on outgoing messages being sent.
return;
}
}
switch (self.cellType) {
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
if (self.displayableText.isTextTruncated) {
[self.delegate didTapTruncatedTextMessage:self.viewItem];
return;
}
break;
case OWSMessageCellType_StillImage:
[self.delegate didTapImageViewItem:self.viewItem
attachmentStream:self.attachmentStream
imageView:self.stillImageView];
break;
case OWSMessageCellType_AnimatedImage:
[self.delegate didTapImageViewItem:self.viewItem
attachmentStream:self.attachmentStream
imageView:self.animatedImageView];
break;
case OWSMessageCellType_Audio:
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
return;
case OWSMessageCellType_Video:
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
return;
case OWSMessageCellType_GenericAttachment:
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
break;
case OWSMessageCellType_DownloadingAttachment: {
OWSAssert(self.attachmentPointer);
if (self.attachmentPointer.state == TSAttachmentPointerStateFailed) {
[self.delegate didTapFailedIncomingAttachment:self.viewItem
attachmentPointer:self.attachmentPointer];
}
break;
}
}
if (sender.state != UIGestureRecognizerStateRecognized) {
DDLogInfo(@"%@ Ignoring tap on message: %@", self.logTag, self.viewItem.interaction.debugDescription);
return;
}
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
[self.delegate didTapFailedOutgoingMessage:outgoingMessage];
return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
// Ignore taps on outgoing messages being sent.
return;
}
}
if (self.hasText && self.displayableText.isTextTruncated) {
[self.delegate didTapTruncatedTextMessage:self.viewItem];
return;
}
}
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender
- (void)handleMediaTapGesture:(UITapGestureRecognizer *)sender
{
OWSAssert(self.delegate);
if (sender.state != UIGestureRecognizerStateRecognized) {
DDLogInfo(@"%@ Ignoring tap on message: %@", self.logTag, self.viewItem.interaction.debugDescription);
return;
}
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
[self.delegate didTapFailedOutgoingMessage:outgoingMessage];
return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
// Ignore taps on outgoing messages being sent.
return;
}
}
switch (self.cellType) {
case OWSMessageCellType_Unknown:
break;
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
if (self.displayableText.isTextTruncated) {
[self.delegate didTapTruncatedTextMessage:self.viewItem];
return;
}
break;
case OWSMessageCellType_StillImage:
[self.delegate didTapImageViewItem:self.viewItem
attachmentStream:self.attachmentStream
imageView:self.stillImageView];
break;
case OWSMessageCellType_AnimatedImage:
[self.delegate didTapImageViewItem:self.viewItem
attachmentStream:self.attachmentStream
imageView:self.animatedImageView];
break;
case OWSMessageCellType_Audio:
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
return;
case OWSMessageCellType_Video:
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
return;
case OWSMessageCellType_GenericAttachment:
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
break;
case OWSMessageCellType_DownloadingAttachment: {
OWSAssert(self.attachmentPointer);
if (self.attachmentPointer.state == TSAttachmentPointerStateFailed) {
[self.delegate didTapFailedIncomingAttachment:self.viewItem attachmentPointer:self.attachmentPointer];
}
break;
}
}
}
- (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender
{
OWSAssert(self.delegate);
// We "eagerly" respond when the long press begins, not when it ends.
if (sender.state == UIGestureRecognizerStateBegan) {
CGPoint location = [sender locationInView:self];
[self showMenuController:location];
[self showTextMenuController:location];
}
}
- (void)handleMediaLongPressGesture:(UILongPressGestureRecognizer *)sender
{
OWSAssert(self.delegate);
// We "eagerly" respond when the long press begins, not when it ends.
if (sender.state == UIGestureRecognizerStateBegan) {
CGPoint location = [sender locationInView:self];
[self showMediaMenuController:location];
}
}
@ -1286,7 +1340,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - UIMenuController
- (void)showMenuController:(CGPoint)fromLocation
- (void)showTextMenuController:(CGPoint)fromLocation
{
// We don't want taps on messages to hide the keyboard,
// so we only let messages become first responder
@ -1301,7 +1355,29 @@ NS_ASSUME_NONNULL_BEGIN
// We use custom action selectors so that we can control
// the ordering of the actions in the menu.
NSArray *menuItems = self.viewItem.menuControllerItems;
NSArray *menuItems = self.viewItem.textMenuControllerItems;
[UIMenuController sharedMenuController].menuItems = menuItems;
CGRect targetRect = CGRectMake(fromLocation.x, fromLocation.y, 1, 1);
[[UIMenuController sharedMenuController] setTargetRect:targetRect inView:self];
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
}
- (void)showMediaMenuController:(CGPoint)fromLocation
{
// We don't want taps on messages to hide the keyboard,
// so we only let messages become first responder
// while they are trying to present the menu controller.
self.isPresentingMenuController = YES;
[self becomeFirstResponder];
if ([UIMenuController sharedMenuController].isMenuVisible) {
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
}
// We use custom action selectors so that we can control
// the ordering of the actions in the menu.
NSArray *menuItems = self.viewItem.mediaMenuControllerItems;
[UIMenuController sharedMenuController].menuItems = menuItems;
CGRect targetRect = CGRectMake(fromLocation.x, fromLocation.y, 1, 1);
[[UIMenuController sharedMenuController] setTargetRect:targetRect inView:self];
@ -1313,19 +1389,29 @@ NS_ASSUME_NONNULL_BEGIN
return [self.viewItem canPerformAction:action];
}
- (void)copyAction:(nullable id)sender
- (void)copyTextAction:(nullable id)sender
{
[self.viewItem copyAction];
[self.viewItem copyTextAction];
}
- (void)shareAction:(nullable id)sender
- (void)copyMediaAction:(nullable id)sender
{
[self.viewItem shareAction];
[self.viewItem copyMediaAction];
}
- (void)saveAction:(nullable id)sender
- (void)shareTextAction:(nullable id)sender
{
[self.viewItem saveAction];
[self.viewItem shareTextAction];
}
- (void)shareMediaAction:(nullable id)sender
{
[self.viewItem shareMediaAction];
}
- (void)saveMediaAction:(nullable id)sender
{
[self.viewItem saveMediaAction];
}
- (void)deleteAction:(nullable id)sender

View File

@ -90,11 +90,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
#pragma mark - UIMenuController
- (NSArray<UIMenuItem *> *)menuControllerItems;
- (NSArray<UIMenuItem *> *)textMenuControllerItems;
- (NSArray<UIMenuItem *> *)mediaMenuControllerItems;
- (BOOL)canPerformAction:(SEL)action;
- (void)copyAction;
- (void)shareAction;
- (void)saveAction;
- (void)copyMediaAction;
- (void)copyTextAction;
- (void)shareMediaAction;
- (void)shareTextAction;
- (void)saveMediaAction;
- (void)deleteAction;
- (SEL)metadataActionSelector;

View File

@ -34,6 +34,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return @"OWSMessageCellType_GenericAttachment";
case OWSMessageCellType_DownloadingAttachment:
return @"OWSMessageCellType_DownloadingAttachment";
case OWSMessageCellType_Unknown:
return @"OWSMessageCellType_Unknown";
}
}
@ -478,41 +480,70 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
#pragma mark - UIMenuController
- (NSArray<UIMenuItem *> *)menuControllerItems
- (NSArray<UIMenuItem *> *)textMenuControllerItems
{
return @[
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION",
@"Short name for edit menu item to copy contents of media message.")
action:self.copyTextActionSelector],
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SHARE_ACTION",
@"Short name for edit menu item to share contents of media message.")
action:self.shareTextActionSelector],
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
@"Short name for edit menu item to show message metadata.")
action:self.metadataActionSelector],
// FIXME this is going to be confusing if the text/attachment look like separate entities.
// we can re-enable this once it's clear that deleting the text would also delete the attachment.
// [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
// @"Short name for edit menu item to delete contents of media
// message.")
// action:self.deleteActionSelector],
];
}
- (NSArray<UIMenuItem *> *)mediaMenuControllerItems
{
return @[
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SHARE_ACTION",
@"Short name for edit menu item to share contents of media message.")
action:self.shareActionSelector],
action:self.shareMediaActionSelector],
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
@"Short name for edit menu item to show message metadata.")
action:self.metadataActionSelector],
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION",
@"Short name for edit menu item to copy contents of media message.")
action:self.copyActionSelector],
action:self.copyMediaActionSelector],
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
@"Short name for edit menu item to delete contents of media message.")
action:self.deleteActionSelector],
// TODO: Do we want a save action?
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION",
@"Short name for edit menu item to save contents of media message.")
action:self.saveActionSelector],
action:self.saveMediaActionSelector],
];
}
- (SEL)copyActionSelector
- (SEL)copyTextActionSelector
{
return NSSelectorFromString(@"copyAction:");
return NSSelectorFromString(@"copyTextAction:");
}
- (SEL)saveActionSelector
- (SEL)copyMediaActionSelector
{
return NSSelectorFromString(@"saveAction:");
return NSSelectorFromString(@"copyMediaAction:");
}
- (SEL)shareActionSelector
- (SEL)saveMediaActionSelector
{
return NSSelectorFromString(@"shareAction:");
return NSSelectorFromString(@"saveMediaAction:");
}
- (SEL)shareTextActionSelector
{
return NSSelectorFromString(@"shareTextAction:");
}
- (SEL)shareMediaActionSelector
{
return NSSelectorFromString(@"shareMediaAction:");
}
- (SEL)deleteActionSelector
@ -528,12 +559,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// We only use custom actions in UIMenuController.
- (BOOL)canPerformAction:(SEL)action
{
if (action == self.copyActionSelector) {
return [self hasActionContent];
} else if (action == self.saveActionSelector) {
return [self canSave];
} else if (action == self.shareActionSelector) {
return [self hasActionContent];
if (action == self.copyTextActionSelector) {
return [self hasTextActionContent];
} else if (action == self.copyMediaActionSelector) {
return [self hasMediaActionContent];
} else if (action == self.saveMediaActionSelector) {
return [self canSaveMedia];
} else if (action == self.shareTextActionSelector) {
return [self hasTextActionContent];
} else if (action == self.shareMediaActionSelector) {
return [self hasMediaActionContent];
} else if (action == self.deleteActionSelector) {
return YES;
} else if (action == self.metadataActionSelector) {
@ -543,14 +578,40 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
}
- (void)copyAction
- (void)copyTextAction
{
switch (self.messageCellType) {
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
case OWSMessageCellType_GenericAttachment: {
OWSAssert(self.displayableText);
[UIPasteboard.generalPasteboard setString:self.displayableText.fullText];
break;
}
case OWSMessageCellType_DownloadingAttachment: {
OWSFail(@"%@ Can't copy not-yet-downloaded attachment", self.logTag);
break;
}
case OWSMessageCellType_Unknown: {
OWSFail(@"%@ No text to copy", self.logTag);
break;
}
}
}
- (void)copyMediaAction
{
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage: {
OWSFail(@"%@ No media to copy", self.logTag);
break;
}
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
@ -576,14 +637,38 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
}
- (void)shareAction
- (void)shareTextAction
{
switch (self.messageCellType) {
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
case OWSMessageCellType_GenericAttachment: {
OWSAssert(self.displayableText);
[AttachmentSharing showShareUIForText:self.displayableText.fullText];
break;
}
case OWSMessageCellType_DownloadingAttachment: {
OWSFail(@"%@ Can't share not-yet-downloaded attachment", self.logTag);
break;
}
case OWSMessageCellType_Unknown: {
OWSFail(@"%@ No text to share", self.logTag)
}
}
}
- (void)shareMediaAction
{
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
OWSFail(@"No media to share.");
break;
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
@ -598,9 +683,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
}
- (BOOL)canSave
- (BOOL)canSaveMedia
{
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
return NO;
@ -619,9 +705,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
}
- (void)saveAction
- (void)saveMediaAction
{
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
OWSFail(@"%@ Cannot save text data.", self.logTag);
@ -668,13 +755,18 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self.interaction remove];
}
- (BOOL)hasActionContent
- (BOOL)hasTextActionContent
{
return self.hasText && self.displayableText.fullText.length > 0;
}
- (BOOL)hasMediaActionContent
{
switch (self.messageCellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
OWSAssert(self.displayableText);
return self.displayableText.fullText.length > 0;
return NO;
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:

View File

@ -311,7 +311,7 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)longPressGesture:(UIGestureRecognizer *)sender
- (void)longPressTextGesture:(UIGestureRecognizer *)sender
{
// We "eagerly" respond when the long press begins, not when it ends.
if (sender.state == UIGestureRecognizerStateBegan) {
@ -325,7 +325,30 @@ NS_ASSUME_NONNULL_BEGIN
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
}
NSArray *menuItems = self.viewItem.menuControllerItems;
NSArray *menuItems = self.viewItem.textMenuControllerItems;
[UIMenuController sharedMenuController].menuItems = menuItems;
CGPoint location = [sender locationInView:self.view];
CGRect targetRect = CGRectMake(location.x, location.y, 1, 1);
[[UIMenuController sharedMenuController] setTargetRect:targetRect inView:self.view];
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
}
}
- (void)longPressMediaGesture:(UIGestureRecognizer *)sender
{
// We "eagerly" respond when the long press begins, not when it ends.
if (sender.state == UIGestureRecognizerStateBegan) {
if (!self.viewItem) {
return;
}
[self.view becomeFirstResponder];
if ([UIMenuController sharedMenuController].isMenuVisible) {
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
}
NSArray *menuItems = self.viewItem.mediaMenuControllerItems;
[UIMenuController sharedMenuController].menuItems = menuItems;
CGPoint location = [sender locationInView:self.view];
CGRect targetRect = CGRectMake(location.x, location.y, 1, 1);
@ -342,19 +365,29 @@ NS_ASSUME_NONNULL_BEGIN
return [self.viewItem canPerformAction:action];
}
- (void)copyAction:(nullable id)sender
- (void)copyTextAction:(nullable id)sender
{
[self.viewItem copyAction];
[self.viewItem copyTextAction];
}
- (void)shareAction:(nullable id)sender
- (void)copyMediaAction:(nullable id)sender
{
[self.viewItem shareAction];
[self.viewItem copyMediaAction];
}
- (void)saveAction:(nullable id)sender
- (void)shareTextAction:(nullable id)sender
{
[self.viewItem saveAction];
[self.viewItem shareTextAction];
}
- (void)shareMediaAction:(nullable id)sender
{
[self.viewItem shareMediaAction];
}
- (void)saveMediaAction:(nullable id)sender
{
[self.viewItem saveMediaAction];
}
- (void)deleteAction:(nullable id)sender