diff --git a/README.md b/README.md index 9e0adc1..ca1b0ca 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ In Project Settings, on the tab "Package Dependencies", click "+" and add + + + + + + + + + diff --git a/Sources/Login/Resources/Media.xcassets/Contents.json b/Sources/Login/Resources/Media.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/Login/Resources/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Login/Resources/en.lproj/Localizable.strings b/Sources/Login/Resources/en.lproj/Localizable.strings index 8764538..2a15f31 100644 --- a/Sources/Login/Resources/en.lproj/Localizable.strings +++ b/Sources/Login/Resources/en.lproj/Localizable.strings @@ -1,4 +1,9 @@ -// MARK: LoginView +// MARK: - General +"ok" = "OK"; +"select" = "Select"; +"done" = "Done"; + +// MARK: - LoginView "account_screen_title" = "Welcome to Artemis!"; "account_screen_subtitle" = "Interactive Learning with Individual Feedback"; "login_please_sign_in_account" = "Please sign in with your %@ account."; @@ -11,20 +16,16 @@ "account_change_artemis_instance_label" = "Select university"; "login_forgot_password_label" = "Did you forget your password?"; "login_username_validation_tum_info_label" = "If you are a TUM student, your username should have the format ab12xyz. Do not include '@mytum.de' or '@tum.de'."; -// captcha + +// MARK: Captcha "account_captcha_title" = "You have entered your password incorrectly too many times :-("; "account_captcha_message" = "Please go to [%@](%@), sign in with your account and solve the [CAPTCHA](%@). After you have solved it, try to log in again here."; "account_captcha_alert_message" = "You entered your password incorrectly. Solve the capture to continue."; -// MARK: InstanceSelectionView -"account_select_artemis_instance_select_text" = "Please select your university:"; +// MARK: - InstanceSelectionView +"account_select_artemis_instance_select_title" = "Please Select Your University"; "account_select_artemis_instance_custom_instance" = "Your custom Artemis instance URL"; "account_select_artemis_instance_error" = "The URL is incorrect or does not link to an Artemis instance!"; -// MARK: Errors +// MARK: - Errors "account_session_expired_error" = "Your session expired. Please login again!"; - -// MARK: General -"ok" = "OK"; -"select" = "Select"; -"done" = "Done"; diff --git a/Sources/Login/Services/LoginService/LoginService.swift b/Sources/Login/Services/LoginService/LoginService.swift index 272b98a..1e47f31 100644 --- a/Sources/Login/Services/LoginService/LoginService.swift +++ b/Sources/Login/Services/LoginService/LoginService.swift @@ -5,7 +5,6 @@ // Created by Sven Andabaka on 09.01.23. // -import Foundation import Common public protocol LoginService { diff --git a/Sources/Login/Services/LoginService/LoginServiceImpl.swift b/Sources/Login/Services/LoginService/LoginServiceImpl.swift index fc4e106..8cb041e 100644 --- a/Sources/Login/Services/LoginService/LoginServiceImpl.swift +++ b/Sources/Login/Services/LoginService/LoginServiceImpl.swift @@ -5,13 +5,12 @@ // Created by Sven Andabaka on 09.01.23. // -import Foundation +import Account import APIClient -import UserStore import Common import PushNotifications -import Account import SharedServices +import UserStore class LoginServiceImpl: LoginService { private let client = APIClient() diff --git a/Sources/Login/ViewModels/LoginViewModel.swift b/Sources/Login/ViewModels/LoginViewModel.swift index d14d4c6..8ea68c7 100644 --- a/Sources/Login/ViewModels/LoginViewModel.swift +++ b/Sources/Login/ViewModels/LoginViewModel.swift @@ -1,10 +1,10 @@ -import Foundation import APIClient -import Common -import UserStore import Combine +import Common +import Foundation import ProfileInfo import SharedModels +import UserStore @MainActor open class LoginViewModel: ObservableObject { @@ -33,7 +33,7 @@ open class LoginViewModel: ObservableObject { @Published public var usernamePattern: String? @Published public var showUsernameWarning = false - @Published public var instituiton: InstitutionIdentifier = .tum + @Published public var institution: InstitutionIdentifier = .tum private var cancellables: Set = Set() @@ -43,14 +43,14 @@ open class LoginViewModel: ObservableObject { self?.username = UserSession.shared.username ?? "" self?.password = UserSession.shared.password ?? "" self?.loginExpired = UserSession.shared.tokenExpired - self?.instituiton = UserSession.shared.institution ?? .tum + self?.institution = UserSession.shared.institution ?? .tum } }.store(in: &cancellables) username = UserSession.shared.username ?? "" password = UserSession.shared.password ?? "" loginExpired = UserSession.shared.tokenExpired - instituiton = UserSession.shared.institution ?? .tum + institution = UserSession.shared.institution ?? .tum } public func login() async { diff --git a/Sources/Login/Views/InstitutionSelectionView.swift b/Sources/Login/Views/InstitutionSelectionView.swift index f093a84..f12a3b3 100644 --- a/Sources/Login/Views/InstitutionSelectionView.swift +++ b/Sources/Login/Views/InstitutionSelectionView.swift @@ -5,42 +5,42 @@ // Created by Sven Andabaka on 01.03.23. // -import SwiftUI -import UserStore import DesignLibrary import ProfileInfo +import SwiftUI +import UserStore public struct InstitutionSelectionView: View { - @Binding var institution: InstitutionIdentifier var handleProfileInfoCompletion: @MainActor (ProfileInfo?) -> Void - public init(institution: Binding, handleProfileInfoCompletion: @escaping @MainActor (ProfileInfo?) -> Void) { + public init( + institution: Binding, + handleProfileInfoCompletion: @escaping @MainActor (ProfileInfo?) -> Void + ) { self._institution = institution self.handleProfileInfoCompletion = handleProfileInfoCompletion } public var body: some View { - List { - Text(R.string.localizable.account_select_artemis_instance_select_text()) - .font(.headline) - ForEach(InstitutionIdentifier.allCases) { institutionIdentifier in - Group { - if case .custom = institutionIdentifier { - CustomInstanceCell(currentInstitution: $institution, - institution: institutionIdentifier, - handleProfileInfoCompletion: handleProfileInfoCompletion) - } else { - InstanceCell(currentInstitution: $institution, - institution: institutionIdentifier, - handleProfileInfoCompletion: handleProfileInfoCompletion) - } + List(InstitutionIdentifier.allCases) { institutionIdentifier in + Group { + if case .custom = institutionIdentifier { + CustomInstanceCell( + currentInstitution: $institution, + institution: institutionIdentifier, + handleProfileInfoCompletion: handleProfileInfoCompletion) + } else { + InstanceCell( + currentInstitution: $institution, + institution: institutionIdentifier, + handleProfileInfoCompletion: handleProfileInfoCompletion) } - .listRowSeparator(.hidden) } + .listRowSeparator(.hidden) } - .listStyle(PlainListStyle()) + .listStyle(.plain) } } @@ -54,7 +54,6 @@ private struct CustomInstanceCell: View { @State private var isLoading = false var institution: InstitutionIdentifier - var handleProfileInfoCompletion: @MainActor (ProfileInfo?) -> Void var body: some View { @@ -75,33 +74,10 @@ private struct CustomInstanceCell: View { .textFieldStyle(ArtemisTextField()) .keyboardType(.URL) .background(Color.gray.opacity(0.2)) - Button(R.string.localizable.select()) { - guard let url = URL(string: customUrl) else { - showErrorAlert = true - return - } - UserSession.shared.saveInstitution(identifier: .custom(url)) - - isLoading = true - - Task { - let result = await ProfileInfoServiceFactory.shared.getProfileInfo() - isLoading = false - switch result { - case .loading: - isLoading = true - case .failure: - showErrorAlert = true - UserSession.shared.saveInstitution(identifier: .tum) - case .done(let response): - handleProfileInfoCompletion(response) - dismiss() - } - } - } - .buttonStyle(ArtemisButton()) - .loadingIndicator(isLoading: $isLoading) - .alert(R.string.localizable.account_select_artemis_instance_error(), isPresented: $showErrorAlert, actions: { }) + Button(R.string.localizable.select(), action: select) + .buttonStyle(ArtemisButton()) + .loadingIndicator(isLoading: $isLoading) + .alert(R.string.localizable.account_select_artemis_instance_error(), isPresented: $showErrorAlert, actions: { }) } .frame(maxWidth: .infinity) .padding(.l) @@ -110,16 +86,42 @@ private struct CustomInstanceCell: View { if case .custom(let url) = institution { customUrl = url?.absoluteString ?? "" } - }.onAppear { + } + .onAppear { if case .custom(let url) = currentInstitution { customUrl = url?.absoluteString ?? "" } } } + + @MainActor + func select() { + guard let url = URL(string: customUrl) else { + showErrorAlert = true + return + } + UserSession.shared.saveInstitution(identifier: .custom(url)) + + isLoading = true + + Task { + let result = await ProfileInfoServiceFactory.shared.getProfileInfo() + isLoading = false + switch result { + case .loading: + isLoading = true + case .failure: + showErrorAlert = true + UserSession.shared.saveInstitution(identifier: .tum) + case .done(let response): + handleProfileInfoCompletion(response) + dismiss() + } + } + } } private struct InstanceCell: View { - @Environment(\.dismiss) var dismiss @Binding var currentInstitution: InstitutionIdentifier @@ -128,7 +130,6 @@ private struct InstanceCell: View { @State private var isLoading = false var institution: InstitutionIdentifier - var handleProfileInfoCompletion: @MainActor (ProfileInfo?) -> Void var body: some View { @@ -148,33 +149,35 @@ private struct InstanceCell: View { .cardModifier() .loadingIndicator(isLoading: $isLoading) .alert(R.string.localizable.account_select_artemis_instance_error(), isPresented: $showErrorAlert, actions: { }) - .onTapGesture { - UserSession.shared.saveInstitution(identifier: institution) - Task { - let result = await ProfileInfoServiceFactory.shared.getProfileInfo() - isLoading = false - switch result { - case .loading: - isLoading = true - case .failure: - showErrorAlert = true - UserSession.shared.saveInstitution(identifier: .tum) - case .done(let response): - handleProfileInfoCompletion(response) - dismiss() - } + .onTapGesture(perform: select) + } + + @MainActor + func select() { + UserSession.shared.saveInstitution(identifier: institution) + Task { + let result = await ProfileInfoServiceFactory.shared.getProfileInfo() + isLoading = false + switch result { + case .loading: + isLoading = true + case .failure: + showErrorAlert = true + UserSession.shared.saveInstitution(identifier: .tum) + case .done(let response): + handleProfileInfoCompletion(response) + dismiss() } } } } -struct InstitutionLogo: View { - +private struct InstitutionLogo: View { var institution: InstitutionIdentifier var body: some View { if institution.logo == nil { - Image("Artemis-Logo") + Image("Artemis-Logo", bundle: .module) .resizable() .scaledToFit() } else { @@ -187,7 +190,7 @@ struct InstitutionLogo: View { .resizable() .scaledToFit() case .failure: - Image("Artemis-Logo") + Image("Artemis-Logo", bundle: .module) .resizable() .scaledToFit() @unknown default: @@ -198,12 +201,10 @@ struct InstitutionLogo: View { } } -extension InstitutionIdentifier { +// MARK: - InstitutionIdentifier+Logo +extension InstitutionIdentifier { var logo: URL? { - switch self { - default: - return URL(string: "public/images/logo.png", relativeTo: self.baseURL) - } + URL(string: "public/images/logo.png", relativeTo: self.baseURL) } } diff --git a/Sources/Login/Views/LoginView.swift b/Sources/Login/Views/LoginView.swift index b2b2500..e0959ed 100644 --- a/Sources/Login/Views/LoginView.swift +++ b/Sources/Login/Views/LoginView.swift @@ -1,7 +1,7 @@ -import Foundation -import SwiftUI import Common import DesignLibrary +import Foundation +import SwiftUI public struct LoginView: View { enum FocusField { @@ -10,20 +10,19 @@ public struct LoginView: View { @StateObject private var viewModel = LoginViewModel() - @State private var showInstituionSelection = false + @State private var isInstitutionSelectionPresented = false @FocusState private var focusedField: FocusField? - public init() { } + public init() {} public var body: some View { GeometryReader { geometry in ScrollView { VStack(spacing: .xl) { - header .padding(.top, .xl) - Text(R.string.localizable.login_please_sign_in_account(viewModel.instituiton.shortName)) + Text(R.string.localizable.login_please_sign_in_account(viewModel.institution.shortName)) .font(.customBody) .multilineTextAlignment(.center) .padding(.top, -.l) @@ -58,27 +57,27 @@ public struct LoginView: View { .toggleStyle(.switch) .tint(Color.Artemis.toggleColor) } - .frame(maxWidth: 520) - .onSubmit { - if focusedField == .username { - focusedField = .password - } else if focusedField == .password { - focusedField = nil - viewModel.isLoading = true - Task { - await viewModel.login() - } + .frame(maxWidth: 520) + .onSubmit { + if focusedField == .username { + focusedField = .password + } else if focusedField == .password { + focusedField = nil + viewModel.isLoading = true + Task { + await viewModel.login() } } - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Spacer() + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() - Button(R.string.localizable.done()) { - focusedField = nil - } + Button(R.string.localizable.done()) { + focusedField = nil } } + } Button(R.string.localizable.login_perform_login_button_text()) { viewModel.isLoading = true @@ -99,14 +98,20 @@ public struct LoginView: View { } Button(R.string.localizable.account_change_artemis_instance_label()) { - showInstituionSelection = true + isInstitutionSelectionPresented = true } - .sheet(isPresented: $showInstituionSelection) { - InstitutionSelectionView(institution: $viewModel.instituiton, - handleProfileInfoCompletion: viewModel.handleProfileInfoReceived) + .sheet(isPresented: $isInstitutionSelectionPresented) { + NavigationStack { + InstitutionSelectionView( + institution: $viewModel.institution, + handleProfileInfoCompletion: viewModel.handleProfileInfoReceived + ) + .navigationTitle(R.string.localizable.account_select_artemis_instance_select_title()) + .navigationBarTitleDisplayMode(.inline) + } } } - .padding(.bottom, .m) + .padding(.bottom, .m) } .padding(.horizontal, .l) .frame(minHeight: geometry.size.height) @@ -114,19 +119,27 @@ public struct LoginView: View { } .scrollDisabled(!viewModel.captchaRequired && focusedField != .password) } - .loadingIndicator(isLoading: $viewModel.isLoading) - .background(Color.Artemis.loginBackgroundColor) - .alert(isPresented: $viewModel.showError, error: viewModel.error, actions: {}) - .alert(isPresented: $viewModel.loginExpired) { - Alert(title: Text(R.string.localizable.account_session_expired_error()), - dismissButton: .default(Text(R.string.localizable.ok()), - action: { viewModel.resetLoginExpired() })) - } - .task { - await viewModel.getProfileInfo() - } + .loadingIndicator(isLoading: $viewModel.isLoading) + .background(Color.Artemis.loginBackgroundColor) + .alert(isPresented: $viewModel.showError, error: viewModel.error, actions: {}) + .alert(isPresented: $viewModel.loginExpired) { + Alert( + title: Text(R.string.localizable.account_session_expired_error()), + dismissButton: .default( + Text(R.string.localizable.ok()), + action: { + viewModel.resetLoginExpired() + } + ) + ) + } + .task { + await viewModel.getProfileInfo() + } } +} +private extension LoginView { var header: some View { VStack(spacing: .l) { Text(R.string.localizable.account_screen_title()) diff --git a/Sources/SharedModels/Conversation/BaseConversation.swift b/Sources/SharedModels/Conversation/BaseConversation.swift index 3328d2f..ec2ad6a 100644 --- a/Sources/SharedModels/Conversation/BaseConversation.swift +++ b/Sources/SharedModels/Conversation/BaseConversation.swift @@ -35,6 +35,7 @@ public protocol BaseConversation: Codable { var unreadMessagesCount: Int? { get set } var isFavorite: Bool? { get } var isHidden: Bool? { get } + var isMuted: Bool? { get } var isCreator: Bool? { get } var isMember: Bool? { get } var numberOfMembers: Int? { get } diff --git a/Sources/SharedModels/Conversation/Channel.swift b/Sources/SharedModels/Conversation/Channel.swift index 7576d3d..bd15176 100644 --- a/Sources/SharedModels/Conversation/Channel.swift +++ b/Sources/SharedModels/Conversation/Channel.swift @@ -18,6 +18,7 @@ public struct Channel: BaseConversation { public var unreadMessagesCount: Int? public var isFavorite: Bool? public var isHidden: Bool? + public var isMuted: Bool? public var isCreator: Bool? public var isMember: Bool? public var numberOfMembers: Int? diff --git a/Sources/SharedModels/Conversation/GroupChat.swift b/Sources/SharedModels/Conversation/GroupChat.swift index a74ccae..9b18abc 100644 --- a/Sources/SharedModels/Conversation/GroupChat.swift +++ b/Sources/SharedModels/Conversation/GroupChat.swift @@ -18,6 +18,7 @@ public struct GroupChat: BaseConversation { public var unreadMessagesCount: Int? public var isFavorite: Bool? public var isHidden: Bool? + public var isMuted: Bool? public var isCreator: Bool? public var isMember: Bool? public var numberOfMembers: Int? diff --git a/Sources/SharedModels/Conversation/OneToOneChat.swift b/Sources/SharedModels/Conversation/OneToOneChat.swift index 37ef8e7..3629496 100644 --- a/Sources/SharedModels/Conversation/OneToOneChat.swift +++ b/Sources/SharedModels/Conversation/OneToOneChat.swift @@ -18,6 +18,7 @@ public struct OneToOneChat: BaseConversation { public var unreadMessagesCount: Int? public var isFavorite: Bool? public var isHidden: Bool? + public var isMuted: Bool? public var isCreator: Bool? public var isMember: Bool? public var numberOfMembers: Int? diff --git a/Sources/SharedModels/Conversation/UnknownConversation.swift b/Sources/SharedModels/Conversation/UnknownConversation.swift index 07500f5..7d676c2 100644 --- a/Sources/SharedModels/Conversation/UnknownConversation.swift +++ b/Sources/SharedModels/Conversation/UnknownConversation.swift @@ -18,6 +18,7 @@ public struct UnknownConversation: BaseConversation { public var unreadMessagesCount: Int? public var isFavorite: Bool? public var isHidden: Bool? + public var isMuted: Bool? public var isCreator: Bool? public var isMember: Bool? public var numberOfMembers: Int?