Skip to content

Commit

Permalink
Prevent stacking of toast messages (#460)
Browse files Browse the repository at this point in the history
  • Loading branch information
pinarol authored Oct 4, 2024
1 parent 7019478 commit 784e8a6
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 15 deletions.
6 changes: 6 additions & 0 deletions Sources/GravatarUI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@

/* The title of the dismiss button on a confirmation dialog. */
"AvatarPicker.Dismiss.title" = "Dismiss";

/* This confirmation message shows when the user picks a different avatar. */
"AvatarPickerViewModel.Update.Success" = "Avatar updated! It may take a few minutes to appear everywhere.";

/* This error message shows when the user attempts to pick a different avatar and fails. */
"AvatarPickerViewModel.Update.Fail" = "Oops, something didn't quite work out while trying to change your avatar.";
27 changes: 16 additions & 11 deletions Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class AvatarPickerViewModel: ObservableObject {
}
}

private var avatarSelectionTask: Task<Avatar?, Never>?
private var avatarSelectionTask: Task<Void, Never>?
private var authToken: String?
private var selectedAvatarResult: Result<String, Error>? {
didSet {
Expand Down Expand Up @@ -75,24 +75,22 @@ class AvatarPickerViewModel: ObservableObject {
}
}

func selectAvatar(with id: String) async -> Avatar? {
func selectAvatar(with id: String) {
guard
let email,
let authToken,
grid.selectedAvatar?.id != id,
grid.model(with: id)?.state == .loaded
else { return nil }
else { return }

avatarSelectionTask?.cancel()

avatarSelectionTask = Task {
await postAvatarSelection(with: id, authToken: authToken, identifier: .email(email))
}

return await avatarSelectionTask?.value
}

func postAvatarSelection(with avatarID: String, authToken: String, identifier: ProfileIdentifier) async -> Avatar? {
func postAvatarSelection(with avatarID: String, authToken: String, identifier: ProfileIdentifier) async {
defer {
grid.setState(to: .loaded, onAvatarWithID: avatarID)
}
Expand All @@ -101,18 +99,15 @@ class AvatarPickerViewModel: ObservableObject {

do {
let response = try await profileService.selectAvatar(token: authToken, profileID: identifier, avatarID: avatarID)

toastManager.showToast("Avatar updated! It may take a few minutes to appear everywhere.", type: .info)
toastManager.showToast(Localized.avatarUpdateSuccess, type: .info)

selectedAvatarResult = .success(response.imageId)
return response
} catch APIError.responseError(let reason) where reason.cancelled {
// NoOp.
} catch {
toastManager.showToast("Oops, something didn't quite work out while trying to change your avatar.", type: .error)
toastManager.showToast(Localized.avatarUpdateFail, type: .error)
grid.selectAvatar(withID: selectedAvatarResult?.value())
}
return nil
}

func fetchAvatars() async {
Expand Down Expand Up @@ -257,6 +252,16 @@ extension AvatarPickerViewModel {
value: "Oops, there was an error uploading the image.",
comment: "A generic error message to show on an error dialog when the upload fails."
)
static let avatarUpdateSuccess = SDKLocalizedString(
"AvatarPickerViewModel.Update.Success",
value: "Avatar updated! It may take a few minutes to appear everywhere.",
comment: "This confirmation message shows when the user picks a different avatar."
)
static let avatarUpdateFail = SDKLocalizedString(
"AvatarPickerViewModel.Update.Fail",
value: "Oops, something didn't quite work out while trying to change your avatar.",
comment: "This error message shows when the user attempts to pick a different avatar and fails."
)
}
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/GravatarUI/SwiftUI/Toast/Toast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ struct Toast: View {
VStack {
Toast(toast: .init(
message: "Avatar updated! It may take a few minutes to appear everywhere.",
type: .info
type: .info,
stackingBehavior: .avoidStackingWithSameMessage
)) { _ in
}

Toast(toast: .init(
message: "Something went wrong.",
type: .error
type: .error,
stackingBehavior: .alwaysStack
)) { _ in
}
}
Expand Down
26 changes: 24 additions & 2 deletions Sources/GravatarUI/SwiftUI/Toast/ToastManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@ import SwiftUI
class ToastManager: ObservableObject {
@Published var toasts: [ToastItem] = []

func showToast(_ message: String, type: ToastType = .info) {
let toast = ToastItem(message: message, type: type)
func showToast(_ message: String, type: ToastType = .info, stackingBehavior: ToastStackingBehavior = .avoidStackingWithSameMessage) {
let toast = ToastItem(message: message, type: type, stackingBehavior: stackingBehavior)
dismissExistingIfNeeded(upcomingToast: toast)
toasts.append(toast)
DispatchQueue.main.asyncAfter(deadline: .now() + calculateToastDuration(for: message)) {
self.removeToast(toast.id)
}
}

func dismissExistingIfNeeded(upcomingToast: ToastItem) {
toasts.filter { item in
switch upcomingToast.stackingBehavior {
case .avoidStackingWithSameMessage:
upcomingToast.message == item.message
case .alwaysStack:
false
}
}.forEach { element in
removeToast(element.id)
}
}

func removeToast(_ toastID: UUID) {
withAnimation {
toasts.removeAll { $0.id == toastID }
Expand All @@ -30,8 +44,16 @@ enum ToastType: Int {
case error
}

enum ToastStackingBehavior: Equatable {
/// Dismiss the toast with the same message before showing the new one.
case avoidStackingWithSameMessage
/// Stack the message without dismissing the existing ones.
case alwaysStack
}

struct ToastItem: Identifiable, Equatable {
let id: UUID = .init()
let message: String
let type: ToastType
let stackingBehavior: ToastStackingBehavior
}

0 comments on commit 784e8a6

Please sign in to comment.