Merge branch 'master' into exp-backoff

This commit is contained in:
Bryan Montz
2023-02-27 06:23:38 -06:00
127 changed files with 3337 additions and 849 deletions

View File

@@ -26,14 +26,16 @@ struct EventDetailBar: View {
HStack {
if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray)
Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -41,7 +43,8 @@ struct EventDetailBar: View {
if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray)
Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -49,6 +52,21 @@ struct EventDetailBar: View {
}
}
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reposts_count", value: nil, table: nil), locale: locale, count)
}
func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reactions_count", value: nil, table: nil), locale: locale, count)
}
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "zaps_count", value: nil, table: nil), locale: locale, count)
}
struct EventDetailBar_Previews: PreviewProvider {
static var previews: some View {
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")

View File

@@ -129,26 +129,14 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
let filtered = newValue.filter { Set("0123456789").contains($0) }
if filtered != newValue {
default_zap_amount = filtered
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
}
if filtered == "" {
set_default_zap_amount(pubkey: state.pubkey, amount: 1000)
return
}
guard let amt = Int(filtered) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
}
@@ -220,10 +208,10 @@ struct ConfigView: View {
}
}
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
}
}
}
}
@@ -346,3 +334,18 @@ struct ConfigView_Previews: PreviewProvider {
}
}
}
func handle_string_amount(new_value: String) -> Int? {
let filtered = new_value.filter { Set("0123456789").contains($0) }
if filtered == "" {
return nil
}
guard let amt = Int(filtered) else {
return nil
}
return amt
}

View File

@@ -37,9 +37,7 @@ struct DMChatView: View {
var Header: some View {
let profile = damus_state.profiles.lookup(id: pubkey)
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)

View File

@@ -41,6 +41,9 @@ struct DirectMessagesView: View {
ForEach(dms, id: \.0) { tup in
MaybeEvent(tup)
.padding(.top, 10)
Divider()
.padding([.top], 10)
}
}
}

View File

@@ -23,7 +23,7 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
struct EventDetailView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let _ = test_damus_state()
EventDetailView()
}
}

View File

@@ -61,10 +61,8 @@ struct EventView: View {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
VStack(alignment: .leading) {
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
@@ -86,9 +84,6 @@ struct EventView: View {
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
.padding([.top], 6)
}
Divider()
.padding([.top], 4)
}
}
}

View File

@@ -71,8 +71,10 @@ struct BuilderEventView: View {
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
.border(Color.gray.opacity(0.2), width: 1)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1.0)
)
.onAppear {
self.load()
}

View File

@@ -11,6 +11,14 @@ struct EventBody: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
let should_show_img: Bool
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil) {
self.damus_state = damus_state
self.event = event
self.size = size
self.should_show_img = should_show_img ?? should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
}
var content: String {
event.get_content(damus_state.keypair.privkey)
@@ -21,8 +29,6 @@ struct EventBody: View {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content))
.frame(maxWidth: .infinity, alignment: .leading)
}

View File

@@ -47,9 +47,9 @@ struct EventMenuContext: View {
notify(.update_bookmarks, event)
} label: {
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
let unBookmarkString = NSLocalizedString("Un-Bookmark", comment: "Context menu option for un-bookmarking a note")
let bookmarkString = NSLocalizedString("Bookmark", comment: "Context menu optoin for bookmarking a note")
Label(isBookmarked ? unBookmarkString : bookmarkString, systemImage: imageName)
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.")
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.")
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

View File

@@ -31,10 +31,7 @@ struct EventProfile: View {
var body: some View {
HStack(alignment: .center) {
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey))
NavigationLink(destination: pv) {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
}
}

View File

@@ -26,13 +26,15 @@ struct ReplyDescription_Previews: PreviewProvider {
}
}
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.current) -> String {
let desc = make_reply_description(event.tags)
let pubkeys = desc.pubkeys
let n = desc.others
let bundle = bundleForLocale(locale: locale)
if desc.pubkeys.count == 0 {
return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.")
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
}
let names: [String] = pubkeys.map {
@@ -40,20 +42,16 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
return Profile.displayName(profile: prof, pubkey: $0)
}
let othersCount = n - pubkeys.count
if names.count > 1 {
let othersCount = n - pubkeys.count
if othersCount == 0 {
return String(format: "Replying to %@ & %@", names[0], names[1])
return String(format: NSLocalizedString("Replying to %@ & %@", bundle: bundle, comment: "Label to indicate that the user is replying to 2 users."), locale: locale, names[0], names[1])
} else {
return String(format: "Replying to %@, %@ & %d others", names[0], names[1], othersCount)
return String(format: bundle.localizedString(forKey: "replying_to_two_and_others", value: nil, table: nil), locale: locale, othersCount, names[0], names[1])
}
}
if othersCount == 0 {
return String(format: "Replying to %@", names[0])
} else {
return String(format: "Replying to %@ & %d others", names[0], othersCount)
}
return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, names[0])
}

