diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 7f8b4401d..36003d260 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -391,6 +391,10 @@ 2C1ABDCF257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; }; 2C1ABDD0257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; }; 2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDE4257F883400AEDFB6 /* ABContact.m */; }; + 2C1C68072D51229500A7F98A /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1C68062D51229500A7F98A /* CalendarEvent.swift */; }; + 2C1C68082D51338400A7F98A /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1C68062D51229500A7F98A /* CalendarEvent.swift */; }; + 2C1C68092D51338400A7F98A /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1C68062D51229500A7F98A /* CalendarEvent.swift */; }; + 2C1C680A2D51338400A7F98A /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1C68062D51229500A7F98A /* CalendarEvent.swift */; }; 2C1D13A3253760EE00EC0533 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1D13A1253760EE00EC0533 /* LaunchScreen.xib */; }; 2C1EF36B25505DCE007C9768 /* NCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1EF36A25505DCE007C9768 /* NCNavigationController.m */; }; 2C1EF36D25505DCE007C9768 /* NCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1EF36A25505DCE007C9768 /* NCNavigationController.m */; }; @@ -900,6 +904,7 @@ 2C1ABDCD257E939600AEDFB6 /* NCContact.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCContact.m; sourceTree = ""; }; 2C1ABDE3257F883400AEDFB6 /* ABContact.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ABContact.h; sourceTree = ""; }; 2C1ABDE4257F883400AEDFB6 /* ABContact.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ABContact.m; sourceTree = ""; }; + 2C1C68062D51229500A7F98A /* CalendarEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEvent.swift; sourceTree = ""; }; 2C1D13A2253760EE00EC0533 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 2C1EF36925505DCE007C9768 /* NCNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCNavigationController.h; sourceTree = ""; }; 2C1EF36A25505DCE007C9768 /* NCNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCNavigationController.m; sourceTree = ""; }; @@ -2185,6 +2190,7 @@ 1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */, 1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */, 1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */, + 2C1C68062D51229500A7F98A /* CalendarEvent.swift */, ); name = Chat; sourceTree = ""; @@ -2872,6 +2878,7 @@ 1FF136132BFB6FCD006A6101 /* RLMSupport.swift in Sources */, 1F77A5ED2AB9A408007B6037 /* NCChatMessage.m in Sources */, 1F77A5EB2AB9A3EE007B6037 /* BGTaskHelper.swift in Sources */, + 2C1C680A2D51338400A7F98A /* CalendarEvent.swift in Sources */, 1F205C532CEF91C500AAA673 /* UserAbsence.swift in Sources */, 1FF136182BFB74D0006A6101 /* NCChatMessage.swift in Sources */, 1F77A5FC2AB9A4ED007B6037 /* NCRoom.m in Sources */, @@ -3019,6 +3026,7 @@ 1F1B50472B90CDF800B0F2F4 /* TalkCapabilities.m in Sources */, 2CA1CCD01F1E1779002FE6A2 /* SearchTableViewController.m in Sources */, 1F1C0D8929AFB89900D17C6D /* VLCKitVideoViewController.swift in Sources */, + 2C1C68072D51229500A7F98A /* CalendarEvent.swift in Sources */, 2C9B0B98217F6DBA00A4752C /* NCNotificationController.m in Sources */, 2CC3166E2CC698E1007CBE16 /* TextFieldTableViewCell.swift in Sources */, 2C36A04A261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m in Sources */, @@ -3177,6 +3185,7 @@ 2C62AFFD24C1BDA5007E460A /* NCMessageParameter.m in Sources */, 1F35F8EC2AEEBC1400044BDA /* UIScrollView+SLKAdditions.m in Sources */, 1FF136172BFB74CF006A6101 /* NCChatMessage.swift in Sources */, + 2C1C68092D51338400A7F98A /* CalendarEvent.swift in Sources */, 2CF338E22CED388B0029CACC /* AvatarView.swift in Sources */, 1FF4DA8A2C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */, 2C62B00C24C1BDC1007E460A /* NCNotification.m in Sources */, @@ -3259,6 +3268,7 @@ F644A2DF2CE28C8D00E2ED81 /* NCChatFileStatus.swift in Sources */, 2C1ABDCF257E939600AEDFB6 /* NCContact.m in Sources */, 2CC001DC24A37AD400A20167 /* NCAppBranding.m in Sources */, + 2C1C68082D51338400A7F98A /* CalendarEvent.swift in Sources */, 2C4446D42658147900DF1DBC /* TalkAccount.m in Sources */, 1FDCC3E329EC787400DEB39B /* AvatarManager.swift in Sources */, 1FF4DA852C025DC000C1B952 /* NCAPISessionManager.swift in Sources */, diff --git a/NextcloudTalk/CalendarEvent.swift b/NextcloudTalk/CalendarEvent.swift new file mode 100644 index 000000000..d46cd7b56 --- /dev/null +++ b/NextcloudTalk/CalendarEvent.swift @@ -0,0 +1,57 @@ +// +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +struct CalendarEvent { + + var calendarAppUrl: String + var calendarUri: String + var location: String + var recurrenceId: String + var start: Int + var summary: String + var uri: String + + init(dictionary: [String: Any]) { + self.calendarAppUrl = dictionary["calendarAppUrl"] as? String ?? "" + self.calendarUri = dictionary["calendarUri"] as? String ?? "" + self.location = dictionary["location"] as? String ?? "" + self.recurrenceId = dictionary["recurrenceId"] as? String ?? "" + self.start = dictionary["start"] as? Int ?? -1 + self.summary = dictionary["summary"] as? String ?? "" + self.uri = dictionary["uri"] as? String ?? "" + } + + func readableStartTime() -> String { + let eventDate = Date(timeIntervalSince1970: TimeInterval(start)) + let now = Date() + + // Event happening now + if eventDate <= now { + return NSLocalizedString("Now", comment: "Indicates an event happening right now") + } + + // Event happening following days (except today or tomorrow) + let calendar = Calendar.current + if let nextWeek = calendar.date(byAdding: .day, value: 7, to: now), + !calendar.isDateInToday(eventDate), !calendar.isDateInTomorrow(eventDate), + eventDate < calendar.startOfDay(for: nextWeek) { + return eventDate.formatted( + .dateTime + .weekday(.wide) + .hour(.conversationalTwoDigits(amPM: .wide)) + .minute(.defaultDigits)) + } + + // Event happening today, tomorrow or later than a week + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.doesRelativeDateFormatting = true + + return dateFormatter.string(from: eventDate) + } +} diff --git a/NextcloudTalk/ChatViewController.swift b/NextcloudTalk/ChatViewController.swift index 025046557..3571c4fce 100644 --- a/NextcloudTalk/ChatViewController.swift +++ b/NextcloudTalk/ChatViewController.swift @@ -57,7 +57,7 @@ import SwiftyAttributes private var lobbyCheckTimer: Timer? - // MARK: - Call buttons in NavigationBar + // MARK: - Buttons in NavigationBar func getCallOptionsBarButton() -> BarButtonItemWithActivity { let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20) @@ -158,6 +158,37 @@ import SwiftyAttributes return callOptionsButton }() + private lazy var upcomingEventsButton: BarButtonItemWithActivity = { + let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20) + let buttonImage = UIImage(systemName: "calendar", withConfiguration: symbolConfiguration) + let upcomingEventsButton = BarButtonItemWithActivity(width: 50, with: buttonImage) + + let deferredUpcomingEvents = UIDeferredMenuElement { [weak self] completion in + guard let self = self else { return } + + NCAPIController.sharedInstance().upcomingEvents(self.room, forAccount: self.account) { events in + let actions: [UIAction] + if !events.isEmpty { + actions = events.map { event in + UIAction(title: event.summary, subtitle: event.readableStartTime(), handler: { _ in }) + } + } else { + actions = [UIAction(title: NSLocalizedString("No upcoming events", comment: ""), attributes: .disabled, handler: { _ in })] + } + + completion(actions) + } + } + + upcomingEventsButton.innerButton.menu = UIMenu(children: [deferredUpcomingEvents]) + upcomingEventsButton.innerButton.showsMenuAsPrimaryAction = true + + upcomingEventsButton.accessibilityLabel = NSLocalizedString("Upcoming events", comment: "") + upcomingEventsButton.accessibilityHint = NSLocalizedString("Double tap to display upcoming events", comment: "") + + return upcomingEventsButton + }() + private var messageExpirationTimer: Timer? public override init?(forRoom room: NCRoom, withAccount account: TalkAccount) { @@ -209,10 +240,18 @@ import SwiftyAttributes public override func viewDidLoad() { super.viewDidLoad() + var barButtonsItems: [UIBarButtonItem] = [] + // Call options if room.supportsCalling { - self.navigationItem.rightBarButtonItems = [callOptionsButton] + barButtonsItems.append(callOptionsButton) + } + // Upcoming events + if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityScheduleMeeting, forAccountId: room.accountId) { + barButtonsItems.append(upcomingEventsButton) } + self.navigationItem.rightBarButtonItems = barButtonsItems + // No sharing options in federation v1 if room.isFederated { // When hiding the button it is still respected in the layout constraints @@ -291,7 +330,7 @@ import SwiftyAttributes // Check if new messages were added while the app was inactive (eg. via background-refresh) self.checkForNewStoredMessages() - if !self.offlineMode { + if !self.offlineMode { NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false) } diff --git a/NextcloudTalk/NCAPIControllerExtensions.swift b/NextcloudTalk/NCAPIControllerExtensions.swift index 472e53a05..2af196d82 100644 --- a/NextcloudTalk/NCAPIControllerExtensions.swift +++ b/NextcloudTalk/NCAPIControllerExtensions.swift @@ -680,4 +680,27 @@ import Foundation completionBlock(message) } } + + // MARK: - Upcoming events + + @nonobjc + func upcomingEvents(_ room: NCRoom, forAccount account: TalkAccount, completionBlock: @escaping (_ events: [CalendarEvent]) -> Void) { + guard let encodedRoomLink = room.linkURL?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), + let apiSessionManager = self.apiSessionManagers.object(forKey: account.accountId) as? NCAPISessionManager + else { + completionBlock([]) + return + } + + let urlString = "\(account.server)/ocs/v2.php/apps/dav/api/v1/events/upcoming?location=\(encodedRoomLink)" + + apiSessionManager.getOcs(urlString, account: account) { ocsResponse, error in + if error == nil, let events = ocsResponse?.dataDict?["events"] as? [[String: Any]] { + let calendarEvents = events.map { CalendarEvent(dictionary: $0) } + completionBlock(calendarEvents) + } else { + completionBlock([]) + } + } + } } diff --git a/NextcloudTalk/NCDatabaseManager.h b/NextcloudTalk/NCDatabaseManager.h index a52e6df8c..a2a911f3a 100644 --- a/NextcloudTalk/NCDatabaseManager.h +++ b/NextcloudTalk/NCDatabaseManager.h @@ -80,6 +80,7 @@ extern NSString * const kCapabilityCallNotificationState; extern NSString * const kCapabilityCallForceMute; extern NSString * const kCapabilityTalkPollsDrafts; extern NSString * const kCapabilityEditDraftPoll; +extern NSString * const kCapabilityScheduleMeeting; extern NSString * const kNotificationsCapabilityExists; extern NSString * const kNotificationsCapabilityTestPush; diff --git a/NextcloudTalk/NCDatabaseManager.m b/NextcloudTalk/NCDatabaseManager.m index 3c2ac1fde..f42bba441 100644 --- a/NextcloudTalk/NCDatabaseManager.m +++ b/NextcloudTalk/NCDatabaseManager.m @@ -81,6 +81,7 @@ NSString * const kCapabilityForceMute = @"force-mute"; NSString * const kCapabilityTalkPollsDrafts = @"talk-polls-drafts"; NSString * const kCapabilityEditDraftPoll = @"edit-draft-poll"; +NSString * const kCapabilityScheduleMeeting = @"schedule-meeting"; NSString * const kNotificationsCapabilityExists = @"exists"; NSString * const kNotificationsCapabilityTestPush = @"test-push"; diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index a76c2a8b9..445b225bd 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -817,6 +817,9 @@ /* No comment provided by engineer. */ "Double tap to display call options" = "Double tap to display call options"; +/* No comment provided by engineer. */ +"Double tap to display upcoming events" = "Double tap to display upcoming events"; + /* No comment provided by engineer. */ "Double tap to edit profile" = "Double tap to edit profile"; @@ -1360,6 +1363,9 @@ /* No comment provided by engineer. */ "No shared items" = "No shared items"; +/* No comment provided by engineer. */ +"No upcoming events" = "No upcoming events"; + /* No comment provided by engineer. */ "No user found" = "No user found"; @@ -1384,6 +1390,9 @@ /* No comment provided by engineer. */ "Notifications: %@" = "Notifications: %@"; +/* Indicates an event happening right now */ +"Now" = "Now"; + /* No comment provided by engineer. */ "Off" = "Off"; @@ -2011,6 +2020,9 @@ /* No comment provided by engineer. */ "Unread messages" = "Unread messages"; +/* No comment provided by engineer. */ +"Upcoming events" = "Upcoming events"; + /* No comment provided by engineer. */ "Update" = "Update";