Skip to content
Merged
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
17 changes: 15 additions & 2 deletions Core/Core/Features/People/APIUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ public struct APIProfile: Codable, Equatable {
public let uuid: String?
public let account_uuid: String?
public let time_zone: String?
public let permissions: APIProfile.Permissions?

public struct Permissions: Codable, Equatable {
public let canUpdateName: Bool?
public let canUpdateAvatar: Bool?

enum CodingKeys: String, CodingKey {
case canUpdateName = "can_update_name"
case canUpdateAvatar = "can_update_avatar"
}
}
}

#if DEBUG
Expand Down Expand Up @@ -244,7 +255,8 @@ extension APIProfile {
k5_user: Bool? = nil,
uuid: String? = nil,
account_uuid: String? = nil,
time_zone: String? = nil
time_zone: String? = nil,
permissions: APIProfile.Permissions? = nil
) -> APIProfile {
return APIProfile(
id: id,
Expand All @@ -259,7 +271,8 @@ extension APIProfile {
k5_user: k5_user,
uuid: uuid,
account_uuid: account_uuid,
time_zone: time_zone
time_zone: time_zone,
permissions: permissions
)
}
}
Expand Down
7 changes: 5 additions & 2 deletions Core/Core/Features/Profile/UserProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public final class UserProfile: NSManagedObject {
@NSManaged public var uuid: String?
@NSManaged public var accountUUID: String?
@NSManaged public var defaultTimeZone: String?
@NSManaged public var canUpdateName: Bool
@NSManaged public var canUpdateAvatar: Bool
}