View File

@@ -18,20 +18,17 @@ struct TextEvent: View {
HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
let is_anon = event_is_anonymous(ev: event)
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
}
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
Spacer()
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
let pk = is_anon ? "anon" : pubkey
EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
@@ -68,3 +65,18 @@ struct TextEvent_Previews: PreviewProvider {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
}
}
func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
for t in ev.tags {
if t.count >= 1 && t[0] == tag {
return true
}
}
return false
}
func event_is_anonymous(ev: NostrEvent) -> Bool {
return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon")
}

View File

@@ -19,7 +19,7 @@ struct FollowButtonView: View {
Button {
follow_state = perform_follow_btn_action(follow_state, target: target)
} label: {
Text(follow_btn_txt(follow_state, follows_you: follows_you))
Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
.frame(width: 105, height: 30)
//.padding(.vertical, 10)
.font(.caption.weight(.bold))

View File

@@ -29,7 +29,6 @@ struct FollowersView: View {
@EnvironmentObject var followers: FollowersModel
var body: some View {
let profile = damus_state.profiles.lookup(id: whos)
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(followers.contacts ?? [], id: \.self) { pk in
@@ -38,7 +37,7 @@ struct FollowersView: View {
}
.padding()
}
.navigationBarTitle(NSLocalizedString("\(Profile.displayName(profile: profile, pubkey: whos))'s Followers", comment: "Navigation bar title for view that shows who is following a user."))
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
.onAppear {
followers.subscribe()
}
@@ -56,8 +55,6 @@ struct FollowingView: View {
let whos: String
var body: some View {
let profile = damus_state.profiles.lookup(id: whos)
let who = Profile.displayName(profile: profile, pubkey: whos)
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(following.contacts, id: \.self) { pk in
@@ -72,7 +69,7 @@ struct FollowingView: View {
.onDisappear {
following.unsubscribe()
}
.navigationBarTitle(NSLocalizedString("\(who) following", comment: "Navigation bar title for view that shows who a user is following."))
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
}
}

View File

@@ -0,0 +1,214 @@
//
// RepostGroupView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import SwiftUI
enum EventGroupType {
case repost(EventGroup)
case reaction(EventGroup)
case zap(ZapGroup)
case profile_zap(ZapGroup)
var events: [NostrEvent] {
switch self {
case .repost(let grp):
return grp.events
case .reaction(let grp):
return grp.events
case .zap(let zapgrp):
return zapgrp.zap_requests()
case .profile_zap(let zapgrp):
return zapgrp.zap_requests()
}
}
}
enum ReactingTo {
case your_post
case tagged_in
case your_profile
}
func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
guard let ev else {
return .your_profile
}
if ev.pubkey == our_pubkey {
return .your_post
}
return .tagged_in
}
func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String {
let alice_pk = ev.pubkey
let alice_prof = profiles.lookup(id: alice_pk)
return Profile.displayName(profile: alice_prof, pubkey: alice_pk)
}
/**
Returns a notification string describing user actions in response to an event group type.
The localization keys read by this function are the following (although some keys may not actually be used in practice):
"??" - returned when there are no events associated with the specified event group type.
"reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
"reacted_your_post_1" - returned when 1 reaction occurred to the current user's post
"reacted_your_post_2" - returned when 2 reactions occurred to the current user's post
"reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
"reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
"reposted_your_post_1" - returned when 1 repost occurred to the current user's post
"reposted_your_post_2" - returned when 2 reposts occurred to the current user's post
"reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
"zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
"zapped_your_post_1" - returned when 1 zap occurred to the current user's post
"zapped_your_post_2" - returned when 2 zaps occurred to the current user's post
"zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
*/
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String {
let verb = reacting_to_verb(group: group)
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev)
let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))"
let bundle = bundleForLocale(locale: locale)
switch group.events.count {
case 0:
return NSLocalizedString("??", comment: "")
case 1:
let ev = group.events.first!
let profile = profiles.lookup(id: ev.pubkey)
let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey)
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name)
case 2:
let alice_name = event_author_name(profiles: profiles, group.events[0])
let bob_name = event_author_name(profiles: profiles, group.events[1])
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name)
default:
let alice_name = event_author_name(profiles: profiles, group.events.first!)
let count = group.events.count - 1
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name)
}
}
func reacting_to_verb(group: EventGroupType) -> String {
switch group {
case .reaction:
return "reacted"
case .repost:
return "reposted"
case .zap: fallthrough
case .profile_zap:
return "zapped"
}
}
struct EventGroupView: View {
let state: DamusState
let event: NostrEvent?
let group: EventGroupType
var GroupDescription: some View {
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))")
}
func ZapIcon(_ zapgrp: ZapGroup) -> some View {
let fmt = format_msats_abbrev(zapgrp.msat_total)
return VStack(alignment: .center) {
Image(systemName: "bolt.fill")
.foregroundColor(.orange)
Text("\(fmt)")
.foregroundColor(Color.orange)
}
}
var GroupIcon: some View {
Group {
switch group {
case .repost:
Image(systemName: "arrow.2.squarepath")
.foregroundColor(Color("DamusGreen"))
case .reaction:
Image("shaka-full")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
case .profile_zap(let zapgrp):
ZapIcon(zapgrp)
case .zap(let zapgrp):
ZapIcon(zapgrp)
}
}
}
var body: some View {
HStack(alignment: .top) {
GroupIcon
.frame(width: PFP_SIZE + 10)
VStack(alignment: .leading) {
ProfilePicturesView(state: state, events: group.events)
GroupDescription
if let event {
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: event.id)) {
Text(event.content)
.padding([.top], 1)
.foregroundColor(.gray)
}
.buttonStyle(.plain)
}
}
}
.padding([.top], 6)
}
}
let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}"
let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1)
let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1)
let test_reposts = [test_repost_1, test_repost_2]
let test_event_group = EventGroup(events: test_reposts)
struct EventGroupView_Previews: PreviewProvider {
static var previews: some View {
VStack {
EventGroupView(state: test_damus_state(), event: test_event, group: .repost(test_event_group))
.frame(height: 200)
.padding()
EventGroupView(state: test_damus_state(), event: test_event, group: .reaction(test_event_group))
.frame(height: 200)
.padding()
}
}
}

