diff --git a/CanvasPlusPlayground.xcodeproj/project.pbxproj b/CanvasPlusPlayground.xcodeproj/project.pbxproj index 70861c0..22d57f7 100644 --- a/CanvasPlusPlayground.xcodeproj/project.pbxproj +++ b/CanvasPlusPlayground.xcodeproj/project.pbxproj @@ -123,6 +123,10 @@ B7C0A3CB2D2F919F003E5A36 /* PinButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C0A3CA2D2F919F003E5A36 /* PinButton.swift */; }; B7C0A3CD2D3023CA003E5A36 /* PinnedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C0A3CC2D3023CA003E5A36 /* PinnedItem.swift */; }; B7C0A3CF2D31F339003E5A36 /* PinnedItemCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C0A3CE2D31F339003E5A36 /* PinnedItemCard.swift */; }; + B7D7512B2D3D5D8000F7B8B8 /* AllAnnouncementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D7512A2D3D5D7700F7B8B8 /* AllAnnouncementsView.swift */; }; + B7D7512D2D3D652300F7B8B8 /* GetAnnouncementsBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D7512C2D3D652300F7B8B8 /* GetAnnouncementsBatchRequest.swift */; }; + B7D7512F2D3D660B00F7B8B8 /* AllAnnouncementsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D7512E2D3D660B00F7B8B8 /* AllAnnouncementsManager.swift */; }; + B7D751312D3D688C00F7B8B8 /* AnnouncementRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D751302D3D688C00F7B8B8 /* AnnouncementRow.swift */; }; B7D95D772D07C3D3002AD955 /* ICSParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D95D762D07C3D3002AD955 /* ICSParser.swift */; }; B7D95DB72D0A8D78002AD955 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D95DB62D0A8D75002AD955 /* SettingsView.swift */; }; B7E59A102D2002A5001836FE /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7E59A0F2D2002A3001836FE /* Sidebar.swift */; }; @@ -254,6 +258,10 @@ B7C0A3CA2D2F919F003E5A36 /* PinButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinButton.swift; sourceTree = ""; }; B7C0A3CC2D3023CA003E5A36 /* PinnedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedItem.swift; sourceTree = ""; }; B7C0A3CE2D31F339003E5A36 /* PinnedItemCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedItemCard.swift; sourceTree = ""; }; + B7D7512A2D3D5D7700F7B8B8 /* AllAnnouncementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllAnnouncementsView.swift; sourceTree = ""; }; + B7D7512C2D3D652300F7B8B8 /* GetAnnouncementsBatchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAnnouncementsBatchRequest.swift; sourceTree = ""; }; + B7D7512E2D3D660B00F7B8B8 /* AllAnnouncementsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllAnnouncementsManager.swift; sourceTree = ""; }; + B7D751302D3D688C00F7B8B8 /* AnnouncementRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementRow.swift; sourceTree = ""; }; B7D95D762D07C3D3002AD955 /* ICSParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICSParser.swift; sourceTree = ""; }; B7D95DB62D0A8D75002AD955 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; B7E59A0F2D2002A3001836FE /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; @@ -293,6 +301,7 @@ A3049B732D15DBEE002F3166 /* API Requests */ = { isa = PBXGroup; children = ( + B7D7512C2D3D652300F7B8B8 /* GetAnnouncementsBatchRequest.swift */, A3049B762D15DC36002F3166 /* GetCourseRequest.swift */, A3049B742D15DC1C002F3166 /* GetCoursesRequest.swift */, A3049B782D15E162002F3166 /* GetCourseRootFolder.swift */, @@ -338,10 +347,13 @@ A324BA512D079646005F53FA /* Announcements */ = { isa = PBXGroup; children = ( + B7D7512A2D3D5D7700F7B8B8 /* AllAnnouncementsView.swift */, + B7D7512E2D3D660B00F7B8B8 /* AllAnnouncementsManager.swift */, 9B92A4242C93856100C21CFC /* CourseAnnouncementManager.swift */, 9B6663E22C9853BC0060990E /* HTMLTextView.swift */, 9B9C2E052C93F94C00E4B16B /* CourseAnnouncementDetailView.swift */, 9B9C2E032C93F6CD00E4B16B /* CourseAnnouncementsView.swift */, + B7D751302D3D688C00F7B8B8 /* AnnouncementRow.swift */, A373DC1F2D1D2B9D00215019 /* Models */, ); path = Announcements; @@ -767,6 +779,7 @@ A3049B7B2D15E2B7002F3166 /* GetFilesInFolderRequest.swift in Sources */, A3CF88DB2D192194000ACDF3 /* EnrollmentAPI.swift in Sources */, B7F950322D118EAF004BB470 /* String+StripHTML.swift in Sources */, + B7D7512D2D3D652300F7B8B8 /* GetAnnouncementsBatchRequest.swift in Sources */, A324BA6A2D0817AB005F53FA /* FoldersPageView.swift in Sources */, B7A26EE92CCB62A30084704A /* NavigationModel.swift in Sources */, 192EC04A2C963B9000AF8528 /* AssignmentAPI.swift in Sources */, @@ -803,6 +816,7 @@ B7E59A142D2004A4001836FE /* CourseListCell.swift in Sources */, B53D95A22CA0A22A00647EE9 /* PeopleView.swift in Sources */, A373DC312D28176100215019 /* APIModule.swift in Sources */, + B7D7512F2D3D660B00F7B8B8 /* AllAnnouncementsManager.swift in Sources */, A3049B752D15DC1C002F3166 /* GetCoursesRequest.swift in Sources */, B76455042C8DF61B002DF00E /* CourseView.swift in Sources */, A3049B682D0F3ECA002F3166 /* QuizzesViewModel.swift in Sources */, @@ -840,6 +854,7 @@ B7C0A3CB2D2F919F003E5A36 /* PinButton.swift in Sources */, B7F950392D1279A1004BB470 /* UserAPI.swift in Sources */, A3049B662D0F3E32002F3166 /* CourseQuizzesView.swift in Sources */, + B7D7512B2D3D5D8000F7B8B8 /* AllAnnouncementsView.swift in Sources */, A3E7F3892C954E0500DC4300 /* CanvasRequest.swift in Sources */, A35191412D283589001E415F /* ModulesViewModel.swift in Sources */, A3E7F3912C99317100DC4300 /* CourseTabsManager.swift in Sources */, @@ -876,6 +891,7 @@ A3D161492D169C23004055FB /* APIRequest+Storage.swift in Sources */, 9B6663E32C9853BC0060990E /* HTMLTextView.swift in Sources */, B7AD550C2CD4257B00FB09BB /* IntelligenceOnboardingView.swift in Sources */, + B7D751312D3D688C00F7B8B8 /* AnnouncementRow.swift in Sources */, A3049B792D15E162002F3166 /* GetCourseRootFolder.swift in Sources */, 7F8535742C98DE3C0023E384 /* CourseGradeView.swift in Sources */, A3049B7F2D160C03002F3166 /* GetTabsRequest.swift in Sources */, diff --git a/CanvasPlusPlayground/Common/Network/API Requests/GetAnnouncementsBatchRequest.swift b/CanvasPlusPlayground/Common/Network/API Requests/GetAnnouncementsBatchRequest.swift new file mode 100644 index 0000000..9a3c831 --- /dev/null +++ b/CanvasPlusPlayground/Common/Network/API Requests/GetAnnouncementsBatchRequest.swift @@ -0,0 +1,90 @@ +// +// GetAnnouncementsBatchRequest.swift +// CanvasPlusPlayground +// +// Created by Rahul on 1/19/25. +// + +import Foundation + +struct GetAnnouncementsBatchRequest: CacheableArrayAPIRequest { + typealias Subject = AnnouncementAPI + + var path: String { "announcements" } + + var queryParameters: [QueryParameter] { + [ + ("start_date", startDate?.ISO8601Format()), + ("end_date", endDate?.ISO8601Format()), + // ("active_only", activeOnly), ONLY FOR TEACHERS + ("latest_only", latestOnly), + ("per_page", perPage) + ] + + contextCodes.map { ("context_codes[]", $0) } + + include.map { ("include[]", $0) } + } + + // MARK: Query Params + /// At least one context code must be provided here otherwise request fails + let contextCodes: [String] + let startDate: Date? + let endDate: Date? + // let activeOnly: Bool? ONLY FOR TEACHERS + let latestOnly: Bool? + let include: [String] + let perPage: Int + + init( + courseIds: [String], + startDate: Date? = nil, + endDate: Date? = nil, + latestOnly: Bool? = nil, + include: [String] = [], + perPage: Int = 50 + ) { + assert(!courseIds.isEmpty) + + self.contextCodes = courseIds.map { "course_\($0)" } + self.startDate = startDate + self.endDate = endDate + self.latestOnly = latestOnly + self.include = include + self.perPage = perPage + } + + var requestId: String? { "announcements/\(contextCodes.joined(separator: "_"))" } + var requestIdKey: ParentKeyPath { + .createReadable(\.contextCode) + } + var idPredicate: Predicate { + let contextCodes = contextCodes as [String?] + let contextCodePred = contextCodes.isEmpty ? .true : #Predicate { announcement in + contextCodes.contains(announcement.contextCode) + } + + return contextCodePred + } + var customPredicate: Predicate { + let startDatePredicate: Predicate + if let startDate { + startDatePredicate = #Predicate { announcement in + if let createdAt = announcement.createdAt { + startDate <= (createdAt) + } else { true } + } + } else { startDatePredicate = .true } + + let endDatePredicate: Predicate + if let endDate { + endDatePredicate = #Predicate { announcement in + if let createdAt = announcement.createdAt { + endDate >= createdAt + } else { true } + } + } else { endDatePredicate = .true } + + return #Predicate { announcement in + startDatePredicate.evaluate(announcement) && endDatePredicate.evaluate(announcement) + } + } +} diff --git a/CanvasPlusPlayground/Common/Network/CanvasRequest.swift b/CanvasPlusPlayground/Common/Network/CanvasRequest.swift index ff4fc50..525b884 100644 --- a/CanvasPlusPlayground/Common/Network/CanvasRequest.swift +++ b/CanvasPlusPlayground/Common/Network/CanvasRequest.swift @@ -36,6 +36,7 @@ struct CanvasRequest { GetTabsRequest(courseId: courseId) } + /// Fetches Announcements for one course static func getAnnouncements( courseId: String, startDate: Date = .distantPast, @@ -50,6 +51,21 @@ struct CanvasRequest { ) } + /// Fetches announcements for all courses with id's in `courseIds` + static func getAnnouncements( + courseIds: [String], + startDate: Date = .distantPast, + endDate: Date = .now, + perPage: Int = 15 + ) -> GetAnnouncementsBatchRequest { + GetAnnouncementsBatchRequest( + courseIds: courseIds, + startDate: startDate, + endDate: endDate, + perPage: perPage + ) + } + static func getAssignment(id: String, courseId: String) -> GetAssignmentRequest { GetAssignmentRequest(assignmentId: id, courseId: courseId) } diff --git a/CanvasPlusPlayground/Common/Utilities/String+StripHTML.swift b/CanvasPlusPlayground/Common/Utilities/String+StripHTML.swift index 192fc5b..044a08c 100644 --- a/CanvasPlusPlayground/Common/Utilities/String+StripHTML.swift +++ b/CanvasPlusPlayground/Common/Utilities/String+StripHTML.swift @@ -9,6 +9,8 @@ import Foundation extension String { func stripHTML() -> String { - return replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) + replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "&", with: "&") } } diff --git a/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsManager.swift b/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsManager.swift new file mode 100644 index 0000000..b301862 --- /dev/null +++ b/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsManager.swift @@ -0,0 +1,64 @@ +// +// AllAnnouncementsManager.swift +// CanvasPlusPlayground +// +// Created by Rahul on 1/19/25. +// + +import Foundation + +@Observable class AllAnnouncementsManager { + var announcements: [(Announcement, Course?)] = [] + + func fetchAnnouncements(courses: [Course]) async { + guard !courses.isEmpty else { return } + + let courseIds = courses.map { $0.id } + + let announcements: [Announcement]? = try? await CanvasService.shared.loadAndSync( + CanvasRequest.getAnnouncements(courseIds: courseIds), + onCacheReceive: { (cached: [Announcement]?) in + guard let cached else { return } + + setAnnouncements(cached, courses: courses) + }, + onNewBatch: { batchAnnouncements in + setBatchAnnouncements(batchAnnouncements, courses: courses) + } + ) + + guard let announcements else { + print("Failed to fetch announcements.") + return + } + + setAnnouncements(announcements, courses: courses) + } + + private func setAnnouncements(_ announcements: [Announcement], courses: [Course]) { + DispatchQueue.main.async { + self.announcements = announcements.map { announcement in + guard let contextCode = announcement.contextCode else { + return (announcement, nil) + } + + let course = courses.first(where: { course in + contextCode.contains(course.id) + }) + + return (announcement, course) + }.sorted { $0.0.createdAt ?? Date() > $1.0.createdAt ?? Date() } + } + } + + private func setBatchAnnouncements( + _ announcements: [Announcement], + courses: [Course] + ) { + let newAnnouncements = self.announcements.map(\.0) + announcements.filter { + !self.announcements.map(\.0).contains($0) + } + + setAnnouncements(newAnnouncements, courses: courses) + } +} diff --git a/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsView.swift b/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsView.swift new file mode 100644 index 0000000..dc43712 --- /dev/null +++ b/CanvasPlusPlayground/Features/Announcements/AllAnnouncementsView.swift @@ -0,0 +1,52 @@ +// +// AllAnnouncementsView.swift +// CanvasPlusPlayground +// +// Created by Rahul on 1/19/25. +// + +import SwiftUI + +struct AllAnnouncementsView: View { + @Environment(CourseManager.self) private var courseManager + + @State private var announcementsManager = AllAnnouncementsManager() + @State private var selectedAnnouncement: Announcement? + @State private var isLoadingAnnouncements = false + + var body: some View { + List(selection: $selectedAnnouncement) { + ForEach(announcementsManager.announcements, id: \.0) { announcement, course in + NavigationLink { + CourseAnnouncementDetailView(announcement: announcement) + } label: { + AnnouncementRow( + course: course, + announcement: announcement, + showCourseName: true + ) + } + } + } + .navigationTitle("All Announcements") + .statusToolbarItem("Announcements", isVisible: isLoadingAnnouncements) + .task { + await loadAnnouncements() + } + .refreshable { + await loadAnnouncements() + } + .onChange(of: courseManager.courses) { _, _ in + Task { + await loadAnnouncements() + } + } + } + + private func loadAnnouncements() async { + isLoadingAnnouncements = true + await announcementsManager + .fetchAnnouncements(courses: courseManager.courses) + isLoadingAnnouncements = false + } +} diff --git a/CanvasPlusPlayground/Features/Announcements/AnnouncementRow.swift b/CanvasPlusPlayground/Features/Announcements/AnnouncementRow.swift new file mode 100644 index 0000000..050c23b --- /dev/null +++ b/CanvasPlusPlayground/Features/Announcements/AnnouncementRow.swift @@ -0,0 +1,121 @@ +// +// AnnouncementRow.swift +// CanvasPlusPlayground +// +// Created by Rahul on 1/19/25. +// + +import SwiftUI + +struct AnnouncementRow: View { + let course: Course? + let announcement: Announcement + var showCourseName = false + + // MARK: Drawing Constants + private let unreadIndicatorWidth: CGFloat = 10 + + var body: some View { + VStack(alignment: .announcementRowAlignment) { + if showCourseName { + courseName + } + header + detail + } + .contextMenu { + PinButton( + itemID: announcement.id, + courseID: course?.id, + type: .announcement + ) + } + .swipeActions(edge: .leading) { + PinButton( + itemID: announcement.id, + courseID: course?.id, + type: .announcement + ) + } + .id(announcement.id) + } + + private var courseName: some View { + Text(course?.displayName.uppercased() ?? "") + .font(.caption) + .foregroundStyle(course?.rgbColors?.color ?? .accentColor) + .alignmentGuide(.announcementRowAlignment) { context in + context[.leading] + } + } + + private var header: some View { + HStack { + Group { + if !(announcement.isRead ?? false) { + Circle() + .fill(.tint) + } else { + Spacer().frame(width: unreadIndicatorWidth) + } + } + .frame(width: unreadIndicatorWidth, height: unreadIndicatorWidth) + + Text(announcement.title ?? "") + .alignmentGuide(.announcementRowAlignment) { context in + context[.leading] + } + + Spacer() + + if let createdAt = announcement.createdAt { + Text(createdAt.formatted(.relative(presentation: .named))) + .foregroundStyle(.secondary) + } + } + } + + private var detail: some View { + HStack { + Spacer().frame(width: unreadIndicatorWidth) + + Group { + if let summary = announcement.summary { + Text(Image(systemName: "wand.and.sparkles")) + .foregroundStyle( + course?.rgbColors?.color ?? .accentColor + ) + + + Text(summary) + } else { + Text( + announcement.message? + .stripHTML() + .trimmingCharacters( + in: .whitespacesAndNewlines + ) + ?? "" + ) + } + } + .lineLimit(2) + .foregroundStyle(.secondary) + .controlSize(.small) + .alignmentGuide(.announcementRowAlignment) { context in + context[.leading] + } + } + } +} + +extension HorizontalAlignment { + private struct AnnouncementRowAlignment: AlignmentID { + static func defaultValue(in context: ViewDimensions) -> CGFloat { + context[HorizontalAlignment.center] + } + } + + fileprivate static let announcementRowAlignment = HorizontalAlignment( + AnnouncementRowAlignment.self + ) +} diff --git a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementDetailView.swift b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementDetailView.swift index 216c873..7cc758b 100644 --- a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementDetailView.swift +++ b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementDetailView.swift @@ -73,6 +73,7 @@ struct CourseAnnouncementDetailView: View { .onAppear { announcement.isRead = true } + .id(announcement.id) } private var summarySection: some View { diff --git a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementManager.swift b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementManager.swift index b1f0b9f..7e8a9bc 100644 --- a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementManager.swift +++ b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementManager.swift @@ -38,5 +38,4 @@ import Foundation func setAnnouncements(_ announcements: [Announcement]) { self.announcements = announcements } - } diff --git a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementsView.swift b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementsView.swift index 88f092b..002b6e0 100644 --- a/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementsView.swift +++ b/CanvasPlusPlayground/Features/Announcements/CourseAnnouncementsView.swift @@ -55,102 +55,3 @@ struct CourseAnnouncementsView: View { isLoadingAnnouncements = false } } - -private struct AnnouncementRow: View { - let course: Course - let announcement: Announcement - - // MARK: Drawing Constants - private let unreadIndicatorWidth: CGFloat = 10 - - var body: some View { - VStack(alignment: .announcementRowAlignment) { - header - detail - } - .contextMenu { - PinButton( - itemID: announcement.id, - courseID: course.id, - type: .announcement - ) - } - .swipeActions(edge: .leading) { - PinButton( - itemID: announcement.id, - courseID: course.id, - type: .announcement - ) - } - } - - private var header: some View { - HStack { - Group { - if !(announcement.isRead ?? false) { - Circle() - .fill(.tint) - } else { - Spacer().frame(width: unreadIndicatorWidth) - } - } - .frame(width: unreadIndicatorWidth, height: unreadIndicatorWidth) - - Text(announcement.title ?? "") - .alignmentGuide(.announcementRowAlignment) { context in - context[.leading] - } - - Spacer() - - if let createdAt = announcement.createdAt { - Text(createdAt.formatted(.relative(presentation: .named))) - .foregroundStyle(.secondary) - } - } - } - - private var detail: some View { - HStack { - Spacer().frame(width: unreadIndicatorWidth) - - Group { - if let summary = announcement.summary { - Text(Image(systemName: "wand.and.sparkles")) - .foregroundStyle( - course.rgbColors?.color ?? .accentColor - ) - + - Text(summary) - } else { - Text( - announcement.message? - .stripHTML() - .trimmingCharacters( - in: .whitespacesAndNewlines - ) - ?? "" - ) - } - } - .lineLimit(2) - .foregroundStyle(.secondary) - .controlSize(.small) - .alignmentGuide(.announcementRowAlignment) { context in - context[.leading] - } - } - } -} - -extension HorizontalAlignment { - private struct AnnouncementRowAlignment: AlignmentID { - static func defaultValue(in context: ViewDimensions) -> CGFloat { - context[HorizontalAlignment.center] - } - } - - fileprivate static let announcementRowAlignment = HorizontalAlignment( - AnnouncementRowAlignment.self - ) -} diff --git a/CanvasPlusPlayground/Features/Announcements/Models/Announcement.swift b/CanvasPlusPlayground/Features/Announcements/Models/Announcement.swift index 75dc934..30da5ff 100644 --- a/CanvasPlusPlayground/Features/Announcements/Models/Announcement.swift +++ b/CanvasPlusPlayground/Features/Announcements/Models/Announcement.swift @@ -19,6 +19,7 @@ final class Announcement: Cacheable { var title: String? var createdAt: Date? var message: String? + var contextCode: String? // MARK: Custom Properties var isRead: Bool? @@ -29,11 +30,13 @@ final class Announcement: Cacheable { self.title = api.title self.createdAt = api.created_at self.message = api.message + self.contextCode = api.context_code } func merge(with other: Announcement) { self.title = other.title self.message = other.message self.createdAt = other.createdAt + self.contextCode = other.contextCode } } diff --git a/CanvasPlusPlayground/Features/Announcements/Models/AnnouncementAPI.swift b/CanvasPlusPlayground/Features/Announcements/Models/AnnouncementAPI.swift index 67c16ff..fe756c4 100644 --- a/CanvasPlusPlayground/Features/Announcements/Models/AnnouncementAPI.swift +++ b/CanvasPlusPlayground/Features/Announcements/Models/AnnouncementAPI.swift @@ -16,6 +16,7 @@ struct AnnouncementAPI: APIResponse { var title: String? var created_at: Date? var message: String? + var context_code: String? // swiftlint:enable identifier_name func createModel() -> Announcement { diff --git a/CanvasPlusPlayground/Features/Navigation/HomeView.swift b/CanvasPlusPlayground/Features/Navigation/HomeView.swift index 82a3db2..52443bb 100644 --- a/CanvasPlusPlayground/Features/Navigation/HomeView.swift +++ b/CanvasPlusPlayground/Features/Navigation/HomeView.swift @@ -18,6 +18,7 @@ struct HomeView: View { @EnvironmentObject private var llmEvaluator: LLMEvaluator @State private var columnVisibility = NavigationSplitViewVisibility.all + @State private var isLoadingCourses = false @SceneStorage("CourseListView.selectedNavigationPage") private var selectedNavigationPage: NavigationPage? @@ -39,6 +40,10 @@ struct HomeView: View { NavigationSplitView(columnVisibility: $columnVisibility) { Sidebar() + .statusToolbarItem("Courses", isVisible: isLoadingCourses) + .refreshable { + await loadCourses() + } } content: { contentView } detail: { @@ -53,6 +58,13 @@ struct HomeView: View { navigationModel.selectedNavigationPage = selectedNavigationPage navigationModel.selectedCoursePage = selectedCoursePage } + .task { + if StorageKeys.needsAuthorization { + navigationModel.showAuthorizationSheet = true + } else { + await loadCourses() + } + } .onChange(of: navigationModel.selectedNavigationPage) { _, new in selectedNavigationPage = new } @@ -79,7 +91,7 @@ struct HomeView: View { CourseView(course: selectedCourse) } else if let selectedNavigationPage { switch selectedNavigationPage { - case .announcements: Text("All Announcements") + case .announcements: AllAnnouncementsView() case .toDoList: AggregatedAssignmentsView() case .pinned: PinnedItemsView() default: EmptyView() @@ -88,6 +100,13 @@ struct HomeView: View { ContentUnavailableView("Select a course", systemImage: "folder") } } + + private func loadCourses() async { + isLoadingCourses = true + await courseManager.getCourses() + await profileManager.getCurrentUserAndProfile() + isLoadingCourses = false + } } #Preview { diff --git a/CanvasPlusPlayground/Features/Navigation/Sidebar.swift b/CanvasPlusPlayground/Features/Navigation/Sidebar.swift index ebc7432..b709a61 100644 --- a/CanvasPlusPlayground/Features/Navigation/Sidebar.swift +++ b/CanvasPlusPlayground/Features/Navigation/Sidebar.swift @@ -14,8 +14,6 @@ struct Sidebar: View { @Environment(CourseManager.self) private var courseManager @Environment(ProfileManager.self) private var profileManager - @State private var isLoadingCourses = false - var body: some View { @Bindable var navigationModel = navigationModel @@ -49,7 +47,6 @@ struct Sidebar: View { .navigationSplitViewColumnWidth(min: 275, ideal: 275) #endif .listStyle(.sidebar) - .statusToolbarItem("Courses", isVisible: isLoadingCourses) #if os(iOS) .toolbar { ToolbarItem(placement: .cancellationAction) { @@ -59,23 +56,6 @@ struct Sidebar: View { } } #endif - .task { - if StorageKeys.needsAuthorization { - navigationModel.showAuthorizationSheet = true - } else { - await loadCourses() - } - } - .refreshable { - await loadCourses() - } - } - - private func loadCourses() async { - isLoadingCourses = true - await courseManager.getCourses() - await profileManager.getCurrentUserAndProfile() - isLoadingCourses = false } } diff --git a/CanvasPlusPlayground/Features/Pinned Items/PinButton.swift b/CanvasPlusPlayground/Features/Pinned Items/PinButton.swift index ba13df4..478b24f 100644 --- a/CanvasPlusPlayground/Features/Pinned Items/PinButton.swift +++ b/CanvasPlusPlayground/Features/Pinned Items/PinButton.swift @@ -11,7 +11,7 @@ struct PinButton: View { @Environment(PinnedItemsManager.self) private var pinnedItemsManager let itemID: String - let courseID: String + let courseID: String? let type: PinnedItem.PinnedItemType var isItemPinned: Bool { diff --git a/CanvasPlusPlayground/Features/Pinned Items/PinnedItemsManager.swift b/CanvasPlusPlayground/Features/Pinned Items/PinnedItemsManager.swift index b378cbf..5db083e 100644 --- a/CanvasPlusPlayground/Features/Pinned Items/PinnedItemsManager.swift +++ b/CanvasPlusPlayground/Features/Pinned Items/PinnedItemsManager.swift @@ -27,9 +27,11 @@ class PinnedItemsManager { func togglePinnedItem( itemID: String, - courseID: String, + courseID: String?, type: PinnedItem.PinnedItemType ) { + guard let courseID else { return } + if pinnedItems.contains(where: { $0.id == itemID && $0.courseID == courseID && $0.type == type }) {