Skip to content

Commit

Permalink
Allow removing avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Oct 6, 2024
1 parent d1fa02b commit c10c7cb
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 55 deletions.
4 changes: 4 additions & 0 deletions Fyreplace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
4D9B3B452C36F46F00A8F7AD /* NSTextContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D9B3B442C36F46F00A8F7AD /* NSTextContentType.swift */; };
4D9B3B472C36F50300A8F7AD /* UITextContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D9B3B462C36F50300A8F7AD /* UITextContentType.swift */; };
4D9DC5032C11BF2500BA0507 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D9DC5022C11BF2500BA0507 /* Config.swift */; };
4DA04EE22CAEEAD800B70D73 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA04EE12CAEEAD100B70D73 /* CGFloat.swift */; };
4DA7BFB72C5FD479005CC4FF /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA7BFB62C5FD479005CC4FF /* PerformanceTests.swift */; };
4DA7BFBB2C5FDEC1005CC4FF /* FakeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA7BFBA2C5FDEC1005CC4FF /* FakeClient.swift */; };
4DB10B502C4FEBFC00634BF6 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB10B4F2C4FEBFC00634BF6 /* HelpCommands.swift */; };
Expand Down Expand Up @@ -151,6 +152,7 @@
4D9B3B442C36F46F00A8F7AD /* NSTextContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSTextContentType.swift; sourceTree = "<group>"; };
4D9B3B462C36F50300A8F7AD /* UITextContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextContentType.swift; sourceTree = "<group>"; };
4D9DC5022C11BF2500BA0507 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
4DA04EE12CAEEAD100B70D73 /* CGFloat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGFloat.swift; sourceTree = "<group>"; };
4DA7BFB62C5FD479005CC4FF /* PerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = "<group>"; };
4DA7BFBA2C5FDEC1005CC4FF /* FakeClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeClient.swift; sourceTree = "<group>"; };
4DB10B4F2C4FEBFC00634BF6 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -416,6 +418,7 @@
4D9B3B432C36E64F00A8F7AD /* Extensions */ = {
isa = PBXGroup;
children = (
4DA04EE12CAEEAD100B70D73 /* CGFloat.swift */,
4DE785812C88B248000EC4E5 /* String.swift */,
4D9B3B442C36F46F00A8F7AD /* NSTextContentType.swift */,
4D9B3B462C36F50300A8F7AD /* UITextContentType.swift */,
Expand Down Expand Up @@ -641,6 +644,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4DA04EE22CAEEAD800B70D73 /* CGFloat.swift in Sources */,
4D9B3B3D2C34B13E00A8F7AD /* LogoHeader.swift in Sources */,
4DA7BFBB2C5FDEC1005CC4FF /* FakeClient.swift in Sources */,
4DE785822C88B248000EC4E5 /* String.swift in Sources */,
Expand Down
9 changes: 9 additions & 0 deletions Fyreplace/Extensions/CGFloat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

extension CGFloat {
#if os(macOS)
static var logoSize: Self { 60 }
#else
static var logoSize: Self { 80 }
#endif
}
2 changes: 1 addition & 1 deletion Fyreplace/Fakes/FakeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ extension FakeClient {
func deleteCurrentUserAvatar(_: Operations.deleteCurrentUserAvatar.Input) async throws
-> Operations.deleteCurrentUserAvatar.Output
{
fatalError("Not implemented")
return .noContent(.init())
}

func getCurrentUser(_: Operations.getCurrentUser.Input) async throws
Expand Down
38 changes: 24 additions & 14 deletions Fyreplace/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@
}
}
},
"EditableAvatar.ContextMenu.Change" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Change avatar"
}
}
}
},
"EditableAvatar.ContextMenu.Remove" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove avatar"
}
}
}
},
"Environment.Default" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -614,12 +634,12 @@
}
}
},
"Settings.DateJoined" : {
"Settings.DateJoined:%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Joined on"
"value" : "Joined: %@"
}
}
}
Expand Down Expand Up @@ -684,16 +704,6 @@
}
}
},
"Settings.Header" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Profile"
}
}
}
},
"Settings.Logout" : {
"localizations" : {
"en" : {
Expand All @@ -704,12 +714,12 @@
}
}
},
"Settings.Username" : {
"Settings.Profile.Header" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Username"
"value" : "Profile"
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions Fyreplace/Views/Components/Avatar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ struct Avatar: View {
var body: some View {
ZStack {
if let avatar = user?.avatar, !avatar.isEmpty {
AsyncImage(url: .init(string: avatar)) {
$0.resizable().scaledToFill()
} placeholder: {
ProgressView()
GeometryReader { geometry in
let size = min(geometry.size.width, geometry.size.height)
AsyncImage(url: .init(string: avatar)) { image in
image
.resizable()
.scaledToFill()
.frame(width: size, height: size)
} placeholder: {
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
} else {
Image(systemName: "person.crop.circle.fill")
Expand Down
34 changes: 31 additions & 3 deletions Fyreplace/Views/Components/EditableAvatar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,62 @@ struct EditableAvatar: View {

let avatarSelected: (Data) async -> Void

let avatarRemoved: () async -> Void

@EnvironmentObject
private var eventBus: EventBus

@State
private var showEditOverlay = false

@State
private var showPhotosPicker = false

@State
private var avatarItem: PhotosPickerItem?

var body: some View {
let opacity = showEditOverlay ? 1.0 : 0.0
let blurred = showEditOverlay
PhotosPicker(selection: $avatarItem) {
Button {
showPhotosPicker = true
} label: {
Avatar(user: user, blurred: blurred)
.overlay {
Image(systemName: "pencil")
.scaleEffect(2)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.background(.black.opacity(0.5))
.foregroundStyle(.white)
.clipShape(.circle)
.opacity(opacity)
.clipShape(.circle)
}
}
.animation(.default.speed(3), value: showEditOverlay)
.buttonStyle(.borderless)
.photosPicker(isPresented: $showPhotosPicker, selection: $avatarItem)
.onHover { showEditOverlay = $0 }
.contextMenu {
Button {
showPhotosPicker = true
} label: {
Label("EditableAvatar.ContextMenu.Change", systemImage: "photo")
}
.disabled(user == nil)

Button(role: .destructive) {
avatarItem = nil

Task {
await avatarRemoved()
}
} label: {
Label("EditableAvatar.ContextMenu.Remove", systemImage: "trash")
}
.disabled(user?.avatar.isEmpty ?? true)
}
.dropDestination(for: Data.self) { items, _ in
guard let data = items.first else { return false }
avatarItem = nil
Expand Down
7 changes: 1 addition & 6 deletions Fyreplace/Views/Forms/LogoHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ struct LogoHeader<ImageContent, TextContent>: View where ImageContent: View, Tex
VStack {
HStack {
Spacer()
imageContent()
#if os(macOS)
.frame(width: 60, height: 60)
#else
.frame(width: 80, height: 80)
#endif
imageContent().frame(width: .logoSize, height: .logoSize)
Spacer()
}
#if os(macOS)
Expand Down
60 changes: 39 additions & 21 deletions Fyreplace/Views/Screens/SettingsScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,55 @@ struct SettingsScreen: View, SettingsScreenProtocol {
@State
var currentUser: Components.Schemas.User?

@Environment(\.config)
private var config

@Namespace
private var namespace

@State
private var showPhotosPicker = false

@State
private var avatarItem: PhotosPickerItem?

var body: some View {
DynamicForm {
Section {
LabeledContent(
"Settings.Username", value: currentUser?.username ?? .init(localized: "Loading")
)
LabeledContent("Settings.DateJoined") {
DateText(date: currentUser?.dateCreated)
}
let logoutButton = Button("Settings.Logout", role: .destructive, action: logout)

HStack {
Spacer()
Button("Settings.Logout", role: .destructive, action: logout)
#if !os(macOS)
EditableAvatar(
user: currentUser,
avatarSelected: updateAvatar,
avatarRemoved: removeAvatar
)
.frame(width: .logoSize, height: .logoSize)

VStack(alignment: .leading, spacing: 4) {
Spacer()
Text(verbatim: currentUser?.username ?? .init(localized: "Loading"))
.font(.headline)
DateJoinedText(date: currentUser?.dateCreated)
.foregroundStyle(.secondary)
Spacer()
}

#if os(macOS)
Spacer()
logoutButton
#endif
}

#if !os(macOS)
HStack {
Spacer()
logoutButton
Spacer()
}
#endif
} header: {
LogoHeader {
EditableAvatar(user: currentUser, avatarSelected: updateAvatar)
} textContent: {
Text("Settings.Header")
}
Text("Settings.Profile.Header")
}
}
.navigationTitle(Destination.settings.titleKey)
Expand All @@ -57,7 +80,7 @@ struct SettingsScreen: View, SettingsScreenProtocol {
}
}

private struct DateText: View {
private struct DateJoinedText: View {
let date: Date?

var body: some View {
Expand All @@ -68,12 +91,7 @@ private struct DateText: View {
let dateFormatStyle = Date.FormatStyle.DateStyle.abbreviated
#endif

Text(
verbatim: date.formatted(
date: dateFormatStyle,
time: .shortened
)
)
Text("Settings.DateJoined:\(date.formatted(date: dateFormatStyle, time: .shortened))")
} else {
Text("Loading")
}
Expand Down
18 changes: 18 additions & 0 deletions Fyreplace/Views/Screens/SettingsScreenProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ extension SettingsScreenProtocol {
}
}

func removeAvatar() async {
await call {
let response = try await api.deleteCurrentUserAvatar()

switch response {
case .noContent:
currentUser?.avatar = ""
return nil

case .unauthorized:
return .authorizationIssue()

case .forbidden, .default:
return .error()
}
}
}

func logout() {
token = ""
}
Expand Down
24 changes: 18 additions & 6 deletions FyreplaceTests/Screens/SettingsScreenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ struct SettingsScreenTests {
#expect(screen.currentUser != nil)
}

@Test("Too large avatar produces a failure")
func tooLargeAvatarProducesFailure() async throws {
@Test("Updating avatar with a too large image produces a failure")
func updateAvatarTooLargeProducesFailure() async throws {
let eventBus = StoringEventBus()
let screen = FakeScreen(eventBus: eventBus, api: .fake())
await screen.getCurrentUser()
Expand All @@ -29,8 +29,8 @@ struct SettingsScreenTests {
#expect(screen.currentUser?.avatar == "")
}

@Test("Not image avatar produces a failure")
func notImageAvatarProducesFailure() async throws {
@Test("Updating avatar with an invalid image produces a failure")
func updateAvatarNotImageProducesFailure() async throws {
let eventBus = StoringEventBus()
let screen = FakeScreen(eventBus: eventBus, api: .fake())
await screen.getCurrentUser()
Expand All @@ -40,8 +40,8 @@ struct SettingsScreenTests {
#expect(screen.currentUser?.avatar == "")
}

@Test("Valid avatar produces no failures")
func validAvatarProducesNoFailures() async throws {
@Test("Updating avatar with a valid image produces no failures")
func updateAvatarValidProducesNoFailures() async throws {
let eventBus = StoringEventBus()
let screen = FakeScreen(eventBus: eventBus, api: .fake())
await screen.getCurrentUser()
Expand All @@ -50,4 +50,16 @@ struct SettingsScreenTests {
#expect(eventBus.storedEvents.isEmpty)
#expect(screen.currentUser?.avatar == FakeClient.avatar)
}

@Test("Removing avatar produces no failures")
func removeAvatarProducesNoFailures() async throws {
let eventBus = StoringEventBus()
let screen = FakeScreen(eventBus: eventBus, api: .fake())
await screen.getCurrentUser()
await screen.updateAvatar(
with: try await .init(collecting: FakeClient.normalImageBody, upTo: 64))
await screen.removeAvatar()
#expect(eventBus.storedEvents.isEmpty)
#expect(screen.currentUser?.avatar == "")
}
}

0 comments on commit c10c7cb

Please sign in to comment.