View File

@@ -0,0 +1,86 @@
//
// NotificationItemView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import SwiftUI
enum ShowItem {
case show(NostrEvent?)
case dontshow(NostrEvent?)
}
func notification_item_event(events: EventCache, notif: NotificationItem) -> ShowItem {
switch notif {
case .repost(let evid, _):
return .dontshow(events.lookup(evid))
case .reply(let ev):
return .show(ev)
case .reaction(let evid, _):
return .dontshow(events.lookup(evid))
case .event_zap(let evid, _):
return .dontshow(events.lookup(evid))
case .profile_zap:
return .show(nil)
}
}
struct NotificationItemView: View {
let state: DamusState
let item: NotificationItem
var show_item: ShowItem {
notification_item_event(events: state.events, notif: item)
}
func Item(_ ev: NostrEvent?) -> some View {
Group {
switch item {
case .repost(_, let evgrp):
EventGroupView(state: state, event: ev, group: .repost(evgrp))
case .event_zap(_, let zapgrp):
EventGroupView(state: state, event: ev, group: .zap(zapgrp))
case .profile_zap(let grp):
EventGroupView(state: state, event: nil, group: .profile_zap(grp))
case .reaction(_, let evgrp):
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
case .reply(let ev):
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) {
EventView(damus: state, event: ev, has_action_bar: true)
}
.buttonStyle(.plain)
}
Divider()
.padding([.top,.bottom], 5)
}
}
var body: some View {
Group {
switch show_item {
case .show(let ev):
Item(ev)
case .dontshow(let ev):
if let ev {
Item(ev)
}
}
}
}
}
let test_notification_item: NotificationItem = .repost("evid", test_event_group)
struct NotificationItemView_Previews: PreviewProvider {
static var previews: some View {
NotificationItemView(state: test_damus_state(), item: test_notification_item)
}
}

View File

