Skip to content

Commit 940abfb

Browse files
committed
Added navigation bar dropdown option
1 parent 20b41b3 commit 940abfb

File tree

9 files changed

+201
-31
lines changed

9 files changed

+201
-31
lines changed

Session.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,8 @@
11741174
FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
11751175
FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; };
11761176
FE2883272EA70C640097E240 /* MessageSelectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2883262EA70C640097E240 /* MessageSelectionManager.swift */; };
1177+
FE28832B2EA74D440097E240 /* ManualDropdownPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE28832A2EA74D440097E240 /* ManualDropdownPresenter.swift */; };
1178+
FE28832D2EA74D680097E240 /* CustomMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE28832C2EA74D680097E240 /* CustomMenuView.swift */; };
11771179
FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; };
11781180
FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; };
11791181
/* End PBXBuildFile section */
@@ -2469,6 +2471,8 @@
24692471
FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_MakeBrokenProfileTimestampsNullable.swift; sourceTree = "<group>"; };
24702472
FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = "<group>"; };
24712473
FE2883262EA70C640097E240 /* MessageSelectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSelectionManager.swift; sourceTree = "<group>"; };
2474+
FE28832A2EA74D440097E240 /* ManualDropdownPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualDropdownPresenter.swift; sourceTree = "<group>"; };
2475+
FE28832C2EA74D680097E240 /* CustomMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMenuView.swift; sourceTree = "<group>"; };
24722476
FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = "<group>"; };
24732477
FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = "<group>"; };
24742478
/* End PBXFileReference section */
@@ -5306,6 +5310,8 @@
53065310
FE2883252EA70C5D0097E240 /* Selection */ = {
53075311
isa = PBXGroup;
53085312
children = (
5313+
FE28832C2EA74D680097E240 /* CustomMenuView.swift */,
5314+
FE28832A2EA74D440097E240 /* ManualDropdownPresenter.swift */,
53095315
FE2883262EA70C640097E240 /* MessageSelectionManager.swift */,
53105316
);
53115317
path = Selection;
@@ -7073,6 +7079,7 @@
70737079
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
70747080
9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */,
70757081
7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */,
7082+
FE28832D2EA74D680097E240 /* CustomMenuView.swift in Sources */,
70767083
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */,
70777084
7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */,
70787085
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */,
@@ -7084,6 +7091,7 @@
70847091
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */,
70857092
FD12A8472AD63C3400EEBA0D /* PagedObservationSource.swift in Sources */,
70867093
FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */,
7094+
FE28832B2EA74D440097E240 /* ManualDropdownPresenter.swift in Sources */,
70877095
7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */,
70887096
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
70897097
FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */,

Session/Conversations/ConversationVC+Interaction.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3585,17 +3585,28 @@ extension ConversationVC: SelectionManagerDelegate {
35853585
}
35863586

35873587
func showInfo(for message: MessageViewModel, withSender sender: UIBarButtonItem) {
3588-
/*guard
3589-
let actions = ContextMenuVC.navigationActions(
3590-
for: message,
3591-
in: viewModel.threadData,
3592-
delegate: self,
3593-
using: viewModel.dependencies
3594-
)
3595-
else { return }*/
3588+
if dropdownPresenter != nil {
3589+
dropdownPresenter?.hide()
3590+
dropdownPresenter = nil
3591+
}
3592+
3593+
let actions = ContextMenuVC.navigationActions(
3594+
for: message,
3595+
in: viewModel.threadData,
3596+
delegate: self,
3597+
using: viewModel.dependencies
3598+
) ?? []
35963599

3597-
// TODO: - Show context menu below navigationbar for now navigate to info page
3598-
info(message)
3599-
resetSelection()
3600+
let presenter = ManualDropdownPresenter()
3601+
self.dropdownPresenter = presenter
3602+
3603+
presenter.show(
3604+
actions: actions,
3605+
anchorView: navigationController?.navigationBar.subviews.first,
3606+
using: viewModel.dependencies
3607+
) { [weak self] in
3608+
self?.dropdownPresenter = nil
3609+
self?.resetSelection()
3610+
}
36003611
}
36013612
}

Session/Conversations/ConversationVC.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
124124
delegate: self
125125
)
126126

