Compare commits
17 Commits
tyiu/navig
...
tyiu/refac
| Author | SHA1 | Date | |
|---|---|---|---|
|
6cfb9f7c75
|
|||
|
|
1b161fefd0 | ||
|
|
0b9a274e67 | ||
|
|
2bbbb5db65 | ||
|
|
bffa42a13a | ||
|
|
8097cfdfb8 | ||
|
|
af912b1a0e | ||
|
|
51cd34c9c2 | ||
|
|
a6745af519 | ||
|
|
631220fdcb | ||
|
|
5aa0d6c3e1 | ||
| f9982e992a | |||
|
|
0eaebb80f1 | ||
| 6f23b69f2c | |||
|
|
ec50c75062 | ||
|
|
0293b61d7a | ||
| 50e4452016 |
@@ -110,21 +110,6 @@ static inline int peek_char(struct cursor *cur, int ind) {
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static inline int pull_byte(struct cursor *cur, u8 *byte) {
|
||||
if (cur->p >= cur->end)
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
||||
int d1, d2, d3, ind;
|
||||
const u8 *start = cur->p;
|
||||
|
||||
@@ -26,7 +26,7 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize);
|
||||
/**
|
||||
* hex_encode - Create a nul-terminated hex string
|
||||
* @buf: the buffer to read the data from
|
||||
* @bufsize: the length of @buf
|
||||
* @bufsize: the length of buf
|
||||
* @dest: the string to fill
|
||||
* @destsize: the max size of the string
|
||||
*
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; };
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */; };
|
||||
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */; };
|
||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; };
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; };
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; };
|
||||
@@ -444,6 +446,8 @@
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; };
|
||||
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupporterBadge.swift; sourceTree = "<group>"; };
|
||||
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoldSupportGradient.swift; sourceTree = "<group>"; };
|
||||
4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
|
||||
4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; };
|
||||
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; };
|
||||
@@ -1041,6 +1045,7 @@
|
||||
children = (
|
||||
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
|
||||
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
|
||||
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
|
||||
);
|
||||
path = Gradients;
|
||||
sourceTree = "<group>";
|
||||
@@ -1218,6 +1223,7 @@
|
||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
||||
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -1730,6 +1736,7 @@
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
@@ -1837,6 +1844,7 @@
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2128,7 +2136,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2175,7 +2183,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
||||
@@ -14,9 +14,13 @@ fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2, damus_grad_c3]
|
||||
|
||||
struct DamusGradient: View {
|
||||
var body: some View {
|
||||
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
DamusGradient.gradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusGradient_Previews: PreviewProvider {
|
||||
|
||||
29
damus/Components/Gradients/GoldSupportGradient.swift
Normal file
29
damus/Components/Gradients/GoldSupportGradient.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// GoldSupportGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
|
||||
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
|
||||
|
||||
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
|
||||
|
||||
let GoldGradient: LinearGradient =
|
||||
LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
|
||||
struct GoldGradientView: View {
|
||||
var body: some View {
|
||||
GoldGradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct GoldGradientView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GoldGradientView()
|
||||
}
|
||||
}
|
||||
73
damus/Components/SupporterBadge.swift
Normal file
73
damus/Components/SupporterBadge.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// SupporterBadge.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SupporterBadge: View {
|
||||
let percent: Int
|
||||
|
||||
let size: CGFloat = 17
|
||||
|
||||
var body: some View {
|
||||
if percent < 100 {
|
||||
Image("star.fill")
|
||||
.resizable()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundColor(support_level_color(percent))
|
||||
} else {
|
||||
Image("star.fill")
|
||||
.resizable()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func support_level_color(_ percent: Int) -> Color {
|
||||
if percent == 0 {
|
||||
return .gray
|
||||
}
|
||||
|
||||
let percent_f = Double(percent) / 100.0
|
||||
let cutoff = 0.5
|
||||
let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
|
||||
let s = 0.9; // Saturation
|
||||
let b = 0.9; // Brightness
|
||||
|
||||
return Color(hue: h, saturation: s, brightness: b)
|
||||
}
|
||||
|
||||
struct SupporterBadge_Previews: PreviewProvider {
|
||||
static func Level(_ p: Int) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
SupporterBadge(percent: p)
|
||||
.frame(width: 50)
|
||||
Text("\(p)")
|
||||
.frame(width: 50)
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
Level(1)
|
||||
Level(10)
|
||||
Level(20)
|
||||
Level(30)
|
||||
Level(40)
|
||||
Level(50)
|
||||
}
|
||||
Level(60)
|
||||
Level(70)
|
||||
Level(80)
|
||||
Level(90)
|
||||
Level(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,8 +208,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
return
|
||||
}
|
||||
|
||||
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
|
||||
let amount_msat = Int64(zap_amount) * 1000
|
||||
let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
|
||||
let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings)
|
||||
let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state)
|
||||
let zapreq = mzapreq.potentially_anon_outer_request.ev
|
||||
@@ -239,7 +238,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
@@ -259,9 +258,20 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
return
|
||||
}
|
||||
|
||||
guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv),
|
||||
case .nwc(let pzap_state) = pending_zap_state
|
||||
else {
|
||||
var flusher: OnFlush? = nil
|
||||
// Don't donate on custom zaps
|
||||
if !is_custom && damus_state.settings.donation_percent > 0 {
|
||||
flusher = .once({ pe in
|
||||
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
||||
Task.init { @MainActor in
|
||||
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: flusher)
|
||||
|
||||
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ struct ContentView: View {
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
VStack {
|
||||
NavigationLink(destination: WalletView(model: damus_state!.wallet), isActive: $wallet_open) {
|
||||
NavigationLink(destination: WalletView(damus_state: damus, model: damus_state!.wallet), isActive: $wallet_open) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||
@@ -240,6 +240,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
popToRoot()
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
@@ -250,11 +251,13 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func open_profile(id: String) {
|
||||
popToRoot()
|
||||
self.active_profile = id
|
||||
self.profile_open = true
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
popToRoot()
|
||||
self.active_search = filt
|
||||
self.search_open = true
|
||||
}
|
||||
@@ -394,7 +397,7 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
ds.postbox.send(ev)
|
||||
if let profile = ds.profiles.profiles[ev.pubkey] {
|
||||
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
ds.postbox.send(profile.event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel()) }
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore())) }
|
||||
}
|
||||
|
||||
@@ -154,10 +154,14 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
if resp.response.error == nil {
|
||||
nwc_success(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
}
|
||||
|
||||
guard let err = resp.response.error else {
|
||||
nwc_success(state: self.damus_state, resp: resp)
|
||||
return
|
||||
}
|
||||
|
||||
print("nwc error: \(err)")
|
||||
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
}
|
||||
}
|
||||
@@ -731,7 +735,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
var old_nip05: String? = nil
|
||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
old_nip05 = mprof.profile.nip05
|
||||
if mprof.timestamp > ev.created_at {
|
||||
if mprof.event.created_at > ev.created_at {
|
||||
// skip if we already have an newer profile
|
||||
return
|
||||
}
|
||||
@@ -748,7 +752,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
print("validated nip05 for '\(nip05)'")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Task { @MainActor in
|
||||
profiles.validated[ev.pubkey] = validated
|
||||
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
@@ -1199,7 +1203,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||
identifier = "myBoostNotification"
|
||||
case .like:
|
||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content)
|
||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
|
||||
identifier = "myLikeNotification"
|
||||
case .dm:
|
||||
title = displayName
|
||||
|
||||
@@ -19,8 +19,11 @@ let fallback_zap_amount = 1000
|
||||
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
|
||||
self.value = loaded
|
||||
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
|
||||
// try to load from deprecated non-pubkey-keyed setting
|
||||
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||
self.value = loaded
|
||||
UserDefaults.standard.set(loaded, forKey: self.key)
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
} else {
|
||||
self.value = default_value
|
||||
}
|
||||
@@ -48,8 +51,11 @@ let fallback_zap_amount = 1000
|
||||
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
|
||||
self.value = val
|
||||
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
|
||||
// try to load from deprecated non-pubkey-keyed setting
|
||||
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||
self.value = val
|
||||
UserDefaults.standard.set(val.to_string(), forKey: self.key)
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
} else {
|
||||
self.value = default_value
|
||||
}
|
||||
@@ -137,6 +143,9 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
||||
var disable_animation: Bool
|
||||
|
||||
@Setting(key: "donation_percent", default_value: 0)
|
||||
var donation_percent: Int
|
||||
|
||||
// Helper for inverse of disable_animation.
|
||||
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
||||
|
||||
@@ -14,14 +14,17 @@ enum WalletConnectState {
|
||||
}
|
||||
|
||||
class WalletModel: ObservableObject {
|
||||
let settings: UserSettingsStore?
|
||||
var settings: UserSettingsStore
|
||||
private(set) var previous_state: WalletConnectState
|
||||
var inital_percent: Int
|
||||
|
||||
@Published private(set) var connect_state: WalletConnectState
|
||||
|
||||
init() {
|
||||
self.connect_state = .none
|
||||
init(state: WalletConnectState, settings: UserSettingsStore) {
|
||||
self.connect_state = state
|
||||
self.previous_state = .none
|
||||
self.settings = nil
|
||||
self.settings = settings
|
||||
self.inital_percent = settings.donation_percent
|
||||
}
|
||||
|
||||
init(settings: UserSettingsStore) {
|
||||
@@ -34,6 +37,7 @@ class WalletModel: ObservableObject {
|
||||
self.previous_state = .none
|
||||
self.connect_state = .none
|
||||
}
|
||||
self.inital_percent = settings.donation_percent
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
@@ -42,7 +46,7 @@ class WalletModel: ObservableObject {
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
self.settings?.nostr_wallet_connect = nil
|
||||
self.settings.nostr_wallet_connect = nil
|
||||
self.connect_state = .none
|
||||
self.previous_state = .none
|
||||
}
|
||||
@@ -52,7 +56,7 @@ class WalletModel: ObservableObject {
|
||||
}
|
||||
|
||||
func connect(_ nwc: WalletConnectURL) {
|
||||
self.settings?.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||
notify(.attached_wallet, nwc)
|
||||
self.connect_state = .existing(nwc)
|
||||
self.previous_state = .existing(nwc)
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
class Profile: Codable {
|
||||
var value: [String: AnyCodable]
|
||||
|
||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
|
||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?, damus_donation: Int?) {
|
||||
self.value = [:]
|
||||
self.name = name
|
||||
self.display_name = display_name
|
||||
@@ -21,12 +21,17 @@ class Profile: Codable {
|
||||
self.lud06 = lud06
|
||||
self.lud16 = lud16
|
||||
self.nip05 = nip05
|
||||
self.damus_donation = damus_donation
|
||||
}
|
||||
|
||||
private func str(_ str: String) -> String? {
|
||||
return get_val(str)
|
||||
}
|
||||
|
||||
private func int(_ key: String) -> Int? {
|
||||
return get_val(key)
|
||||
}
|
||||
|
||||
private func get_val<T>(_ v: String) -> T? {
|
||||
guard let val = self.value[v] else{
|
||||
return nil
|
||||
@@ -52,6 +57,10 @@ class Profile: Codable {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
private func set_int(_ key: String, _ val: Int?) {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
var reactions: Bool? {
|
||||
get { return get_val("reactions"); }
|
||||
set(s) { set_val("reactions", s) }
|
||||
@@ -77,6 +86,11 @@ class Profile: Codable {
|
||||
set(s) { set_str("about", s) }
|
||||
}
|
||||
|
||||
var damus_donation: Int? {
|
||||
get { return int("damus_donation"); }
|
||||
set(s) { set_int("damus_donation", s) }
|
||||
}
|
||||
|
||||
var picture: String? {
|
||||
get { return str("picture"); }
|
||||
set(s) { set_str("picture", s) }
|
||||
@@ -180,7 +194,7 @@ class Profile: Codable {
|
||||
}
|
||||
|
||||
func make_test_profile() -> Profile {
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com")
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
}
|
||||
|
||||
func make_ln_url(_ str: String?) -> URL? {
|
||||
|
||||
@@ -492,11 +492,11 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
|
||||
return ev
|
||||
}
|
||||
|
||||
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent {
|
||||
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
|
||||
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
||||
tags.append(["e", liked.id])
|
||||
tags.append(["p", liked.pubkey])
|
||||
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags)
|
||||
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: privkey)
|
||||
|
||||
@@ -601,7 +601,7 @@ enum MakeZapRequest {
|
||||
|
||||
var private_inner_request: ZapRequest {
|
||||
switch self {
|
||||
case .priv(let _, let pzr):
|
||||
case .priv(_, let pzr):
|
||||
return pzr.req
|
||||
case .normal(let zr):
|
||||
return zr
|
||||
@@ -966,6 +966,28 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
|
||||
If the known kind is not a `NostrKind.like`, it will return `nil`.
|
||||
If the event content is an empty string or `+`, it will map that to a heart ❤️ emoji.
|
||||
If the event content is a "-", it will map that to a dislike 👎 emoji.
|
||||
Otherwise, it will return the event content at face value without transforming it.
|
||||
*/
|
||||
func to_reaction_emoji(ev: NostrEvent) -> String? {
|
||||
guard ev.known_kind == NostrKind.like else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ev.content {
|
||||
case "", "+":
|
||||
return "❤️"
|
||||
case "-":
|
||||
return "👎"
|
||||
default:
|
||||
return ev.content
|
||||
}
|
||||
}
|
||||
|
||||
extension [ReferencedId] {
|
||||
var pRefs: [ReferencedId] {
|
||||
get {
|
||||
|
||||
@@ -17,7 +17,7 @@ class Profiles {
|
||||
qos: .userInteractive,
|
||||
attributes: .concurrent)
|
||||
|
||||
var profiles: [String: TimestampedProfile] = [:]
|
||||
private var profiles: [String: TimestampedProfile] = [:]
|
||||
var validated: [String: NIP05] = [:]
|
||||
var nip05_pubkey: [String: String] = [:]
|
||||
var zappers: [String: String] = [:]
|
||||
@@ -26,6 +26,12 @@ class Profiles {
|
||||
return validated[pk]
|
||||
}
|
||||
|
||||
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
|
||||
return queue.sync {
|
||||
return profiles.enumerated()
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_zapper(pubkey: String) -> String? {
|
||||
if let zapper = zappers[pubkey] {
|
||||
return zapper
|
||||
|
||||
@@ -14,6 +14,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi
|
||||
if new_zap.request.id == zap.request.id {
|
||||
// replace pending
|
||||
if !new_zap.is_pending && zap.is_pending {
|
||||
print("nwc: replacing pending with real zap \(new_zap.request.id)")
|
||||
zaps[i] = new_zap
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -22,18 +22,27 @@ class Relayer {
|
||||
}
|
||||
}
|
||||
|
||||
enum OnFlush {
|
||||
case once((PostedEvent) -> Void)
|
||||
case all((PostedEvent) -> Void)
|
||||
}
|
||||
|
||||
class PostedEvent {
|
||||
let event: NostrEvent
|
||||
let skip_ephemeral: Bool
|
||||
var remaining: [Relayer]
|
||||
let flush_after: Date?
|
||||
var flushed_once: Bool
|
||||
let on_flush: OnFlush?
|
||||
|
||||
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date? = nil) {
|
||||
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date?, on_flush: OnFlush?) {
|
||||
self.event = event
|
||||
self.skip_ephemeral = skip_ephemeral
|
||||
self.flush_after = flush_after
|
||||
self.on_flush = on_flush
|
||||
self.flushed_once = false
|
||||
self.remaining = remaining.map {
|
||||
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
||||
Relayer(relay: $0, attempts: 0, retry_after: 10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +118,19 @@ class PostBox {
|
||||
guard let ev = self.events[event_id] else {
|
||||
return false
|
||||
}
|
||||
|
||||
if let on_flush = ev.on_flush {
|
||||
switch on_flush {
|
||||
case .once(let cb):
|
||||
if !ev.flushed_once {
|
||||
ev.flushed_once = true
|
||||
cb(ev)
|
||||
}
|
||||
case .all(let cb):
|
||||
cb(ev)
|
||||
}
|
||||
}
|
||||
|
||||
let prev_count = ev.remaining.count
|
||||
ev.remaining = ev.remaining.filter { $0.relay != relay_id }
|
||||
let after_count = ev.remaining.count
|
||||
@@ -132,7 +154,7 @@ class PostBox {
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil) {
|
||||
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) {
|
||||
// Don't add event if we already have it
|
||||
if events[event.id] != nil {
|
||||
return
|
||||
@@ -140,7 +162,7 @@ class PostBox {
|
||||
|
||||
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
|
||||
let after = delay.map { d in Date.now.addingTimeInterval(d) }
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after)
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
|
||||
|
||||
events[event.id] = posted_ev
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ struct WalletConnectURL: Equatable {
|
||||
}
|
||||
|
||||
init?(str: String) {
|
||||
guard let url = URL(string: str), url.scheme == "nostrwalletconnect",
|
||||
guard let url = URL(string: str),
|
||||
url.scheme == "nostrwalletconnect" || url.scheme == "nostr+walletconnect",
|
||||
let pk = url.host, pk.utf8.count == 64,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let items = components.queryItems,
|
||||
@@ -182,7 +183,8 @@ func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
||||
pool.send(.subscribe(sub), to: [url.relay.id])
|
||||
}
|
||||
|
||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) -> NostrEvent? {
|
||||
@discardableResult
|
||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = make_wallet_pay_invoice_request(invoice: invoice)
|
||||
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
@@ -190,14 +192,14 @@ func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: Str
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay))
|
||||
subscribe_to_nwc(url: url, pool: pool)
|
||||
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: 5.0)
|
||||
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
|
||||
func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
||||
func nwc_success(state: DamusState, resp: FullWalletResponse) {
|
||||
// find the pending zap and mark it as pending-confirmed
|
||||
for kv in zapcache.our_zaps {
|
||||
for kv in state.zaps.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
@@ -211,14 +213,29 @@ func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse)
|
||||
|
||||
if nwc_state.update_state(state: .confirmed) {
|
||||
// notify the zaps model of an update so it can mark them as paid
|
||||
evcache.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
|
||||
state.events.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
|
||||
print("NWC success confirmed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||
let percent_f = Double(percent) / 100.0
|
||||
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||
|
||||
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||
// we failed... oh well. no donation for us.
|
||||
print("damus-donation failed to fetch invoice")
|
||||
return
|
||||
}
|
||||
|
||||
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||
}
|
||||
|
||||
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
||||
// find a pending zap with the nwc request id associated with this response and remove it
|
||||
for kv in zapcache.our_zaps {
|
||||
|
||||
@@ -440,15 +440,14 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? {
|
||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zappable = payreq.allowsNostr ?? false
|
||||
let amount: Int64 = Int64(sats) * 1000
|
||||
|
||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||
var query = [URLQueryItem(name: "amount", value: "\(msats)")]
|
||||
|
||||
if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
@@ -489,7 +488,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int,
|
||||
|
||||
// make sure it's the correct amount
|
||||
guard let bolt11 = decode_bolt11(result.pr),
|
||||
.specific(amount) == bolt11.amount
|
||||
.specific(msats) == bolt11.amount
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class Zaps {
|
||||
self.event_counts = [:]
|
||||
self.event_totals = [:]
|
||||
}
|
||||
|
||||
|
||||
func remove_zap(reqid: String) -> Zapping? {
|
||||
var res: Zapping? = nil
|
||||
for kv in our_zaps {
|
||||
|
||||
@@ -23,7 +23,7 @@ struct AlbyButton: View {
|
||||
HStack {
|
||||
Image("alby")
|
||||
|
||||
Text("Connect to Alby")
|
||||
Text("Attach Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.")
|
||||
}
|
||||
.offset(x: -25)
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
||||
|
||||
@@ -140,7 +140,7 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search
|
||||
}
|
||||
|
||||
// search profile cache as well
|
||||
for tup in profiles.profiles.enumerated() {
|
||||
for tup in profiles.enumerated() {
|
||||
let pk = tup.element.key
|
||||
let prof = tup.element.value.profile
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ struct EventProfileName: View {
|
||||
|
||||
@State var display_name: DisplayName?
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
@@ -23,6 +24,7 @@ struct EventProfileName: View {
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.size = size
|
||||
self._donation = State(wrappedValue: profile?.damus_donation)
|
||||
}
|
||||
|
||||
var friend_type: FriendType? {
|
||||
@@ -45,6 +47,15 @@ struct EventProfileName: View {
|
||||
return profile.reactions == false
|
||||
}
|
||||
|
||||
var supporter: Int? {
|
||||
guard let donation, donation > 0
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return donation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
switch current_display_name {
|
||||
@@ -73,6 +84,10 @@ struct EventProfileName: View {
|
||||
Image("zap-hashtag")
|
||||
.frame(width: 14, height: 14)
|
||||
}
|
||||
|
||||
if let supporter {
|
||||
SupporterBadge(percent: supporter)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
@@ -81,6 +96,7 @@ struct EventProfileName: View {
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
donation = update.profile.damus_donation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ struct ProfileName: View {
|
||||
|
||||
@State var display_name: DisplayName?
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
|
||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) {
|
||||
self.pubkey = pubkey
|
||||
@@ -75,6 +76,17 @@ struct ProfileName: View {
|
||||
return profile.reactions == false
|
||||
}
|
||||
|
||||
var supporter: Int? {
|
||||
guard let profile,
|
||||
let donation = profile.damus_donation,
|
||||
donation > 0
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return donation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
Text(verbatim: "\(prefix)\(name_choice)")
|
||||
@@ -90,6 +102,9 @@ struct ProfileName: View {
|
||||
Image("zap-hashtag")
|
||||
.frame(width: 14, height: 14)
|
||||
}
|
||||
if let supporter {
|
||||
SupporterBadge(percent: supporter)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
@@ -98,6 +113,7 @@ struct ProfileName: View {
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
donation = profile?.damus_donation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR
|
||||
func make_preview_profiles(_ pubkey: String) -> Profiles {
|
||||
let profiles = Profiles()
|
||||
let picture = "http://cdn.jb55.com/img/red-me.jpg"
|
||||
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com")
|
||||
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com", damus_donation: nil)
|
||||
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
|
||||
profiles.add(id: pubkey, profile: ts_profile)
|
||||
return profiles
|
||||
|
||||
@@ -496,8 +496,11 @@ struct ProfileView_Previews: PreviewProvider {
|
||||
func test_damus_state() -> DamusState {
|
||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let damus = DamusState.empty
|
||||
let settings = UserSettingsStore()
|
||||
settings.donation_percent = 100
|
||||
settings.default_zap_amount = 1971
|
||||
|
||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
|
||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)
|
||||
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
|
||||
damus.profiles.add(id: pubkey, profile: tsprof)
|
||||
return damus
|
||||
|
||||
@@ -12,10 +12,7 @@ struct ReactionView: View {
|
||||
let reaction: NostrEvent
|
||||
|
||||
var content: String {
|
||||
if reaction.content == "" || reaction.content == "+" {
|
||||
return "❤️"
|
||||
}
|
||||
return reaction.content
|
||||
return to_reaction_emoji(ev: reaction) ?? ""
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -224,5 +224,5 @@ struct SaveKeysView_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
|
||||
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil)
|
||||
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func make_hashtagable(_ str: String) -> String {
|
||||
|
||||
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
|
||||
let new = search.lowercased()
|
||||
return profiles.profiles.enumerated().reduce(into: []) { acc, els in
|
||||
return profiles.enumerated().reduce(into: []) { acc, els in
|
||||
let pk = els.element.key
|
||||
let prof = els.element.value.profile
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ struct SideMenuView: View {
|
||||
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
|
||||
}
|
||||
|
||||
NavigationLink(destination: WalletView(model: damus_state.wallet)) {
|
||||
NavigationLink(destination: WalletView(damus_state: damus_state, model: damus_state.wallet)) {
|
||||
HStack {
|
||||
Image("wallet")
|
||||
.tint(DamusColors.adaptableBlack)
|
||||
|
||||
@@ -99,6 +99,6 @@ struct ConnectWalletView: View {
|
||||
|
||||
struct ConnectWalletView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ConnectWalletView(model: WalletModel())
|
||||
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,22 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WalletView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: WalletModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
init(damus_state: DamusState, model: WalletModel? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self._model = ObservedObject(wrappedValue: model ?? damus_state.wallet)
|
||||
self._settings = ObservedObject(wrappedValue: damus_state.settings)
|
||||
}
|
||||
|
||||
func MainWalletView(nwc: WalletConnectURL) -> some View {
|
||||
VStack {
|
||||
SupportDamus
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(nwc.relay.id)")
|
||||
|
||||
if let lud16 = nwc.lud16 {
|
||||
@@ -21,10 +33,133 @@ struct WalletView: View {
|
||||
BigButton("Disconnect Wallet") {
|
||||
self.model.disconnect()
|
||||
}
|
||||
|
||||
}
|
||||
.navigationTitle("Wallet")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.padding()
|
||||
}
|
||||
|
||||
func donation_binding() -> Binding<Double> {
|
||||
return Binding(get: {
|
||||
return Double(model.settings.donation_percent)
|
||||
}, set: { v in
|
||||
model.settings.donation_percent = Int(v)
|
||||
})
|
||||
}
|
||||
|
||||
static let min_donation: Double = 0.0
|
||||
static let max_donation: Double = 100.0
|
||||
|
||||
var percent: Double {
|
||||
Double(model.settings.donation_percent) / 100.0
|
||||
}
|
||||
|
||||
var tip_msats: String {
|
||||
let msats = Int64(percent * Double(model.settings.default_zap_amount * 1000))
|
||||
let s = format_msats_abbrev(msats)
|
||||
// TODO: fix formatting and remove this hack
|
||||
let parts = s.split(separator: ".")
|
||||
if parts.count == 1 {
|
||||
return s
|
||||
}
|
||||
if let end = parts[safe: 1] {
|
||||
if end.allSatisfy({ c in c.isNumber }) {
|
||||
return String(parts[0])
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var SupportDamus: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(DamusGradient.gradient.opacity(0.5))
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
HStack {
|
||||
Image("logo-nobg")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
Text("Support Damus")
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text("Help build the future of decentralized communication on the web.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("An additional percentage of each zap will be sent to support Damus development ")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.white)
|
||||
|
||||
let binding = donation_binding()
|
||||
|
||||
HStack {
|
||||
Slider(value: binding,
|
||||
in: WalletView.min_donation...WalletView.max_donation,
|
||||
label: { })
|
||||
Text("\(Int(binding.wrappedValue))%")
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 80)
|
||||
}
|
||||
|
||||
HStack{
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(Image("zap.fill")) \(format_msats_abbrev(Int64(model.settings.default_zap_amount) * 1000))")
|
||||
.font(.title)
|
||||
.foregroundColor(percent == 0 ? .gray : .yellow)
|
||||
.frame(width: 120)
|
||||
}
|
||||
|
||||
Text("Zap")
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text("+")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(Image("zap.fill")) \(tip_msats)")
|
||||
.font(.title)
|
||||
.foregroundColor(percent == 0 ? .gray : Color.yellow)
|
||||
.frame(width: 120)
|
||||
}
|
||||
|
||||
Text(percent == 0 ? "🩶" : "💜")
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, profile: damus_state.profiles.lookup(id: damus_state.pubkey), size: .small)
|
||||
|
||||
/*
|
||||
Slider(value: donation_binding(),
|
||||
in: WalletView.min...WalletView.max,
|
||||
step: 1,
|
||||
minimumValueLabel: { Text("\(WalletView.min)") },
|
||||
maximumValueLabel: { Text("\(WalletView.max)") },
|
||||
label: { Text("label") }
|
||||
)
|
||||
*/
|
||||
}
|
||||
.padding(25)
|
||||
}
|
||||
.frame(height: 370)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch model.connect_state {
|
||||
case .new:
|
||||
@@ -33,12 +168,41 @@ struct WalletView: View {
|
||||
ConnectWalletView(model: model)
|
||||
case .existing(let nwc):
|
||||
MainWalletView(nwc: nwc)
|
||||
.onAppear() {
|
||||
model.inital_percent = settings.donation_percent
|
||||
}
|
||||
.onChange(of: settings.donation_percent) { p in
|
||||
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
profile.damus_donation = p
|
||||
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: damus_state.pubkey, profile: profile))
|
||||
}
|
||||
.onDisappear {
|
||||
guard let keypair = damus_state.keypair.to_full(),
|
||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
|
||||
model.inital_percent != profile.damus_donation
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
profile.damus_donation = settings.donation_percent
|
||||
let meta = make_metadata_event(keypair: keypair, metadata: profile)
|
||||
let tsprofile = TimestampedProfile(profile: profile, timestamp: meta.created_at, event: meta)
|
||||
damus_state.profiles.add(id: damus_state.pubkey, profile: tsprofile)
|
||||
damus_state.postbox.send(meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_wallet_connect_url = WalletConnectURL(pubkey: "pk", relay: .init("wss://relay.damus.io")!, keypair: test_damus_state().keypair.to_full()!, lud16: "jb55@sendsats.com")
|
||||
|
||||
struct WalletView_Previews: PreviewProvider {
|
||||
static let tds = test_damus_state()
|
||||
static var previews: some View {
|
||||
WalletView(model: WalletModel())
|
||||
WalletView(damus_state: tds, model: WalletModel(state: .existing(test_wallet_connect_url), settings: tds.settings))
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -291,6 +291,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
||||
<target>Connect To Relay</target>
|
||||
<note>Label for section for adding a relay server.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect to Alby" xml:space="preserve">
|
||||
<source>Connect to Alby</source>
|
||||
<target>Connect to Alby</target>
|
||||
<note>Button to connect to Alby, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connected Relays" xml:space="preserve">
|
||||
<source>Connected Relays</source>
|
||||
<target>Connected Relays</target>
|
||||
@@ -471,11 +476,6 @@ Sentence composed of 2 variables to describe how many people are following a use
|
||||
<target>Disconnect From Relay</target>
|
||||
<note>Button to disconnect from the relay.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Dismiss" xml:space="preserve">
|
||||
<source>Dismiss</source>
|
||||
<target>Dismiss</target>
|
||||
<note>Button to dismiss a text field alert.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display Name" xml:space="preserve">
|
||||
<source>Display Name</source>
|
||||
<target>Display Name</target>
|
||||
@@ -995,8 +995,7 @@ Button text to indicate that the zap type is a private zap.</note>
|
||||
<trans-unit id="Relay" xml:space="preserve">
|
||||
<source>Relay</source>
|
||||
<target>Relay</target>
|
||||
<note>Label to display relay address.
|
||||
Text field for relay server. Used for testing purposes.</note>
|
||||
<note>Label to display relay address.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Relays" xml:space="preserve">
|
||||
<source>Relays</source>
|
||||
@@ -1067,11 +1066,6 @@ Button text to indicate that the zap type is a private zap.</note>
|
||||
<target>Repost</target>
|
||||
<note>Button to repost a note</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repost Note" xml:space="preserve">
|
||||
<source>Repost Note</source>
|
||||
<target>Repost Note</target>
|
||||
<note>Title text to indicate that the buttons below are meant to be used to repost a note to others.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reposted" xml:space="preserve">
|
||||
<source>Reposted</source>
|
||||
<target>Reposted</target>
|
||||
@@ -1358,6 +1352,11 @@ Button text to indicate that the zap type is a private zap.</note>
|
||||
<target>Universe 🛸</target>
|
||||
<note>Toolbar label for the universal view where posts from all connected relay servers appear.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unmute" xml:space="preserve">
|
||||
<source>Unmute</source>
|
||||
<target>Unmute</target>
|
||||
<note>Button to unmute a profile.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unmute conversation" xml:space="preserve">
|
||||
<source>Unmute conversation</source>
|
||||
<target>Unmute conversation</target>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@
|
||||
<key>one</key>
|
||||
<string>... %d یادداشت دیگر ...</string>
|
||||
<key>other</key>
|
||||
<string>... %d یادداشتهای دیگر ...</string>
|
||||
<string>... %d نوت های دیگر ...</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>followers_count</key>
|
||||
@@ -63,7 +63,7 @@
|
||||
<key>one</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به یک مطلب که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به یک مطلب که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
||||
<string>%2$@ و %1$d نفر دیگر به یک نوت که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reacted_your_post_3</key>
|
||||
@@ -79,7 +79,7 @@
|
||||
<key>one</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به مطلب شما بازخورد دادهاند</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به مطلب شما بازخورد دادهاند</string>
|
||||
<string>%2$@ و %1$d نفر دیگر به نوت شما بازخورد دادهاند</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reacted_your_profile_3</key>
|
||||
@@ -95,7 +95,7 @@
|
||||
<key>one</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به نمایهی شما بازخورد دادهاند</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ و %1$d نفر دیگر به نمایهی شما بازخورد دادهاند</string>
|
||||
<string>%2$@ و %1$d نفر دیگر به پروفایل شما بازخورد دادهاند</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reactions_count</key>
|
||||
@@ -210,6 +210,22 @@
|
||||
<string>بازنشرها</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>sats</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@SATS@</string>
|
||||
<key>SATS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>ساتوشی</string>
|
||||
<key>other</key>
|
||||
<string>ساتوشی</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>sats_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -226,6 +242,38 @@
|
||||
<string>%2$@ ساتوشی</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>zap_notification_no_message</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%1$#@NOTIFICATION@</string>
|
||||
<key>NOTIFICATION</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>@</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ ساتوشی از %3$@ دریافت کردید</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ ساتوشی از %3$@ دریافت کردید</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>zap_notification_with_message</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%1$#@NOTIFICATION@</string>
|
||||
<key>NOTIFICATION</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>@</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ ساتوشی از %3$@ دریافت کردید: "%4$@"</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ ساتوشی از %3$@ دریافت کردید: "%4$@"</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>zapped_tagged_in_3</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -287,7 +335,7 @@
|
||||
<key>one</key>
|
||||
<string>Zap</string>
|
||||
<key>other</key>
|
||||
<string>Zaps</string>
|
||||
<string>زپ</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,4 +32,26 @@ class LikeTests: XCTestCase {
|
||||
XCTAssertEqual(like_ev.last_refid()!.ref_id, id)
|
||||
}
|
||||
|
||||
func testToReactionEmoji() {
|
||||
let privkey = "0fc2092231f958f8d57d66f5e238bb45b6a2571f44c0ce024bbc6f3a9c8a15fe"
|
||||
let pubkey = "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2"
|
||||
let liked = NostrEvent(content: "awesome #[0] post", pubkey: "orig_pk", tags: [["p", "cindy"], ["e", "bob"]])
|
||||
liked.calculate_id()
|
||||
let id = liked.id
|
||||
|
||||
let emptyReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "")
|
||||
let plusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "+")
|
||||
let minusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "-")
|
||||
let heartReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "❤️")
|
||||
let thumbsUpReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "👍")
|
||||
let shakaReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "🤙")
|
||||
|
||||
XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️")
|
||||
XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️")
|
||||
XCTAssertEqual(to_reaction_emoji(ev: minusReaction), "👎")
|
||||
XCTAssertEqual(to_reaction_emoji(ev: heartReaction), "❤️")
|
||||
XCTAssertEqual(to_reaction_emoji(ev: thumbsUpReaction), "👍")
|
||||
XCTAssertEqual(to_reaction_emoji(ev: shakaReaction), "🤙")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user