oficina / meet Public

Merge main into the start-meeting branch, and follow the scheme rename

Main moved twice since this branch was cut: the deep link scheme became
oficina-calls (f92de9f) and Sparkle came onto the renamed Mac app
(925af0c). Both merged without a textual conflict, and one of them was
still wrong afterwards.

StartMeetingTests asserted that an invitation clears what the last
sign-in decided, and it spelled the invitation oficina-meet:// with a
comment explaining that the scheme was deliberately left alone. That was
true when the test was written and is not true now. CallsModel's guard
demands oficina-calls, so the URL no longer reaches the clearing code at
all — openMeetingURL sets an error and returns, canStart stays up, and
the test fails on its first assertion. Git saw two edits to different
lines of different files and merged them; the test and the guard
disagreed anyway. That is the class of miss the rename merge already
made twice, which is why this was looked for rather than assumed absent.

The comment is gone rather than rewritten. It existed to justify a
deliberate exception; there is no longer an exception to justify, and
MeetingLinkTests covers the scheme itself.

GOTOOLCHAIN=go1.26.5 make ci passes; swift build and swift test pass
(9 tests), with Sparkle linked into CallsMac.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 767a9bf8031a3367b33452b0f5e3d7407ac8da74 parent 69de649parent db5d912
17 files changed, +72 −31
  • README.md +1 −1
  • internal/calls/handlers_join.go +1 −1
  • internal/callstest/meeting_settings_test.go +1 −1
  • native/apple/Package.resolved +17 −0
  • native/apple/Package.swift +6 −1
  • native/apple/Sources/CallsKit/CallsLobby.swift +1 −1
  • native/apple/Sources/CallsKit/CallsModel.swift +1 −1
  • native/apple/Sources/CallsKit/Media.swift +2 −2
  • native/apple/Sources/CallsMac/CallsMac.swift +10 −1
  • native/apple/Tests/CallsKitTests/MeetingLinkTests.swift +5 −5
  • native/apple/Tests/CallsKitTests/StartMeetingTests.swift +1 −5
  • native/ios/project.yml +2 −2
  • native/mac-updates.json +6 −0
  • native/scripts/build-mac.sh +6 −5
  • native/scripts/bundle-mac.py +10 −3
  • native/scripts/generate-iphone-icon.swift +1 −1
  • test/browser/call.mjs +1 −1

CI passed — run details