127+
// Reference to dropdown view
128+
var dropdownPresenter: ManualDropdownPresenter?
129+
130+
// Search
127131
lazy var searchController: ConversationSearchController = {
128132
let result: ConversationSearchController = ConversationSearchController(
129133
threadId: self.viewModel.threadData.threadId
@@ -1738,12 +1742,9 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
17381742
tableSize: tableView.bounds.size,
17391743
using: viewModel.dependencies
17401744
)
1745+
cell.setSelectedState(selectedMessages.contains(cellViewModel))
17411746
cell.delegate = self
17421747

1743-
let isSelected = selectedMessages.contains(cellViewModel)
1744-
1745-
cell.setSelectedState(isSelected)
1746-
17471748
return cell
17481749

17491750
default: preconditionFailure("Other sections should have no content")

Session/Conversations/Message Cells/MessageCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ protocol MessageCellDelegate: ReactionDelegate {
153153
func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?)
154154
func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool)
155155
func handleReadMoreButtonTapped(_ cell: UITableViewCell, for cellViewModel: MessageViewModel)
156-
func handleCellSelection(for cellViewModel: MessageViewModel, cell: UITableViewCell)
156+
func handleCellSelection(for cellViewModel: MessageViewModel, cell: UITableViewCell) // Added for selection handling of tapped item and tapped cell
157157
}
158158

