Add EventMenuBuilder to manage ordering on event menu actions

This commit is contained in:
ismailgulek
2022-01-10 17:31:28 +03:00
parent 4903e69fc3
commit 1a82945d87
5 changed files with 274 additions and 74 deletions

View File

@@ -0,0 +1,50 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Class to build an event menu.
@objcMembers
class EventMenuBuilder: NSObject {
private var items: [EventMenuItemType: UIAlertAction] = [:]
/// Returns true if no items or only one item with the type `EventMenuItemType.cancel`.
var isEmpty: Bool {
return items.isEmpty || (items.count == 1 && items.first?.key == .cancel)
}
/// Add a menu item.
/// - Parameters:
/// - type: item type
/// - action: alert action
func addItem(withType type: EventMenuItemType,
action: UIAlertAction) {
items[type] = action
}
/// Builds the action menu items.
/// - Returns: alert actions. Sorted by item types.
func build() -> [UIAlertAction] {
items.sorted(by: { $0.key < $1.key }).map { $1 }
}
/// Reset the builder. Builder will be empty after this method call.
func reset() {
items.removeAll()
}
}

View File

@@ -0,0 +1,49 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Type of an event menu item. Ordering of the cases is important. See `EventMenuBuilder`.
@objc
enum EventMenuItemType: Int {
case viewInRoom
case copy
case retrySending
case cancelSending
case cancelDownloading
case saveMedia
case quote
case forward
case permalink
case share
case removePoll
case endPoll
case reactionHistory
case viewSource
case viewDecryptedSource
case viewEncryption
case report
case remove
case cancel
}
extension EventMenuItemType: Comparable {
static func < (lhs: EventMenuItemType, rhs: EventMenuItemType) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}

View File

@@ -254,6 +254,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
@property (nonatomic, strong) ShareManager *shareManager;
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
@@ -345,6 +346,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
formattedBodyParser = [FormattedBodyParser new];
self.eventMenuBuilder = [EventMenuBuilder new];
_showMissedDiscussionsBadge = YES;
_scrollToBottomHidden = YES;
@@ -3249,17 +3251,19 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
currentAlert = nil;
}
[self.eventMenuBuilder reset];
MXWeakify(self);
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
BOOL showThreadOption = RiotSettings.shared.enableThreads
&& !self.roomDataSource.threadId
&& !selectedEvent.threadId;
&& !self.roomDataSource.threadId
&& !selectedEvent.threadId;
if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell])
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCopy
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3271,9 +3275,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Add actions for a failed event
if (selectedEvent.sentState == MXEventSentStateFailed)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n retry]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRetrySending
action:[UIAlertAction actionWithTitle:[VectorL10n retry]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3282,9 +3287,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil];
}]];
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRemove
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3298,9 +3304,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
{
// if in the thread and selected event is the root event
// add "View in room" action
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewInRoom
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self.delegate roomViewController:self
showRoomWithId:self.roomDataSource.roomId
@@ -3329,9 +3336,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
selectedEvent.sentState == MXEventSentStateEncrypting ||
selectedEvent.sentState == MXEventSentStateSending)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
self->currentAlert = nil;
@@ -3346,9 +3354,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3363,9 +3372,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
}]];
@@ -3373,9 +3383,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3402,9 +3413,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
attachment.type == MXKAttachmentTypeImage ||
attachment.type == MXKAttachmentTypeVideo ||
attachment.type == MXKAttachmentTypeVoiceMessage)) {
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
}]];
@@ -3414,9 +3426,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
{
if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeSaveMedia
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3451,9 +3464,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
if ([MXMediaManager existingUploaderWithId:uploadId])
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
@@ -3485,9 +3499,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
{
if (BuildSettings.messageDetailsAllowShare)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3532,9 +3547,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId;
if ([MXMediaManager existingDownloaderWithIdentifier:downloadId])
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelDownloading
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3553,9 +3569,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (BuildSettings.messageDetailsAllowPermalink)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3581,9 +3598,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (BuildSettings.messageDetailsAllowViewSource)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewSource
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3596,9 +3614,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Add "View Decrypted Source" for e2ee event we can decrypt
if (selectedEvent.isEncrypted && selectedEvent.clearEvent)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewDecryptedSource
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3608,26 +3627,31 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}]];
}
}
// Do not allow to redact the event that enabled encryption (m.room.encryption)
// because it breaks everything
if (selectedEvent.eventType != MXEventTypeRoomEncryption)
{
NSString *title;
UIAlertActionStyle style = UIAlertActionStyleDefault;
UIAlertActionStyle style;
EventMenuItemType itemType;
if (selectedEvent.eventType == MXEventTypePollStart)
{
title = [VectorL10n roomEventActionRemovePoll];
style = UIAlertActionStyleDefault;
itemType = EventMenuItemTypeRemovePoll;
}
else
{
title = [VectorL10n roomEventActionRedact];
style = UIAlertActionStyleDestructive;
itemType = EventMenuItemTypeRemove;
}
[actionsMenu addAction:[UIAlertAction actionWithTitle:title
style:style
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:itemType
action:[UIAlertAction actionWithTitle:title
style:style
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3649,11 +3673,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}]];
}
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId]) {
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId]) {
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId])
{
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId])
{
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeEndPoll
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self.delegate roomViewController:self endPollWithEventIdentifier:selectedEvent.eventId];
@@ -3666,9 +3693,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Add reaction history if event contains reactions
if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReactionHistory
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3680,9 +3708,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReport
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3772,9 +3801,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted)
{
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewEncryption
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self cancelEventSelection];
@@ -3784,9 +3814,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}]];
}
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancel
action:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
[self hideContextualMenuAnimated:YES];
@@ -3795,8 +3826,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
// Do not display empty action sheet
if (actionsMenu.actions.count > 1)
if (!self.eventMenuBuilder.isEmpty)
{
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
// build actions and add them to the alert
NSArray<UIAlertAction*> *actions = [self.eventMenuBuilder build];
for (UIAlertAction *action in actions)
{
[actionsMenu addAction:action];
}
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId];
CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];

View File

@@ -0,0 +1,59 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import UIKit
@testable import Riot
class EventMenuBuilderTests: XCTestCase {
func testSorting() {
let builder = EventMenuBuilder()
let title2 = "Title 2"
builder.addItem(withType: .copy,
action: UIAlertAction(title: "Title 1", style: .default, handler: nil))
builder.addItem(withType: .viewInRoom,
action: UIAlertAction(title: title2, style: .default, handler: nil))
let actions = builder.build()
XCTAssertEqual(actions.first?.title, title2, "Item with title 'title2' must come first in the result")
}
func testEmptiness() {
let builder = EventMenuBuilder()
XCTAssertTrue(builder.isEmpty, "Builder must be empty after initialization")
builder.addItem(withType: .cancel,
action: UIAlertAction(title: "Cancel", style: .default, handler: nil))
XCTAssertTrue(builder.isEmpty, "Builder must still be empty after adding only a cancel action")
builder.addItem(withType: .share,
action: UIAlertAction(title: "some_title", style: .default, handler: nil))
XCTAssertFalse(builder.isEmpty, "Builder must not be empty after adding an item")
builder.reset()
XCTAssertTrue(builder.isEmpty, "Builder must be empty again after reset")
}
}

View File

@@ -64,3 +64,5 @@ targets:
- path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift
- path: ../Riot/Managers/KeyValueStorage/
- path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift
- path: ../Riot/Modules/Room/EventMenu/EventMenuBuilder.swift
- path: ../Riot/Modules/Room/EventMenu/EventMenuItemType.swift