Skip to content

Commit

Permalink
General: Release (#80)
Browse files Browse the repository at this point in the history
* `Development`: Preview message detail view (#77)

* Format

* Preview MessageCell

* Rename showHeader: isHeaderVisible

* BaseMessage+IsContinuation

* Make ConversationViewModel internal

* Fix warning:

- Reference to property 'stompClient' in closure requires explicit use of 'self' to make capture semantics explicit; this is an error in Swift 6

* Preview ReactionsView

* Preview ConversationDaySection

* Preview MessageDetailView

* `Communication`: Usability of the conversation and thread views (#69)

* Refine MessageCell

* Align reaction leading

* Apply system to send message overlay

* Highlight message, not author

* Pass isEmojiPickerButtonVisible through the environment

* Hide image by height, not opacity

* Inflect "reply"

* `Communication`: Support user mentions and channel references (#47)

* Format

* Update overlay modifier

- Deprecated: https://developer.apple.com/documentation/swiftui/view/overlay(_:alignment:)

* Fix runtime warning:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

* Use getCourseMembers(courseId:searchLoginOrName:) API

* Fix warning:

- Deprecated: https://developer.apple.com/documentation/uikit/uiapplication/1622918-applicationiconbadgenumber

* Add getChannelsPublicOverview

* Duplicate SendMessageMemberPickerModel

* Move SendMessageView

* Improve search

* Dependency injection

* [R]un when this view initially appears

* Stick to design system

* Rename show*: is*Presented

* Create SendMessageViewModel

* Make Observables final

* Move is[Modal]Presented

* Test SendMessageViewModel

* `Communication`: Navigate to exercises and lectures (#81)

* Fix sheet

* Add OpenURLAction

* Check host

* Inject UserSession

* Split NavigationController

* Format

* `Exercise`: Add pull-to-refresh to ExerciseListView and ExerciseDetailView (#78)

* initial pull-to-refresh feature

* update apollon-ios-module dependency

* `Modeling Exercise`: Improve Submit Button (#79)

* add alert after submitting diagram

* update Apollon-iOS-Module version

* Improve submit button with colors

* `Development`: Refactor ExerciseView (#82)

* Make SendMessageViewModel primary

* Initialize view model at caller

* Move sendMessageType

* Move isEditMode: isEditing

* Format

* Distinguish presentation

* Organize ConversationViewModel

* Extract button action

* Inject dependencies

* Remove conversationViewModel dependency

* Create SendMessageViewModelDelegate; separate isLoading

* Fix warning:

- redundant_type_annotation

* `Communication`: Restore draft message (#83)

* Rename folder

* Create schema

* Fix runtime:

Object 0x600000494860 of class AnyRepository deallocated with non-zero retain count 2. This object's deinit, or something called from it, may have created a strong reference to self which outlived deinit, resulting in a dangling reference.

* Store context

* Create MessagesRepository

* Fix issue:

#83 (comment)

* Add inverse relationship:

https://www.hackingwithswift.com/quick-start/swiftdata/how-to-create-one-to-many-relationships

* Change url to host; fix error:

testRoundtrip(): failed: caught error: "SwiftDataError(_error: SwiftData.SwiftDataError._Error.unsupportedPredicate)"

* Insert course model

* Add message model

* performOnDisappear

* log verbose begin context access

* Rename sendMessageType: configuration

---------

Co-authored-by: Alexander Görtzen <[email protected]>
  • Loading branch information
nityanandaz and AlexanderG2207 authored Mar 4, 2024
1 parent ed4fc47 commit 84de14d
Show file tree
Hide file tree
Showing 44 changed files with 2,419 additions and 993 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ls1intum/apollon-ios-module",
"state" : {
"revision" : "1690e711415330b28e836cd8035e1805c0a4e479",
"version" : "1.0.2"
"revision" : "73a3999b4cdcdd0ae2b86426d65a7b75c6ac3af0",
"version" : "1.0.6"
}
},
{
"identity" : "artemis-ios-core-modules",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ls1intum/artemis-ios-core-modules",
"state" : {
"revision" : "b5b5a7282691d27ea121aadc08b89369f3c8d566",
"version" : "9.0.0"
"revision" : "b14fec4f95b78587c9fa107353d0bca0895288b0",
"version" : "9.1.1"
}
},
{
Expand Down Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "3ec0ab0bca4feb56e8b33e289c9496e89059dd08",
"version" : "7.10.2"
"revision" : "5b92f029fab2cce44386d28588098b5be0824ef5",
"version" : "7.11.0"
}
},
{
Expand Down
6 changes: 4 additions & 2 deletions ArtemisKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let package = Package(
.package(url: "https://github.com/daltoniam/Starscream.git", exact: "4.0.4"),
.package(url: "https://github.com/Kelvas09/EmojiPicker.git", from: "1.0.0"),
.package(url: "https://github.com/ls1intum/apollon-ios-module", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", .upToNextMajor(from: "9.0.0")),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", .upToNextMajor(from: "9.1.0")),
.package(url: "https://github.com/mac-cain13/R.swift.git", from: "7.0.0")
],
targets: [
Expand Down Expand Up @@ -125,6 +125,8 @@ let package = Package(
]),
.testTarget(
name: "ArtemisKitTests",
dependencies: [])
dependencies: [
"Messages"
])
]
)
95 changes: 49 additions & 46 deletions ArtemisKit/Sources/ArtemisKit/RootView.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import SwiftUI
import Login
import Dashboard
import Common
import CourseRegistration
import CourseView
import Dashboard
import Login
import Messages
import Navigation
import PushNotifications
import Common
import Messages
import SwiftUI

public struct RootView: View {

Expand All @@ -28,47 +28,7 @@ public struct RootView: View {
if viewModel.didSetupNotifications {
NavigationStack(path: $navigationController.path) {
DashboardView()
.navigationDestination(for: CoursePath.self) { coursePath in
CourseView(courseId: coursePath.id)
.id(coursePath.id)
}
// Sadly the following navigationDestination have to be here since SwiftUI is ...
.navigationDestination(for: ExercisePath.self) { exercisePath in
if let course = exercisePath.coursePath.course,
let exercise = exercisePath.exercise {
ExerciseDetailView(course: course, exercise: exercise)
} else {
ExerciseDetailView(courseId: exercisePath.coursePath.id, exerciseId: exercisePath.id)
}
}
.navigationDestination(for: LecturePath.self) { lecturePath in
if let course = lecturePath.coursePath.course {
LectureDetailView(course: course, lectureId: lecturePath.id)
} else {
LectureDetailView(courseId: lecturePath.coursePath.id, lectureId: lecturePath.id)
}
}
.navigationDestination(for: MessagePath.self) { messagePath in
if let message = messagePath.message,
let conversationViewModel = messagePath.conversationViewModel as? ConversationViewModel {
MessageDetailView(viewModel: conversationViewModel,
message: message)
} else {
MessageDetailView(viewModel: ConversationViewModel(courseId: messagePath.coursePath.id,
conversationId: messagePath.conversationPath.id),
messageId: messagePath.id)
}
}
.navigationDestination(for: ConversationPath.self) { conversationPath in
if let conversation = conversationPath.conversation,
let course = conversationPath.coursePath.course {
ConversationView(course: course,
conversation: conversation)
} else {
ConversationView(courseId: conversationPath.coursePath.id,
conversationId: conversationPath.id)
}
}
.modifier(NavigationDestinationRootViewModifier())
}
.onChange(of: navigationController.path) {
log.debug("NavigationController count: \(navigationController.path.count)")
Expand All @@ -77,6 +37,13 @@ public struct RootView: View {
.onOpenURL { url in
DeeplinkHandler.shared.handle(url: url)
}
.environment(\.openURL, OpenURLAction { url in
if DeeplinkHandler.shared.handle(url: url) {
return .handled
} else {
return .systemAction
}
})
} else {
PushNotificationSetupView()
}
Expand All @@ -97,3 +64,39 @@ public struct RootView: View {
})
}
}

private struct NavigationDestinationRootViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.navigationDestination(for: CoursePath.self) { coursePath in
CourseView(courseId: coursePath.id)
.id(coursePath.id)
}
.navigationDestination(for: ExercisePath.self) { exercisePath in
if let course = exercisePath.coursePath.course,
let exercise = exercisePath.exercise {
ExerciseDetailView(course: course, exercise: exercise)
} else {
ExerciseDetailView(courseId: exercisePath.coursePath.id, exerciseId: exercisePath.id)
}
}
.navigationDestination(for: LecturePath.self) { lecturePath in
if let course = lecturePath.coursePath.course {
LectureDetailView(course: course, lectureId: lecturePath.id)
} else {
LectureDetailView(courseId: lecturePath.coursePath.id, lectureId: lecturePath.id)
}
}
.navigationDestination(for: ConversationPath.self) { conversationPath in
if let conversation = conversationPath.conversation,
let course = conversationPath.coursePath.course {
ConversationView(course: course,
conversation: conversation)
} else {
ConversationView(courseId: conversationPath.coursePath.id,
conversationId: conversationPath.id)
}
}
.modifier(NavigationDestinationThreadViewModifier())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CourseRegistrationServiceImpl: CourseRegistrationService {
let result = await client.sendRequest(RegisterCourseRequest(courseId: courseId))

switch result {
case .success((let response, _)):
case .success:
return .success
case .failure(let error):
return .failure(error: UserFacingError(error: error))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ModelingExerciseViewModel: BaseViewModel {
}
}

func submitSubmission() async {
func submitSubmission() async throws {
guard var submitSubmission = submission as? ModelingSubmission, let umlModel else {
return
}
Expand All @@ -117,6 +117,7 @@ class ModelingExerciseViewModel: BaseViewModel {
try await exerciseService.updateSubmission(exerciseId: exercise.id, submission: submitSubmission)
} catch {
log.error(String(describing: error))
throw error
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import DesignLibrary

struct EditModelingExerciseView: View {
@StateObject var modelingViewModel: ModelingExerciseViewModel
@State private var showSubmissionAlert = false
@State private var isSubmissionSuccessful = false

init(exercise: Exercise, participationId: Int, problemStatementURL: URLRequest) {
self._modelingViewModel = StateObject(wrappedValue: ModelingExerciseViewModel(exercise: exercise,
Expand Down Expand Up @@ -47,27 +49,70 @@ struct EditModelingExerciseView: View {
if !modelingViewModel.diagramTypeUnsupported {
HStack {
ProblemStatementButton(modelingViewModel: modelingViewModel)
SubmitButton(modelingViewModel: modelingViewModel)
SubmitButton(modelingViewModel: modelingViewModel, showSubmissionAlert: $showSubmissionAlert, isSubmissionSuccessful: $isSubmissionSuccessful)
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.alert(isPresented: $showSubmissionAlert) {
if isSubmissionSuccessful {
return Alert(
title: Text(R.string.localizable.successfulSubmissionAlertTitle()),
message: Text(R.string.localizable.successfulSubmissionAlertMessage())
)
} else {
return Alert(
title: Text(R.string.localizable.failedSubmissionAlertTitle()),
message: Text(R.string.localizable.failedSubmissionAlertMessage())
)
}
}
}
}

struct SubmitButton: View {
@ObservedObject var modelingViewModel: ModelingExerciseViewModel
@Binding var showSubmissionAlert: Bool
@Binding var isSubmissionSuccessful: Bool
@State private var isSubmitting = false

var body: some View {
Button {
Task {
await modelingViewModel.submitSubmission()
}
submit()
} label: {
Text(R.string.localizable.submitSubmission())
}.buttonStyle(ArtemisButton())
ZStack {
Text(R.string.localizable.submitSubmission())
.opacity(isSubmitting ? 0 : 1)
// Show a Progress View, whilst the submision is being submitted
if isSubmitting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.Artemis.primaryButtonTextColor))
}
}
}
.buttonStyle(ArtemisButton(buttonColor: showSubmissionAlert ?
(isSubmissionSuccessful ? Color.Artemis.resultSuccess : Color.Artemis.resultFailedColor) :
Color.Artemis.primaryButtonColor,
buttonTextColor: Color.Artemis.primaryButtonTextColor))
.disabled(isSubmitting)
}

private func submit() {
isSubmitting = true
Task {
do {
try await modelingViewModel.submitSubmission()
isSubmissionSuccessful = true
} catch {
isSubmissionSuccessful = false
}
withAnimation {
isSubmitting = false
showSubmissionAlert.toggle()
}
}
}
}

Expand Down
17 changes: 12 additions & 5 deletions ArtemisKit/Sources/CourseView/ExerciseTab/ExerciseDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public struct ExerciseDetailView: View {

private var showFeedbackButton: Bool {
switch exercise.value {
case .fileUpload, .modeling, .programming, .text:
case .fileUpload, .programming, .text:
return true
default:
return false
Expand Down Expand Up @@ -240,16 +240,23 @@ public struct ExerciseDetailView: View {
.task {
await loadExercise()
}
.refreshable {
await refreshExercise()
}
}

private func loadExercise() async {
if let exercise = exercise.value {
setParticipationAndResultId(from: exercise)
} else {
self.exercise = await ExerciseServiceFactory.shared.getExercise(exerciseId: exerciseId)
if let exercise = self.exercise.value {
setParticipationAndResultId(from: exercise)
}
await refreshExercise()
}
}

private func refreshExercise() async {
self.exercise = await ExerciseServiceFactory.shared.getExercise(exerciseId: exerciseId)
if let exercise = self.exercise.value {
setParticipationAndResultId(from: exercise)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ struct ExerciseListView: View {
}
}
}
.refreshable {
if let courseId = viewModel.course.value?.id {
await viewModel.loadCourse(id: courseId)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
"difficulty" = "Difficulty:";
"categories" = "Categories:";

// Modeling Submission Alerts
"successfulSubmissionAlertTitle" = "Your submission was successful!";
"successfulSubmissionAlertMessage" = "You can change your submission or wait for your feedback.";
"failedSubmissionAlertTitle" = "An error occurred...";
"failedSubmissionAlertMessage" = "Please try again later.";

// Modeling Feedback
"modelingFeedbackElement" = "Element";
"modelingFeedbackPoints" = "Points";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// BaseMessage+IsContinuation.swift
//
//
// Created by Nityananda Zbil on 07.02.24.
//

import Foundation
import SharedModels

// swiftlint:disable:next identifier_name
private let MAX_MINUTES_FOR_GROUPING_MESSAGES = 5

extension BaseMessage {
/// Whether the same author messaged multiple times within 5 minutes.
func isContinuation(of message: some BaseMessage) -> Bool {
guard author == message.author,
let lhs = creationDate,
let rhs = message.creationDate else {
return false
}

return lhs < rhs.addingTimeInterval(TimeInterval(MAX_MINUTES_FOR_GROUPING_MESSAGES * 60))
}
}
11 changes: 11 additions & 0 deletions ArtemisKit/Sources/Messages/Models/ChannelIdAndNameDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// ChannelIdAndNameDTO.swift
//
//
// Created by Nityananda Zbil on 02.12.23.
//

struct ChannelIdAndNameDTO: Codable, Identifiable {
let id: Int
let name: String
}
13 changes: 13 additions & 0 deletions ArtemisKit/Sources/Messages/Models/Schema/Schema.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Schema.swift
//
//
// Created by Nityananda Zbil on 29.02.24.
//

// Alias for the most recent schema

typealias ServerModel = SchemaV1.Server
typealias CourseModel = SchemaV1.Course
typealias ConversationModel = SchemaV1.Conversation
typealias MessageModel = SchemaV1.Message
Loading

0 comments on commit 84de14d

Please sign in to comment.