add account popovers for display name and handle (#1687)

This commit is contained in:
Thai D. V 2023-11-27 15:00:52 +07:00 committed by GitHub
parent 98e8ffe4a3
commit ea5480ef46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 97 additions and 74 deletions

View file

@ -105,7 +105,7 @@ struct AccountSettingsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
HStack { HStack {
AvatarView(account: account, config: .embed) AvatarView(account.avatar, config: .embed)
Text(account.safeDisplayName) Text(account.safeDisplayName)
.font(.headline) .font(.headline)
} }

View file

@ -99,7 +99,7 @@ struct AccountDetailHeaderView: View {
private var accountAvatarView: some View { private var accountAvatarView: some View {
HStack { HStack {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
AvatarView(account: account, config: .account) AvatarView(account.avatar, config: .account)
.accessibilityLabel("accessibility.tabs.profile.user-avatar.label") .accessibilityLabel("accessibility.tabs.profile.user-avatar.label")
if viewModel.isCurrentUser, isSupporter { if viewModel.isCurrentUser, isSupporter {
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.seal.fill")

View file

@ -214,7 +214,7 @@ public struct AccountDetailView: View {
Button { Button {
routerPath.navigate(to: .accountDetailWithAccount(account: account)) routerPath.navigate(to: .accountDetailWithAccount(account: account))
} label: { } label: {
AvatarView(account: account, config: .badge) AvatarView(account.avatar, config: .badge)
.padding(.leading, -4) .padding(.leading, -4)
.accessibilityLabel(account.safeDisplayName) .accessibilityLabel(account.safeDisplayName)

View file

@ -44,7 +44,7 @@ public struct AccountsListRow: View {
public var body: some View { public var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
AvatarView(account: viewModel.account, config: .status) AvatarView(viewModel.account.avatar)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis) EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis)
.font(.scaledSubheadline) .font(.scaledSubheadline)

View file

@ -35,7 +35,7 @@ public struct AppAccountView: View {
private var compactView: some View { private var compactView: some View {
HStack { HStack {
if let account = viewModel.account { if let account = viewModel.account {
AvatarView(account: account) AvatarView(account.avatar)
} else { } else {
ProgressView() ProgressView()
} }
@ -61,7 +61,7 @@ public struct AppAccountView: View {
HStack { HStack {
if let account = viewModel.account { if let account = viewModel.account {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
AvatarView(account: account) AvatarView(account.avatar)
if viewModel.appAccount.id == appAccounts.currentAccount.id { if viewModel.appAccount.id == appAccounts.currentAccount.id {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.white, .green) .foregroundStyle(.white, .green)

View file

@ -73,9 +73,9 @@ public struct AppAccountsSelectorView: View {
private var labelView: some View { private var labelView: some View {
Group { Group {
if let account = currentAccount.account, !currentAccount.isLoadingAccount { if let account = currentAccount.account, !currentAccount.isLoadingAccount {
AvatarView(account: account, config: avatarConfig) AvatarView(account.avatar, config: avatarConfig)
} else { } else {
AvatarView(account: nil, config: avatarConfig) AvatarView(config: avatarConfig)
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.allowsHitTesting(false) .allowsHitTesting(false)
} }

View file

@ -27,7 +27,7 @@ struct ConversationMessageView: View {
if isOwnMessage { if isOwnMessage {
Spacer() Spacer()
} else { } else {
AvatarView(account: message.account, config: .status) AvatarView(message.account.avatar)
.onTapGesture { .onTapGesture {
routerPath.navigate(to: .accountDetailWithAccount(account: message.account)) routerPath.navigate(to: .accountDetailWithAccount(account: message.account))
} }

View file

@ -24,7 +24,7 @@ struct ConversationsListRow: View {
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
AvatarView(account: conversation.accounts.first!) AvatarView(conversation.accounts.first!.avatar)
.accessibilityHidden(true) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack { HStack {

View file

@ -8,49 +8,13 @@ import Models
public struct AvatarView: View { public struct AvatarView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@State private var showPopup = false public let avatar: URL?
@State private var autoDismiss = true
@State private var toggleTask: Task<Void, Never> = Task {}
public let account: Account?
public let config: FrameConfig public let config: FrameConfig
public let hasPopup: Bool
public var body: some View { public var body: some View {
if let account = account { if let avatar {
if hasPopup { AvatarImage(avatar, config: adaptiveConfig)
AvatarImage(account: account, config: adaptiveConfig) .frame(width: config.width, height: config.height)
.frame(width: config.width, height: config.height)
.onHover { hovering in
if hovering {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if !showPopup {
showPopup = true
}
}
} else {
if !showPopup {
toggleTask.cancel()
}
}
}
.hoverEffect(.lift)
.popover(isPresented: $showPopup) {
AccountPopupView(
account: account,
theme: theme,
showPopup: $showPopup,
autoDismiss: $autoDismiss,
toggleTask: $toggleTask
)
}
} else {
AvatarImage(account: account, config: adaptiveConfig)
.frame(width: config.width, height: config.height)
}
} else { } else {
AvatarPlaceHolder(config: adaptiveConfig) AvatarPlaceHolder(config: adaptiveConfig)
} }
@ -66,10 +30,9 @@ public struct AvatarView: View {
return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius) return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
} }
public init(account: Account?, config: FrameConfig = FrameConfig.status, hasPopup: Bool = false) { public init(_ avatar: URL? = nil, config: FrameConfig = .status) {
self.account = account self.avatar = avatar
self.config = config self.config = config
self.hasPopup = hasPopup
} }
public struct FrameConfig: Equatable { public struct FrameConfig: Equatable {
@ -110,7 +73,7 @@ struct PreviewWrapper: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
AvatarView(account: Self.account, config: .status) AvatarView(Self.account.avatar)
.environment(Theme.shared) .environment(Theme.shared)
Toggle("Avatar Shape", isOn: $isCircleAvatar) Toggle("Avatar Shape", isOn: $isCircleAvatar)
} }
@ -147,14 +110,14 @@ struct PreviewWrapper: View {
struct AvatarImage: View { struct AvatarImage: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
public let account: Account public let avatar: URL
public let config: AvatarView.FrameConfig public let config: AvatarView.FrameConfig
var body: some View { var body: some View {
if reasons == .placeholder { if reasons == .placeholder {
AvatarPlaceHolder(config: config) AvatarPlaceHolder(config: config)
} else { } else {
LazyImage(request: ImageRequest(url: account.avatar, processors: [.resize(size: config.size)]) LazyImage(request: ImageRequest(url: avatar, processors: [.resize(size: config.size)])
) { state in ) { state in
if let image = state.image { if let image = state.image {
image image
@ -169,6 +132,11 @@ struct AvatarImage: View {
} }
} }
} }
init(_ avatar: URL, config: AvatarView.FrameConfig) {
self.avatar = avatar
self.config = config
}
} }
struct AvatarPlaceHolder: View { struct AvatarPlaceHolder: View {
@ -181,7 +149,7 @@ struct AvatarPlaceHolder: View {
} }
} }
struct AccountPopupView: View { struct AccountPopoverView: View {
let account: Account let account: Account
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
private let config: AvatarView.FrameConfig = .account private let config: AvatarView.FrameConfig = .account
@ -204,7 +172,7 @@ struct AccountPopupView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .bottomAvatar) { HStack(alignment: .bottomAvatar) {
AvatarImage(account: account, config: adaptiveConfig) AvatarImage(account.avatar, config: adaptiveConfig)
Spacer() Spacer()
makeCustomInfoLabel(title: "account.following", count: account.followingCount ?? 0) makeCustomInfoLabel(title: "account.following", count: account.followingCount ?? 0)
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount ?? 0) makeCustomInfoLabel(title: "account.posts", count: account.statusesCount ?? 0)
@ -317,3 +285,53 @@ private enum BottomAvatarAlignment: AlignmentID {
extension VerticalAlignment { extension VerticalAlignment {
static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self) static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self)
} }
public struct AccountPopoverModifier : ViewModifier {
@Environment(Theme.self) private var theme
@State private var showPopup = false
@State private var autoDismiss = true
@State private var toggleTask: Task<Void, Never> = Task {}
let account: Account
public func body(content: Content) -> some View {
content
.onHover { hovering in
if hovering {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if !showPopup {
showPopup = true
}
}
} else {
if !showPopup {
toggleTask.cancel()
}
}
}
.hoverEffect(.lift)
.popover(isPresented: $showPopup) {
AccountPopoverView(
account: account,
theme: theme,
showPopup: $showPopup,
autoDismiss: $autoDismiss,
toggleTask: $toggleTask
)
}
}
init(_ account: Account) {
self.account = account
}
}
extension View {
public func accountPopover(_ account: Account) -> some View {
modifier(AccountPopoverModifier(account))
}
}

View file

@ -30,7 +30,7 @@ public struct ListEditView: View {
} else { } else {
ForEach(viewModel.accounts) { account in ForEach(viewModel.accounts) { account in
HStack { HStack {
AvatarView(account: account, config: .status) AvatarView(account.avatar)
VStack(alignment: .leading) { VStack(alignment: .leading) {
EmojiTextApp(.init(stringValue: account.safeDisplayName), EmojiTextApp(.init(stringValue: account.safeDisplayName),
emojis: account.emojis) emojis: account.emojis)

View file

@ -52,7 +52,7 @@ struct NotificationRowView: View {
private func makeAvatarView(type: Models.Notification.NotificationType) -> some View { private func makeAvatarView(type: Models.Notification.NotificationType) -> some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
AvatarView(account: notification.accounts[0], hasPopup: true) AvatarView(notification.accounts[0].avatar)
makeNotificationIconView(type: type) makeNotificationIconView(type: type)
.offset(x: -8, y: -8) .offset(x: -8, y: -8)
} }
@ -83,7 +83,7 @@ struct NotificationRowView: View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) { LazyHStack(spacing: 8) {
ForEach(notification.accounts) { account in ForEach(notification.accounts) { account in
AvatarView(account: account, hasPopup: true) AvatarView(account.avatar)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
routerPath.navigate(to: .accountDetailWithAccount(account: account)) routerPath.navigate(to: .accountDetailWithAccount(account: account))

View file

@ -31,7 +31,7 @@ struct StatusEditorAutoCompleteView: View {
viewModel.selectMentionSuggestion(account: account) viewModel.selectMentionSuggestion(account: account)
} label: { } label: {
HStack { HStack {
AvatarView(account: account, config: AvatarView.FrameConfig.badge) AvatarView(account.avatar, config: AvatarView.FrameConfig.badge)
VStack(alignment: .leading) { VStack(alignment: .leading) {
EmojiTextApp(.init(stringValue: account.safeDisplayName), EmojiTextApp(.init(stringValue: account.safeDisplayName),
emojis: account.emojis) emojis: account.emojis)

View file

@ -248,7 +248,7 @@ public struct StatusEditorView: View {
accountCreationEnabled: false, accountCreationEnabled: false,
avatarConfig: .status) avatarConfig: .status)
} else { } else {
AvatarView(account: account, config: AvatarView.FrameConfig.status) AvatarView(account.avatar, config: AvatarView.FrameConfig.status)
.environment(theme) .environment(theme)
.accessibilityHidden(true) .accessibilityHidden(true)
} }

View file

@ -46,7 +46,7 @@ public struct StatusEmbeddedView: View {
private func makeAccountView(account: Account) -> some View { private func makeAccountView(account: Account) -> some View {
HStack(alignment: .center) { HStack(alignment: .center) {
AvatarView(account: account, config: .embed, hasPopup: true) AvatarView(account.avatar, config: .embed)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis) EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledFootnote) .font(.scaledFootnote)

View file

@ -68,7 +68,7 @@ public struct StatusRowView: View {
Button { Button {
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account) viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
} label: { } label: {
AvatarView(account: viewModel.finalStatus.account, config: .status, hasPopup: true) AvatarView(viewModel.finalStatus.account.avatar)
} }
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {

View file

@ -103,7 +103,7 @@ struct StatusRowDetailView: View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) { LazyHStack(spacing: 0) {
ForEach(accounts) { account in ForEach(accounts) { account in
AvatarView(account: account, config: .list, hasPopup: true) AvatarView(account.avatar, config: .list)
.padding(.leading, -4) .padding(.leading, -4)
} }
.transition(.opacity) .transition(.opacity)

View file

@ -42,19 +42,22 @@ struct StatusRowHeaderView: View {
private var accountView: some View { private var accountView: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
if theme.avatarPosition == .top { if theme.avatarPosition == .top {
AvatarView(account: viewModel.finalStatus.account, config: .status, hasPopup: true) AvatarView(viewModel.finalStatus.account.avatar)
.accountPopover(viewModel.finalStatus.account)
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 2) {
Group { Group {
EmojiTextApp(.init(stringValue: viewModel.finalStatus.account.safeDisplayName), EmojiTextApp(.init(stringValue: viewModel.finalStatus.account.safeDisplayName),
emojis: viewModel.finalStatus.account.emojis) emojis: viewModel.finalStatus.account.emojis)
.font(.scaledSubheadline) .font(.scaledSubheadline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
.emojiSize(Font.scaledSubheadlineFont.emojiSize) .emojiSize(Font.scaledSubheadlineFont.emojiSize)
.emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) .emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
.fontWeight(.semibold) .fontWeight(.semibold)
.lineLimit(1) .lineLimit(1)
.accountPopover(viewModel.finalStatus.account)
accountBadgeView accountBadgeView
.font(.footnote) .font(.footnote)
} }
@ -69,6 +72,7 @@ struct StatusRowHeaderView: View {
.font(.scaledFootnote) .font(.scaledFootnote)
.foregroundColor(.gray) .foregroundColor(.gray)
.lineLimit(1) .lineLimit(1)
.accountPopover(viewModel.finalStatus.account)
} }
} }
if theme.avatarPosition == .top { if theme.avatarPosition == .top {
@ -82,6 +86,7 @@ struct StatusRowHeaderView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
.lineLimit(1) .lineLimit(1)
.offset(y: 1) .offset(y: 1)
.accountPopover(viewModel.finalStatus.account)
} }
} }
} }

View file

@ -8,7 +8,7 @@ struct StatusRowReblogView: View {
if viewModel.status.reblog != nil { if viewModel.status.reblog != nil {
HStack(spacing: 2) { HStack(spacing: 2) {
Image("Rocket.Fill") Image("Rocket.Fill")
AvatarView(account: viewModel.status.account, config: .boost, hasPopup: true) AvatarView(viewModel.status.account.avatar, config: .boost)
EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis) EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis)
Text("status.row.was-boosted") Text("status.row.was-boosted")
} }