Skip to content

Commit

Permalink
Allow changing avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Sep 29, 2024
1 parent 64f14b1 commit e202a9d
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 37 deletions.
8 changes: 8 additions & 0 deletions Fyreplace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
4D13AF812C4E907200845FDB /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D13AF802C4E907200845FDB /* Environment.swift */; };
4D30DA5F2C986B6C00499450 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D30DA5E2C986B6C00499450 /* Avatar.swift */; };
4D30DA612C98706C00499450 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D30DA602C98706C00499450 /* Placeholders.swift */; };
4D351AEB2CA6BD45002EEB8F /* SettingsScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D351AEA2CA6BD3A002EEB8F /* SettingsScreenTests.swift */; };
4D351AED2CA6BE2D002EEB8F /* FakeScreenBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D351AEC2CA6BE27002EEB8F /* FakeScreenBase.swift */; };
4D39A4C82BF516B7003FA52E /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */; };
4D4AF71C2C7CE72900621FF3 /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4AF71B2C7CE72900621FF3 /* Tokens.swift */; };
4D4D394A2C086DA2007196D2 /* PublishedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4D39492C086DA2007196D2 /* PublishedScreen.swift */; };
Expand Down Expand Up @@ -104,6 +106,8 @@
4D13AF802C4E907200845FDB /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
4D30DA5E2C986B6C00499450 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
4D30DA602C98706C00499450 /* Placeholders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholders.swift; sourceTree = "<group>"; };
4D351AEA2CA6BD3A002EEB8F /* SettingsScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenTests.swift; sourceTree = "<group>"; };
4D351AEC2CA6BE27002EEB8F /* FakeScreenBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeScreenBase.swift; sourceTree = "<group>"; };
4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
4D4AF71B2C7CE72900621FF3 /* Tokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = "<group>"; };
4D4D39492C086DA2007196D2 /* PublishedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedScreen.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -454,6 +458,8 @@
4DFB906E2C5908BF00D4DABF /* Screens */ = {
isa = PBXGroup;
children = (
4D351AEC2CA6BE27002EEB8F /* FakeScreenBase.swift */,
4D351AEA2CA6BD3A002EEB8F /* SettingsScreenTests.swift */,
4DFB906F2C5908DE00D4DABF /* LoginScreenTests.swift */,
4DFB90752C59173C00D4DABF /* RegisterScreenTests.swift */,
);
Expand Down Expand Up @@ -687,6 +693,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4D351AEB2CA6BD45002EEB8F /* SettingsScreenTests.swift in Sources */,
4D351AED2CA6BE2D002EEB8F /* FakeScreenBase.swift in Sources */,
4D5348612C6646F80001EFDE /* StoringEventBus.swift in Sources */,
4DFB90702C5908DE00D4DABF /* LoginScreenTests.swift in Sources */,
4DFB90762C59173C00D4DABF /* RegisterScreenTests.swift in Sources */,
Expand Down
24 changes: 22 additions & 2 deletions Fyreplace/Fakes/FakeClient.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import OpenAPIRuntime

struct FakeClient: APIProtocol {}

Expand Down Expand Up @@ -268,10 +269,17 @@ extension FakeClient {
static let usedUsername = "used-username"
static let passwordUsername = "password-username"
static let goodUsername = "good-username"

static let badEmail = "bad-email"
static let usedEmail = "used-email"
static let goodEmail = "good-email"

static let notImageBody = HTTPBody(stringLiteral: "Not")
static let largeImageBody = HTTPBody(stringLiteral: "Large")
static let normalImageBody = HTTPBody(stringLiteral: "Normal")

static let avatar = "avatar"

func countBlockedUsers(_: Operations.countBlockedUsers.Input) async throws
-> Operations.countBlockedUsers.Output
{
Expand Down Expand Up @@ -335,10 +343,22 @@ extension FakeClient {
fatalError("Not implemented")
}

func setCurrentUserAvatar(_: Operations.setCurrentUserAvatar.Input) async throws
func setCurrentUserAvatar(_ input: Operations.setCurrentUserAvatar.Input) async throws
-> Operations.setCurrentUserAvatar.Output
{
fatalError("Not implemented")
let normalImage = try await String(collecting: Self.normalImageBody, upTo: 64)
let largeImage = try await String(collecting: Self.largeImageBody, upTo: 64)

return switch input.body {
case let .binary(binary) where try await String(collecting: binary, upTo: 64) == normalImage:
.ok(.init(body: .plainText(.init(stringLiteral: Self.avatar))))

case let .binary(binary) where try await String(collecting: binary, upTo: 64) == largeImage:
.contentTooLarge(.init())

case .binary:
.unsupportedMediaType(.init())
}
}

func setCurrentUserBio(_: Operations.setCurrentUserBio.Input) async throws
Expand Down
18 changes: 18 additions & 0 deletions Fyreplace/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,24 @@
}
}
}
},
"Settings.Error.ContentTooLarge.Message" : {

},
"Settings.Error.ContentTooLarge.Title" : {

},
"Settings.Error.Image.Message" : {

},
"Settings.Error.Image.Title" : {

},
"Settings.Error.UnsupportedMediaType.Message" : {

},
"Settings.Error.UnsupportedMediaType.Title" : {

},
"Settings.Header" : {
"localizations" : {
Expand Down
3 changes: 3 additions & 0 deletions Fyreplace/Views/Components/Avatar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import SwiftUI
struct Avatar: View {
let user: Components.Schemas.User?

var blurred = false

private var tint: Color {
if let user = user {
.init(
Expand Down Expand Up @@ -31,6 +33,7 @@ struct Avatar: View {
.foregroundStyle(.white.gradient, tint.gradient)
}
}
.blur(radius: blurred ? 1 : 0)
.clipShape(.circle)
}
}
Expand Down
16 changes: 16 additions & 0 deletions Fyreplace/Views/Forms/LogoHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ struct LogoHeader<ImageContent, TextContent>: View where ImageContent: View, Tex
.matchedGeometryEffect(id: "header", in: namespace)
}
}

@available(macOS 14.0, *)
@available(iOS 17.0, *)
#Preview {
@Previewable
@Namespace
var namespace

LogoHeader(namespace: namespace) {
Image(systemName: "photo")
.resizable()
.scaledToFit()
} textContent: {
Text(verbatim: "Header text")
}
}
98 changes: 81 additions & 17 deletions Fyreplace/Views/Screens/SettingsScreen.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PhotosUI
import SwiftUI