@@ -0,0 +1,50 @@
//
// NotificationsView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import SwiftUI
struct NotificationsView: View {
let state: DamusState
@ObservedObject var notifications: NotificationsModel
var body: some View {
ScrollViewReader { scroller in
ScrollView {
LazyVStack(alignment: .leading) {
Color.white.opacity(0)
.id("startblock")
.frame(height: 5)
ForEach(notifications.notifications, id: \.id) { item in
NotificationItemView(state: state, item: item)
}
}
.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
handle_scroll_queue(proxy, queue: self.notifications)
}
return Color.clear
})
.padding(.horizontal)
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in
let _ = notifications.flush()
self.notifications.should_queue = false
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
}
}
.onAppear {
let _ = notifications.flush()
}
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
NotificationsView(state: test_damus_state(), notifications: NotificationsModel())
}
}

View File

@@ -0,0 +1,37 @@
//
// ProfilePicturesView.swift
// damus
//
// Created by William Casarin on 2023-02-22.
//
import SwiftUI
struct ProfilePicturesView: View {
let state: DamusState
let events: [NostrEvent]
@State var nav_target: String? = nil
@State var navigating: Bool = false
var body: some View {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
EmptyView()
}
HStack {
ForEach(events.prefix(8)) { ev in
ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles)
.onTapGesture {
nav_target = ev.pubkey
navigating = true
}
}
}
}
}
struct ProfilePicturesView_Previews: PreviewProvider {
static var previews: some View {
ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event])
}
}

View File

@@ -36,41 +36,43 @@ struct ParticipantsView: View {
Spacer()
}
VStack {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
ScrollView {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
} else {
.onTapGesture {
if references.contains(participant) {
// Don't add it twice
references = references.filter {
$0 != participant
}
} else {
references.append(participant)
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
}
}
}
}
}
}
}
Spacer()
}

View File

