Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Canvas.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public struct APIDiscussionTopic: Codable, Equatable {
public let subscription_hold: String?
public var title: String?
public let unread_count: Int?
public let read_state: String?
public let is_checkpointed: Bool?
public let reply_to_entry_required_count: Int?
public let has_sub_assignments: Bool?
Expand Down Expand Up @@ -117,6 +118,7 @@ extension APIDiscussionTopic {
is_checkpointed: Bool? = nil,
reply_to_entry_required_count: Int? = nil,
has_sub_assignments: Bool? = nil,
read_state: String? = "unread",
checkpoints: [APIAssignmentCheckpoint]? = nil
) -> APIDiscussionTopic {
return APIDiscussionTopic(
Expand Down Expand Up @@ -155,6 +157,7 @@ extension APIDiscussionTopic {
subscription_hold: subscription_hold,
title: title,
unread_count: unread_count,
read_state: read_state,
is_checkpointed: is_checkpointed,
reply_to_entry_required_count: reply_to_entry_required_count,
has_sub_assignments: has_sub_assignments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public final class DiscussionTopic: NSManagedObject, WriteableModel {
@NSManaged public var sortByRating: Bool
@NSManaged public var subscribed: Bool
@NSManaged public var title: String?
@NSManaged public var readState: String
@NSManaged public var unreadCount: Int

// Checkpoints
Expand Down Expand Up @@ -88,6 +89,8 @@ public final class DiscussionTopic: NSManagedObject, WriteableModel {
String.localizedStringWithFormat(String(localized: "%d Unread", bundle: .core), unreadCount)
}

public var isRead: Bool { readState == "read" }

@discardableResult
public static func save(_ item: APIDiscussionTopic, apiPosition: Int = 0, in context: NSManagedObjectContext) -> DiscussionTopic {
let model = save(item, in: context)
Expand Down Expand Up @@ -172,7 +175,7 @@ public final class DiscussionTopic: NSManagedObject, WriteableModel {
model.subscribed = item.subscribed == true
model.title = item.title
model.unreadCount = item.unread_count ?? 0

model.readState = item.read_state.defaultToEmpty
model.updateCheckpoints(from: item, in: context)
return model
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Foundation
public protocol ComposeMessageInteractor {
// MARK: - Outputs
var attachments: CurrentValueSubject<[File], Never> { get }
var didUploadFiles: PassthroughSubject<Result<Void, Error>, Never> { get }

// MARK: - Inputs
func createConversation(parameters: MessageParameters) -> Future<URLResponse?, Error>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Foundation
public class ComposeMessageInteractorLive: ComposeMessageInteractor {
// MARK: - Outputs
public let attachments = CurrentValueSubject<[File], Never>([])
public let didUploadFiles = PassthroughSubject<Result<Void, Error>, Never>()

// MARK: - Private
private let context: Context = .currentUser
Expand Down Expand Up @@ -56,7 +57,10 @@ public class ComposeMessageInteractorLive: ComposeMessageInteractor {
self.restrictForFolderPath = restrictForFolderPath
self.scheduler = scheduler
self.publisherProvider = publisherProvider

uploadManager
.didUploadFile
.subscribe(didUploadFiles)
.store(in: &subscriptions)
fileStore.refresh()

setupAttachmentListBinding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Foundation
public class ComposeMessageInteractorPreview: ComposeMessageInteractor {
public var attachments = CurrentValueSubject<[File], Never>([])
public var conversationAttachmentsFolder = CurrentValueSubject<[Folder], Never>([])
public var didUploadFiles = PassthroughSubject<Result<Void, any Error>, Never>()

private var addFileWithURLCalled = false
private var addFileWithFileCalled = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import Combine
import Foundation

class InboxMessageInteractorPreview: InboxMessageInteractor {
public class InboxMessageInteractorPreview: InboxMessageInteractor {
// MARK: - Outputs
public let state = CurrentValueSubject<StoreState, Never>(.loading)
public let messages: CurrentValueSubject<[InboxMessageListItem], Never>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@
<attribute name="position" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="postedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="published" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="readState" attributeType="String"/>
<attribute name="requiredReplyCountRaw" optional="YES" attributeType="Integer 64" usesScalarValueType="YES"/>
<attribute name="requireInitialPost" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="sortByRating" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,8 @@ class ComposeMessageViewModelTests: CoreTestCase {
}

private class ComposeMessageInteractorMock: ComposeMessageInteractor {
var didUploadFiles = PassthroughSubject<Result<Void, Error>, Never>()

var attachments = CurrentValueSubject<[Core.File], Never>([])

var isSuccessfulMockFuture = true
Expand Down
58 changes: 58 additions & 0 deletions Horizon/Horizon/Sources/Common/View/LoaderOverlayModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import HorizonUI
import SwiftUI

private struct LoaderOverlayModifier: ViewModifier {
let isVisible: Bool
let accessibilityLabel: String

func body(content: Content) -> some View {
content
.overlay {
if isVisible {
ZStack {
// Need padding to make the back button visiable
Color.huiColors.surface.pageSecondary
.padding(.top, 100)

HorizonUI.Spinner(
size: .small,
showBackground: true
)
.accessibilityLabel(accessibilityLabel)
}
}
}
}
}

extension View {
func huiLoader(
isVisible: Bool,
accessibilityLabel: String = String(localized: "Loading")
) -> some View {
modifier(
LoaderOverlayModifier(
isVisible: isVisible,
accessibilityLabel: accessibilityLabel
)
)
}
}
33 changes: 33 additions & 0 deletions Horizon/Horizon/Sources/Common/View/RoundedTopCorners.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import HorizonUI
import SwiftUI

extension View {
func roundedTopCorners(
radius: CGFloat = HorizonUI.CornerRadius.level4.attributes.radius
) -> some View {
clipShape(
.rect(
topLeadingRadius: radius,
topTrailingRadius: radius
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ enum AnnouncementsWidgetAssembly {
return AnnouncementsListWidgetView(viewModel: viewModel)
}

private static func makeInteractor() -> NotificationInteractor {
private static func makeInteractor() -> AnnouncementInteractor {
let formatter = NotificationFormatterLive()
let interactor = NotificationInteractorLive(
let interactor = AnnouncementInteractorLive(
userID: AppEnvironment.shared.currentSession?.userID ?? "",
formatter: formatter
isIncludePast: false,
learnCoursesInteractor: GetLearnCoursesInteractorLive()
)
return interactor
}

#if DEBUG
static func makePreview() -> AnnouncementsListWidgetView {
let interactor = NotificationInteractorPreview()
let interactor = AnnouncementInteractorPreview()
let viewModel = AnnouncementsListWidgetViewModel(
interactor: interactor,
router: AppEnvironment.shared.router
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// This file is part of Canvas.
// Copyright (C) 2026-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Core
import Foundation

struct AnnouncementModel: Equatable, RelativeDateRepresentable {
let id: String
let title: String
let content: String
var courseID: String?
var courseName: String?
let date: Date?
let isRead: Bool
let isGlobal: Bool

init(
id: String,
title: String,
content: String,
courseID: String? = nil,
courseName: String? = nil,
date: Date?,
isRead: Bool,
isGlobal: Bool
) {
self.id = id
self.title = title
self.content = content
self.courseID = courseID
self.courseName = courseName
self.date = date
self.isRead = isRead
self.isGlobal = isGlobal
}

init(entity: AccountNotification) {
self.id = entity.id
self.content = entity.message
self.title = entity.subject
self.date = entity.startAt
self.isRead = entity.closed
self.isGlobal = true
}

init(entity: DiscussionTopic, courses: [LearnCourse]) {
self.id = entity.id
self.courseID = entity.courseID
self.title = entity.title.defaultToEmpty
self.content = entity.message.defaultToEmpty
self.date = entity.postedAt
self.isRead = entity.isRead
self.isGlobal = false
self.courseName = courses.first(where: { $0.id == entity.courseID })?.name
}

var accessibilityCourseName: String {
String.localizedStringWithFormat(String(localized: "Course %@", bundle: .horizon), courseName.defaultToEmpty)
}

var accessibilityDate: String {
String.localizedStringWithFormat(String(localized: "Date %@", bundle: .horizon), dateFormatted)
}

var accessibilityTitle: String {
String.localizedStringWithFormat(String(localized: "Title %@", bundle: .horizon), title)
}

static let mock: Self = .init(
id: "1",
title: "Title 1",
content: "",
date: Date(),
isRead: false,
isGlobal: false
)
}
Loading