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:
parent
4ed79ff3c3
commit
e70f270c5c
@ -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 */,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
154
damus/Views/ProfileActionSheetView.swift
Normal file
154
damus/Views/ProfileActionSheetView.swift
Normal file
@ -0,0 +1,154 @@
|
||||
//
|
||||
// ProfileActionSheetView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
92
damus/Views/Zaps/ZapButtonView.swift
Normal file
92
damus/Views/Zaps/ZapButtonView.swift
Normal file
@ -0,0 +1,92 @@
|
||||
//
|
||||
// ZapButtonView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino 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")
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user