159159
extension MessageCellDelegate {

Session/Conversations/Message Cells/VisibleMessageCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
10321032

10331033
override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
10341034
guard let cellViewModel: MessageViewModel = self.viewModel else { return }
1035-
1035+
10361036
let location = gestureRecognizer.location(in: self)
10371037
let tappedAuthorName: Bool = (
10381038
authorLabel.bounds.contains(authorLabel.convert(location, from: self)) &&
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
2+
3+
import UIKit
4+
import SessionUtilitiesKit
5+
import SessionUIKit
6+
7+
class CustomMenuView: UIView {
8+
private lazy var stackView: UIStackView = {
9+
let result = UIStackView()
10+
result.axis = .vertical
11+
result.spacing = 0
12+
result.distribution = .fillEqually
13+
result.translatesAutoresizingMaskIntoConstraints = false
14+
return result
15+
}()
16+
17+
init() {
18+
super.init(frame: .zero)
19+
setupView()
20+
}
21+
22+
required init?(coder: NSCoder) {
23+
fatalError("init(coder:) has not been implemented")
24+
}
25+
26+
private func setupView() {
27+
self.themeBackgroundColor = .contextMenu_background
28+
self.layer.cornerRadius = 8
29+
self.clipsToBounds = true
30+
31+
addSubview(stackView)
32+
33+
NSLayoutConstraint.activate([
34+
stackView.topAnchor.constraint(equalTo: self.topAnchor),
35+
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
36+
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
37+
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
38+
])
39+
}
40+
41+
func createMenuButtons(_ actions: [ContextMenuVC.Action], using dependencies: Dependencies, dismiss: @escaping () -> Void) {
42+
actions.forEach { action in
43+
let item = ContextMenuVC.ActionView(
44+
for: action,
45+
using: dependencies,
46+
dismiss: dismiss
47+
)
48+
stackView.addArrangedSubview(item)
49+
}
50+
51+
let buttonHeight: CGFloat = Values.largeButtonHeight
52+
let menuWidth: CGFloat = Values.menuContainerWidth
53+
let totalHeight = buttonHeight * CGFloat(actions.count)
54+
self.frame = CGRect(origin: .zero, size: CGSize(width: menuWidth, height: totalHeight))
55+
}
56+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
2+
3+
import UIKit
4+
import SessionUtilitiesKit
5+
6+
class ManualDropdownPresenter: NSObject {
7+
private lazy var menuView: CustomMenuView = {
8+
let result = CustomMenuView()
9+
result.layer.shadowOffset = CGSize.zero
10+
result.layer.shadowOpacity = 0.4
11+
result.layer.shadowRadius = 4
12+
return result
13+
}()
14+
15+
private lazy var overlayView: UIView = {
16+
let result = UIView()
17+
result.backgroundColor = .black.withAlphaComponent(0.01)
18+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hide))
19+
result.addGestureRecognizer(tapGesture)
20+
return result
21+
}()
22+
23+
private weak var presentingViewController: UIViewController?
24+
25+
@MainActor
26+
func show(actions: [ContextMenuVC.Action], anchorView: UIView?, using dependencies: Dependencies, completion: @escaping () -> Void) {
27+
guard
28+
let topViewController = dependencies[singleton: .appContext].frontMostViewController,
29+
let barButtonView = anchorView
30+
else {
31+
return
32+
}
33+
34+
menuView.createMenuButtons(
35+
actions,
36+
using: dependencies
37+
) { [weak self] in
38+
self?.menuView.removeFromSuperview()
39+
self?.overlayView.removeFromSuperview()
40+
41+
completion()
42+
}
43+
44+
self.presentingViewController = topViewController
45+
46+
guard let targetView = topViewController.view else {
47+
return
48+
}
49+
50+
overlayView.frame = targetView.bounds
51+
targetView.addSubview(overlayView)
52+
53+
targetView.addSubview(menuView)
54+
55+
let buttonFrame = barButtonView.convert(barButtonView.bounds, to: targetView)
56+
57+
let menuX = buttonFrame.maxX - menuView.frame.width
58+
let menuY = buttonFrame.maxY + 5
59+
60+
menuView.frame.origin = CGPoint(x: menuX, y: menuY)
61+
menuView.alpha = 0
62+
menuView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
63+
64+
UIView.animate(withDuration: 0.2) {
65+
self.menuView.alpha = 1
66+
self.menuView.transform = .identity
67+
}
68+
}
69+
70+
@objc func hide() {
71+
UIView.animate(withDuration: 0.2, animations: {
72+
self.menuView.alpha = 0
73+
self.menuView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
74+
}) { _ in
75+
self.menuView.removeFromSuperview()
76+
self.overlayView.removeFromSuperview()
77+
78+
self.presentingViewController = nil
79+
}
80+
}
81+
}

Session/Conversations/Selection/MessageSelectionManager.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,61 @@ class MessageSelectionManager: NSObject {
2424
}
2525

2626
private func onButtonCreation(icon: UIImage?, accessibilityLabel: String, action: Selector) -> UIBarButtonItem? {
27-
let item = UIBarButtonItem(
27+
let result = UIBarButtonItem(
2828
image: icon,
2929
style: .plain,
3030
target: self,
3131
action: action
3232
)
33-
item.accessibilityLabel = accessibilityLabel
34-
item.isAccessibilityElement = true
35-
return item
33+
result.accessibilityLabel = accessibilityLabel
34+
result.isAccessibilityElement = true
35+
return result
3636
}
3737

3838
@MainActor
3939
func createNavigationActions() -> [UIBarButtonItem] {
4040
// Create all possible buttons, using selectors pointing to the Manager's methods
4141
let replyButtonItem = onButtonCreation(
42-
icon: Lucide.image(icon: .reply, size: 24),
42+
icon: Lucide.image(
43+
icon: .reply,
44+
size: IconSize.medium.size
45+
),
4346
accessibilityLabel: "Reply",
4447
action: #selector(replySelected)
4548
)
4649
let downloadButtonItem = onButtonCreation(
47-
icon: Lucide.image(icon: .download, size: 24),
50+
icon: Lucide.image(
51+
icon: .download,
52+
size: IconSize.medium.size
53+
),
4854
accessibilityLabel: "Download",
4955
action: #selector(saveSelected)
5056
)
5157
let copyButtonItem = onButtonCreation(
52-
icon: Lucide.image(icon: .copy, size: 24),
58+
icon: Lucide.image(
59+
icon: .copy,
60+
size: IconSize.medium.size
61+
),
5362
accessibilityLabel: "Copy",
5463
action: #selector(copySelected)
5564
)
5665
let deleteButtonItem = onButtonCreation(
57-
icon: Lucide.image(icon: .trash2, size: 24),
66+
icon: Lucide.image(
67+
icon: .trash2,
68+
size: IconSize.medium.size
69+
),
5870
accessibilityLabel: "Delete",
5971
action: #selector(deleteSelected)
6072
)
6173
let moreButtonItem = onButtonCreation(
62-
icon: Lucide.image(icon: .ellipsisVertical, size: 24),
74+
icon: Lucide.image(
75+
icon: .ellipsisVertical,
76+
size: IconSize.medium.size
77+
),
6378
accessibilityLabel: "More",
6479
action: #selector(moreOptions)
6580
)
81+
moreButtonItem?.tag = 99
6682

6783
var showDownload: Bool {
6884
guard
@@ -145,11 +161,6 @@ class MessageSelectionManager: NSObject {
145161
guard let selectedMessage = selectedMessages.first else {
146162
return
147163
}
148-
149-
// TODO: Show drop down instead
150164
delegate?.showInfo(for: selectedMessage, withSender: sender)
151-
// delegate?.info(selectedMessage)
152-
//
153-
// delegate?.shouldResetSelectionState()
154165
}
155166
}

SessionUIKit/Style Guide/Values.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public enum Values {
2828
public static let largeButtonHeight = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45)
2929
public static let alertButtonHeight: CGFloat = 51 // 19px tall font with 16px margins
3030

31+
public static let menuContainerWidth: CGFloat = isIPhone5OrSmaller ? CGFloat(180) : CGFloat(210)
32+
3133
public static let accentLineThickness = CGFloat(4)
3234

3335
public static let searchBarHeight = CGFloat(36)

0 commit comments

Comments
 (0)