Skip to content

Commit afb0055

Browse files
Allow updating bio
1 parent 58d1fd4 commit afb0055

File tree

7 files changed

+151
-11
lines changed

7 files changed

+151
-11
lines changed

Fyreplace.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
4D351AEB2CA6BD45002EEB8F /* SettingsScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D351AEA2CA6BD3A002EEB8F /* SettingsScreenTests.swift */; };
2424
4D351AED2CA6BE2D002EEB8F /* FakeScreenBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D351AEC2CA6BE27002EEB8F /* FakeScreenBase.swift */; };
2525
4D39A4C82BF516B7003FA52E /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */; };
26+
4D40ACB42CC3ECBC00B26FDF /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D40ACB32CC3ECB300B26FDF /* User.swift */; };
2627
4D4AF71C2C7CE72900621FF3 /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4AF71B2C7CE72900621FF3 /* Tokens.swift */; };
2728
4D4D394A2C086DA2007196D2 /* PublishedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D4D39492C086DA2007196D2 /* PublishedScreen.swift */; };
2829
4D51F2802C621ADB0018E76E /* ViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D51F27F2C621ADB0018E76E /* ViewProtocol.swift */; };
@@ -111,6 +112,7 @@
111112
4D351AEA2CA6BD3A002EEB8F /* SettingsScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenTests.swift; sourceTree = "<group>"; };
112113
4D351AEC2CA6BE27002EEB8F /* FakeScreenBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeScreenBase.swift; sourceTree = "<group>"; };
113114
4D39A4C72BF516B7003FA52E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
115+
4D40ACB32CC3ECB300B26FDF /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
114116
4D4AF71B2C7CE72900621FF3 /* Tokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = "<group>"; };
115117
4D4D39492C086DA2007196D2 /* PublishedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedScreen.swift; sourceTree = "<group>"; };
116118
4D51F27F2C621ADB0018E76E /* ViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewProtocol.swift; sourceTree = "<group>"; };
@@ -425,6 +427,7 @@
425427
4DE785872C88F392000EC4E5 /* HTTPField.swift */,
426428
4D060BCA2C9438E8008C32D1 /* View.swift */,
427429
4D5251F22C109FAC00018CD2 /* Label+Destination.swift */,
430+
4D40ACB32CC3ECB300B26FDF /* User.swift */,
428431
);
429432
path = Extensions;
430433
sourceTree = "<group>";
@@ -644,6 +647,7 @@
644647
isa = PBXSourcesBuildPhase;
645648
buildActionMask = 2147483647;
646649
files = (
650+
4D40ACB42CC3ECBC00B26FDF /* User.swift in Sources */,
647651
4DA04EE22CAEEAD800B70D73 /* CGFloat.swift in Sources */,
648652
4D9B3B3D2C34B13E00A8F7AD /* LogoHeader.swift in Sources */,
649653
4DA7BFBB2C5FDEC1005CC4FF /* FakeClient.swift in Sources */,

Fyreplace/Extensions/User.swift

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
extension Components.Schemas.User {
2+
static let maxBioSize = 3000
3+
}

Fyreplace/Fakes/FakeClient.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,13 @@ extension FakeClient {
361361
}
362362
}
363363

364-
func setCurrentUserBio(_: Operations.setCurrentUserBio.Input) async throws
364+
func setCurrentUserBio(_ input: Operations.setCurrentUserBio.Input) async throws
365365
-> Operations.setCurrentUserBio.Output
366366
{
367-
fatalError("Not implemented")
367+
return switch input.body {
368+
case let .plainText(text):
369+
.ok(.init(body: .plainText(text)))
370+
}
368371
}
369372

370373
func setUserBanned(_: Operations.setUserBanned.Input) async throws

Fyreplace/Resources/Localizable.xcstrings

