Conference links
Joining the next call is one of the few things anyone opens a calendar client to do, and Google
Calendar's web interface makes it slow: open the event, find the link in the description, click it,
then answer the browser's prompt about the desktop app. Magical resolves the link once, when the
event is decoded, and every surface that shows a meeting can then offer a single control. The wire
fields the link comes out of are in calendar-wire-format.md; the banner
is owned by notifications.md, the join action by actions.md, and
the tray controls by menu-bar.md.
What counts as a joinable link
resolveConferenceUrl(tuple) runs inside parseEventTuple, so an event carries its conferenceUrl
by the time it leaves the poller. Nothing parses a description at render time, and no surface has to
know where a link can hide.
It takes the first of three sources that yields a URL:
- The structured conference block. Entry points are
[typeCode, uri, …]; onlytypeCode3is the video entry, and4and5are dial-in numbers. - The bare Meet URL field, which is Google Meet only.
- The free text, description first and then location, scanned by
findJoinUrlInText.
Matched by path, never by host
JOIN_URL matches four join paths rather than four domains:
teams.microsoft.com/l/meetup-join/ | zoom.us/(j|w)/ | webex.com/(meet/|…j.php) | meet.google.com/
The host alone is not enough to tell a join link from its neighbours. A Teams invitation body also
carries teams.microsoft.com/meetingOptions/ and a dialin.teams.microsoft.com number, and
launching either instead of the meeting is worse than offering no button at all.
The match also stops at the first &, because the character class excludes it along with whitespace
and quotes. A Zoom link keeps a leading ?pwd=… and loses anything after it, which is what
From web URL to desktop client reads the password from.
Unwrapping what the invitation did to the link
findJoinUrlInText decodes HTML entities before it scans, then passes each candidate through
unwrapRedirectors. A raw scan misses links that only exist in encoded or wrapped form, which is
most of them by the time an invitation has crossed two mail systems.
unwrapRedirectors follows up to three hops and understands two wrappers: www.google.com/url?q=
around an outbound link, and *.safelinks.protection.outlook.com/?url= around anything that reached
the invitation through Exchange. A string that does not parse as a URL is returned unchanged rather
than thrown away.
From web URL to desktop client
toNativeMeetingUrl(url) in calendar-service.ts is the only place a scheme is rewritten.
| Provider | Matched | What is launched |
|---|---|---|
| Zoom | zoom.us/j/<id> or zoom.us/w/<id> |
zoommtg://zoom.us/join?action=join&confno=<id>, plus &pwd= when the link carried one |
| Microsoft Teams | teams.microsoft.com or teams.live.com |
The same URL with https:// replaced by msteams:// |
| Google Meet | None | Unchanged. Meet has no desktop app; its web client is the real one |
| Webex | None | Unchanged. Recognised as joinable, opened in the browser |
Asking macOS whether the client is installed
hasApplicationForUrl in native-handlers.ts puts a rewritten URL to
NSWorkspace.URLForApplicationToOpenURL: before it is launched, and joinCall opens the web URL when
nothing answers. Invitations arrive from people on platforms the user has never installed, so a Teams
link on a machine with no Teams client is the ordinary case rather than the edge; it opens a browser
tab rather than the system's "no application is set to open the URL" dialog and no meeting. Only a
rewritten URL is asked about: a Meet or Webex link comes back from toNativeMeetingUrl unchanged,
and there is nothing to fall back from.
The question goes through the N-API addon, magical_addon.node, because Electron cannot reach
LaunchServices; it is the same bridge that draws the dock tile
(dock-icon.md's The drawing engine). One answer is kept per
scheme for as long as the app runs, so a client installed halfway through a session is not used until
the next launch. The answer is no when the addon cannot be asked at all, so a build with no addon
joins in the browser.
Opening in the browser instead
alwaysJoinInBrowser is off by default. Its control is Always open calls in the browser under
Joining calls in the settings window; the helper writes the setting back over JSON-RPC and
CalendarService.joinCall re-reads it with loadSettings() on every call, so a change applies to
the next join without a restart.
Every surface reaches the same launcher, so the preference cannot end up honoured in one place and ignored in another:
flowchart LR
Banner["Notification banner<br/>Join Call"] --> Action["CalendarService.onAlertAction"]
Tray["Popover, agenda card,<br/>status item"] --> RPC["tray.joinCall"]
RPC --> Handler["MenuBarManager"]
Action --> Join["CalendarService.joinCall"]
Handler --> Join
Join --> Pref{"alwaysJoinInBrowser"}
Pref -->|on| Web["shell.openExternal(webUrl)"]
Pref -->|off| Installed{"client installed?"}
Installed -->|no| Web
Installed -->|yes| Native["shell.openExternal(nativeUrl)"]
The tray's handler rewrites nothing itself; it calls this.getCalendarService()?.joinCall(url), so a
call joined from the popover, from an agenda card or from the status item honours the preference
exactly as a banner does.
The diagram leaves out one edge: a client macOS named can still refuse to launch, because a
registration outlives the app it points at. shell.openExternal rejects when it does, and the web URL
is opened instead. joinCall never rejects, whichever way it ends; both callers discard the promise,
so a throw would reach the log as an unhandled rejection and the user would see a Join button that
does nothing.
Where the join controls are
Every control below appears only when the event has a conferenceUrl. None has a disabled state; a
meeting with no call shows nothing, because a dead Join button is worse than none.
| Surface | Control | Path |
|---|---|---|
| Notification banner | Join Call, on the magical.meeting.call category |
alert.action → joinCall |
| Popover hero band | Join, carrying video.fill, beside the meeting title |
tray.joinCall |
| Agenda card | Join, on hover, at the end of the metadata line | tray.joinCall |
| Agenda card, right-click | Join Meeting, under Open in Magical | tray.joinCall |
| Status item, right-click | Bold Join "…", the first item, naming the meeting | tray.joinCall |
The banner's actions are in
../shared/notifications.md's The category and action contract,
the hero band in menu-bar.md's Hero band, the agenda card and its menu in
menu-bar.md's Agenda, and the status item's menu in
actions.md's Video calls.
Naming the service
Three providers are named: Google Meet, Zoom and Microsoft Teams. Everything else is a
Video call. Those three are the ones anyone recognises on sight, and the same three
toNativeMeetingUrl can open in their own client. The link is still found and still opens when
unnamed: JOIN_URL matches Webex, it is not named.
Two functions do this, one per process (conferenceLabel in notification-manager.ts for the
banner, TrayEvent.conferenceServiceName in TrayModel.swift for the band and the agenda row), and
they have to word it identically. One meeting can reach the user twice, once in a banner and once in
the popover, and reading two ways looks like two meetings.
What never reaches the log
A meeting link is a credential to anyone holding it, and the log is written to disk. The tray handler
logs the scheme alone, target.split(':')[0], and the banner path logs only whether the event could
offer a Join button at all. A password lifted out of a pwd= parameter lives in the URL string
passed to shell.openExternal and nowhere else. The line recording a fallback to the browser names
the native scheme for the same reason, never the meeting.