struct SettingsScreen: View, SettingsScreenProtocol {
Expand All @@ -23,22 +24,7 @@ struct SettingsScreen: View, SettingsScreenProtocol {
"Settings.Username", value: currentUser?.username ?? .init(localized: "Loading")
)
LabeledContent("Settings.DateJoined") {
if let user = currentUser {
#if os(macOS)
let dateFormatStyle = Date.FormatStyle.DateStyle.long
#else
let dateFormatStyle = Date.FormatStyle.DateStyle.abbreviated
#endif

Text(
verbatim: user.dateCreated.formatted(
date: dateFormatStyle,
time: .shortened
)
)
} else {
Text("Loading")
}
DateText(date: currentUser?.dateCreated)
}

HStack {
Expand All @@ -50,7 +36,11 @@ struct SettingsScreen: View, SettingsScreenProtocol {
}
} header: {
LogoHeader(namespace: namespace) {
Avatar(user: currentUser)
PickableAvatar(user: currentUser) { item in
Task {
await updateAvatar(with: item)
}
}
} textContent: {
Text("Settings.Header")
}
Expand All @@ -63,10 +53,84 @@ struct SettingsScreen: View, SettingsScreenProtocol {
}
}
}

private func updateAvatar(with item: PhotosPickerItem) async {
if let data = try? await item.loadTransferable(type: Data.self) {
await updateAvatar(with: data)
} else {
eventBus.send(
.failure(
title: "Settings.Error.Image.Title",
text: "Settings.Error.Image.Message"
)
)
}
}
}

#Preview {
NavigationStack {
SettingsScreen()
}
}

private struct DateText: View {
let date: Date?

var body: some View {
if let date {
#if os(macOS)
let dateFormatStyle = Date.FormatStyle.DateStyle.long
#else
let dateFormatStyle = Date.FormatStyle.DateStyle.abbreviated
#endif

Text(
verbatim: date.formatted(
date: dateFormatStyle,
time: .shortened
)
)
} else {
Text("Loading")
}
}
}