+30
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,36 @@
654654
}
655655
}
656656
},
657+
"Settings.Bio.Footer:%lld,%lld" : {
658+
"localizations" : {
659+
"en" : {
660+
"stringUnit" : {
661+
"state" : "translated",
662+
"value" : "%1$lld/%2$lld"
663+
}
664+
}
665+
}
666+
},
667+
"Settings.Bio.Header" : {
668+
"localizations" : {
669+
"en" : {
670+
"stringUnit" : {
671+
"state" : "translated",
672+
"value" : "Bio"
673+
}
674+
}
675+
}
676+
},
677+
"Settings.Bio.Update" : {
678+
"localizations" : {
679+
"en" : {
680+
"stringUnit" : {
681+
"state" : "translated",
682+
"value" : "Save"
683+
}
684+
}
685+
}
686+
},
657687
"Settings.DateJoined:%@" : {
658688
"localizations" : {
659689
"en" : {

Fyreplace/Views/Screens/SettingsScreen.swift

+30-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ struct SettingsScreen: View, SettingsScreenProtocol {
1414
@State
1515
var currentUser: Components.Schemas.User?
1616

17+
@State
18+
var bio = ""
19+
1720
@State
1821
var isLoadingAvatar = false
1922

@@ -29,9 +32,12 @@ struct SettingsScreen: View, SettingsScreenProtocol {
2932
@State
3033
private var avatarItem: PhotosPickerItem?
3134

35+
@FocusState
36+
private var bioFocused: Bool
37+
3238
var body: some View {
3339
DynamicForm {
34-
Section {
40+
Section("Settings.Profile.Header") {
3541
let logoutButton = Button("Settings.Logout", role: .destructive, action: logout)
3642

3743
HStack {
@@ -70,11 +76,32 @@ struct SettingsScreen: View, SettingsScreenProtocol {
7076
Spacer()
7177
}
7278
#endif
73-
} header: {
74-
Text("Settings.Profile.Header")
7579
}
7680

7781
Section {
82+
TextEditor(text: $bio)
83+
.scrollContentBackground(.hidden)
84+
.frame(maxHeight: 160)
85+
86+
HStack {
87+
Spacer()
88+
Button("Settings.Bio.Update") {
89+
Task {
90+
await updateBio()
91+
}
92+
}
93+
.disabled(!canUpdateBio)
94+
#if !os(macOS)
95+
Spacer()
96+
#endif
97+
}
98+
} header: {
99+
Text("Settings.Bio.Header")
100+
} footer: {
101+
Text("Settings.Bio.Footer:\(bio.count),\(Components.Schemas.User.maxBioSize)")
102+
}
103+
104+
Section("Settings.About.Header") {
78105
Link(destination: config.app.info.website) {
79106
Label("App.Help.Website", systemImage: "safari")
80107
}
@@ -94,8 +121,6 @@ struct SettingsScreen: View, SettingsScreenProtocol {
94121
Label("App.Help.SourceCode", systemImage: "curlybraces")
95122
}
96123
.foregroundStyle(.tint)
97-
} header: {
98-
Text("Settings.About.Header")
99124
}
100125
}
101126
.navigationTitle(Destination.settings.titleKey)

Fyreplace/Views/Screens/SettingsScreenProtocol.swift

+37
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ protocol SettingsScreenProtocol: ViewProtocol {
77

88
var token: String { get nonmutating set }
99
var currentUser: Components.Schemas.User? { get nonmutating set }
10+
var bio: String { get nonmutating set }
1011
var isLoadingAvatar: Bool { get nonmutating set }
1112
}
1213

1314
@MainActor
1415
extension SettingsScreenProtocol {
16+
var canUpdateBio: Bool {
17+
bio != currentUser?.bio ?? "" && bio.count <= Components.Schemas.User.maxBioSize
18+
}
19+
1520
func getCurrentUser() async {
1621
await call {
1722
let response = try await api.getCurrentUser()
@@ -21,6 +26,7 @@ extension SettingsScreenProtocol {
2126
switch ok.body {
2227
case let .json(user):
2328
currentUser = user
29+
bio = user.bio
2430
}
2531

2632
return nil
@@ -94,6 +100,37 @@ extension SettingsScreenProtocol {
94100
isLoadingAvatar = false
95101
}
96102

103+
func updateBio() async {
104+
await call {
105+
let response = try await api.setCurrentUserBio(
106+
body: .plainText(bio.isEmpty ? .init() : .init(stringLiteral: bio)))
107+
108+
switch response {
109+
case let .ok(ok):
110+
switch ok.body {
111+
case let .plainText(text):
112+
bio = try await .init(
113+
collecting: text, upTo: Components.Schemas.User.maxBioSize * 4)
114+
currentUser?.bio = bio
115+
}
116+
117+
return nil
118+
119+
case .badRequest(_):
120+
return .failure(
121+
title: "Error.BadRequest.Title",
122+
text: "Error.BadRequest.Message"
123+
)
124+
125+
case .unauthorized:
126+
return .authorizationIssue()
127+
128+
case .forbidden, .default:
129+
return .error()
130+
}
131+
}
132+
}
133+
97134
func logout() {
98135
token = ""
99136
}

FyreplaceTests/Screens/SettingsScreenTests.swift

+42-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct SettingsScreenTests {
99
class FakeScreen: FakeScreenBase, SettingsScreenProtocol {
1010
var token = ""
1111
var currentUser: Components.Schemas.User?
12+
var bio = ""
1213
var isLoadingAvatar = false
1314
}
1415

@@ -25,7 +26,8 @@ struct SettingsScreenTests {
2526
let screen = FakeScreen(eventBus: eventBus, api: .fake())
2627
await screen.getCurrentUser()
2728
await screen.updateAvatar(
28-
with: try await .init(collecting: FakeClient.largeImageBody, upTo: 64))
29+
with: try await .init(collecting: FakeClient.largeImageBody, upTo: 64)
30+
)
2931
#expect(eventBus.storedEvents.count == 1)
3032
#expect(screen.currentUser?.avatar == "")
3133
}
@@ -36,7 +38,8 @@ struct SettingsScreenTests {
3638
let screen = FakeScreen(eventBus: eventBus, api: .fake())
3739
await screen.getCurrentUser()
3840
await screen.updateAvatar(
39-
with: try await .init(collecting: FakeClient.notImageBody, upTo: 64))
41+
with: try await .init(collecting: FakeClient.notImageBody, upTo: 64)
42+
)
4043
#expect(eventBus.storedEvents.count == 1)
4144
#expect(screen.currentUser?.avatar == "")
4245
}
@@ -47,7 +50,8 @@ struct SettingsScreenTests {
4750
let screen = FakeScreen(eventBus: eventBus, api: .fake())
4851
await screen.getCurrentUser()
4952
await screen.updateAvatar(
50-
with: try await .init(collecting: FakeClient.normalImageBody, upTo: 64))
53+
with: try await .init(collecting: FakeClient.normalImageBody, upTo: 64)
54+
)
5155
#expect(eventBus.storedEvents.isEmpty)
5256
#expect(screen.currentUser?.avatar == FakeClient.avatar)
5357
}
@@ -58,9 +62,43 @@ struct SettingsScreenTests {
5862
let screen = FakeScreen(eventBus: eventBus, api: .fake())
5963
await screen.getCurrentUser()
6064
await screen.updateAvatar(
61-
with: try await .init(collecting: FakeClient.normalImageBody, upTo: 64))
65+
with: try await .init(collecting: FakeClient.normalImageBody, upTo: 64)
66+
)
6267
await screen.removeAvatar()
6368
#expect(eventBus.storedEvents.isEmpty)
6469
#expect(screen.currentUser?.avatar == "")
6570
}
71+
72+
@Test("Bio must have correct length")
73+
func bioMustHaveCorrectLength() async throws {
74+
let screen = FakeScreen(eventBus: .init(), api: .fake())
75+
await screen.getCurrentUser()
76+
screen.bio = "Hello"
77+
#expect(screen.canUpdateBio)
78+
screen.bio = .init(repeating: "a", count: Components.Schemas.User.maxBioSize)
79+
#expect(screen.canUpdateBio)
80+
screen.bio += "a"
81+
#expect(!screen.canUpdateBio)
82+
}
83+
84+
@Test("Bio must be different")
85+
func bioMustHaveBeDifferent() async throws {
86+
let screen = FakeScreen(eventBus: .init(), api: .fake())
87+
await screen.getCurrentUser()
88+
screen.bio = "Hello"
89+
#expect(screen.canUpdateBio)
90+
await screen.updateBio()
91+
#expect(!screen.canUpdateBio)
92+
}
93+
94+
@Test("Updating bio produces no failures")
95+
func updateBioProducesNoFailures() async throws {
96+
let eventBus = StoringEventBus()
97+
let screen = FakeScreen(eventBus: eventBus, api: .fake())
98+
await screen.getCurrentUser()
99+
screen.bio = "Hello"
100+
await screen.updateBio()
101+
#expect(eventBus.storedEvents.isEmpty)
102+
#expect(screen.currentUser?.bio == screen.bio)
103+
}
66104
}

0 commit comments

Comments
 (0)