From f9d5fc47a3fb0a4fd0bb9652a7a634293345a5c4 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 18 Aug 2022 16:15:47 +0200 Subject: [PATCH 1/2] App Layout: Context Menus - Fixed --- .../Contents.json | 25 ---- .../home_my_spaces_action.svg | 6 - Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Images.swift | 1 - Riot/Generated/Strings.swift | 8 + .../AllChatsActionProvider.swift | 2 +- .../AllChatsEditActionProvider.swift | 68 ++------- .../AllChatsSpaceActionProvider.swift | 141 ++++++++++++++++++ .../AllChats/AllChatsViewController.swift | 37 ++++- changelog.d/6574.bugfix | 1 + 10 files changed, 193 insertions(+), 98 deletions(-) delete mode 100644 Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json delete mode 100644 Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg create mode 100644 Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift create mode 100644 changelog.d/6574.bugfix diff --git a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json deleted file mode 100644 index d50c95b02..000000000 --- a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "home_my_spaces_action.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg deleted file mode 100644 index 3b956bb6b..000000000 --- a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 5ae490f35..0b761a2af 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2006,6 +2006,7 @@ Tap the + to start adding people."; "leave_space_only_action" = "Don't leave any rooms"; "leave_space_and_all_rooms_action" = "Leave all rooms and spaces"; "spaces_explore_rooms" = "Explore rooms"; +"spaces_explore_rooms_format" = "Explore %@"; "spaces_suggested_room" = "Suggested"; "spaces_explore_rooms_room_number" = "%@ rooms"; "spaces_explore_rooms_one_room" = "1 room"; @@ -2097,6 +2098,7 @@ Tap the + to start adding people."; "spaces_creation_in_one_space" = "in 1 space"; "spaces_invite_people" = "Invite people"; +"spaces_invite_people_format" = "Invite to %@"; "spaces_add_room" = "Add room"; "spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; "spaces_add_space" = "Add space"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index d0373b94f..beb99dc8f 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -115,7 +115,6 @@ internal class Asset: NSObject { internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low") internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork") internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark") - internal static let homeMySpacesAction = ImageAsset(name: "home_my_spaces_action") internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action") internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon") internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 08c2a34af..ec1d90ca3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8047,6 +8047,10 @@ public class VectorL10n: NSObject { public static var spacesExploreRooms: String { return VectorL10n.tr("Vector", "spaces_explore_rooms") } + /// Explore %@ + public static func spacesExploreRoomsFormat(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_explore_rooms_format", p1) + } /// 1 room public static var spacesExploreRoomsOneRoom: String { return VectorL10n.tr("Vector", "spaces_explore_rooms_one_room") @@ -8067,6 +8071,10 @@ public class VectorL10n: NSObject { public static var spacesInvitePeople: String { return VectorL10n.tr("Vector", "spaces_invite_people") } + /// Invite to %@ + public static func spacesInvitePeopleFormat(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_invite_people_format", p1) + } /// Invites coming soon public static var spacesInvitesComingSoonTitle: String { return VectorL10n.tr("Vector", "spaces_invites_coming_soon_title") diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift index 620d903ea..ded2fe69a 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift @@ -28,7 +28,7 @@ class AllChatsActionProvider { // MARK: - RoomActionProviderProtocol var menu: UIMenu { - return UIMenu(title: VectorL10n.allChatsEditLayout, children: [ + return UIMenu(title: "", children: [ self.recentsAction, self.filtersAction, UIMenu(title: "", options: .displayInline, children: [ diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift index af296a4c2..c63b6318f 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift @@ -21,10 +21,6 @@ enum AllChatsEditActionProviderOption { case exploreRooms case createRoom case startChat - case invitePeople - case spaceMembers - case spaceSettings - case leaveSpace case createSpace } @@ -56,30 +52,26 @@ class AllChatsEditActionProvider { var menu: UIMenu { guard parentSpace != nil else { var createActions = [ - self.startChatAction, - self.createRoomAction + self.createRoomAction, + self.startChatAction ] if rootSpaceCount > 0 { - createActions.append(self.createSpaceAction) + createActions.insert(self.createSpaceAction, at: 0) } - return UIMenu(title: VectorL10n.allChatsTitle, children: [ + return UIMenu(title: "", children: [ self.exploreRoomsAction, UIMenu(title: "", options: .displayInline, children: createActions) ]) } - return UIMenu(title: parentName, children: [ + return UIMenu(title: "", children: [ UIMenu(title: "", options: .displayInline, children: [ - self.spaceMembersAction, - self.exploreRoomsAction, - self.spaceSettingsAction + self.exploreRoomsAction ]), UIMenu(title: "", options: .displayInline, children: [ - self.invitePeopleAction, - self.createRoomAction, - self.createSpaceAction - ]), - self.leaveSpaceAction + self.createSpaceAction, + self.createRoomAction + ]) ]) } @@ -139,8 +131,8 @@ class AllChatsEditActionProvider { // MARK: - Private private var exploreRoomsAction: UIAction { - UIAction(title: VectorL10n.spacesExploreRooms, - image: parentSpace == nil ? UIImage(systemName: "list.bullet") : UIImage(systemName: "square.fill.text.grid.1x2")) { [weak self] action in + UIAction(title: parentSpace == nil ? VectorL10n.spacesExploreRooms : VectorL10n.spacesExploreRoomsFormat(parentName), + image: UIImage(systemName: "list.bullet")) { [weak self] action in guard let self = self else { return } self.delegate?.allChatsEditActionProvider(self, didSelect: .exploreRooms) @@ -175,42 +167,4 @@ class AllChatsEditActionProvider { self.delegate?.allChatsEditActionProvider(self, didSelect: .createSpace) } } - - private var invitePeopleAction: UIAction { - UIAction(title: VectorL10n.spacesInvitePeople, - image: UIImage(systemName: "person.badge.plus"), - attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .invitePeople) - } - } - - private var spaceMembersAction: UIAction { - UIAction(title: VectorL10n.roomDetailsPeople, - image: UIImage(systemName: "person.3")) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceMembers) - } - } - - private var spaceSettingsAction: UIAction { - UIAction(title: VectorL10n.allChatsEditMenuSpaceSettings, - image: UIImage(systemName: "gearshape")) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceSettings) - } - } - - private var leaveSpaceAction: UIAction { - UIAction(title: VectorL10n.allChatsEditMenuLeaveSpace(parentName), - image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), - attributes: .destructive) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .leaveSpace) - } - } } diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift new file mode 100644 index 000000000..24020410c --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift @@ -0,0 +1,141 @@ +// +// Copyright 2022 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 UIKit +import MatrixSDK + +enum AllChatsSpaceActionProviderOption { + case invitePeople + case spaceMembers + case spaceSettings + case leaveSpace +} + +protocol AllChatsSpaceActionProviderDelegate: AnyObject { + func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) +} + +/// `AllChatsSpaceActionProvider` provides the menu for accessing space options according to the current space +class AllChatsSpaceActionProvider { + + // MARK: - Properties + + weak var delegate: AllChatsSpaceActionProviderDelegate? + + // MARK: - Private + + private var currentSpace: MXSpace? { + didSet { + spaceName = currentSpace?.summary?.displayname ?? VectorL10n.spaceTag + } + } + private var spaceName: String = VectorL10n.spaceTag + private var isInviteAvailable: Bool = false + + // MARK: - RoomActionProviderProtocol + + var menu: UIMenu { + guard currentSpace != nil else { + return UIMenu(title: "", children: []) + } + + return UIMenu(title: "", children: [ + UIMenu(title: "", options: .displayInline, children: [ + self.spaceSettingsAction, + self.spaceMembersAction, + self.invitePeopleAction + ]), + self.leaveSpaceAction + ]) + } + + // MARK: - Public + + /// Returns an instance of the updated menu accordingly to the given parameters. + /// + /// Some menu items can be disabled depending on the required power levels of the `parentSpace`. Therefore, `updateMenu()` first returns a temporary context menu + /// with all sensible items disabled, asynchronously fetches power levels of the `parentSpace`, then gives a new instance of the menu with, potentially, all sensible items + /// enabled via the `completion` callback. + /// + /// - Parameters: + /// - session: The current `MXSession` instance + /// - space: The current space (`nil` for home space) + /// - completion: callback called once the power levels of the `parentSpace` have been fetched and the menu items have been computed accordingly. + /// - Returns: If the `parentSpace` is `nil`, the context menu, the temporary context menu otherwise. + func updateMenu(with session: MXSession?, space: MXSpace?, completion: @escaping (UIMenu) -> Void) -> UIMenu { + self.currentSpace = space + isInviteAvailable = false + + guard let currentSpace = currentSpace, let spaceRoom = currentSpace.room, let session = session else { + return self.menu + } + + spaceRoom.state { [weak self] roomState in + guard let self = self else { return } + + guard let powerLevels = roomState?.powerLevels, let userId = session.myUserId else { + return + } + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId) + + self.isInviteAvailable = userPowerLevel >= powerLevels.invite + + completion(self.menu) + } + + return self.menu + } + + // MARK: - Private + + private var invitePeopleAction: UIAction { + UIAction(title: VectorL10n.spacesInvitePeopleFormat(spaceName), + image: UIImage(systemName: "square.and.arrow.up"), + attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .invitePeople) + } + } + + private var spaceMembersAction: UIAction { + UIAction(title: VectorL10n.roomDetailsPeople, + image: UIImage(systemName: "person")) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .spaceMembers) + } + } + + private var spaceSettingsAction: UIAction { + UIAction(title: VectorL10n.allChatsEditMenuSpaceSettings, + image: UIImage(systemName: "gearshape")) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .spaceSettings) + } + } + + private var leaveSpaceAction: UIAction { + UIAction(title: VectorL10n.allChatsEditMenuLeaveSpace(spaceName), + image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), + attributes: .destructive) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .leaveSpace) + } + } +} diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index cf749e649..bd6bad3cc 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -37,6 +37,8 @@ class AllChatsViewController: HomeViewController { private let searchController = UISearchController(searchResultsController: nil) + private let spaceActionProvider = AllChatsSpaceActionProvider() + private let editActionProvider = AllChatsEditActionProvider() private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter? @@ -49,6 +51,7 @@ class AllChatsViewController: HomeViewController { super.viewDidLoad() editActionProvider.delegate = self + spaceActionProvider.delegate = self recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue recentsTableView.clipsToBounds = false @@ -59,7 +62,6 @@ class AllChatsViewController: HomeViewController { searchController.obscuresBackgroundDuringPresentation = false searchController.searchResultsUpdater = self - self.setupEditOptions() NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil) } @@ -171,26 +173,37 @@ class AllChatsViewController: HomeViewController { // MARK: - Private @objc private func setupEditOptions() { - self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: AllChatsActionProvider().menu) + guard let currentSpace = self.dataSource?.currentSpace else { + updateRightNavigationItem(with: AllChatsActionProvider().menu) + return + } + + updateRightNavigationItem(with: spaceActionProvider.updateMenu(with: mainSession, space: currentSpace) { [weak self] menu in + self?.updateRightNavigationItem(with: menu) + }) } private func updateUI() { let currentSpace = self.dataSource?.currentSpace self.tabBarController?.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle + setupEditOptions() updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in self?.updateToolbar(with: menu) })) } + private func updateRightNavigationItem(with menu: UIMenu) { + self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu) + } + private func updateToolbar(with menu: UIMenu) { - let currentSpace = self.dataSource?.currentSpace self.navigationController?.isToolbarHidden = false self.update(with: ThemeService.shared().theme) self.tabBarController?.setToolbarItems([ - UIBarButtonItem(image: Asset.Images.homeMySpacesAction.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))), + UIBarButtonItem(image: UIImage(systemName: "square.grid.2x2"), style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))), UIBarButtonItem.flexibleSpace(), - UIBarButtonItem(image: UIImage(systemName: currentSpace == nil ? "square.and.pencil" : "ellipsis.circle"), menu: menu) + UIBarButtonItem(image: UIImage(systemName: "square.and.pencil"), menu: menu) ], animated: true) } @@ -393,6 +406,17 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { createNewRoom() case .startChat: startChat() + case .createSpace: + showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) + } + } + +} + +// MARK: - AllChatsSpaceActionProviderDelegate +extension AllChatsViewController: AllChatsSpaceActionProviderDelegate { + func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) { + switch option { case .invitePeople: showSpaceInvite() case .spaceMembers: @@ -401,11 +425,8 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { showSpaceSettings() case .leaveSpace: showLeaveSpace() - case .createSpace: - showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) } } - } // MARK: - ContactsPickerCoordinatorDelegate diff --git a/changelog.d/6574.bugfix b/changelog.d/6574.bugfix new file mode 100644 index 000000000..b451e9f27 --- /dev/null +++ b/changelog.d/6574.bugfix @@ -0,0 +1 @@ +App Layout: updated context menus according to last design update From 0128c1a3fe34e78751bb50660538c03b5558aa35 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 18 Aug 2022 16:34:04 +0200 Subject: [PATCH 2/2] App Layout: Context Menus - Updated avatar menu --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 8 ++++---- .../ActionProviders/AllChatsSpaceActionProvider.swift | 2 +- Riot/Modules/TabBar/TabBarCoordinator.swift | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0b761a2af..1a8152b55 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -77,6 +77,7 @@ "suggest" = "Suggest"; "edit" = "Edit"; "confirm" = "Confirm"; +"invite_to" = "Invite to %@"; // Activities "loading" = "Loading"; @@ -2098,7 +2099,6 @@ Tap the + to start adding people."; "spaces_creation_in_one_space" = "in 1 space"; "spaces_invite_people" = "Invite people"; -"spaces_invite_people_format" = "Invite to %@"; "spaces_add_room" = "Add room"; "spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; "spaces_add_space" = "Add space"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ec1d90ca3..cf2b7fa68 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2491,6 +2491,10 @@ public class VectorL10n: NSObject { public static func inviteFriendsShareText(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "invite_friends_share_text", p1, p2) } + /// Invite to %@ + public static func inviteTo(_ p1: String) -> String { + return VectorL10n.tr("Vector", "invite_to", p1) + } /// Invite matrix User public static var inviteUser: String { return VectorL10n.tr("Vector", "invite_user") @@ -8071,10 +8075,6 @@ public class VectorL10n: NSObject { public static var spacesInvitePeople: String { return VectorL10n.tr("Vector", "spaces_invite_people") } - /// Invite to %@ - public static func spacesInvitePeopleFormat(_ p1: String) -> String { - return VectorL10n.tr("Vector", "spaces_invite_people_format", p1) - } /// Invites coming soon public static var spacesInvitesComingSoonTitle: String { return VectorL10n.tr("Vector", "spaces_invites_coming_soon_title") diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift index 24020410c..0d9e3d2fa 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift @@ -102,7 +102,7 @@ class AllChatsSpaceActionProvider { // MARK: - Private private var invitePeopleAction: UIAction { - UIAction(title: VectorL10n.spacesInvitePeopleFormat(spaceName), + UIAction(title: VectorL10n.inviteTo(spaceName), image: UIImage(systemName: "square.and.arrow.up"), attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in guard let self = self else { return } diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 221986e14..d059aa5d3 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -743,13 +743,13 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func createAvatarButtonItem(for viewController: UIViewController) { var actions: [UIMenuElement] = [] - actions.append(UIAction(title: VectorL10n.allChatsUserMenuSettings, image: UIImage(systemName: "gearshape")) { [weak self] action in + actions.append(UIAction(title: VectorL10n.settings, image: UIImage(systemName: "gearshape")) { [weak self] action in self?.showSettings() }) var subMenuActions: [UIAction] = [] if BuildSettings.sideMenuShowInviteFriends { - subMenuActions.append(UIAction(title: VectorL10n.sideMenuActionInviteFriends, image: UIImage(systemName: "square.and.arrow.up.fill")) { [weak self] action in + subMenuActions.append(UIAction(title: VectorL10n.inviteTo(AppInfo.current.displayName), image: UIImage(systemName: "envelope")) { [weak self] action in self?.showInviteFriends(from: nil) }) }