@@ -15,7 +15,7 @@ enum NostrPostResult {
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
struct PostView: View {
@State var post: String = ""
@State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
@@ -44,7 +44,14 @@ struct PostView: View {
if replying_to?.known_kind == .chat {
kind = .chat
}
let content = self.post.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
post.replaceCharacters(in: range, with: link)
}
}
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
@@ -52,14 +59,14 @@ struct PostView: View {
if let replying_to {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else {
damus_state.drafts.post = ""
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
dismiss()
}
var is_post_empty: Bool {
return post.allSatisfy { $0.isWhitespace }
return post.string.allSatisfy { $0.isWhitespace }
}
var body: some View {
@@ -74,7 +81,7 @@ struct PostView: View {
if !is_post_empty {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
showPrivateKeyWarning = contentContainsPrivateKey(self.post)
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
if !showPrivateKeyWarning {
self.send_post()
@@ -97,7 +104,7 @@ struct PostView: View {
VStack(alignment: .leading) {
ZStack(alignment: .topLeading) {
TextEditor(text: $post)
TextViewWrapper(attributedText: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
@@ -108,7 +115,7 @@ struct PostView: View {
}
}
if post.isEmpty {
if post.string.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
.padding(.leading, 4)
@@ -120,7 +127,7 @@ struct PostView: View {
}
// This if-block observes @ for tagging
if let searching = get_searching_string(post) {
if let searching = get_searching_string(post.string) {
VStack {
Spacer()
UserSearch(damus_state: damus_state, search: searching, post: $post)
@@ -130,7 +137,7 @@ struct PostView: View {
.onAppear() {
if let replying_to {
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.replies[replying_to] = ""
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
@@ -144,10 +151,10 @@ struct PostView: View {
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = ""
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.padding()

View File

@@ -20,7 +20,8 @@ struct SearchedUser: Identifiable {
struct UserSearch: View {
let damus_state: DamusState
let search: String
@Binding var post: String
@Binding var post: NSMutableAttributedString
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
@@ -39,7 +40,26 @@ struct UserSearch: View {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
while post.string.last != "@" {
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
}
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
var tagString = ""
if let name = user.profile?.name {
tagString = "@\(name)\u{200B} "
}
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
let mutableString = NSMutableAttributedString()
mutableString.append(post)
mutableString.append(tagAttributedString)
post = mutableString
}
}
}
@@ -49,7 +69,7 @@ struct UserSearch: View {
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: String = "some @jb55"
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)

View File

@@ -0,0 +1,46 @@
//
// MaybeAnonPfpView.swift
// damus
//
// Created by William Casarin on 2023-02-26.
//
import SwiftUI
struct MaybeAnonPfpView: View {
let state: DamusState
let is_anon: Bool
let pubkey: String
init(state: DamusState, event: NostrEvent, pubkey: String) {
self.state = state
self.is_anon = event_is_anonymous(ev: event)
self.pubkey = pubkey
}
init(state: DamusState, is_anon: Bool, pubkey: String) {
self.state = state
self.is_anon = is_anon
self.pubkey = pubkey
}
var body: some View {
Group {
if is_anon {
Image(systemName: "person.fill.questionmark")
.font(.largeTitle)
.frame(width: PFP_SIZE, height: PFP_SIZE)
} else {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles)
}
}
}
}
}
struct MaybeAnonPfpView_Previews: PreviewProvider {
static var previews: some View {
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon")
}
}

View File

@@ -67,7 +67,7 @@ struct ProfileName: View {
var body: some View {
HStack(spacing: 2) {
Text(prefix + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
Text(verbatim: "\(prefix)\(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))")
.font(.body)
.fontWeight(prefix == "@" ? .none : .bold)
if let nip05 = current_nip05 {
@@ -136,11 +136,11 @@ struct EventProfileName: View {
.font(.body.weight(.bold))
.padding([.trailing], 2)
Text("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
Text(verbatim: "@\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))")
.foregroundColor(Color("DamusMediumGrey"))
.font(eventviewsize_to_font(size))
} else {
Text(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
Text(verbatim: "\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))")
.font(eventviewsize_to_font(size))
.fontWeight(.bold)
}

View File

@@ -49,6 +49,16 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
}
}
func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "followers_count", value: nil, table: nil), locale: locale, count)
}
func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "relays_count", value: nil, table: nil), locale: locale, count)
}
struct EditButton: View {
let damus_state: DamusState
@@ -100,8 +110,6 @@ struct ProfileView: View {
static let markdown = Markdown()
@State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
@State private var showingEditProfile = false
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false
@@ -110,6 +118,21 @@ struct ProfileView: View {
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: profile)
self._followers = StateObject(wrappedValue: followers)
}
init(damus_state: DamusState, pubkey: String) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
}
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) var openURL
@@ -319,7 +342,8 @@ struct ProfileView: View {
.foregroundColor(.gray)
} else {
let followerCount = followers.count!
Text("\(Text(verbatim: "\(followerCount)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "followers_count", value: nil, table: nil), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
let noun_text = Text(verbatim: "\(followersCountString(followerCount))").font(.subheadline).foregroundColor(.gray)
Text("\(Text("\(followerCount)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
}
}
}
@@ -343,7 +367,8 @@ struct ProfileView: View {
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack {
Text("\(Text(verbatim: "\(profile.following)").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
let noun_text = Text("Following", comment: "Text on the user profile page next to the number of accounts a user is following.").font(.subheadline).foregroundColor(.gray)
Text("\(Text("\(profile.following)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
}
}
.buttonStyle(PlainButtonStyle())
@@ -366,7 +391,8 @@ struct ProfileView: View {
if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text(verbatim: "\(relays.keys.count)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "relays_count", value: nil, table: nil), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
let noun_text = Text(verbatim: "\(relaysCountString(relays.keys.count))").font(.subheadline).foregroundColor(.gray)
let relay_text = Text("\(Text("\(relays.keys.count)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
@@ -443,9 +469,7 @@ struct ProfileView: View {
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
let followers = FollowersModel(damus_state: ds, target: ds.pubkey)
let profile_model = ProfileModel(pubkey: ds.pubkey, damus: ds)
ProfileView(damus_state: ds, profile: profile_model, followers: followers)
ProfileView(damus_state: ds, pubkey: ds.pubkey)
}
}
@@ -506,7 +530,7 @@ struct KeyView: View {
.symbolRenderingMode(.palette)
}
.padding(.leading,4)
Text(abbrev_pubkey(bech32, amount: 16))
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
.foregroundColor(keyColor())
}

View File

@@ -26,7 +26,7 @@ struct RelayFilterView: View {
}
var body: some View {
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
Text("Please choose relays from the list below to filter the current feed:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
.padding()
.padding(.top, 20)
.padding(.bottom, 0)

View File

@@ -38,7 +38,7 @@ struct SaveKeysView: View {
.foregroundColor(.white)
.padding(.bottom, 10)
Text("This is your account ID, you can give this to your friends so that they can follow you. Click to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.")
Text("This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.", comment: "Label to describe that a public key is the user's account ID and what they can do with it.")
.foregroundColor(.white)
.padding(.bottom, 10)

View File

@@ -108,6 +108,7 @@ struct SideMenuView: View {
navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
}
}
.labelStyle(SideMenuLabelStyle())
.padding([.top, .bottom], verticalSpacing)
}
}
@@ -175,6 +176,17 @@ struct SideMenuView: View {
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
}
struct SideMenuLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 8) {
configuration.icon
.frame(width: 24, height: 24)
.aspectRatio(contentMode: .fit)
configuration.title
}
}
}
}
struct Previews_SideMenuView_Previews: PreviewProvider {

View File

@@ -0,0 +1,44 @@
//
// TextViewWrapper.swift
// damus
//
// Created by Swift on 2/24/23.
//
import SwiftUI
struct TextViewWrapper: UIViewRepresentable {
@Binding var attributedText: NSMutableAttributedString
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.systemFont(ofSize: 18)
textView.textColor = UIColor.label
let linkAttributes: [NSAttributedString.Key : Any] = [
NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)]
textView.linkTextAttributes = linkAttributes
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = attributedText
}
func makeCoordinator() -> Coordinator {
Coordinator(attributedText: $attributedText)
}
class Coordinator: NSObject, UITextViewDelegate {
@Binding var attributedText: NSMutableAttributedString
init(attributedText: Binding<NSMutableAttributedString>) {
_attributedText = attributedText
}
func textViewDidChange(_ textView: UITextView) {
attributedText = NSMutableAttributedString(attributedString: textView.attributedText)
}
}
}

View File

@@ -265,6 +265,10 @@ struct ThreadV2View: View {
navigating: $navigating,
selected: false
)
Divider()
.padding(.top, 4)
.padding(.leading, 25 * 2)
}
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
@@ -289,15 +293,20 @@ struct ThreadV2View: View {
).id("main")
// MARK: - Responses of the actual event view
ForEach(thread.childEvents, id: \.id) { event in
MutedEventView(
damus_state: damus,
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
LazyVStack {
ForEach(thread.childEvents, id: \.id) { event in
MutedEventView(
damus_state: damus,
event: event,
scroller: nil,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
Divider()
.padding([.top], 4)
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))

View File

@@ -42,6 +42,9 @@ struct InnerTimelineView: View {
navigating = true
}
.padding(.top, 10)
Divider()
.padding([.top], 10)
}
}
}

View File

@@ -23,7 +23,7 @@ struct LoadMoreButton: View {
Group {
if events.queued > 0 {
Button(action: click) {
Text("Load \(events.queued) more")
Text("Load \(events.queued) more", comment: "Button text for loading more events, where the variable is the number of events.")
}
.font(.system(size: 14, weight: .bold))
.padding(10)

View File

@@ -27,17 +27,6 @@ struct TimelineView: View {
MainContent
}
func handle_scroll(_ proxy: GeometryProxy) {
let offset = -proxy.frame(in: .named("scroll")).origin.y
guard offset >= 0 else {
return
}
let val = offset > 0
if self.events.should_queue != val {
self.events.should_queue = val
}
}
var realtime_bar_opacity: Double {
colorScheme == .dark ? 0.2 : 0.1
}
@@ -55,7 +44,7 @@ struct TimelineView: View {
.disabled(loading)
.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
handle_scroll(proxy)
handle_scroll_queue(proxy, queue: self.events)
}
return Color.clear
})
@@ -82,3 +71,18 @@ struct TimelineView_Previews: PreviewProvider {
}
protocol ScrollQueue {
var should_queue: Bool { get }
func set_should_queue(_ val: Bool)
}
func handle_scroll_queue(_ proxy: GeometryProxy, queue: ScrollQueue) {
let offset = -proxy.frame(in: .named("scroll")).origin.y
guard offset >= 0 else {
return
}
let val = offset > 0
if queue.should_queue != val {
queue.set_should_queue(val)
}
}

View File

@@ -0,0 +1,215 @@
//
// CustomizeZapView.swift
// damus
//
// Created by William Casarin on 2023-02-25.
//
import SwiftUI
import Combine
enum ZapType {
case pub
case anon
case non_zap
}
struct ZapAmountItem: Identifiable, Hashable {
let amount: Int
let icon: String
var id: String {
return icon
}
}
func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem {
let def = get_default_zap_amount(pubkey: pubkey) ?? 1000
return ZapAmountItem(amount: def, icon: "🤙")
}
func get_zap_amount_items(pubkey: String) -> [ZapAmountItem] {
let def_item = get_default_zap_amount_item(pubkey)
var entries = [
ZapAmountItem(amount: 500, icon: "🙂"),
ZapAmountItem(amount: 5000, icon: "💜"),
ZapAmountItem(amount: 10_000, icon: "😍"),
ZapAmountItem(amount: 20_000, icon: "🤩"),
ZapAmountItem(amount: 50_000, icon: "🔥"),
ZapAmountItem(amount: 100_000, icon: "🚀"),
ZapAmountItem(amount: 1_000_000, icon: "🤯"),
]
entries.append(def_item)
entries.sort { $0.amount < $1.amount }
return entries
}
struct CustomizeZapView: View {
let state: DamusState
let event: NostrEvent
let lnurl: String
@State var comment: String
@State var custom_amount: String
@State var custom_amount_sats: Int?
@State var selected_amount: ZapAmountItem
@State var zap_type: ZapType
@State var invoice: String
@State var error: String?
@State var showing_wallet_selector: Bool
@State var zapping: Bool
let zap_amounts: [ZapAmountItem]
@Environment(\.dismiss) var dismiss
init(state: DamusState, event: NostrEvent, lnurl: String) {
self._comment = State(initialValue: "")
self.event = event
self.zap_amounts = get_zap_amount_items(pubkey: state.pubkey)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
self._showing_wallet_selector = State(initialValue: false)
self._custom_amount = State(initialValue: "")
self._zap_type = State(initialValue: .pub)
let selected = get_default_zap_amount_item(state.pubkey)
self._selected_amount = State(initialValue: selected)
self._custom_amount_sats = State(initialValue: nil)
self._zapping = State(initialValue: false)
self.lnurl = lnurl
self.state = state
}
var ZapTypePicker: some View {
Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) {
Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub)
Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
Text("Non-Zap", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap)
}
.pickerStyle(.segmented)
}
var AmountPicker: some View {
Picker(NSLocalizedString("Zap Amount", comment: "Title of picker that allows selection of predefined amounts to zap."), selection: $selected_amount) {
ForEach(zap_amounts) { entry in
let fmt = format_msats_abbrev(Int64(entry.amount) * 1000)
HStack(alignment: .firstTextBaseline) {
Text("\(entry.icon)")
.frame(width: 30)
Text("\(fmt)")
.frame(width: 50)
}
.tag(entry)
}
}
.pickerStyle(.wheel)
}
func receive_zap(notif: Notification) {
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.is_custom else {
return
}
guard zap_ev.event.id == event.id else {
return
}
self.zapping = false
switch zap_ev.type {
case .failed(let err):
switch err {
case .fetching_invoice:
self.error = "Error fetching lightning invoice"
case .bad_lnurl:
self.error = "Invalid lightning address"
}
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(state.pubkey) {
self.invoice = inv
self.showing_wallet_selector = true
} else {
open_with_wallet(wallet: get_default_wallet(state.pubkey).model, invoice: inv)
self.showing_wallet_selector = false
dismiss()
}
}
}
var body: some View {
MainContent
.sheet(isPresented: $showing_wallet_selector) {
SelectWalletView(showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
receive_zap(notif: notif)
}
.ignoresSafeArea()
}
var MainContent: some View {
VStack(alignment: .leading) {
Form {
Section(content: {
AmountPicker
}, header: {
Text("Zap Amount in sats", comment: "Header text to indicate that the picker below it is to choose a pre-defined amount of sats to zap.")
})
Section(content: {
TextField(String("100000"), text: $custom_amount)
.keyboardType(.numberPad)
.onReceive(Just(custom_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.custom_amount = String(parsed)
self.custom_amount_sats = parsed
}
}
}, header: {
Text("Custom Zap Amount", comment: "Header text to indicate that the text field below it is to enter a custom zap amount.")
})
.dismissKeyboardOnTap()
Section(content: {
TextField(NSLocalizedString("Awesome post!", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment)
}, header: {
Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.")
})
.dismissKeyboardOnTap()
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
})
if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats ?? selected_amount.amount
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.zIndex(16)
}
if let error {
Text(error)
.foregroundColor(.red)
}
}
}
}
}
struct CustomizeZapView_Previews: PreviewProvider {
static var previews: some View {
CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "")
.frame(width: 400, height: 600)
}
}