1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

zaps: Improve discoverability of profile zaps

via zappability badges and profile action sheets

This commit improves discoverability of zaps with the following changes:

1. New zap icon appears on profile pictures of events where the author of such event has zaps setup
2. Clicking on a profile picture from an event shows an action sheet that makes it easier to see a preview of their profile, and a zap button

Testing
-------

Devices:
- iPhone 14 Pro simulator
- iPad 10 simulator

iOS:
- 17.0.1
- 16.4

Damus: This commit

Coverage:
1. Checked that zap icon appears on profile pictures on events in different feeds and threads
2. Checked that this zap icon only appears for profiles that have zaps enabled
3. Checked that profile action sheet looks good on both light mode and dark mode
4. Checked that long descriptions are truncated and the "see more" "see less" buttons work
5. Checked that clicking "see more" or "see less" adapts the size of the action sheet (on iPhone)
6. Checked that action sheet looks good whether or not the user has a website link setup
7. Checked that long presses on the zap button in the action sheet bring the same options as the normal profile view
8. Checked all the buttons in the action sheet take the user to the expected place
9. Checked that the original profile view looks good (on both light and dark mode)

Notes:
- Action sheet cannot be resized on iPad.
- Could not test on Mac Catalyst because there seems to be a crash on the creation of a new account

Reference ticket: https://github.com/damus-io/damus/issues/1596

Changelog-Added: Improve discoverability of profile zaps with zappability badges and profile action sheets
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino 2023-10-21 04:44:36 +00:00 committed by William Casarin
parent 4ed79ff3c3
commit e70f270c5c
14 changed files with 453 additions and 137 deletions

View File

@ -430,6 +430,7 @@
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; };
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
@ -437,6 +438,8 @@
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
@ -1136,6 +1139,8 @@
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonView.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
@ -1725,6 +1730,7 @@
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -2236,6 +2242,7 @@
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */,
D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */,
);
path = Zaps;
sourceTree = "<group>";
@ -2960,6 +2967,8 @@
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */,
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,

View File

