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

Profile Settings #1484

Merged
merged 9 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
12 changes: 10 additions & 2 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@
0389DDD32C39E4D40005B808 /* PasteLinkButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDD22C39E4D40005B808 /* PasteLinkButtonView.swift */; };
0389DDD52C39F1290005B808 /* CommunityListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDD42C39F1290005B808 /* CommunityListRow.swift */; };
0389DDDB2C3AB6340005B808 /* ActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */; };
0391E0F92CFF17AE0040CCA8 /* ProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */; };
0391E0FB2D0066240040CCA8 /* ImageUploadMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */; };
0397D4602C66113F002C6CDC /* CommentBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D45F2C66113F002C6CDC /* CommentBodyView.swift */; };
0397D4622C676B46002C6CDC /* ApiSortType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4612C676B46002C6CDC /* ApiSortType+Extensions.swift */; };
0397D4642C676CA8002C6CDC /* FeedSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */; };
Expand Down Expand Up @@ -572,6 +574,8 @@
0389DDD22C39E4D40005B808 /* PasteLinkButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteLinkButtonView.swift; sourceTree = "<group>"; };
0389DDD42C39F1290005B808 /* CommunityListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListRow.swift; sourceTree = "<group>"; };
0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBuilder.swift; sourceTree = "<group>"; };
0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsView.swift; sourceTree = "<group>"; };
0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadMenu.swift; sourceTree = "<group>"; };
0397D45F2C66113F002C6CDC /* CommentBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBodyView.swift; sourceTree = "<group>"; };
0397D4612C676B46002C6CDC /* ApiSortType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApiSortType+Extensions.swift"; sourceTree = "<group>"; };
0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSortPicker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -904,6 +908,7 @@
03267D832BED49CE009D6268 /* AccountSettingsView.swift */,
037DE0742CE023E3007F7B92 /* BlockListView.swift */,
03AB48512CBC042E00567FF9 /* AccountGeneralSettingsView.swift */,
0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */,
03AB48542CBC0B8000567FF9 /* AccountAdvancedSettingsView.swift */,
03AB48562CBC0DFC00567FF9 /* AccountSignInSettingsView.swift */,
03AB48582CBC14CE00567FF9 /* AccountEmailSettingsView.swift */,
Expand Down Expand Up @@ -1512,6 +1517,7 @@
0355F9482C16406E00605248 /* Line.swift */,
03D2A6402C011F3E00ED4FF2 /* ListRow */,
03B431C12C45BA00001A1EB5 /* MarkdownEditorToolbarView.swift */,
0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */,
034B94842C09348400039AF4 /* MarkdownImageView.swift */,
03B431B32C4481C3001A1EB5 /* MarkdownTextEditor.swift */,
03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */,
Expand Down Expand Up @@ -2335,6 +2341,7 @@
CD4D59162B87B38C00B82964 /* UIApplication+Extensions.swift in Sources */,
CDB41E8A2C83C24400BD2DE9 /* Section.swift in Sources */,
CDCA44B22C17675600C092B3 /* Haptic.swift in Sources */,
0391E0FB2D0066240040CCA8 /* ImageUploadMenu.swift in Sources */,
035BE08B2BDD903100F77D73 /* NavigationModel.swift in Sources */,
033F84BB2C2ACB96002E3EDF /* CommentView.swift in Sources */,
03AFD0E52C3C14D50054B8AD /* InstanceStubProviding+Extensions.swift in Sources */,
Expand Down Expand Up @@ -2478,6 +2485,7 @@
039F58972C7B68F100C61658 /* AboutMlemView.swift in Sources */,
035EDEFB2C2DF98700F51144 /* CommunityListRowBody.swift in Sources */,
CD77437F2C1BA5CE0085BB43 /* MultiplatformView.swift in Sources */,
0391E0F92CFF17AE0040CCA8 /* ProfileSettingsView.swift in Sources */,
03E614E72C0BCDC200F692A4 /* FullyQualifiedLinkView.swift in Sources */,
B1B78D642A51D53900F72485 /* AppDelegate.swift in Sources */,
);
Expand Down Expand Up @@ -2938,8 +2946,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mlemgroup/MlemMiddleware";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.50.0;
branch = "sjmarf/profile-settings";
kind = branch;
Sjmarf marked this conversation as resolved.
Show resolved Hide resolved
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mlemgroup/MlemMiddleware",
"state" : {
"revision" : "1458bfe5a0fd16a882ff1aaea86e0bff1ac7b1a3",
"version" : "0.50.0"
"branch" : "sjmarf/profile-settings",
"revision" : "ee8d65661e41ebd85516b92f9ef20c685111b1f7"
}
},
{
Expand Down
7 changes: 7 additions & 0 deletions Mlem/App/Views/Root/Tabs/Profile/Profile View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ struct ProfileView: View {
var body: some View {
if let person = appState.firstPerson {
PersonView(person: .init(person), isProfileTab: true)
.toolbar {
ToolbarItem(placement: .secondaryAction) {
Button("Edit", systemImage: Icons.edit) {
navigation.openSheet(.settings(.profile))
}
}
}
.id(person.actorId)
} else if let instance = appState.firstSession.instance {
InstanceView(instance: instance)
Expand Down
6 changes: 6 additions & 0 deletions Mlem/App/Views/Root/Tabs/Settings/AccountSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ struct AccountSettingsView: View {

if appState.firstSession is UserSession {
Section {
NavigationLink(
"My Profile",
systemImage: Icons.personFill,
destination: .settings(.profile)
)
.tint(palette.colorfulAccent(5))
NavigationLink(
"Sign-In & Security",
systemImage: "key.fill",
Expand Down
224 changes: 224 additions & 0 deletions Mlem/App/Views/Root/Tabs/Settings/ProfileSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//
// ProfileSettingsView.swift
// Mlem
//
// Created by Sjmarf on 2024-12-03.
//

import MlemMiddleware
import SwiftUI

struct ProfileSettingsView: View {
@Environment(NavigationLayer.self) var navigation
@Environment(Palette.self) var palette

@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss

let person: Person4
@State var displayName: String

@State var bioTextView: UITextView = .init()
@State var bioHasChanged: Bool = false
@State var uploadHistory: ImageUploadHistoryManager = .init()

@State var avatarUrl: URL?
@State var avatarManager: ImageUploadManager = .init()

@State var bannerUrl: URL?
@State var bannerManager: ImageUploadManager = .init()

@State var isSubmitting: Bool = false

init(person: Person4) {
self.person = person
self._displayName = .init(wrappedValue: person.displayName == person.name ? "" : person.displayName)
bioTextView.text = person.description ?? ""
self._avatarUrl = .init(wrappedValue: person.avatar)
self._bannerUrl = .init(wrappedValue: person.banner)
}

var minTextEditorHeight: CGFloat {
UIFont.preferredFont(forTextStyle: .body).lineHeight * 6 + 20
}

var body: some View {
Form {
Section("Display Name") {
TextField("Display Name", text: $displayName, prompt: Text(person.name))
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
} footer: {
Text("The name that is displayed on your profile. This is not the same as your username, which cannot be changed.")
}
Section("Biography") {
MarkdownTextEditor(
onChange: { newValue in
bioHasChanged = (person.description ?? "") != newValue
},
prompt: "Write a bit about yourself...",
textView: bioTextView,
insets: .init(
top: Constants.main.standardSpacing,
left: Constants.main.standardSpacing,
bottom: Constants.main.standardSpacing,
right: Constants.main.standardSpacing
),
firstResponder: false,
sizingOffset: 10,
content: {
MarkdownEditorToolbarView(
textView: bioTextView,
uploadHistory: uploadHistory,
imageUploadApi: person.api
)
}
)
.frame(
maxWidth: .infinity,
minHeight: minTextEditorHeight,
maxHeight: .infinity,
alignment: .topLeading
)
.listRowInsets(.init())
}
avatarSection
bannerSection
}
.navigationTitle("My Profile")
.navigationBarTitleDisplayMode(.inline)
.scrollDismissesKeyboard(.interactively)
.navigationBarBackButtonHidden(showToolbarOptions)
.interactiveDismissDisabled(showToolbarOptions)
.toolbar {
if showToolbarOptions {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") {
displayName = person.displayName == person.name ? "" : person.displayName
bioTextView.text = person.description ?? ""
bioHasChanged = false
avatarUrl = person.avatar
bannerUrl = person.banner
}
.disabled(isSubmitting)
}
ToolbarItem(placement: .topBarTrailing) {
if isSubmitting {
ProgressView()
} else {
Button("Save") {
Task { @MainActor in
await submit()
}
}
}
}
} else if navigation.isInsideSheet {
ToolbarItem(placement: .topBarTrailing) {
CloseButtonView()
}
}
}
}

var showToolbarOptions: Bool {
let originalDisplayName = (person.displayName == person.name) ? "" : person.displayName
return bioHasChanged || displayName != originalDisplayName || avatarUrl != person.avatar || bannerUrl != person.banner
}

@ViewBuilder
var avatarSection: some View {
Section {
HStack(spacing: 15) {
CircleCroppedImageView(url: avatarUrl, frame: 48, fallback: .person)
.id(avatarUrl)
Text("Avatar")
Spacer()
CircleImageUploadButton(imageManager: avatarManager, url: $avatarUrl, api: person.api)
}
.onChange(of: avatarManager.image?.url) {
avatarUrl = avatarManager.image?.url
}
}
}

@ViewBuilder
var bannerSection: some View {
Section {
VStack(spacing: 0) {
if let bannerUrl {
LargeImageView(url: bannerUrl, shouldBlur: false, cornerRadius: 0)
.id(bannerUrl)
.aspectRatio(contentMode: .fill)
.frame(height: 150)
.clipped()
} else {
palette.secondary.opacity(0.5)
.frame(height: 150)
}
HStack(spacing: 15) {
Text("Banner")
Spacer()
CircleImageUploadButton(imageManager: bannerManager, url: $bannerUrl, api: person.api)
}
.padding(.horizontal, 15)
.padding(.vertical, 10)
}
.onChange(of: bannerManager.image?.url) {
bannerUrl = bannerManager.image?.url
}
}
.listRowInsets(.init())
}

@MainActor
func submit() async {
isSubmitting = true
do {
try await person.updateProfile(
displayName: displayName.isEmpty ? nil : displayName,
description: bioTextView.text.isEmpty ? nil : bioTextView.text,
avatar: avatarUrl,
banner: bannerUrl
)
dismiss()
} catch {
handleError(error)
}
isSubmitting = false
}
}

private struct CircleImageUploadButton: View {
let imageManager: ImageUploadManager
@Binding var url: URL?
let api: ApiClient

var body: some View {
Group {
if url != nil {
Button {
url = nil
} label: {
Image(systemName: "xmark.circle.fill")
Sjmarf marked this conversation as resolved.
Show resolved Hide resolved
.resizable()
}
} else {
switch imageManager.state {
case .uploading:
ProgressView()
.controlSize(.extraLarge)
default:
ImageUploadMenu(imageManager: imageManager, imageUploadApi: api) {
Image(systemName: "plus.circle.fill")
Sjmarf marked this conversation as resolved.
Show resolved Hide resolved
.resizable()
}
}
}
}
.aspectRatio(contentMode: .fit)
.frame(height: 48)
.symbolRenderingMode(.hierarchical)
.fontWeight(.thin)
}
}
38 changes: 38 additions & 0 deletions Mlem/App/Views/Shared/ImageUploadMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// ImageUploadMenu.swift
// Mlem
//
// Created by Sjmarf on 2024-12-04.
//

import MlemMiddleware
import SwiftUI

struct ImageUploadMenu<Label: View>: View {
@Environment(NavigationLayer.self) var navigation

let imageManager: ImageUploadManager
let imageUploadApi: ApiClient
@ViewBuilder let label: () -> Label

init(imageManager: ImageUploadManager, imageUploadApi: ApiClient, @ViewBuilder label: @escaping () -> Label) {
self.imageManager = imageManager
self.imageUploadApi = imageUploadApi
self.label = label
}

var body: some View {
Menu(content: {
Button("Photo Library", systemImage: Icons.photo) {
navigation.showPhotosPicker(for: imageManager, api: imageUploadApi)
}
Button("Choose File", systemImage: "folder") {
Sjmarf marked this conversation as resolved.
Show resolved Hide resolved
navigation.showFilePicker(for: imageManager, api: imageUploadApi)
}
Button("Paste", systemImage: Icons.paste) {
navigation.uploadImageFromClipboard(for: imageManager, api: imageUploadApi)
}
}, label: label)
.disabled(imageManager.state != .idle)
}
}
15 changes: 11 additions & 4 deletions Mlem/App/Views/Shared/Images/Wrappers/LargeImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,29 @@ struct LargeImageView: View {

let url: URL?
let shouldBlur: Bool
var onTapActions: (() -> Void)?
let onTapActions: (() -> Void)?
let cornerRadius: CGFloat
@State var blurred: Bool = false

init(url: URL?, shouldBlur: Bool, onTapActions: (() -> Void)? = nil) {
init(
url: URL?,
shouldBlur: Bool,
cornerRadius: CGFloat = Constants.main.mediumItemCornerRadius,
onTapActions: (() -> Void)? = nil
) {
self.url = url
self.onTapActions = onTapActions
self.shouldBlur = shouldBlur
self.cornerRadius = cornerRadius
self._blurred = .init(wrappedValue: shouldBlur)
}

@State private var loading: MediaLoadingState?

var body: some View {
DynamicMediaView(url: url)
DynamicMediaView(url: url, cornerRadius: cornerRadius)
.dynamicBlur(blurred: blurred)
.clipShape(.rect(cornerRadius: Constants.main.mediumItemCornerRadius))
.clipShape(.rect(cornerRadius: cornerRadius))
.overlay {
NsfwOverlay(blurred: $blurred, shouldBlur: shouldBlur)
}
Expand Down
Loading
Loading