johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import SwiftUI
struct OwnProfileView: View {
@Environment(Account.self) private var account
var body: some View {
if let handle = account.person?.handle {
ProfileScreen(actor: handle)
.id(handle)
} else if account.checking {
ProgressView()
} else {
SignInForm(why: "Sign in to see your profile.")
}
}
}
struct ProfileScreen: View {
@Environment(Account.self) private var account
let actor: String
private struct Answer: Sendable { let profile: Profile; let repos: [Repo] }
var body: some View {
Loading(load: { [actor] in
async let profile: Profile = Client.shared.query("getProfile", ["actor": actor])
async let repos: RepoList = Client.shared.query("getActorRepos", ["actor": actor])
return try await Answer(profile: profile, repos: repos.repos)
}, refresh: account.follows) { answer in
ProfilePage(profile: answer.profile, repos: answer.repos)
}
.navigationTitle(actor.hasPrefix("did:") ? "Profile" : actor)
.navigationBarTitleDisplayMode(.inline)
// Out here, so that an account whose profile won't load can still sign out.
.toolbar {
if actor == account.person?.handle || actor == account.person?.did {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(value: Route.settings) { Oct(name: "gear", size: Icon.xl).foregroundStyle(.primary) }
.accessibilityLabel("Settings")
}
}
}
}
}
private struct ProfilePage: View {
@Environment(Account.self) private var account
let profile: Profile
let repos: [Repo]
@State private var reporting: Reporting?
@State private var failure: String?
private var other: Bool { account.signedIn && account.person?.did != profile.did }
var body: some View {
let p = profile
ScrollView {
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: Space.s3_5) {
HStack(spacing: Space.s3_5) {
Avatar(url: p.avatar, name: p.handle, did: p.did, size: Size.Avatar.xlarge)
VStack(alignment: .leading, spacing: Space.s0_5) {
Text(p.displayName ?? p.handle).font(.title2.bold())
Text(p.handle).foregroundStyle(.secondary)
if let pronouns = p.pronouns { Text(pronouns).font(.footnote).foregroundStyle(.secondary) }
}
}
if let about = p.description, !about.isEmpty { Text(about) }
if let link = p.links?.first { LinkLine(link: link) }
HStack(spacing: Space.s1_5) {
Oct(name: "person").foregroundStyle(.secondary)
count(.followers(p.handle), p.followersCount ?? 0, (p.followersCount ?? 0) == 1 ? "follower" : "followers")
Text("·").foregroundStyle(.secondary)
count(.following(p.handle), p.followsCount ?? 0, "following")
}
.font(.subheadline)
if account.signedIn, account.person?.did != p.did {
FollowButton(did: p.did, following: p.viewer?.following != nil)
}
}
.padding(.horizontal, Space.s4)
.padding(.top, Space.s2)
.padding(.bottom, Space.s5)
.frame(maxWidth: .infinity, alignment: .leading)
Sections {
PageRow(.repositories(p.handle), "Repositories", "repo", .neutral, repos.count)
}
if let orgs = p.orgs, !orgs.isEmpty {
SectionTitle(icon: "organization", title: "Organizations") { EmptyView() }
Sections {
ForEach(Array(orgs.enumerated()), id: \.element.actor.did) { index, org in
if index > 0 { RowDivider() }
NavigationLink(value: Route.profile(org.actor.handle)) {
HStack(spacing: Space.s3) {
Avatar(url: org.actor.avatar, name: org.actor.handle, did: org.actor.did, size: Size.Avatar.regular)
Text(org.actor.handle)
Spacer()
RowChevron(name: "chevron-right")
}
.padding(.horizontal, Space.s4)
.padding(.vertical, Space.s2)
.frame(minHeight: Size.row)
.contentShape(Rectangle())
}
.buttonStyle(RowPress())
}
}
}
if !repos.isEmpty {
SectionTitle(icon: "clock", title: "Recent") { EmptyView() }
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: Space.s3) {
ForEach(repos.prefix(6)) { RepoCard(repo: $0) }
}
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, Space.s4)
}
}
}
.padding(.bottom, Space.s6)
}
.background(Color.groupedPage)
.toolbar {
if other {
ToolbarItem(placement: .topBarTrailing) {
Menu {
let blocking = profile.viewer?.blocking != nil
Button("\(blocking ? "Unblock" : "Block") \(profile.handle)") { toggleBlock(undo: blocking) }
Button("Report \(profile.handle)") { reporting = Reporting(subject: profile.did, what: profile.handle) }
NavigationLink("Blocked accounts", value: Route.blocks)
} label: {
SwiftUI.Label("More about \(profile.handle)", systemImage: "ellipsis")
}
}
}
}
.sheet(item: $reporting) { ReportSheet(reporting: $0) }
.alert("That didn't work", isPresented: Binding(get: { failure != nil }, set: { if !$0 { failure = nil } })) {
Button("OK", role: .cancel) {}
} message: {
Text(failure ?? "")
}
}
private func toggleBlock(undo: Bool) {
Task {
do {
try await Client.shared.send(Writes.block(subject: profile.did, undo: undo))
account.follows += 1
} catch {
failure = error.localizedDescription
}
}
}
private func count(_ route: Route, _ number: Int, _ word: String) -> some View {
NavigationLink(value: route) {
Text("\(Text("\(number)").fontWeight(.semibold)) \(Text(word).foregroundStyle(.secondary))")
}
.buttonStyle(.plain)
}
}
/// Who follows an account, or whom it follows.
struct FollowsView: View {
let actor: String
let followers: Bool
var body: some View {
PagedList<Person, ActorRow>(empty: followers ? "No followers yet." : "Not following anyone yet.", fetch: { [actor, followers] cursor in
let answer: ActorPage = try await Client.shared.query(followers ? "getFollowers" : "getFollows", ["actor": actor, "cursor": cursor])
return .init(items: answer.actors, cursor: answer.cursor)
}) { ActorRow(person: $0) }
.navigationTitle(followers ? "Followers" : "Following")
.navigationBarTitleDisplayMode(.inline)
}
}
private struct ActorRow: View {
let person: Person
var body: some View {
NavigationLink(value: Route.profile(person.handle)) {
HStack(spacing: Space.s3) {
Avatar(url: person.avatar, name: person.handle, did: person.did, size: Size.Avatar.medium)
Text(person.handle).lineLimit(1)
}
}
}
}
private struct FollowButton: View {
@Environment(Account.self) private var account
let did: String
@State var following: Bool
@State private var busy = false
@State private var failure: String?
var body: some View {
Button {
Task { await toggle() }
} label: {
HStack(spacing: Space.s1_5) {
if !following { Image(systemName: "plus") }
Text(following ? "Following" : "Follow").fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(HeaderButton())
.disabled(busy)
.alert("That didn't work", isPresented: Binding(get: { failure != nil }, set: { if !$0 { failure = nil } })) {
Button("OK", role: .cancel) {}
} message: {
Text(failure ?? "")
}
}
private func toggle() async {
busy = true
defer { busy = false }
do {
try await Client.shared.send(Writes.follow(did: did, undo: following))
following.toggle()
account.follows += 1
} catch {
failure = error.localizedDescription
}
}
}
private struct RepoStats: View {
let repo: Repo
var body: some View {
HStack(spacing: Space.s3) {
if let stars = repo.starCount {
HStack(spacing: Space.s1) {
Oct(name: "star")
Text("\(stars)")
}
}
if let language = repo.language {
HStack(spacing: Space.s1_5) {
Circle().fill(Color.hex(language.color) ?? .neutral).frame(width: 10, height: 10)
Text(language.name)
}
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
private struct RepoCard: View {
let repo: Repo
var body: some View {
NavigationLink(value: Route.repo(repo.ref)) {
VStack(alignment: .leading, spacing: Space.s1_5) {
HStack(spacing: Space.s1_5) {
Avatar(url: repo.owner.avatar, name: repo.owner.handle, did: repo.owner.did, size: Size.Avatar.xsmall)
Text(repo.owner.handle).font(.subheadline).foregroundStyle(.secondary).lineLimit(1)
}
Text(repo.name).font(.headline).lineLimit(1)
if let about = repo.description { Text(about).font(.subheadline).lineLimit(3) }
Spacer(minLength: 4)
RepoStats(repo: repo)
}
.padding(Space.s3_5)
.frame(width: 280, alignment: .leading)
.frame(maxHeight: .infinity, alignment: .topLeading)
.background(Color.card, in: RoundedRectangle(cornerRadius: Radius.large))
}
.buttonStyle(.plain)
}
}
struct RepositoriesView: View {
let actor: String
var body: some View {
Loading(load: { [actor] in
let list: RepoList = try await Client.shared.query("getActorRepos", ["actor": actor])
return list.repos
}) { repos in
List { ForEach(repos) { repo in
NavigationLink(value: Route.repo(repo.ref)) {
VStack(alignment: .leading, spacing: Space.s1_5) {
HStack(spacing: Space.s1_5) {
Text(repo.name).font(.body.weight(.semibold)).lineLimit(1)
if repo.private == true {
Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary).accessibilityLabel("Private")
}
}
if let about = repo.description, !about.isEmpty { Text(about).lineLimit(3) }
RepoStats(repo: repo)
}
}
}.listRowBackground(Color.page) }
.listStyle(.plain)
}
.navigationTitle("Repositories")
.navigationBarTitleDisplayMode(.inline)
}
}