init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "jitsi@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,81 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ClockKit
class ComplicationController: NSObject, CLKComplicationDataSource {
// MARK: - Timeline Configuration
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
handler([])
}
func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
handler(.showOnLockScreen)
}
// MARK: - Timeline Population
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
// Call the handler with the current timeline entry
getLocalizableSampleTemplate(for: complication) {template in
guard let template = template else {
handler(nil)
return
}
handler(CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template))
}
}
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries prior to the given date
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries after to the given date
handler(nil)
}
// MARK: - Placeholder Templates
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
// This method will be called once per supported complication, and the results will be cached
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "jitsi")!)
if complication.family == .circularSmall {
let small = CLKComplicationTemplateCircularSmallRingImage()
small.imageProvider = imageProvider
small.ringStyle = .closed
small.fillFraction = 0
handler(small)
} else if complication.family == .utilitarianSmall {
let utilitarian = CLKComplicationTemplateUtilitarianSmallSquare()
utilitarian.imageProvider = imageProvider
handler(utilitarian)
} else if complication.family == .modularSmall {
let modular = CLKComplicationTemplateModularSmallRingImage()
modular.imageProvider = imageProvider
modular.ringStyle = .closed
modular.fillFraction = 0
handler(modular)
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import WatchConnectivity
import WatchKit
class ExtensionDelegate: NSObject, WCSessionDelegate, WKExtensionDelegate {
var currentContext : JitsiMeetContext = JitsiMeetContext()
static var currentJitsiMeetContext: JitsiMeetContext {
get {
return (WKExtension.shared().delegate as! ExtensionDelegate).currentContext
}
}
func applicationDidFinishLaunching() {
// Start Watch Connectivity
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once youre done.
backgroundTask.setTaskCompletedWithSnapshot(false)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
func session(_ session: WCSession, activationDidCompleteWith
activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WATCH Session activation failed with error: \(error.localizedDescription)")
return
}
print("WATCH Session activated with state: \(activationState.rawValue)")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
DispatchQueue.main.async {
let newContext = JitsiMeetContext(context: applicationContext)
print("WATCH got new context: \(newContext.description)");
// Update context on the root controller which displays the recent list
let controller = WKExtension.shared().rootInterfaceController as! InterfaceController
controller.updateUI(newContext)
// If the current controller is not the in-call controller and we have a
// conference URL, show the in-call controller
if let currentController = WKExtension.shared().visibleInterfaceController as? InterfaceController {
// Go to the in-call controller only if the conference URL has changed, because the user may have
// clicked the back button
if newContext.conferenceURL != nil
&& self.currentContext.conferenceURL != newContext.conferenceURL {
currentController.pushController(withName: "InCallController", context: newContext)
}
} else if let inCallController = WKExtension.shared().visibleInterfaceController as? InCallController {
if newContext.conferenceURL == nil {
inCallController.popToRootController()
} else {
inCallController.updateUI(newContext)
}
}
self.currentContext = newContext;
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import WatchConnectivity
import WatchKit
import Foundation
class InCallController: WKInterfaceController {
@IBOutlet var mutedButton: WKInterfaceButton!
@IBOutlet var roomLabel: WKInterfaceLabel!
@IBOutlet var timer: WKInterfaceTimer!
@IBAction func hangupClicked() {
sendCommand(JitsiMeetCommands.CMD_HANG_UP, message: nil)
}
@IBAction func muteClicked() {
if var micMuted = ExtensionDelegate.currentJitsiMeetContext.micMuted {
micMuted = !micMuted;
sendCommand(
JitsiMeetCommands.CMD_SET_MUTED,
message: [
"muted": micMuted ? "true" : "false"
])
updateMutedButton(withMuted: micMuted)
}
}
func sendCommand(_ command: JitsiMeetCommands, message: [String : Any]?) {
if WCSession.isSupported() {
let session = WCSession.default
var data = [String: Any]()
if let sessionID = ExtensionDelegate.currentJitsiMeetContext.sessionID {
if message != nil {
message!.forEach { data[$0] = $1 }
}
data["command"] = command.rawValue;
data["sessionID"] = sessionID;
session.sendMessage(data, replyHandler: nil, errorHandler: nil)
}
}
}
func updateUI(_ newContext: JitsiMeetContext) {
var conferenceURL = newContext.conferenceURL
if let joinConferenceURL = newContext.joinConferenceURL {
sendCommand(JitsiMeetCommands.CMD_JOIN_CONFERENCE, message: [ "data" : joinConferenceURL ])
conferenceURL = joinConferenceURL
}
let newRoomName = conferenceURL != nil ? conferenceURL!.components(separatedBy: "/").last : ""
roomLabel.setText(newRoomName)
if let newTimestamp = newContext.conferenceTimestamp {
restartTimer(newTimestamp)
}
if let newMuted = newContext.micMuted {
updateMutedButton(withMuted: newMuted)
}
}
func restartTimer(_ conferenceTimestamp: Int64) {
if (conferenceTimestamp != 0) {
let newDate = Date(timeIntervalSince1970: TimeInterval(conferenceTimestamp / 1000))
timer.setDate(newDate)
timer.start();
print("WATCH timer set date to: \(newDate) and start")
} else {
print("WATCH timer stop")
timer.stop();
}
}
func updateMutedButton(withMuted isMuted: Bool) {
if isMuted {
mutedButton.setBackgroundImageNamed("mute-on.png")
} else {
mutedButton.setBackgroundImageNamed("mute-off.png")
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let data = context as? JitsiMeetContext {
updateUI(data)
}
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Jitsi Meet Companion Extension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>CLKComplicationSupportedFamilies</key>
<array>
<string>CLKComplicationFamilyModularSmall</string>
<string>CLKComplicationFamilyUtilitarianSmall</string>
<string>CLKComplicationFamilyCircularSmall</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>org.jitsi.meet.watchkit</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
</dict>
</plist>

View File

@@ -0,0 +1,94 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import WatchKit
import WatchConnectivity
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet var infoLabel: WKInterfaceLabel!
@IBOutlet var table: WKInterfaceTable!
override func didAppear(){
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
}
func updateUI(_ newContext:JitsiMeetContext) {
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
table.setHidden(true)
infoLabel.setHidden(false)
} else {
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
table.setHidden(false)
infoLabel.setHidden(true)
}
}
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
// Updating the # of rows only if it actually changed prevents from blinking the UI
if (table.numberOfRows != recents.count) {
table.setNumberOfRows(recents.count, withRowType: "MeetingRowType")
}
for (index, entry) in recents.enumerated() {
let entryDict = entry as! NSDictionary
let roomURL = entryDict["conference"] as! NSString
let timestamp = entryDict["date"] as! NSNumber
// Prepare values
let room = roomURL.components(separatedBy: "/").last
let date = Date(timeIntervalSince1970: timestamp.doubleValue / 1000) // timestamp is taken with Date.now() in JS, which uses milliseconds
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.current
dateFormatter.locale = NSLocale.current
dateFormatter.dateFormat = "HH:mm yyyy-MM-dd"
let strDate = dateFormatter.string(from: date)
// Update row controller
let controller = table.rowController(at: index) as! MeetingRowController
controller.room = room
controller.roomUrl = roomURL as String
controller.roomLabel.setText(room)
controller.timeLabel.setText(strDate)
// Change the background for the active meeting
if (controller.roomUrl == currentContext.conferenceURL) {
controller.rowGroup.setBackgroundColor(UIColor(red: 0.125, green: 0.58, blue: 0.98, alpha: 1))
} else {
controller.rowGroup.setBackgroundColor(UIColor(red: 0.949, green: 0.956, blue: 1, alpha: 0.14))
}
}
}
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
let controller = table.rowController(at: rowIndex) as! MeetingRowController
let currentContext = ExtensionDelegate.currentJitsiMeetContext
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
let actionContext = JitsiMeetContext(jmContext: currentContext)
actionContext.joinConferenceURL = controller.roomUrl
print("WATCH contextForSegue: \(actionContext.description)");
return actionContext;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This needs to be in sync with features/mobile/watchos/constants.js
enum JitsiMeetCommands : String {
typealias RawValue = String
case CMD_HANG_UP = "hangup";
case CMD_JOIN_CONFERENCE = "joinConference";
case CMD_SET_MUTED = "setMuted";
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
class JitsiMeetContext {
private var dictionary : [String : Any]
var joinConferenceURL : String? = nil;
init() {
dictionary = [:]
}
init(context: [String : Any]) {
dictionary = context
}
init(jmContext: JitsiMeetContext) {
dictionary = jmContext.dictionary
joinConferenceURL = jmContext.joinConferenceURL
}
var conferenceURL : String? {
get {
return dictionary["conferenceURL"] as? String
}
}
var conferenceTimestamp : Int64? {
get {
return dictionary["conferenceTimestamp"] as? Int64;
}
}
var sessionID : Int64? {
get {
return dictionary["sessionID"] as? Int64;
}
}
var recentURLs : NSArray? {
get {
return dictionary["recentURLs"] as? NSArray
}
}
var micMuted : Bool? {
get {
return (dictionary["micMuted"] as? NSNumber)?.boolValue ?? nil;
}
}
public var description: String {
return "JitsiMeetContext[conferenceURL: \(String(describing: conferenceURL)), conferenceTimestamp: \(String(describing:conferenceTimestamp)), sessionID: \(String(describing:sessionID)), recentURLs: \(String(describing:recentURLs)), joinConferenceURL: \(String(describing:joinConferenceURL)) "
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import WatchKit
class MeetingRowController: NSObject {
@IBOutlet var roomLabel: WKInterfaceLabel!
@IBOutlet var timeLabel: WKInterfaceLabel!
@IBOutlet var rowGroup: WKInterfaceGroup!
var room: String!
var roomUrl: String!
}