@ -20,6 +20,20 @@ struct NeutralButtonStyle: ButtonStyle {
}
}
struct NeutralCircleButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.padding(20)
.background(DamusColors.neutral1)
.cornerRadius(9999)
.overlay(
RoundedRectangle(cornerRadius: 9999)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
struct NeutralButtonStyle_Previews: PreviewProvider {
static var previews: some View {

View File

@ -11,12 +11,19 @@ import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
let textAlignment: NSTextAlignment
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
self.attributedString = attributedString
self.textAlignment = textAlignment ?? NSTextAlignment.natural
self.size = size
}
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
@ -24,6 +31,7 @@ struct SelectableText: View {
textColor: UIColor.label,
font: eventviewsize_to_uifont(size),
fixedWidth: selectedTextWidth,
textAlignment: self.textAlignment,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
@ -48,6 +56,7 @@ struct SelectableText: View {
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
let textAlignment: NSTextAlignment
@Binding var height: CGFloat
@ -61,12 +70,14 @@ struct SelectableText: View {
view.textContainerInset = .zero
view.textContainerInset.left = 1.0
view.textContainerInset.right = 1.0
view.textAlignment = textAlignment
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
uiView.textAlignment = self.textAlignment
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)

View File

@ -9,33 +9,57 @@ import SwiftUI
struct WebsiteLink: View {
let url: URL
let style: StyleVariant
@Environment(\.openURL) var openURL
init(url: URL, style: StyleVariant? = nil) {
self.url = url
self.style = style ?? .normal
}
var body: some View {
HStack {
Image("link")
.foregroundColor(.gray)
.font(.footnote)
.resizable()
.frame(width: 16, height: 16)
.foregroundColor(self.style == .accent ? .white : .gray)
.padding(.vertical, 5)
.padding([.leading], 10)
Button(action: {
openURL(url)
}, label: {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
.foregroundColor(self.style == .accent ? .white : .accentColor)
.truncationMode(.tail)
.lineLimit(1)
})
.padding(.vertical, 5)
.padding([.trailing], 10)
}
.background(
self.style == .accent ?
AnyView(RoundedRectangle(cornerRadius: 50).fill(PinkGradient))
: AnyView(Color.clear)
)
}
var link_text: String {
url.host ?? url.absoluteString
}
enum StyleVariant {
case normal
case accent
}
}
struct WebsiteLink_Previews: PreviewProvider {
static var previews: some View {
WebsiteLink(url: URL(string: "https://jb55.com")!)
.previewDisplayName("Normal")
WebsiteLink(url: URL(string: "https://jb55.com")!, style: .accent)
.previewDisplayName("Accent")
}
}

View File

@ -22,6 +22,7 @@ enum Sheets: Identifiable {
case post(PostAction)
case report(ReportTarget)
case event(NostrEvent)
case profile_action(Pubkey)
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter
@ -42,6 +43,7 @@ enum Sheets: Identifiable {
case .user_status: return "user_status"
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
case .event(let ev): return "event-" + ev.id.hex()
case .profile_action(let pubkey): return "profile-action-" + pubkey.npub
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
case .select_wallet: return "select-wallet"
case .filter: return "filter"
@ -316,6 +318,8 @@ struct ContentView: View {
.presentationDragIndicator(.visible)
case .event:
EventDetailView()
case .profile_action(let pubkey):
ProfileActionSheetView(damus_state: damus_state!, pubkey: pubkey)
case .zap(let zapsheet):
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
case .select_wallet(let select):

View File

@ -37,9 +37,9 @@ struct EventProfile: View {
var body: some View {
HStack(alignment: .center, spacing: 10) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true)
.onTapGesture {
damus_state.nav.push(route: .ProfileByKey(pubkey: pubkey))
notify(.present_sheet(Sheets.profile_action(pubkey)))
}
VStack(alignment: .leading, spacing: 0) {

View File

@ -10,15 +10,23 @@ import SwiftUI
struct AboutView: View {
let state: DamusState
let about: String
let max_about_length = 280
let max_about_length: Int
let text_alignment: NSTextAlignment
@State var show_full_about: Bool = false
@State private var about_string: AttributedString? = nil
init(state: DamusState, about: String, max_about_length: Int? = nil, text_alignment: NSTextAlignment? = nil) {
self.state = state
self.about = about
self.max_about_length = max_about_length ?? 280
self.text_alignment = text_alignment ?? .natural
}
var body: some View {
Group {
if let about_string {
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline)
SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
if truncated_about != nil {
if show_full_about {

View File

@ -21,16 +21,16 @@ struct MaybeAnonPfpView: View {
}
var body: some View {
Group {
ZStack {
if is_anon {
Image("question")
.resizable()
.font(.largeTitle)
.frame(width: size, height: size)
} else {
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
notify(.present_sheet(Sheets.profile_action(pubkey)))
}
}
}

View File

@ -7,84 +7,6 @@
import SwiftUI
fileprivate struct KeyView: View {
let pubkey: Pubkey
@Environment(\.colorScheme) var colorScheme
@State private var isCopied = false
func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
private func copyPubkey(_ pubkey: String) {
UIPasteboard.general.string = pubkey
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
withAnimation {
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
isCopied = false
}
}
}
}
func pubkey_context_menu(pubkey: Pubkey) -> some View {
return self.contextMenu {
Button {
UIPasteboard.general.string = pubkey.npub
} label: {
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2")
}
}
}
var body: some View {
let bech32 = pubkey.npub
HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
.foregroundColor(keyColor())
.padding(5)
.padding([.leading, .trailing], 5)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
if isCopied {
HStack {
Image("check-circle")
.resizable()
.frame(width: 20, height: 20)
Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied."))
.font(.footnote)
.layoutPriority(1)
}
.foregroundColor(DamusColors.green)
} else {
HStack {
Button {
copyPubkey(bech32)
} label: {
Label {
Text("Public key", comment: "Label indicating that the text is a user's public account key.")
} icon: {
Image("copy2")
.resizable()
.contentShape(Rectangle())
.foregroundColor(.accentColor)
.frame(width: 20, height: 20)
}
.labelStyle(IconOnlyLabelStyle())
.symbolRenderingMode(.hierarchical)
}
}
}
}
}
}
struct ProfileNameView: View {
let pubkey: Pubkey
let damus: DamusState
@ -116,7 +38,7 @@ struct ProfileNameView: View {
Spacer()
KeyView(pubkey: pubkey)
PubkeyView(pubkey: pubkey)
.pubkey_context_menu(pubkey: pubkey)
}
}

View File

@ -69,38 +69,59 @@ struct ProfilePicView: View {
let highlight: Highlight
let profiles: Profiles
let disable_animation: Bool
let zappability_indicator: Bool
@State var picture: String?
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil) {
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil) {
self.pubkey = pubkey
self.profiles = profiles
self.size = size
self.highlight = highlight
self._picture = State(initialValue: picture)
self.disable_animation = disable_animation
self.zappability_indicator = show_zappability ?? false
}
func get_lnurl() -> String? {
return profiles.lookup_with_timestamp(pubkey).unsafeUnownedValue?.lnurl
}
var body: some View {
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation)
.onReceive(handle_notify(.profile_updated)) { updated in
guard updated.pubkey == self.pubkey else {
return
ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation)
.onReceive(handle_notify(.profile_updated)) { updated in
guard updated.pubkey == self.pubkey else {
return
}
switch updated {
case .manual(_, let profile):
if let pic = profile.picture {
self.picture = pic
}
case .remote(pubkey: let pk):
let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn.unsafeUnownedValue
if let pic = profile?.picture {
self.picture = pic
}
}
}
switch updated {
case .manual(_, let profile):
if let pic = profile.picture {
self.picture = pic
}
case .remote(pubkey: let pk):
let profile_txn = profiles.lookup(id: pk)
let profile = profile_txn.unsafeUnownedValue
if let pic = profile?.picture {
self.picture = pic
}
}
if self.zappability_indicator, let lnurl = self.get_lnurl(), lnurl != "" {
Image("zap.fill")
.resizable()
.frame(
width: size * 0.24,
height: size * 0.24
)
.padding(size * 0.04)
.foregroundColor(.white)
.background(Color.orange)
.clipShape(Circle())
}
}
}
}

View File

@ -221,39 +221,13 @@ struct ProfileView: View {
.accentColor(DamusColors.white)
}
func lnButton(lnurl: String, unownedProfile: Profile?, pubkey: Pubkey) -> some View {
let reactions = unownedProfile?.reactions ?? true
let button_img = reactions ? "zap.fill" : "zap"
let lud16 = unownedProfile?.lud16
return Button(action: { [lnurl] in
present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl))
}) {
Image(button_img)
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View {
return ZapButtonView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in
Image(reactions_enabled ? "zap.fill" : "zap")
.foregroundColor(reactions_enabled ? .orange : Color.primary)
.profile_button_style(scheme: colorScheme)
.contextMenu { [lud16, reactions, lnurl] in
if reactions == false {
Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.")
}
if let lud16 {
Button {
UIPasteboard.general.string = lud16
} label: {
Label(lud16, image: "copy2")
}
} else {
Button {
UIPasteboard.general.string = lnurl
} label: {
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy")
}
}
}
.cornerRadius(24)
}
.cornerRadius(24)
}
var dmButton: some View {
@ -283,7 +257,7 @@ struct ProfileView: View {
let lnurl = record.lnurl,
lnurl != ""
{
lnButton(lnurl: lnurl, unownedProfile: profile, pubkey: pubkey)
lnButton(unownedProfile: profile, record: record)
}
dmButton

View File

@ -0,0 +1,154 @@
//
// ProfileActionSheetView.swift
// damus
//
// Created by Daniel DAquino on 2023-10-20.
//
import SwiftUI
struct ProfileActionSheetView: View {
let damus_state: DamusState
let pfp_size: CGFloat = 90.0
@StateObject var profile: ProfileModel
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
@State private var sheetHeight: CGFloat = .zero
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
init(damus_state: DamusState, pubkey: Pubkey) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
}
func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
}
func profile_data() -> ProfileRecord? {
let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey)
return profile_txn.unsafeUnownedValue
}
func get_profile() -> Profile? {
return self.profile_data()?.profile
}
var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
return VStack(alignment: .center, spacing: 10) {
Button(
action: {
damus_state.nav.push(route: Route.DMChat(dms: dm_model))
dismiss()
},
label: {
Image("messages")
.profile_button_style(scheme: colorScheme)
}
)
.buttonStyle(NeutralCircleButtonStyle())
Text(NSLocalizedString("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen"))
.foregroundStyle(.secondary)
.font(.caption)
}
}
var zapButton: some View {
if let lnurl = self.profile_data()?.lnurl, lnurl != "" {
return AnyView(
VStack(alignment: .center, spacing: 10) {
ZapButtonView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in
Image(reactions_enabled ? "zap.fill" : "zap")
.foregroundColor(reactions_enabled ? .orange : Color.primary)
.profile_button_style(scheme: colorScheme)
}
.buttonStyle(NeutralCircleButtonStyle())
Text(NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen"))
.foregroundStyle(.secondary)
.font(.caption)
}
)
}
else {
return AnyView(EmptyView())
}
}
var profileName: some View {
let display_name = Profile.displayName(profile: self.get_profile(), pubkey: self.profile.pubkey).displayName
return HStack(alignment: .center, spacing: 10) {
Text(display_name)
.font(.title)
}
}
var body: some View {
VStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
if let url = self.profile_data()?.profile?.website_url {
WebsiteLink(url: url, style: .accent)
.padding(.top, -15)
}
profileName
PubkeyView(pubkey: profile.pubkey)
if let about = self.profile_data()?.profile?.about {
AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center)
.padding(.top)
}
HStack(spacing: 20) {
self.dmButton
self.zapButton
}
.padding()
Button(
action: {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: profile.pubkey))
dismiss()
},
label: {
HStack {
Spacer()
Text(NSLocalizedString("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing"))
Image(systemName: "arrow.up.right")
Spacer()
}
}
)
.buttonStyle(NeutralCircleButtonStyle())
}
.padding()
.padding(.top, 20)
.overlay {
GeometryReader { geometry in
Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height)
}
}
.onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in
sheetHeight = newHeight
}
.presentationDetents([.height(sheetHeight)])
}
}
struct InnerHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
#Preview {
ProfileActionSheetView(damus_state: test_damus_state, pubkey: test_pubkey)
}

