Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use modern UIMenu #2545

Merged
merged 13 commits into from
Jan 28, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Detect Stickers when dropped, pasted or picked from Gallery (#2535)
- Modernize menus (#2543)
- Fix: In 'View Log', hide keyboard when scrolling down (#2541)
- Fix: Experimental location sharing now ends at the specified interval even if you don't move (#2537)
- minimum system version is iOS 14 now (all iOS 13 devices can upgrade to iOS 14) (#2459)
Expand Down
43 changes: 22 additions & 21 deletions deltachat-ios/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}
actions.append(action("contact", "person.crop.circle", showContactList))

completion([UIMenu(options: .displayInline, children: actions)])
completion(actions)
})
])
}
Expand All @@ -1246,30 +1246,15 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
present(alert, animated: true, completion: nil)
}

private func showMoreMenu() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
if canResend() {
alert.addAction(UIAlertAction(title: String.localized("resend"), style: .default, handler: onResendActionPressed(_:)))
}
if canShare() {
alert.addAction(UIAlertAction(title: String.localized("menu_share"), style: .default, handler: onShareActionPressed(_:)))
}
if canInfo() {
alert.addAction(UIAlertAction(title: String.localized("info"), style: .default, handler: onInfoActionPressed(_:)))
}
alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}

private func onResendActionPressed(_ action: UIAlertAction) {
private func onResendActionPressed() {
if let rows = tableView.indexPathsForSelectedRows {
let selectedMsgIds = rows.compactMap { messageIds[$0.row] }
dcContext.resendMessages(msgIds: selectedMsgIds)
setEditing(isEditing: false)
}
}

private func onShareActionPressed(_ action: UIAlertAction) {
private func onShareActionPressed() {
if let rows = tableView.indexPathsForSelectedRows {
let selectedMsgIds = rows.compactMap { messageIds[$0.row] }
if let msgId = selectedMsgIds.first {
Expand All @@ -1279,7 +1264,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}
}

private func onInfoActionPressed(_ action: UIAlertAction) {
private func onInfoActionPressed() {
if let rows = tableView.indexPathsForSelectedRows, let firstRow = rows.first {
info(at: firstRow)
}
Expand Down Expand Up @@ -2430,8 +2415,24 @@ extension ChatViewController: ChatEditingDelegate {
}
}

func onMorePressed() {
showMoreMenu()
func onMorePressed() -> UIMenu {
var actions = [UIMenuElement]()
if canResend() {
actions.append(UIAction(title: String.localized("resend"), image: UIImage(systemName: "paperplane")) { [weak self] _ in
self?.onResendActionPressed()
})
}
if canShare() {
actions.append(UIAction(title: String.localized("menu_share"), image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in
self?.onShareActionPressed()
})
}
if canInfo() {
actions.append(UIAction(title: String.localized("info"), image: UIImage(systemName: "info.circle")) { [weak self] _ in
self?.onInfoActionPressed()
})
}
return UIMenu(children: actions)
}

func onForwardPressed() {
Expand Down
14 changes: 8 additions & 6 deletions deltachat-ios/Chat/Views/ChatEditingBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public protocol ChatEditingDelegate: AnyObject {
func onForwardPressed()
func onCancelPressed()
func onCopyPressed()
func onMorePressed()
func onMorePressed() -> UIMenu
}

public class ChatEditingBar: UIView {
Expand Down Expand Up @@ -110,19 +110,21 @@ public class ChatEditingBar: UIView {
])

copyButton.addTarget(self, action: #selector(ChatEditingBar.onCopyPressed), for: .touchUpInside)
moreButton.addTarget(self, action: #selector(ChatEditingBar.onMorePressed), for: .touchUpInside)
forwardButton.addTarget(self, action: #selector(ChatEditingBar.onForwardPressed), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(ChatEditingBar.onDeletePressed), for: .touchUpInside)

moreButton.showsMenuAsPrimaryAction = true
moreButton.menu = UIMenu() // otherwise .menuActionTriggered is not triggered
moreButton.addAction(UIAction { [weak self] _ in
guard let self else { return }
moreButton.menu = delegate?.onMorePressed()
}, for: .menuActionTriggered)
}

@objc func onCopyPressed() {
delegate?.onCopyPressed()
}

@objc func onMorePressed() {
delegate?.onMorePressed()
}

@objc func onForwardPressed() {
delegate?.onForwardPressed()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ class InstantOnboardingViewController: UIViewController {
private var qrCodeData: String?
private lazy var menuButton: UIBarButtonItem = {
let image = UIImage(systemName: "ellipsis.circle")
let menuButton = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(InstantOnboardingViewController.showMenu(_:)))
menuButton.tintColor = DcColors.primary
return menuButton
return UIBarButtonItem(image: image, menu: moreButtonMenu())
}()

private lazy var proxyShieldButton: UIBarButtonItem = {
let button = UIBarButtonItem(image: UIImage(systemName: "checkmark.shield"), style: .plain, target: self, action: #selector(InstantOnboardingViewController.showProxySettings(_:)))
button.tintColor = DcColors.primary
return button
let image = UIImage(systemName: "checkmark.shield")
return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(showProxySettings))
}()

var progressAlertHandler: ProgressAlertHandler?
Expand Down Expand Up @@ -221,22 +218,16 @@ class InstantOnboardingViewController: UIViewController {
present(alertController, animated: true)
}

@objc private func showMenu(_ sender: Any) {
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)

let showProxySettings = UIAlertAction(title: String.localized("proxy_settings"), style: .default) { [weak self] _ in
self?.showProxySettings(sender)
}

let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel)

sheet.addAction(showProxySettings)
sheet.addAction(cancelAction)

present(sheet, animated: true)
private func moreButtonMenu() -> UIMenu {
let actions = [
UIAction(title: String.localized("proxy_use_proxy"), image: UIImage(systemName: "shield")) { [weak self] _ in
self?.showProxySettings()
},
Comment on lines +223 to +225
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more of a UX-thing, but if we only have one entry a menu, does it make a lot of sense to show that menu -- and not the menu-entry itself? In other words: Why does the BarButtonItem show a menu and not the Proxy-button?

It feels like an extra step for the user to access the proxy-settings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if we only have one entry a menu, does it make a lot of sense to show that menu

yes - as the symbol is not self-explaining for most users. therefore, an additional descriptive text is better.

when proxy is then enabled, the icon is shown directly - this is also what is done in the main app.

]
return UIMenu(children: actions)
}

@objc private func showProxySettings(_ sender: Any) {
@objc private func showProxySettings() {
let proxySettingsController = ProxySettingsViewController(dcContext: dcContext, dcAccounts: dcAccounts)
navigationController?.pushViewController(proxySettingsController, animated: true)
}
Expand Down
39 changes: 21 additions & 18 deletions deltachat-ios/Controller/BackupTransferViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ class BackupTransferViewController: UIViewController {
return UIBarButtonItem(title: String.localized("cancel"), style: .plain, target: self, action: #selector(cancelButtonPressed))
}

private lazy var moreButton: UIBarButtonItem = {
let image = UIImage(systemName: "ellipsis.circle")
return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(moreButtonPressed))
}()

private let statusLine: UILabel
private let experimentalLine: UILabel
private let qrContentView: UIImageView
Expand Down Expand Up @@ -100,7 +95,7 @@ class BackupTransferViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = cancelButton
navigationItem.rightBarButtonItem = moreButton
updateMenuItems()

triggerLocalNetworkPrivacyAlert()

Expand All @@ -124,6 +119,7 @@ class BackupTransferViewController: UIViewController {
self.statusLine.text = "➊ " + String.localized("multidevice_same_network_hint")
+ "\n\n➋ " + String.localized("multidevice_install_dc_on_other_device")
+ "\n\n➌ " + String.localized("multidevice_tap_scan_on_other_device")
self.updateMenuItems()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self else { return }
self.dcBackupProvider?.wait()
Expand Down Expand Up @@ -190,6 +186,7 @@ class BackupTransferViewController: UIViewController {
self.statusLine.textAlignment = .center
experimentalLine.isHidden = true
self.qrContentView.isHidden = true
updateMenuItems()
}
}
}
Expand Down Expand Up @@ -264,20 +261,26 @@ class BackupTransferViewController: UIViewController {
}
}

@objc private func moreButtonPressed() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
alert.addAction(UIAlertAction(title: String.localized("troubleshooting"), style: .default, handler: { _ in
self.navigationController?.pushViewController(HelpViewController(dcContext: self.dcContext, fragment: "#multiclient"), animated: true)
}))
if !self.qrContentView.isHidden {
alert.addAction(UIAlertAction(title: String.localized("menu_copy_to_clipboard"), style: .default, handler: { [weak self] _ in
private func updateMenuItems() {
let menu = moreButtonMenu()
let button = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu)
navigationItem.rightBarButtonItem = button
}

private func moreButtonMenu() -> UIMenu {
var actions = [UIMenuElement]()
if !qrContentView.isHidden {
actions.append(UIAction(title: String.localized("menu_copy_to_clipboard"), image: UIImage(systemName: "document.on.document")) { [weak self] _ in
guard let self else { return }
self.warnAboutCopiedQrCodeOnAbort = true
UIPasteboard.general.string = self.dcBackupProvider?.getQr()
}))
warnAboutCopiedQrCodeOnAbort = true
UIPasteboard.general.string = dcBackupProvider?.getQr()
})
}
alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
actions.append(UIAction(title: String.localized("troubleshooting"), image: UIImage(systemName: "questionmark.circle")) { [weak self] _ in
guard let self else { return }
navigationController?.pushViewController(HelpViewController(dcContext: dcContext, fragment: "#multiclient"), animated: true)
})
return UIMenu(children: actions)
}
}

Expand Down
24 changes: 12 additions & 12 deletions deltachat-ios/Controller/ChatListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1031,10 +1031,10 @@ extension ChatListViewController: ChatListEditingBarDelegate {
setLongTapEditing(false)
}

func onMorePressed() {
guard let userDefaults = UserDefaults.shared, let viewModel else { return }
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
func onMorePressed() -> UIMenu {
guard let userDefaults = UserDefaults.shared, let viewModel else { return UIMenu() }
let chatIds = viewModel.chatIdsFor(indexPaths: tableView.indexPathsForSelectedRows)
var actions = [UIMenuElement]()

if #available(iOS 17.0, *),
chatIds.count == 1,
Expand All @@ -1051,33 +1051,34 @@ extension ChatListViewController: ChatListEditingBarDelegate {
}

let chatPresentInHomescreenWidget = allHomescreenChatsIds.contains(chatId)
let action: UIAlertAction
let action: UIAction
if chatPresentInHomescreenWidget {
action = UIAlertAction(title: String.localized("remove_from_widget"), style: .default) { [weak self] _ in
action = UIAction(title: String.localized("remove_from_widget"), image: UIImage(systemName: "minus.square")) { [weak self] _ in
guard let self else { return }
userDefaults.removeChatFromHomescreenWidget(accountId: self.dcContext.id, chatId: chatId)
setLongTapEditing(false)
}
} else {
action = UIAlertAction(title: String.localized("add_to_widget"), style: .default) { [weak self] _ in
action = UIAction(title: String.localized("add_to_widget"), image: UIImage(systemName: "plus.square")) { [weak self] _ in
guard let self else { return }
userDefaults.addChatToHomescreenWidget(accountId: self.dcContext.id, chatId: chatId)
setLongTapEditing(false)
}
}
alert.addAction(action)
actions.append(action)
}

let onlyPinndedSelected = viewModel.hasOnlyPinnedChatsSelected(in: tableView.indexPathsForSelectedRows)
let pinTitle = String.localized(onlyPinndedSelected ? "unpin" : "pin")
alert.addAction(UIAlertAction(title: pinTitle, style: .default) { [weak self] _ in
let pinImage = UIImage(systemName: onlyPinndedSelected ? "pin.slash" : "pin")
actions.append(UIAction(title: pinTitle, image: pinImage) { [weak self] _ in
guard let self else { return }
viewModel.pinChatsToggle(indexPaths: tableView.indexPathsForSelectedRows)
setLongTapEditing(false)
})

if viewModel.hasAnyUnmutedChatSelected(in: tableView.indexPathsForSelectedRows) {
alert.addAction(UIAlertAction(title: String.localized("menu_mute"), style: .default) { [weak self] _ in
actions.append(UIAction(title: String.localized("menu_mute"), image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
guard let self else { return }
MuteDialog.show(viewController: self) { [weak self] duration in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could return a sub-menu instead maybe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i also thouht about that, but the mute dialog is also available at other places, so it seems better to have the same UI everywhere

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe those other places could use the menu too? 🤔 But can be another PR, this already addresses a lot of them.

Copy link
Member Author

@r10s r10s Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, at least whatsapp and signal also use an actionSheet for the concrete setting. i also have rarely seen a UIMenu in response to a swipe-to-action gesture (where mute is also available)

but yes, if, then it anyway would be another pr, so this is no blocker

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I don't think swipe action is a great place for it.

guard let self else { return }
Expand All @@ -1086,14 +1087,13 @@ extension ChatListViewController: ChatListEditingBarDelegate {
}
})
} else {
alert.addAction(UIAlertAction(title: String.localized("menu_unmute"), style: .default) { [weak self] _ in
actions.append(UIAction(title: String.localized("menu_unmute"), image: UIImage(systemName: "speaker.wave.2")) { [weak self] _ in
guard let self else { return }
viewModel.setMuteDurations(in: tableView.indexPathsForSelectedRows, duration: 0)
setLongTapEditing(false)
})
}

alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
return UIMenu(children: actions)
}
}
5 changes: 5 additions & 0 deletions deltachat-ios/Controller/FullMessageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import DcCore
class FullMessageViewController: WebViewViewController {

var loadButton: UIBarButtonItem {
// to not encourages people to get used to tap the load button
// just to see whether the message they get will change, this is a very generic icon.
// (best would be if we know before if an HTML message contains images and thelike,
// but we don't and this is probably also not worth the effort. so we used the second best approach :)
let image = UIImage(systemName: "ellipsis.circle")

let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(showLoadOptions))
button.accessibilityLabel = String.localized("load_remote_content")
return button
Expand Down
50 changes: 25 additions & 25 deletions deltachat-ios/Controller/HelpViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class HelpViewController: WebViewViewController {

private lazy var moreButton: UIBarButtonItem = {
let image = UIImage(systemName: "ellipsis.circle")
return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(moreButtonPressed))
return UIBarButtonItem(image: image, menu: moreButtonMenu())
}()

override func viewDidLoad() {
Expand Down Expand Up @@ -66,29 +66,29 @@ class HelpViewController: WebViewViewController {
}
}

@objc private func moreButtonPressed() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
alert.addAction(UIAlertAction(title: String.localized("global_menu_help_learn_desktop"), style: .default, handler: { _ in
if let url = URL(string: "https://delta.chat") {
UIApplication.shared.open(url)
}
}))
alert.addAction(UIAlertAction(title: String.localized("privacy_policy"), style: .default, handler: { _ in
if let url = URL(string: "https://delta.chat/gdpr") {
UIApplication.shared.open(url)
}
}))
alert.addAction(UIAlertAction(title: String.localized("global_menu_help_contribute_desktop"), style: .default, handler: { _ in
if let url = URL(string: "https://github.com/deltachat/deltachat-ios") {
UIApplication.shared.open(url)
}
}))
alert.addAction(UIAlertAction(title: String.localized("global_menu_help_report_desktop"), style: .default, handler: { _ in
if let url = URL(string: "https://github.com/deltachat/deltachat-ios/issues") {
UIApplication.shared.open(url)
}
}))
alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
private func moreButtonMenu() -> UIMenu {
let actions = [
UIAction(title: String.localized("delta_chat_homepage"), image: UIImage(systemName: "globe")) { _ in
if let url = URL(string: "https://delta.chat") {
UIApplication.shared.open(url)
}
},
UIAction(title: String.localized("privacy_policy"), image: UIImage(systemName: "hand.raised")) { _ in
if let url = URL(string: "https://delta.chat/gdpr") {
UIApplication.shared.open(url)
}
},
UIAction(title: String.localized("contribute"), image: UIImage(systemName: "wrench.and.screwdriver")) { _ in
if let url = URL(string: "https://delta.chat/contribute") {
UIApplication.shared.open(url)
}
},
UIAction(title: String.localized("global_menu_help_report_desktop"), image: UIImage(systemName: "ant")) { _ in
if let url = URL(string: "https://github.com/deltachat/deltachat-ios/issues") {
UIApplication.shared.open(url)
}
},
]
return UIMenu(children: actions)
}
}
Loading
Loading