mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
feat: add join button for conference calls
This commit is contained in:
parent
92b69acfc6
commit
be546b48ff
4 changed files with 157 additions and 23 deletions
|
|
@ -747,6 +747,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"%@ %@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ %2$@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ Models" : {
|
||||
"localizations" : {
|
||||
"fr" : {
|
||||
|
|
@ -10809,6 +10819,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Join the meeting" : {
|
||||
|
||||
},
|
||||
"Keyboard Backlight HUD" : {
|
||||
"localizations" : {
|
||||
|
|
@ -12429,6 +12442,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Meeting starts at %@" : {
|
||||
|
||||
},
|
||||
"Memory Usage" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ class CalendarService: CalendarServiceProviding {
|
|||
}
|
||||
|
||||
private func performAccessRequest(for type: EKEntityType) async throws -> Bool {
|
||||
if #available(macOS 14.0, *) {
|
||||
switch type {
|
||||
case .event:
|
||||
return try await store.requestFullAccessToEvents()
|
||||
|
|
@ -48,18 +47,11 @@ class CalendarService: CalendarServiceProviding {
|
|||
@unknown default:
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return try await store.requestAccess(to: type)
|
||||
}
|
||||
}
|
||||
|
||||
private func hasAccess(to entityType: EKEntityType) -> Bool {
|
||||
let status = EKEventStore.authorizationStatus(for: entityType)
|
||||
if #available(macOS 14.0, *) {
|
||||
return status == .fullAccess
|
||||
} else {
|
||||
return status == .authorized
|
||||
}
|
||||
}
|
||||
|
||||
func calendars() async -> [CalendarModel] {
|
||||
|
|
@ -168,7 +160,8 @@ extension EventModel {
|
|||
participants: .init(from: event),
|
||||
timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : event.timeZone,
|
||||
hasRecurrenceRules: event.hasRecurrenceRules || event.isDetached,
|
||||
priority: nil
|
||||
priority: nil,
|
||||
conferenceURL: event.extractConferenceURL()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +185,8 @@ extension EventModel {
|
|||
participants: [],
|
||||
timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : reminder.timeZone,
|
||||
hasRecurrenceRules: reminder.hasRecurrenceRules,
|
||||
priority: .init(from: reminder.priority)
|
||||
priority: .init(from: reminder.priority),
|
||||
conferenceURL: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -271,11 +265,7 @@ private extension EKCalendar {
|
|||
}
|
||||
|
||||
var isDelegate: Bool {
|
||||
if #available(macOS 13.0, *) {
|
||||
return source.isDelegate
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +281,60 @@ private extension EKEvent {
|
|||
let endOfDay = calendar.dateInterval(of: .day, for: endDate)?.end
|
||||
return startDate == startOfDay && endDate == endOfDay
|
||||
}
|
||||
|
||||
/// Extract conference call URL from various sources
|
||||
func extractConferenceURL() -> URL? {
|
||||
// First try the URL field if it's a conference URL
|
||||
if let eventURL = url, isConferenceURL(eventURL) {
|
||||
return eventURL
|
||||
}
|
||||
|
||||
// Then try to extract from location field
|
||||
if let location = location, let conferenceURL = extractURLFromText(location) {
|
||||
return conferenceURL
|
||||
}
|
||||
|
||||
// Finally try to extract from notes
|
||||
if let notes = notes, let conferenceURL = extractURLFromText(notes) {
|
||||
return conferenceURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Check if a URL is likely a conference URL
|
||||
private func isConferenceURL(_ url: URL) -> Bool {
|
||||
let host = url.host?.lowercased() ?? ""
|
||||
let conferenceHosts = [
|
||||
"zoom.us",
|
||||
"teams.microsoft.com",
|
||||
"meet.google.com",
|
||||
"webex.com",
|
||||
"gotomeeting.com",
|
||||
"bluejeans.com",
|
||||
"whereby.com",
|
||||
"meet.jit.si",
|
||||
"discord.gg",
|
||||
"discord.com",
|
||||
"facetime.apple.com"
|
||||
]
|
||||
|
||||
return conferenceHosts.contains { host.contains($0) }
|
||||
}
|
||||
|
||||
/// Extract first valid conference URL from text
|
||||
private func extractURLFromText(_ text: String) -> URL? {
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
let matches = detector?.matches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
|
||||
|
||||
for match in matches ?? [] {
|
||||
if let url = match.url, isConferenceURL(url) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension Calendar {
|
||||
|
|
|
|||
|
|
@ -429,6 +429,11 @@ struct EventListView: View {
|
|||
.foregroundColor(Color(white: 0.65))
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
// Show Join button if conference URL is available
|
||||
if let conferenceURL = event.conferenceURL {
|
||||
ConferenceJoinButton(url: conferenceURL, event: event)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
|
|
@ -484,6 +489,74 @@ struct ReminderToggle: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct ConferenceJoinButton: View {
|
||||
let url: URL
|
||||
let event: EventModel
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
private var isJoinable: Bool {
|
||||
// Allow joining 15 minutes before the event starts
|
||||
let joinableTime = event.start.addingTimeInterval(-15 * 60)
|
||||
return Date() >= joinableTime
|
||||
}
|
||||
|
||||
private var buttonText: String {
|
||||
if event.eventStatus == .inProgress {
|
||||
return "Rejoin"
|
||||
} else {
|
||||
return "Join"
|
||||
}
|
||||
}
|
||||
|
||||
private var conferenceService: String? {
|
||||
let host = url.host?.lowercased() ?? ""
|
||||
if host.contains("zoom.us") {
|
||||
return "Zoom"
|
||||
} else if host.contains("teams.microsoft.com") {
|
||||
return "Teams"
|
||||
} else if host.contains("meet.google.com") {
|
||||
return "Meet"
|
||||
} else if host.contains("webex.com") {
|
||||
return "Webex"
|
||||
} else if host.contains("facetime.apple.com") {
|
||||
return "FaceTime"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openURL(url)
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "video.fill")
|
||||
.font(.system(size: 9))
|
||||
if let service = conferenceService {
|
||||
Text("\(buttonText) \(service)")
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text(buttonText)
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
.foregroundColor(isJoinable ? .white : Color(white: 0.5))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
isJoinable
|
||||
? Color.accentColor.opacity(0.8)
|
||||
: Color.gray.opacity(0.3)
|
||||
)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.disabled(!isJoinable)
|
||||
.help(isJoinable ? "Join the meeting" : "Meeting starts at \(event.start.formatted(date: .omitted, time: .shortened))")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CalendarView()
|
||||
.frame(width: 250)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ struct EventModel: Equatable, Identifiable {
|
|||
let timeZone: TimeZone?
|
||||
let hasRecurrenceRules: Bool
|
||||
let priority: Priority?
|
||||
let conferenceURL: URL?
|
||||
}
|
||||
|
||||
enum AttendanceStatus: Comparable {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue