Skip to content

Commit 49b9112

Browse files
feat: add multi course invitation
1 parent a343b80 commit 49b9112

File tree

3 files changed

+76
-52
lines changed

3 files changed

+76
-52
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// This file is part of Canvas.
3+
// Copyright (C) 2025-present Instructure, Inc.
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU Affero General Public License as
7+
// published by the Free Software Foundation, either version 3 of the
8+
// License, or (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU Affero General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU Affero General Public License
16+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
//
18+
19+
import Foundation
20+
21+
struct InvistedCourse: Identifiable, Equatable {
22+
let id: String
23+
let name: String
24+
let enrollmentID: String
25+
}

Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,46 +32,41 @@ struct DashboardView: View {
3232
}
3333

3434
var body: some View {
35-
InstUI.BaseScreen(
36-
state: viewModel.state,
37-
config: .init(
38-
refreshable: true,
39-
loaderBackgroundColor: .huiColors.surface.pagePrimary
40-
),
41-
refreshAction: viewModel.reload
42-
) { _ in
43-
LazyVStack(spacing: .zero) {
44-
if viewModel.courses.isEmpty, viewModel.state == .data {
45-
Text("You aren’t currently enrolled in a course.", bundle: .horizon)
46-
.padding(.huiSpaces.space24)
47-
.frame(maxWidth: .infinity, alignment: .leading)
48-
.foregroundStyle(Color.huiColors.text.body)
49-
.huiTypography(.h3)
35+
VStack(spacing: .zero) {
36+
if viewModel.state == .data {
37+
invitedCoursesView
38+
}
39+
InstUI.BaseScreen(
40+
state: viewModel.state,
41+
config: .init(
42+
refreshable: true,
43+
loaderBackgroundColor: .huiColors.surface.pagePrimary
44+
),
45+
refreshAction: viewModel.reload
46+
) { _ in
47+
LazyVStack(spacing: .zero) {
48+
if viewModel.courses.isEmpty, viewModel.state == .data {
49+
Text("You aren’t currently enrolled in a course.", bundle: .horizon)
50+
.padding(.huiSpaces.space24)
51+
.frame(maxWidth: .infinity, alignment: .leading)
52+
.foregroundStyle(Color.huiColors.text.body)
53+
.huiTypography(.h3)
5054

51-
} else {
52-
contentView(courses: viewModel.courses)
53-
.padding(.bottom, .huiSpaces.space16)
55+
} else {
56+
contentView(courses: viewModel.courses)
57+
.padding(.bottom, .huiSpaces.space16)
58+
}
5459
}
5560
}
5661
}
5762
.toolbar(.hidden)
5863
.safeAreaInset(edge: .top, spacing: .zero) { navigationBar }
5964
.scrollIndicators(.hidden, axes: .vertical)
6065
.background(Color.huiColors.surface.pagePrimary)
66+
.animation(.smooth, value: viewModel.invitedCourses)
6167
.alert(isPresented: $viewModel.isAlertPresented) {
6268
Alert(title: Text("Something went wrong", bundle: .horizon), message: Text(viewModel.errorMessage))
6369
}
64-
.huiToast(
65-
viewModel: .init(
66-
text: viewModel.toastTitle,
67-
style: .info,
68-
dismissAfter: nil,
69-
confirmActionButton: .init(
70-
title: String(localized: "Accept", bundle: .horizon),
71-
action: { viewModel.acceptInvitation() })
72-
),
73-
isPresented: $viewModel.toastIsPresented
74-
)
7570
}
7671

7772
private func contentView(courses: [DashboardCourse]) -> some View {
@@ -163,9 +158,20 @@ struct DashboardView: View {
163158
.background(Color.huiColors.surface.pagePrimary)
164159
}
165160

166-
private var nameLabel: some View {
167-
Text(viewModel.title)
168-
.huiTypography(.p1)
161+
private var invitedCoursesView: some View {
162+
ForEach(viewModel.invitedCourses) { course in
163+
HorizonUI
164+
.Toast(
165+
viewModel: .init(
166+
text: course.name,
167+
style: .info,
168+
dismissAfter: nil,
169+
confirmActionButton: .init(
170+
title: String(localized: "Accept", bundle: .horizon),
171+
action: { viewModel.acceptInvitation(course: course) })
172+
)) { viewModel.declineInvitation(course: course) }
173+
.padding(.huiSpaces.space12)
174+
}
169175
}
170176
}
171177

Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ class DashboardViewModel {
2626
// MARK: - Outputs
2727

2828
private(set) var state: InstUI.ScreenState = .loading
29-
private(set) var toastTitle = ""
3029
private(set) var errorMessage = ""
3130
var title: String = ""
32-
var courses: [DashboardCourse] = []
33-
31+
private(set) var courses: [DashboardCourse] = []
32+
private(set) var invitedCourses: [InvistedCourse] = []
3433
// MARK: - Input / Outputs
3534

36-
var toastIsPresented = false
3735
var isAlertPresented = false
3836

3937
// MARK: - Dependencies
@@ -46,7 +44,6 @@ class DashboardViewModel {
4644
private var getDashboardCoursesCancellable: AnyCancellable?
4745
private var refreshCompletedModuleItemCancellable: AnyCancellable?
4846
private var subscriptions = Set<AnyCancellable>()
49-
private var invitedCourse: DashboardCourse?
5047

5148
// MARK: - Init
5249

@@ -76,12 +73,9 @@ class DashboardViewModel {
7673
getDashboardCoursesCancellable = getCoursesInteractor.getDashboardCourses(ignoreCache: ignoreCache)
7774
.sink { [weak self] items in
7875
self?.courses = items.filter({ $0.state == DashboardCourse.EnrollmentState.active.rawValue })
79-
if let invitedCourse = items.first(where: { $0.state == DashboardCourse.EnrollmentState.invited.rawValue }) {
80-
let message = String(localized: "You have been invited to join", bundle: .horizon)
81-
self?.toastTitle = "\(message) \(invitedCourse.name)"
82-
self?.toastIsPresented = true
83-
self?.invitedCourse = invitedCourse
84-
}
76+
let invitedCourses = items.filter({ $0.state == DashboardCourse.EnrollmentState.invited.rawValue })
77+
let message = String(localized: "You have been invited to join", bundle: .horizon)
78+
self?.invitedCourses = invitedCourses.map { .init(id: $0.courseId, name: "\(message) \($0.name)", enrollmentID: $0.enrollmentID) }
8579
self?.state = .data
8680
completion?()
8781
}
@@ -112,14 +106,11 @@ class DashboardViewModel {
112106
router.route(to: "/courses/\(id)", from: viewController)
113107
}
114108

115-
func acceptInvitation() {
116-
guard let invitedCourse else {
117-
return
118-
}
109+
func acceptInvitation(course: InvistedCourse) {
119110
state = .loading
120111
let useCase = HandleCourseInvitation(
121-
courseID: invitedCourse.courseId,
122-
enrollmentID: invitedCourse.enrollmentID,
112+
courseID: course.id,
113+
enrollmentID: course.enrollmentID,
123114
isAccepted: true
124115
)
125116
ReactiveStore(useCase: useCase)
@@ -129,17 +120,19 @@ class DashboardViewModel {
129120
self?.state = .data
130121
self?.errorMessage = error.localizedDescription
131122
self?.isAlertPresented = true
132-
self?.toastIsPresented = false
133123
}
134124

135125
}, receiveValue: { [weak self] _ in
136126
self?.reload(completion: {})
137-
self?.toastIsPresented = false
138-
127+
self?.declineInvitation(course: course)
139128
})
140129
.store(in: &subscriptions)
141130
}
142131

132+
func declineInvitation(course: InvistedCourse) {
133+
invitedCourses.removeAll(where: { $0.id == course.id } )
134+
}
135+
143136
func reload(completion: @escaping () -> Void) {
144137
getCourses(
145138
ignoreCache: true,

0 commit comments

Comments
 (0)