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>
17 files changed,
+72
−31
README.md+1 −1internal/calls/handlers_join.go+1 −1internal/callstest/meeting_settings_test.go+1 −1native/apple/Package.resolved+17 −0native/apple/Package.swift+6 −1native/apple/Sources/CallsKit/CallsLobby.swift+1 −1native/apple/Sources/CallsKit/CallsModel.swift+1 −1native/apple/Sources/CallsKit/Media.swift+2 −2native/apple/Sources/CallsMac/CallsMac.swift+10 −1native/apple/Tests/CallsKitTests/MeetingLinkTests.swift+5 −5native/apple/Tests/CallsKitTests/StartMeetingTests.swift+1 −5native/ios/project.yml+2 −2native/mac-updates.json+6 −0native/scripts/build-mac.sh+6 −5native/scripts/bundle-mac.py+10 −3native/scripts/generate-iphone-icon.swift+1 −1test/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'), |