From 51723fdf244190100830990f65e44fb86eb7bfdd Mon Sep 17 00:00:00 2001 From: matcaampos Date: Thu, 5 Mar 2026 16:13:35 -0300 Subject: [PATCH 1/3] Add 'Join Current Meeting' keyboard shortcut Add support for joining an in-progress meeting directly. Introduces Array.currentEvent(linkRequired:) to find the currently running event while respecting visibility/dismissal rules, a new KeyboardShortcuts.Name.joinCurrentEventShortcut, and UI/behavior changes: MenuBuilder now selects joinCurrent vs joinNext action/shortcut when the next event has already started; StatusBarItemController registers the new shortcut and implements joinCurrentMeeting() (opens current event or posts a notification if none); Preferences General tab gains recorder entries for the new shortcut. --- MeetingBar/Core/Models/MBEvent+Helpers.swift | 39 +++++++++++++++++++ .../Extensions/KeyboardShortcutsNames.swift | 1 + MeetingBar/UI/StatusBar/MenuBuilder.swift | 5 ++- .../StatusBar/StatusBarItemController.swift | 13 +++++++ .../UI/Views/Preferences/GeneralTab.swift | 8 ++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/MeetingBar/Core/Models/MBEvent+Helpers.swift b/MeetingBar/Core/Models/MBEvent+Helpers.swift index 7c647ac7..f9b13c0e 100644 --- a/MeetingBar/Core/Models/MBEvent+Helpers.swift +++ b/MeetingBar/Core/Models/MBEvent+Helpers.swift @@ -172,4 +172,43 @@ public extension Array where Element == MBEvent { } return nextEvent } + + /// Returns the event that is currently in progress. + /// Unlike `nextEvent()`, this does not depend on ongoing visibility settings. + func currentEvent(linkRequired: Bool = false) -> MBEvent? { + let now = Date() + + for event in self { + guard event.startDate <= now, event.endDate > now else { + continue + } + if Defaults[.dismissedEvents].contains(where: { $0.id == event.id }) { + continue + } + if event.isAllDay { + continue + } + if event.meetingLink == nil, linkRequired { + continue + } + if event.participationStatus == .declined { + continue + } + if event.participationStatus == .pending, + Defaults[.showPendingEvents] == .hide || Defaults[.showPendingEvents] == .show_inactive { + continue + } + if event.participationStatus == .tentative, + Defaults[.showTentativeEvents] == .hide || Defaults[.showTentativeEvents] == .show_inactive { + continue + } + if event.status == .canceled { + continue + } + + return event + } + + return nil + } } diff --git a/MeetingBar/Extensions/KeyboardShortcutsNames.swift b/MeetingBar/Extensions/KeyboardShortcutsNames.swift index ad166298..ea5f3a06 100644 --- a/MeetingBar/Extensions/KeyboardShortcutsNames.swift +++ b/MeetingBar/Extensions/KeyboardShortcutsNames.swift @@ -14,6 +14,7 @@ extension KeyboardShortcuts.Name { static let createMeetingShortcut = Self("createMeetingShortcut") static let openMenuShortcut = Self("openMenuShortcut") static let joinEventShortcut = Self("joinEventShortcut") + static let joinCurrentEventShortcut = Self("joinCurrentEventShortcut") static let openClipboardShortcut = Self("openClipboardShortcut") static let toggleMeetingTitleVisibilityShortcut = Self("toggleMeetingTitleVisibilityShortcut") } diff --git a/MeetingBar/UI/StatusBar/MenuBuilder.swift b/MeetingBar/UI/StatusBar/MenuBuilder.swift index e42ee158..a6884d5a 100644 --- a/MeetingBar/UI/StatusBar/MenuBuilder.swift +++ b/MeetingBar/UI/StatusBar/MenuBuilder.swift @@ -77,10 +77,13 @@ struct MenuBuilder { let joinItem = NSMenuItem( title: itemTitle, - action: #selector(StatusBarItemController.joinNextMeeting), + action: nextEvent.startDate < now + ? #selector(StatusBarItemController.joinCurrentMeeting) + : #selector(StatusBarItemController.joinNextMeeting), keyEquivalent: "" ) joinItem.target = target + joinItem.setShortcut(for: nextEvent.startDate < now ? .joinCurrentEventShortcut : .joinEventShortcut) items.append(joinItem) } diff --git a/MeetingBar/UI/StatusBar/StatusBarItemController.swift b/MeetingBar/UI/StatusBar/StatusBarItemController.swift index f663446a..a3c294ce 100644 --- a/MeetingBar/UI/StatusBar/StatusBarItemController.swift +++ b/MeetingBar/UI/StatusBar/StatusBarItemController.swift @@ -127,6 +127,10 @@ final class StatusBarItemController { private func setupKeyboardShortcuts() { KeyboardShortcuts.onKeyUp(for: .createMeetingShortcut, action: createMeeting) + KeyboardShortcuts.onKeyUp(for: .joinCurrentEventShortcut) { + Task { @MainActor in self.joinCurrentMeeting() } + } + KeyboardShortcuts.onKeyUp(for: .joinEventShortcut) { Task { @MainActor in self.joinNextMeeting() } } @@ -400,6 +404,15 @@ final class StatusBarItemController { createMeeting() } + @objc + func joinCurrentMeeting() { + if let currentEvent = events.currentEvent() { + currentEvent.openMeeting() + } else { + sendNotification("status_bar_section_join_current_meeting".loco(), "next_meeting_empty_message".loco()) + } + } + @objc func joinNextMeeting() { if let nextEvent = events.nextEvent() { diff --git a/MeetingBar/UI/Views/Preferences/GeneralTab.swift b/MeetingBar/UI/Views/Preferences/GeneralTab.swift index caa797bc..95439e21 100644 --- a/MeetingBar/UI/Views/Preferences/GeneralTab.swift +++ b/MeetingBar/UI/Views/Preferences/GeneralTab.swift @@ -43,6 +43,9 @@ struct ShortcutsSection: View { Text("preferences_general_shortcut_create_meeting".loco()) KeyboardShortcuts.Recorder(for: .createMeetingShortcut) + Text("status_bar_section_join_current_meeting".loco() + ":") + KeyboardShortcuts.Recorder(for: .joinCurrentEventShortcut) + Text("preferences_general_shortcut_join_next".loco()) KeyboardShortcuts.Recorder(for: .joinEventShortcut) @@ -74,6 +77,11 @@ struct ShortcutsModal: View { Spacer() KeyboardShortcuts.Recorder(for: .createMeetingShortcut) } + HStack { + Text("status_bar_section_join_current_meeting".loco() + ":") + Spacer() + KeyboardShortcuts.Recorder(for: .joinCurrentEventShortcut) + } HStack { Text("preferences_general_shortcut_join_next".loco()) Spacer() From dcccc4928bfd747bcfe4a8a5d424d8a3d8f4c9ef Mon Sep 17 00:00:00 2001 From: matcaampos Date: Thu, 5 Mar 2026 17:11:32 -0300 Subject: [PATCH 2/3] Add docs and link filter to currentEvent Update MBEvent+Helpers to document currentEvent and add a linkRequired parameter (default false) so callers can opt to consider only events with meeting links. Add descriptive comments for global keyboard shortcut names in KeyboardShortcutsNames. Also add documentation for joinCurrentMeeting in StatusBarItemController to clarify behavior when no active event is present. Changes are focused on widening API clarity and improving inline documentation across these components. --- MeetingBar/Core/Models/MBEvent+Helpers.swift | 3 +++ MeetingBar/Extensions/KeyboardShortcutsNames.swift | 6 ++++++ MeetingBar/UI/StatusBar/StatusBarItemController.swift | 3 +++ 3 files changed, 12 insertions(+) diff --git a/MeetingBar/Core/Models/MBEvent+Helpers.swift b/MeetingBar/Core/Models/MBEvent+Helpers.swift index f9b13c0e..456eeccd 100644 --- a/MeetingBar/Core/Models/MBEvent+Helpers.swift +++ b/MeetingBar/Core/Models/MBEvent+Helpers.swift @@ -174,7 +174,10 @@ public extension Array where Element == MBEvent { } /// Returns the event that is currently in progress. + /// /// Unlike `nextEvent()`, this does not depend on ongoing visibility settings. + /// - Parameter linkRequired: If `true`, only events with a meeting link are considered. + /// - Returns: The currently running event that passes filters, or `nil`. func currentEvent(linkRequired: Bool = false) -> MBEvent? { let now = Date() diff --git a/MeetingBar/Extensions/KeyboardShortcutsNames.swift b/MeetingBar/Extensions/KeyboardShortcutsNames.swift index ea5f3a06..16ba1f7e 100644 --- a/MeetingBar/Extensions/KeyboardShortcutsNames.swift +++ b/MeetingBar/Extensions/KeyboardShortcutsNames.swift @@ -11,10 +11,16 @@ import KeyboardShortcuts extension KeyboardShortcuts.Name: @unchecked @retroactive Sendable {} extension KeyboardShortcuts.Name { + /// Global shortcut used to create an ad-hoc meeting. static let createMeetingShortcut = Self("createMeetingShortcut") + /// Global shortcut used to open the status bar menu. static let openMenuShortcut = Self("openMenuShortcut") + /// Global shortcut used to join the nearest meeting (current or next). static let joinEventShortcut = Self("joinEventShortcut") + /// Global shortcut used to join only the currently running meeting. static let joinCurrentEventShortcut = Self("joinCurrentEventShortcut") + /// Global shortcut used to open a meeting link from clipboard. static let openClipboardShortcut = Self("openClipboardShortcut") + /// Global shortcut used to toggle status bar meeting title visibility. static let toggleMeetingTitleVisibilityShortcut = Self("toggleMeetingTitleVisibilityShortcut") } diff --git a/MeetingBar/UI/StatusBar/StatusBarItemController.swift b/MeetingBar/UI/StatusBar/StatusBarItemController.swift index a3c294ce..072308e5 100644 --- a/MeetingBar/UI/StatusBar/StatusBarItemController.swift +++ b/MeetingBar/UI/StatusBar/StatusBarItemController.swift @@ -405,6 +405,9 @@ final class StatusBarItemController { } @objc + /// Joins the meeting link for the event currently in progress. + /// + /// If there is no active event, a user-facing notification is shown. func joinCurrentMeeting() { if let currentEvent = events.currentEvent() { currentEvent.openMeeting() From 80c5a5814dad63abaf9a632716453dbc265a4248 Mon Sep 17 00:00:00 2001 From: matcaampos Date: Thu, 5 Mar 2026 17:17:15 -0300 Subject: [PATCH 3/3] Add tests for currentEvent and join actions Add unit tests covering currentEvent() behavior and menu join actions. NextEventTests: verify currentEvent() returns a running event and that the linkRequired parameter filters out events without links. MenuBuilderTests: assert the current meeting menu item uses the joinCurrentMeeting selector and add a test ensuring a future event produces the "status_bar_section_join_next_meeting" title and uses the joinNextMeeting selector. --- MeetingBarTests/NextEventTests.swift | 28 +++++++++++++++++++ .../StatusBarItem/MenuBuilderTests.swift | 14 ++++++++++ 2 files changed, 42 insertions(+) diff --git a/MeetingBarTests/NextEventTests.swift b/MeetingBarTests/NextEventTests.swift index fb9c1f50..99e54b58 100644 --- a/MeetingBarTests/NextEventTests.swift +++ b/MeetingBarTests/NextEventTests.swift @@ -131,4 +131,32 @@ class NextEventTests: BaseTestCase { let array = [future, running] XCTAssertEqual(array.nextEvent(), running) } + + func test_currentEvent_returnsRunningEvent() { + let running = makeFakeEvent( + id: "RUN", + start: now.addingTimeInterval(-120), + end: now.addingTimeInterval(600), + withLink: true + ) + let future = makeFakeEvent( + id: "FUT", + start: now.addingTimeInterval(120), + end: now.addingTimeInterval(600), + withLink: true + ) + + XCTAssertEqual([future, running].currentEvent(), running) + } + + func test_currentEvent_respectsLinkRequirement() { + let runningWithoutLink = makeFakeEvent( + id: "NO-LINK", + start: now.addingTimeInterval(-120), + end: now.addingTimeInterval(600), + withLink: false + ) + + XCTAssertNil([runningWithoutLink].currentEvent(linkRequired: true)) + } } diff --git a/MeetingBarTests/StatusBarItem/MenuBuilderTests.swift b/MeetingBarTests/StatusBarItem/MenuBuilderTests.swift index 5b1cc1de..9a3d1078 100644 --- a/MeetingBarTests/StatusBarItem/MenuBuilderTests.swift +++ b/MeetingBarTests/StatusBarItem/MenuBuilderTests.swift @@ -52,9 +52,23 @@ final class MenuBuilderTests: BaseTestCase { XCTAssertEqual(MenuBuilder.plainTitles(of: items)[0], "status_bar_section_join_current_meeting".loco()) + XCTAssertEqual(items[0].action, #selector(StatusBarItemController.joinCurrentMeeting)) XCTAssertTrue(items.contains { $0.action == #selector(StatusBarItemController.createMeetingAction) }) } + func test_joinSectionFutureEventUsesJoinNextAction() { + let future = makeFakeEvent( + id: "F", + start: Date().addingTimeInterval(300), + end: Date().addingTimeInterval(1_200) + ) + let items = MenuBuilder(target: Dummy()) + .buildJoinSection(nextEvent: future) + + XCTAssertEqual(items[0].title, "status_bar_section_join_next_meeting".loco()) + XCTAssertEqual(items[0].action, #selector(StatusBarItemController.joinNextMeeting)) + } + func test_joinSectionWithoutEvent() { let items = MenuBuilder(target: Dummy()) .buildJoinSection(nextEvent: nil)