diff --git a/README.md b/README.md
index 87bd1bb..5bd3d35 100644
--- a/README.md
+++ b/README.md
@@ -190,7 +190,7 @@ at entry; admitted participants keep their participation. Turning the waiting ro
admits queued guests up to the call capacity.
Invitation and browser holding pages offer **Join in the app**, using the
-`oficina-meet://join?url=…` URL scheme registered by the Mac and iOS bundles.
+`oficina-calls://join?url=…` URL scheme registered by the Mac and iOS bundles.
The app opens the meeting link in its lobby for review, then uses the existing
browser authorization flow (including anonymous guest sessions). Browser joining
remains available when the app is not installed.
diff --git a/internal/calls/handlers_join.go b/internal/calls/handlers_join.go
index d6aac7b..22365e2 100644
--- a/internal/calls/handlers_join.go
+++ b/internal/calls/handlers_join.go
@@ -94,5 +94,5 @@ func (a *app) holding(w http.ResponseWriter, r *http.Request) {
// Only app-constructed meeting URLs are promoted to the custom URL scheme.
func nativeJoinURL(meeting string) template.URL {
- return template.URL("oficina-meet://join?url=" + url.QueryEscape(meeting))
+ return template.URL("oficina-calls://join?url=" + url.QueryEscape(meeting))
}
diff --git a/internal/callstest/meeting_settings_test.go b/internal/callstest/meeting_settings_test.go
index 66fe97f..ce44cdc 100644
--- a/internal/callstest/meeting_settings_test.go
+++ b/internal/callstest/meeting_settings_test.go
@@ -53,7 +53,7 @@ func TestAnonymousGuestJoinsWithoutMemberPowers(t *testing.T) {
x, sqid, token := memberWithMeeting(t)
g := x.browser(t)
r := g.get("/j/" + token)
- if r.Code != 200 || !strings.Contains(r.Body.String(), "oficina-meet://join?") {
+ if r.Code != 200 || !strings.Contains(r.Body.String(), "oficina-calls://join?") {
t.Fatal("missing guest/app choice", r.Code)
}
if r := g.post("/j/"+token+"/guest", url.Values{"name": {" "}}); r.Code != 422 {
diff --git a/native/apple/Package.resolved b/native/apple/Package.resolved
index c68b7f5..d797920 100644
--- a/native/apple/Package.resolved
+++ b/native/apple/Package.resolved
@@ -1,5 +1,22 @@
{
"pins" : [
+ {
+ "identity" : "native",
+ "kind" : "remoteSourceControl",
+ "location" : "https://amadan.net/rastrillo/native",
+ "state" : {
+ "revision" : "4c61d6283a188c95647e1d0e609e69d29c2e7fa1"
+ }
+ },
+ {
+ "identity" : "sparkle",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/sparkle-project/Sparkle",
+ "state" : {
+ "revision" : "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a",
+ "version" : "2.9.6"
+ }
+ },
{
"identity" : "webrtc",
"kind" : "remoteSourceControl",
diff --git a/native/apple/Package.swift b/native/apple/Package.swift
index 9f3b436..522f2fd 100644
--- a/native/apple/Package.swift
+++ b/native/apple/Package.swift
@@ -1,5 +1,9 @@
// swift-tools-version: 5.9
import PackageDescription
+import Foundation
+
+let sharedNative: Package.Dependency = ProcessInfo.processInfo.environment["OFICINA_NATIVE_PACKAGE_PATH"].map { .package(path: $0) }
+ ?? .package(url: "https://amadan.net/rastrillo/native", revision: "4c61d6283a188c95647e1d0e609e69d29c2e7fa1")
let package = Package(
name: "CallsNative",
@@ -10,13 +14,14 @@ let package = Package(
.executable(name: "CallsInterop", targets: ["CallsInterop"]),
],
dependencies: [
+ sharedNative,
.package(url: "https://github.com/stasel/WebRTC.git", exact: "152.0.0")
],
targets: [
.binaryTarget(name: "CallsBridge", path: "../build/CallsBridge.xcframework"),
.target(
name: "CallsKit", dependencies: ["CallsBridge", .product(name: "WebRTC", package: "WebRTC")]),
- .executableTarget(name: "CallsMac", dependencies: ["CallsKit"], resources: [.copy("Resources/AppIcon.png"), .copy("Resources/MacIcon.png")]),
+ .executableTarget(name: "CallsMac", dependencies: [.product(name: "RastrilloMacUpdates", package: "native", condition: .when(platforms: [.macOS])), "CallsKit"], resources: [.copy("Resources/AppIcon.png"), .copy("Resources/MacIcon.png")]),
.executableTarget(name: "CallsInterop", dependencies: ["CallsKit"]),
.testTarget(name: "CallsKitTests", dependencies: ["CallsKit"]),
]
diff --git a/native/apple/Sources/CallsKit/CallsLobby.swift b/native/apple/Sources/CallsKit/CallsLobby.swift
index 0448f5d..67d272d 100644
--- a/native/apple/Sources/CallsKit/CallsLobby.swift
+++ b/native/apple/Sources/CallsKit/CallsLobby.swift
@@ -15,7 +15,7 @@ import SwiftUI
.font(.largeTitle).foregroundStyle(.tint)
.padding(18).background(.tint.opacity(0.1), in: RoundedRectangle(cornerRadius: 22))
.accessibilityHidden(true)
- Text("OFICINA MEET").font(.caption.bold()).tracking(2).foregroundStyle(.secondary)
+ Text("OFICINA CALLS").font(.caption.bold()).tracking(2).foregroundStyle(.secondary)
Text(heading).font(.largeTitle.bold()).fixedSize(horizontal: false, vertical: true)
Text(subheading).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
diff --git a/native/apple/Sources/CallsKit/CallsModel.swift b/native/apple/Sources/CallsKit/CallsModel.swift
index 4bf2100..555ba08 100644
--- a/native/apple/Sources/CallsKit/CallsModel.swift
+++ b/native/apple/Sources/CallsKit/CallsModel.swift
@@ -46,7 +46,7 @@ import Foundation
error = "Leave the current meeting before opening another."
return
}
- guard url.scheme == "oficina-meet", url.host == "join",
+ guard url.scheme == "oficina-calls", url.host == "join",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let target = components.queryItems?.first(where: { $0.name == "url" })?.value,
let meeting = URL(string: target), meeting.scheme == "https",
diff --git a/native/apple/Sources/CallsKit/Media.swift b/native/apple/Sources/CallsKit/Media.swift
index 1621062..12224f2 100644
--- a/native/apple/Sources/CallsKit/Media.swift
+++ b/native/apple/Sources/CallsKit/Media.swift
@@ -152,8 +152,8 @@ import WebRTC
with: configuration(ice),
constraints: RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil),
delegate: self)
- if let audio = engine.localAudio { _ = pc?.add(audio, streamIds: ["meet"]) }
- if let video = engine.localVideo { _ = pc?.add(video, streamIds: ["meet"]) }
+ if let audio = engine.localAudio { _ = pc?.add(audio, streamIds: ["calls"]) }
+ if let video = engine.localVideo { _ = pc?.add(video, streamIds: ["calls"]) }
}
func close() {
closed = true
diff --git a/native/apple/Sources/CallsMac/CallsMac.swift b/native/apple/Sources/CallsMac/CallsMac.swift
index 8252709..c059dfa 100644
--- a/native/apple/Sources/CallsMac/CallsMac.swift
+++ b/native/apple/Sources/CallsMac/CallsMac.swift
@@ -1,3 +1,6 @@
+#if os(macOS)
+import RastrilloMacUpdates
+#endif
import AppKit
@preconcurrency import CallsBridge
import CallsKit
@@ -5,6 +8,9 @@ import SwiftUI
import WebRTC
@main struct CallsMacApp: App {
+ #if os(macOS)
+ @State private var updater = NativeUpdater()
+ #endif
@NSApplicationDelegateAdaptor(CallsAppDelegate.self) private var appDelegate
@StateObject private var model = CallsModel()
var body: some Scene {
@@ -13,7 +19,10 @@ import WebRTC
.onOpenURL { model.openMeetingURL($0) }
}
.defaultSize(width: 1000, height: 760)
- .commands { CommandGroup(replacing: .newItem) {} }
+ .commands {
+ CommandGroup(replacing: .newItem) {}
+ NativeUpdateCommands(updater: updater)
+ }
}
}
diff --git a/native/apple/Tests/CallsKitTests/MeetingLinkTests.swift b/native/apple/Tests/CallsKitTests/MeetingLinkTests.swift
index d3d2e07..b4dc91b 100644
--- a/native/apple/Tests/CallsKitTests/MeetingLinkTests.swift
+++ b/native/apple/Tests/CallsKitTests/MeetingLinkTests.swift
@@ -4,16 +4,16 @@ import XCTest
final class MeetingLinkTests: XCTestCase {
@MainActor func testInvitationOpensInLobbyWithoutJoiningOrStartingAuthentication() {
let model = CallsModel()
- model.openMeetingURL(URL(string: "oficina-meet://join?url=https%3A%2F%2Fmeet.example.com%2Fj%2Finvitation")!)
- XCTAssertEqual(model.address, "https://meet.example.com/j/invitation")
+ model.openMeetingURL(URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.example.com%2Fj%2Finvitation")!)
+ XCTAssertEqual(model.address, "https://calls.example.com/j/invitation")
XCTAssertFalse(model.joining)
XCTAssertFalse(model.authenticating)
}
@MainActor func testRejectsUntrustedURLShapesAndDoesNotInterruptCall() {
let model = CallsModel()
- for target in ["file:///j/token", "https://user:secret@meet.example.com/j/token", "https://meet.example.com/settings"] {
- var link = URLComponents(string: "oficina-meet://join")!
+ for target in ["file:///j/token", "https://user:secret@calls.example.com/j/token", "https://calls.example.com/settings"] {
+ var link = URLComponents(string: "oficina-calls://join")!
link.queryItems = [URLQueryItem(name: "url", value: target)]
model.openMeetingURL(link.url!)
XCTAssertEqual(model.address, "")
@@ -21,7 +21,7 @@ final class MeetingLinkTests: XCTestCase {
}
model.joining = true
model.address = "current"
- model.openMeetingURL(URL(string: "oficina-meet://join?url=https%3A%2F%2Fmeet.example.com%2Fm%2Froom")!)
+ model.openMeetingURL(URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.example.com%2Fm%2Froom")!)
XCTAssertEqual(model.address, "current")
XCTAssertTrue(model.joining)
}
diff --git a/native/apple/Tests/CallsKitTests/StartMeetingTests.swift b/native/apple/Tests/CallsKitTests/StartMeetingTests.swift
index eef7c51..f460fc9 100644
--- a/native/apple/Tests/CallsKitTests/StartMeetingTests.swift
+++ b/native/apple/Tests/CallsKitTests/StartMeetingTests.swift
@@ -50,12 +50,8 @@ final class StartMeetingTests: XCTestCase {
model.signedIn = true
model.canStart = true
model.room = "abc123"
- // oficina-meet, not oficina-calls: the custom scheme was deliberately
- // not renamed, for the same reason the /m/ and /j/ routes were not —
- // it is a registered handler, and links already sent and apps already
- // installed are matched against it.
model.openMeetingURL(
- URL(string: "oficina-meet://join?url=https%3A%2F%2Fcalls.example.com%2Fj%2Finvitation")!)
+ URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.example.com%2Fj%2Finvitation")!)
XCTAssertFalse(model.canStart, "a link is a sign-in to one meeting, not to the instance")
XCTAssertEqual(model.room, "")
XCTAssertFalse(model.signedIn)
diff --git a/native/ios/project.yml b/native/ios/project.yml
index 35e85b9..185319d 100644
--- a/native/ios/project.yml
+++ b/native/ios/project.yml
@@ -25,8 +25,8 @@ targets:
path: ../build/ios-info.plist
properties:
CFBundleURLTypes:
- - CFBundleURLName: net.amadan.oficina.meet.join
- CFBundleURLSchemes: [oficina-meet]
+ - CFBundleURLName: net.amadan.oficina.calls.join
+ CFBundleURLSchemes: [oficina-calls]
dependencies:
- package: CallsNative
product: CallsKit
diff --git a/native/mac-updates.json b/native/mac-updates.json
new file mode 100644
index 0000000..bad934c
--- /dev/null
+++ b/native/mac-updates.json
@@ -0,0 +1,6 @@
+{
+ "bundle_id": "net.amadan.oficina.calls",
+ "feed_url": "https://raw.amadan.net/oficina/native-releases/main/calls/appcast.xml",
+ "public_ed_key": "/FxO7cZnM7P5c9ZrUPUJvpCnR65sNmrzte+I5vHarE8=",
+ "keychain_account": "oficina"
+}
diff --git a/native/scripts/build-mac.sh b/native/scripts/build-mac.sh
index 0e9ad65..12a3634 100755
--- a/native/scripts/build-mac.sh
+++ b/native/scripts/build-mac.sh
@@ -1,9 +1,10 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
-./scripts/build-bridge.sh
-swift scripts/generate-mac-icon.swift
-swift build --package-path apple
-python3 scripts/bundle-mac.py
-codesign --force --deep --sign - build/Calls.app
+if [ "${OFICINA_SKIP_PREPARE:-0}" != 1 ]; then ./scripts/build-bridge.sh; fi
+configuration="${OFICINA_BUILD_CONFIGURATION:-debug}"
+swift build --package-path apple --configuration "$configuration" --product CallsMac
+python3 scripts/bundle-mac.py "$configuration"
+shared_native="${OFICINA_NATIVE_PACKAGE_PATH:-apple/.build/checkouts/native}"
+python3 "$shared_native/tools/mac_updates.py" build/Calls.app --package apple "$@"
printf 'Built %s/build/Calls.app\n' "$PWD"
diff --git a/native/scripts/bundle-mac.py b/native/scripts/bundle-mac.py
index f034c71..94c30b7 100644
--- a/native/scripts/bundle-mac.py
+++ b/native/scripts/bundle-mac.py
@@ -3,13 +3,18 @@ from pathlib import Path
import plistlib
import shutil
import subprocess
+import sys
+configuration = sys.argv[1] if len(sys.argv) > 1 else 'debug'
+if configuration not in ('debug', 'release'): raise ValueError('invalid build configuration')
root = Path(__file__).resolve().parent.parent
+products = Path(subprocess.check_output(['swift', 'build', '--package-path', str(root / 'apple'), '--configuration', configuration, '--show-bin-path'], text=True).strip())
bundle = root / 'build/Calls.app/Contents'
+if bundle.parent.exists(): shutil.rmtree(bundle.parent)
(bundle / 'MacOS').mkdir(parents=True, exist_ok=True)
(bundle / 'Frameworks').mkdir(parents=True, exist_ok=True)
(bundle / 'Resources').mkdir(parents=True, exist_ok=True)
-shutil.copy2(root / 'apple/.build/debug/CallsMac', bundle / 'MacOS/CallsMac')
+shutil.copy2(products / 'CallsMac', bundle / 'MacOS/CallsMac')
subprocess.run(['swift', str(root / 'scripts/generate-mac-icon.swift')], check=True)
icon = root / 'apple/Sources/CallsMac/Resources/MacIcon.png'
shutil.copy2(icon, bundle / 'Resources/MacIcon.png')
@@ -27,8 +32,8 @@ for size in (16, 32, 128, 256, 512):
subprocess.run(['iconutil', '-c', 'icns', str(iconset), '-o',
str(bundle / 'Resources/CallsMac.icns')], check=True)
info = {
- 'CFBundleURLTypes': [{'CFBundleURLName': 'net.amadan.oficina.meet.join',
- 'CFBundleURLSchemes': ['oficina-meet']}],
+ 'CFBundleURLTypes': [{'CFBundleURLName': 'net.amadan.oficina.calls.join',
+ 'CFBundleURLSchemes': ['oficina-calls']}],
'CFBundleName': 'Calls', 'CFBundleDisplayName': 'Calls',
'CFBundleIdentifier': 'net.amadan.oficina.calls',
'CFBundleExecutable': 'CallsMac', 'CFBundlePackageType': 'APPL',
@@ -53,4 +58,6 @@ for xc in [root / 'build/CallsBridge.xcframework', *list((root / 'apple/.build/a
dest = bundle / 'Frameworks' / source.name
if dest.exists(): shutil.rmtree(dest)
shutil.copytree(source, dest, symlinks=True)
+for resource in products.glob('*.bundle'):
+ shutil.copytree(resource, bundle / 'Resources' / resource.name, symlinks=True)
subprocess.run(['install_name_tool', '-add_rpath', '@executable_path/../Frameworks', str(bundle / 'MacOS/CallsMac')], check=True)
diff --git a/native/scripts/generate-iphone-icon.swift b/native/scripts/generate-iphone-icon.swift
index 62a8bcf..e647b87 100644
--- a/native/scripts/generate-iphone-icon.swift
+++ b/native/scripts/generate-iphone-icon.swift
@@ -1,7 +1,7 @@
import Foundation
// Export the approved Oficina artwork already held by the asset catalog.
-// Regenerate it from oficina/estilo with brand/distribute.py meet <checkout>.
+// Regenerate it from oficina/estilo with brand/distribute.py calls <checkout>.
let script = URL(fileURLWithPath: #filePath).standardizedFileURL
let native = script.deletingLastPathComponent().deletingLastPathComponent()
let source = native.appendingPathComponent("ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png")
diff --git a/test/browser/call.mjs b/test/browser/call.mjs
index 08dab25..cf2cffe 100644
--- a/test/browser/call.mjs
+++ b/test/browser/call.mjs
@@ -1150,7 +1150,7 @@ const run = {
const guest = await newContext('anonymous guest');
const page = await guest.newPage();
await page.goto(origin + '/j/' + token);
- check((await page.getByRole('link', { name: 'Join in the app' }).getAttribute('href')).startsWith('oficina-meet://join?'), 'invitation offers the native app');
+ check((await page.getByRole('link', { name: 'Join in the app' }).getAttribute('href')).startsWith('oficina-calls://join?'), 'invitation offers the native app');
await page.getByLabel('Your name').fill('Visitor');
await Promise.all([
page.waitForURL('**/m/' + sqid + '/join'),