extension UserProfile: WriteableModel {
Expand All @@ -42,14 +44,15 @@ extension UserProfile: WriteableModel {
model.id = item.id.value
model.name = item.name
model.shortName = item.short_name
model.email = item.primary_email
model.email = item.primary_email ?? model.email
model.locale = item.locale
model.loginID = item.login_id
model.avatarURL = item.avatar_url?.rawValue
model.calendarURL = item.calendar?.ics
model.pronouns = item.pronouns
model.isK5User = (item.k5_user == true)

model.canUpdateName = item.permissions?.canUpdateName ?? model.canUpdateName
model.canUpdateAvatar = item.permissions?.canUpdateAvatar ?? model.canUpdateAvatar
// The "/users/self/profile" api does not return accountUUID and since they share
// the same Core Data entity, it would get overriden with a null value after fetching "/users/self"
if model.uuid == nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,8 @@
<attribute name="accountUUID" optional="YES" attributeType="String"/>
<attribute name="avatarURL" optional="YES" attributeType="URI"/>
<attribute name="calendarURL" optional="YES" attributeType="URI"/>
<attribute name="canUpdateAvatar" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="canUpdateName" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="defaultTimeZone" optional="YES" attributeType="String"/>
<attribute name="email" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
Expand Down
9 changes: 6 additions & 3 deletions Horizon/Horizon/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
},
"Display Name" : {

},
"Display Name can only be changed by your institution." : {

},
"Done" : {

Expand Down Expand Up @@ -369,6 +372,9 @@
},
"Full Name" : {

},
"Full Name can only be changed by your institution." : {

},
"Georgetown (-04:00/-04:00)" : {

Expand Down Expand Up @@ -737,9 +743,6 @@
},
"Regenerate Flashcards" : {

},
"Required" : {

},
"Result: %@" : {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Core

protocol GetUserInteractor {
func getUser() -> AnyPublisher<UserProfile, Error>
func canUpdateName() -> AnyPublisher<Bool, Error>
}

final class GetUserInteractorLive: GetUserInteractor {
Expand All @@ -32,4 +33,11 @@ final class GetUserInteractorLive: GetUserInteractor {
.compactMap { $0.first }
.eraseToAnyPublisher()
}

func canUpdateName() -> AnyPublisher<Bool, Error> {
ReactiveStore(useCase: GetSelfUserIncludingUUID())
.getEntities(ignoreCache: true)
.map { $0.first?.canUpdateName ?? false }
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,11 @@ final class GetUserInteractorPreview: GetUserInteractor {
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}

func canUpdateName() -> AnyPublisher<Bool, any Error> {
Just(true)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,31 @@ struct ProfileView: View {
.padding(.horizontal, .huiSpaces.space24)
.padding(.vertical, .huiSpaces.space48)
}
.alert(isPresented: $viewModel.isAlertErrorPresented) {
Alert(title: Text(viewModel.errorMessage), message: Text(viewModel.errorMessage))
}
}
.overlay { loaderView }
}

@ViewBuilder
private var loaderView: some View {
if viewModel.isLoaderVisible {
ZStack {
Color.huiColors.surface.pageSecondary
.ignoresSafeArea()
HorizonUI.Spinner(size: .small, showBackground: true)
}
}
}
private var nameView: some View {
HorizonUI.TextInput(
$viewModel.name,
label: String(localized: "Full Name", bundle: .horizon),
error: viewModel.nameError,
helperText: !viewModel.canUpdateName
? String(localized: "Full Name can only be changed by your institution.", bundle: .horizon)
: nil,
disabled: viewModel.nameDisabled
)
}
Expand All @@ -59,7 +76,9 @@ struct ProfileView: View {
$viewModel.displayName,
label: String(localized: "Display Name", bundle: .horizon),
error: viewModel.displayNameError,
helperText: String(localized: "Required", bundle: .horizon),
helperText: !viewModel.canUpdateName
? String(localized: "Display Name can only be changed by your institution.", bundle: .horizon)
: nil,
disabled: viewModel.displayNameDisabled
)
}
Expand All @@ -76,7 +95,7 @@ struct ProfileView: View {
private var saveButton: some View {
SavingButton(
title: String(localized: "Save Changes", bundle: .horizon),
isLoading: $viewModel.isLoading,
isLoading: $viewModel.saveLoaderIsVisiable,
isDisabled: $viewModel.isSaveDisabled,
onSave: viewModel.save
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,29 @@ final class ProfileViewModel {
}
}
var nameDisabled: Bool {
isLoading
saveLoaderIsVisiable || !canUpdateName
}
var nameError: String = " "
private(set) var errorMessage: String = ""
private(set) var canUpdateName = true
private(set) var isLoaderVisible = true
var displayName: String = "" {
didSet {
validateDisplayName()
updateSaveDisabled()
}
}
var displayNameDisabled: Bool {
isLoading
saveLoaderIsVisiable || !canUpdateName
}
var displayNameError: String = " "
var email: String = ""
var isSaveDisabled: Bool = true
var isLoading: Bool = true
var saveLoaderIsVisiable: Bool = false

// MARK: - Inputs / Output

var isAlertErrorPresented: Bool = false

// MARK: - Private

Expand All @@ -72,29 +79,34 @@ final class ProfileViewModel {
) {
self.updateUserProfileInteractor = updateUserProfileInteractor

getUserInteractor
.getUser()
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] user in
self?.nameOriginal = user.name
self?.displayNameOriginal = user.shortName ?? ""
self?.email = user.email ?? ""
self?.validate()
self?.isLoading = false
let userProfile = getUserInteractor.getUser()
let userPermission = getUserInteractor.canUpdateName()

Publishers.Zip(userProfile, userPermission)
.sink { [weak self] completion in
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
self?.isAlertErrorPresented = true
}
)
self?.isLoaderVisible = false
} receiveValue: { [weak self] (user, canUpdateName) in
self?.canUpdateName = canUpdateName
self?.nameOriginal = user.name
self?.displayNameOriginal = user.shortName ?? ""
self?.email = user.email ?? ""
self?.validate()
}
.store(in: &subscriptions)
}

// MARK: - Input Actions

func save() {
isLoading = true
saveLoaderIsVisiable = true
updateUserProfileInteractor.set(name: name, shortName: displayName)
.sink(
receiveCompletion: { [weak self] _ in
self?.isLoading = false
self?.saveLoaderIsVisiable = false
},
receiveValue: { [weak self] userProfile in
self?.nameOriginal = userProfile.name
Expand Down