View File

@ -7,6 +7,89 @@
import SwiftUI
struct PubkeyView: View {
let pubkey: Pubkey
@Environment(\.colorScheme) var colorScheme
@State private var isCopied = false
func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
private func copyPubkey(_ pubkey: String) {
UIPasteboard.general.string = pubkey
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
withAnimation {
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
isCopied = false
}
}
}
}
func pubkey_context_menu(pubkey: Pubkey) -> some View {
return self.contextMenu {
Button {
UIPasteboard.general.string = pubkey.npub
} label: {
Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2")
}
}
}
var body: some View {
let bech32 = pubkey.npub
HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
.foregroundColor(keyColor())
.padding(5)
.padding([.leading], 5)
HStack {
if isCopied {
Image("check-circle")
.resizable()
.foregroundColor(DamusColors.green)
.frame(width: 20, height: 20)
Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied."))
.font(.footnote)
.layoutPriority(1)
.foregroundColor(DamusColors.green)
} else {
Button {
copyPubkey(bech32)
} label: {
Label {
Text("Public key", comment: "Label indicating that the text is a user's public account key.")
} icon: {
Image("copy2")
.resizable()
.contentShape(Rectangle())
.foregroundColor(colorScheme == .light ? DamusColors.darkGrey : DamusColors.lightGrey)
.frame(width: 20, height: 20)
}
.labelStyle(IconOnlyLabelStyle())
.symbolRenderingMode(.hierarchical)
}
}
}
.padding([.trailing], 10)
}
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(colorScheme == .light ? DamusColors.adaptableGrey : DamusColors.neutral1))
}
}
#Preview {
PubkeyView(pubkey: test_pubkey)
}
func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String {
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
}

