Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the initial avatar action menu [...] #570

Merged
merged 9 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "more-horizontal.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions Sources/GravatarUI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/* An option in the avatar menu that deletes the avatar */
"AvatarPicker.AvatarAction.delete" = "Delete";

/* An option in the avatar menu that shares the avatar */
"AvatarPicker.AvatarAction.share" = "Share";

/* Title of a message advising the user that something went wrong while trying to log in. */
"AvatarPicker.ContentLoading.Failure.LogInError.title" = "Login required";

Expand Down
44 changes: 44 additions & 0 deletions Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
import SwiftUI

enum AvatarAction: String, CaseIterable, Identifiable {
case share
case delete

var id: String { rawValue }

var icon: Image {
switch self {
case .delete:
Image(systemName: "trash")
case .share:
Image(systemName: "square.and.arrow.up")
}
}

var localizedTitle: String {
switch self {
case .delete:
SDKLocalizedString(
"AvatarPicker.AvatarAction.delete",
value: "Delete",
comment: "An option in the avatar menu that deletes the avatar"
)
case .share:
SDKLocalizedString(
"AvatarPicker.AvatarAction.share",
value: "Share",
comment: "An option in the avatar menu that shares the avatar"
)
}
}

var role: ButtonRole? {
switch self {
case .delete:
.destructive
case .share:
nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ struct AvatarPickerView<ImageEditor: ImageEditorView>: View {
onFailedUploadTapped: { failedUploadInfo in
uploadError = failedUploadInfo
isUploadErrorDialogPresented = true
},
onAvatarActionTap: { avatar, action in
// TODO: Replace
print("Avatar action tapped: \(avatar.id), \(action.rawValue)")
}
)
.padding(.horizontal, Constants.horizontalPadding)
Expand All @@ -304,6 +308,10 @@ struct AvatarPickerView<ImageEditor: ImageEditorView>: View {
onFailedUploadTapped: { failedUploadInfo in
uploadError = failedUploadInfo
isUploadErrorDialogPresented = true
},
onAvatarActionTap: { avatar, action in
// TODO: Replace
print("Avatar action tapped: \(avatar.id), \(action.rawValue)")
}
)
.padding(.top, .DS.Padding.medium)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct AvatarGrid<ImageEditor: ImageEditorView>: View {
let onAvatarTap: (AvatarImageModel) -> Void
let onImagePickerDidPickImage: (UIImage) -> Void
let onFailedUploadTapped: (FailedUploadInfo) -> Void
let onAvatarActionTap: (AvatarImageModel, AvatarAction) -> Void

var body: some View {
LazyVGrid(columns: gridItems, spacing: AvatarGridConstants.avatarSpacing) {
Expand All @@ -45,7 +46,10 @@ struct AvatarGrid<ImageEditor: ImageEditorView>: View {
grid.selectedAvatar?.id == avatar.id
},
onAvatarTap: onAvatarTap,
onFailedUploadTapped: onFailedUploadTapped
onFailedUploadTapped: onFailedUploadTapped,
onActionTap: { action in
onAvatarActionTap(avatar, action)
}
)
}
}
Expand All @@ -68,6 +72,8 @@ struct AvatarGrid<ImageEditor: ImageEditorView>: View {
grid.append(newAvatarModel(image))
} onFailedUploadTapped: { _ in
// No op. inside the preview.
} onAvatarActionTap: { _, _ in
// No op. inside the preview.
}
.padding()
Button("Add avatar cell") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct AvatarPickerAvatarView: View {
let shouldSelect: () -> Bool
let onAvatarTap: (AvatarImageModel) -> Void
let onFailedUploadTapped: (FailedUploadInfo) -> Void
let onActionTap: (AvatarAction) -> Void

var body: some View {
AvatarView(
Expand Down Expand Up @@ -54,12 +55,46 @@ struct AvatarPickerAvatarView: View {
}
.cornerRadius(AvatarGridConstants.avatarCornerRadius)
case .loaded:
EmptyView()
VStack {
Spacer()
HStack {
Spacer()
actionsMenu()
}
}
}
}.onTapGesture {
onAvatarTap(avatar)
}
}

@ViewBuilder
func ellipsisView() -> some View {
Image("more-horizontal", bundle: Bundle.module).renderingMode(.template)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative: We can set the rendering mode directly in the asset catalog.
That's good if the icon is always meant to be used this way.

Copy link
Contributor Author

@pinarol pinarol Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but asset catalogs are more fragile. It is easy to lose that setting if we change the icon. Setting it programmatically is safer.

.tint(.white)
.background(Color(uiColor: UIColor.gravatarBlack.withAlphaComponent(0.4)))
.cornerRadius(2)
.padding(CGFloat.DS.Padding.half)
}

@ViewBuilder
func actionsMenu() -> some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe @ViewBuilder attribute is not needed here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Removing.

Menu {
ForEach(AvatarAction.allCases) { action in
Button(role: action.role) {
onActionTap(action)
} label: {
Label {
Text(action.localizedTitle)
} icon: {
action.icon
}
}
}
} label: {
ellipsisView()
}
}
}

#Preview {
Expand All @@ -68,5 +103,6 @@ struct AvatarPickerAvatarView: View {
false
} onAvatarTap: { _ in
} onFailedUploadTapped: { _ in
} onActionTap: { _ in
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct HorizontalAvatarGrid: View {

let onAvatarTap: (AvatarImageModel) -> Void
let onFailedUploadTapped: (FailedUploadInfo) -> Void
let onAvatarActionTap: (AvatarImageModel, AvatarAction) -> Void

var body: some View {
ScrollView(.horizontal) {
Expand All @@ -22,7 +23,10 @@ struct HorizontalAvatarGrid: View {
grid.selectedAvatar?.id == avatar.id
},
onAvatarTap: onAvatarTap,
onFailedUploadTapped: onFailedUploadTapped
onFailedUploadTapped: onFailedUploadTapped,
onActionTap: { action in
onAvatarActionTap(avatar, action)
}
)
}
}
Expand All @@ -48,5 +52,7 @@ struct HorizontalAvatarGrid: View {
grid.selectAvatar(withID: avatar.id)
} onFailedUploadTapped: { _ in
// No op. Inside the preview.
} onAvatarActionTap: { _, _ in
// No op. Inside the preview.
}
}