private struct PickableAvatar: View {
let user: Components.Schemas.User?

let avatarSelected: (PhotosPickerItem) -> Void

@State
private var showEditOverlay = false

@State
private var avatarItem: PhotosPickerItem?

var body: some View {
let opacity = showEditOverlay ? 1.0 : 0.0
let blurred = showEditOverlay
PhotosPicker(selection: $avatarItem) {
Avatar(user: user, blurred: blurred)
.overlay {
Image(systemName: "pencil")
.scaleEffect(2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.background(.black.opacity(0.5))
.foregroundStyle(.white)
.clipShape(.circle)
.opacity(opacity)
}
}
.animation(.default.speed(3), value: showEditOverlay)
.buttonStyle(.borderless)
.onHover { showEditOverlay = $0 }
.onChange(of: avatarItem) {
if let item = $0 {
avatarSelected(item)
}
}
}
}
38 changes: 38 additions & 0 deletions Fyreplace/Views/Screens/SettingsScreenProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import PhotosUI
import SwiftUI

@MainActor
protocol SettingsScreenProtocol: ViewProtocol {
var api: APIProtocol { get }
Expand All @@ -6,6 +9,7 @@ protocol SettingsScreenProtocol: ViewProtocol {
var currentUser: Components.Schemas.User? { get nonmutating set }
}

@MainActor
extension SettingsScreenProtocol {
func getCurrentUser() async {
await call {
Expand All @@ -29,6 +33,40 @@ extension SettingsScreenProtocol {
}
}

func updateAvatar(with data: Data) async {
await call {
let response = try await api.setCurrentUserAvatar(body: .binary(.init(data)))

switch response {
case let .ok(ok):
switch ok.body {
case let .plainText(text):
currentUser?.avatar = try await .init(collecting: text, upTo: 1024)
}

return nil

case .contentTooLarge:
return .failure(
title: "Settings.Error.ContentTooLarge.Title",
text: "Settings.Error.ContentTooLarge.Message"
)

case .unsupportedMediaType:
return .failure(
title: "Settings.Error.UnsupportedMediaType.Title",
text: "Settings.Error.UnsupportedMediaType.Message"
)

case .unauthorized:
return .authorizationIssue()

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

func logout() {
token = ""
}
Expand Down
11 changes: 11 additions & 0 deletions FyreplaceTests/Screens/FakeScreenBase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@testable import Fyreplace

open class FakeScreenBase {
var eventBus: EventBus
var api: APIProtocol

init(eventBus: EventBus, api: APIProtocol) {
self.eventBus = eventBus
self.api = api
}
}
10 changes: 1 addition & 9 deletions FyreplaceTests/Screens/LoginScreenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,12 @@ import Testing
@Suite("Login screen")
@MainActor
struct LoginScreenTests {
class FakeScreen: LoginScreenProtocol {
var eventBus: EventBus
var api: APIProtocol

class FakeScreen: FakeScreenBase, LoginScreenProtocol {
var isLoading = false
var identifier = ""
var randomCode = ""
var isWaitingForRandomCode = false
var token = ""

init(eventBus: EventBus, api: APIProtocol) {
self.eventBus = eventBus
self.api = api
}
}

@Test("Identifier must have correct length")
Expand Down
10 changes: 1 addition & 9 deletions FyreplaceTests/Screens/RegisterScreenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,14 @@ import Testing
@Suite("Register screen")
@MainActor
struct RegisterScreenTests {
class FakeScreen: RegisterScreenProtocol {
var eventBus: EventBus
var api: APIProtocol

class FakeScreen: FakeScreenBase, RegisterScreenProtocol {
var isLoading = false
var username = ""
var email = ""
var randomCode = ""
var isWaitingForRandomCode = false
var isRegistering = false
var token = ""

init(eventBus: EventBus, api: APIProtocol) {
self.eventBus = eventBus
self.api = api
}
}

@Test("Username must have correct length")
Expand Down
Loading

0 comments on commit e202a9d

Please sign in to comment.