View File

@ -0,0 +1,92 @@
//
// ZapButtonView.swift
// damus
//
// Created by Daniel DAquino on 2023-10-20.
//
import SwiftUI
struct ZapButtonView<Content: View>: View {
typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content
typealias ActionFunction = () -> Void
let pubkey: Pubkey
@ViewBuilder let label: ContentViewFunction
let action: ActionFunction?
let reactions_enabled: Bool
let lud16: String?
let lnurl: String?
init(pubkey: Pubkey, reactions_enabled: Bool, lud16: String?, lnurl: String?, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
self.pubkey = pubkey
self.label = label
self.action = action
self.reactions_enabled = reactions_enabled
self.lud16 = lud16
self.lnurl = lnurl
}
init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
self.pubkey = pubkey
self.label = label
self.action = action
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey)
let record = profile_txn.unsafeUnownedValue
self.reactions_enabled = record?.profile?.reactions ?? true
self.lud16 = record?.profile?.lud06
self.lnurl = record?.lnurl
}
init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
self.pubkey = profileModel.pubkey
self.label = label
self.action = action
self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true
self.lud16 = unownedProfileRecord?.profile?.lud16
self.lnurl = unownedProfileRecord?.lnurl
}
var body: some View {
Button(
action: {
if let lnurl {
present_sheet(.zap(target: .profile(self.pubkey), lnurl: lnurl))
}
action?()
},
label: {
self.label(self.reactions_enabled, self.lud16, self.lnurl)
}
)
.contextMenu {
if self.reactions_enabled == false {
Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.")
}
if let lud16 {
Button {
UIPasteboard.general.string = lud16
} label: {
Label(lud16, image: "copy2")
}
} else {
Button {
UIPasteboard.general.string = lnurl
} label: {
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy")
}
}
}
.disabled(lnurl == nil)
}
}
#Preview {
ZapButtonView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in
Image("zap.fill")
})
}