Compare commits

..

24 Commits

Author SHA1 Message Date
tyiu 5bf5402919 Fix side menu label size so that translations in longer languages fit without wrapping
Changelog-Fixed: Fix side menu label size so that translations in longer languages fit without wrapping
2023-05-26 00:13:36 -04:00
gladiusKatana f9a572faa2 dynamically set .isScrollEnabled in TextViewWrapper (true if UserSearch is present) 2023-05-24 20:41:03 -07:00
gladiusKatana 0f805d7ea7 override .isScrollEnabled in TextViewWrapper (ie, set to false at UITextView creation) 2023-05-24 20:41:03 -07:00
William Casarin 52ca33ef6a script: fetch popular users 2023-05-24 18:26:18 -07:00
tyiu 1769b08147 Fix reaction notification title to be consistent with ReactionView
Changelog-Fixed: Fix reaction notification title to be consistent with ReactionView
Closes: #1137
2023-05-24 15:20:02 -07:00
Bryan Montz 812213ff2b Add Damus splash screen
Changelog-Updated: Add Damus splash screen
2023-05-24 15:17:08 -07:00
transifex-integration[bot] d6ecf14b55 Apply translations
Closes: #1134
2023-05-24 15:16:45 -07:00
William Casarin 47a74257c8 nwc debugging 2023-05-24 15:16:45 -07:00
William Casarin 1b161fefd0 nwc: make sure to support nostr+walletconnect scheme
not sure why we have 2 schemes
2023-05-15 12:54:42 -07:00
William Casarin 0b9a274e67 postbox: change initial retry_after from 2 to 10 seconds 2023-05-15 12:54:09 -07:00
William Casarin 2bbbb5db65 Fix a few bugs with donations 2023-05-15 12:53:36 -07:00
William Casarin bffa42a13a Supporter Badges 2023-05-15 11:57:37 -07:00
William Casarin 8097cfdfb8 Include donation_amount on profile 2023-05-15 09:59:51 -07:00
William Casarin af912b1a0e v1.5-1 2023-05-15 09:59:22 -07:00
William Casarin 51cd34c9c2 c: move parse_digit to remove warning 2023-05-15 09:59:10 -07:00
William Casarin a6745af519 Implement damus zap split donations using NWC 2023-05-15 09:41:26 -07:00
William Casarin 631220fdcb ui: add support damus ui in WalletView
This appears after you've connected your wallet
2023-05-14 22:24:12 -07:00
William Casarin 5aa0d6c3e1 settings: add donation_percent to settings
This will be used in damus donations splits
2023-05-14 22:24:12 -07:00
tyiu f9982e992a Migrate away from sticky deprecated non-pubkey-scoped settings
Changelog-Fixed: Migrate away from sticky deprecated non-pubkey-scoped settings
Closes: #1124
2023-05-14 21:08:56 -07:00
transifex-integration[bot] 0eaebb80f1 Apply translations
in:

- nl
- de
- cs
- hu_HU
- sv_SE
- ar
- fa
- pl_PL
- ja

Closes: #1122
2023-05-14 21:06:20 -07:00
tyiu 6f23b69f2c Export strings for translation 2023-05-14 20:54:13 -07:00
William Casarin ec50c75062 ui: expose raw LinearGradient in DamusGradient
This will be used in background-fill applications
2023-05-14 20:47:53 -07:00
William Casarin 0293b61d7a Rename 'Connect to Alby' to 'Attach Alby Wallet' 2023-05-14 14:07:04 -07:00
tyiu 50e4452016 Fix nostr URL scheme to open properly even if there's already a different view open
Closes: #1130
Changelog-Fixed: Fix nostr URL scheme to open properly even if there's already a different view open
2023-05-14 11:36:46 -07:00
63 changed files with 793 additions and 122 deletions
-15
View File
@@ -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)
+16
View File
@@ -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;
+1 -1
View File
@@ -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
*
+16 -2
View File
@@ -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 */; };
@@ -267,6 +269,7 @@
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
@@ -444,6 +447,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>"; };
@@ -696,6 +701,7 @@
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
@@ -990,6 +996,7 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
50DA11252A16A23F00236234 /* Launch.storyboard */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
);
@@ -1041,6 +1048,7 @@
children = (
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
);
path = Gradients;
sourceTree = "<group>";
@@ -1218,6 +1226,7 @@
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1552,6 +1561,7 @@
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */,
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
4C198DF129F88C6B004C165C /* License.txt in Resources */,
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
@@ -1730,6 +1740,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 +1848,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 +2140,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;
@@ -2143,6 +2155,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
@@ -2175,7 +2188,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;
@@ -2190,6 +2203,7 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
+12
View File
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "gradient.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@@ -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 {
@@ -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
View 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(verbatim: p.formatted())
.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)
}
}
}
+19 -6
View File
@@ -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,12 +258,26 @@ 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 {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
return
}
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here
}
+2 -2
View File
@@ -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) {
@@ -397,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)
}
}
+1 -1
View File
@@ -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())) }
}
+12 -9
View File
@@ -132,11 +132,11 @@ class HomeModel: ObservableObject {
case .nwc_request:
break
case .nwc_response:
handle_nwc_response(ev)
handle_nwc_response(ev, relay: relay_id)
}
}
func handle_nwc_response(_ ev: NostrEvent) {
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
Task { @MainActor in
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
@@ -148,21 +148,24 @@ class HomeModel: ObservableObject {
// since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
print("nwc: got response, removed \(resp.req_id) from the postbox")
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
} else {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove")
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
}
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 {
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
return
}
print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
}
}
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
@@ -731,7 +734,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 +751,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 +1202,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
+11 -2
View File
@@ -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.
+10 -6
View File
@@ -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)
+16 -2
View File
@@ -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? {
+25 -3
View File
@@ -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 {
+7 -1
View File
@@ -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
+1
View File
@@ -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
}
+31 -4
View File
@@ -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
@@ -128,11 +150,16 @@ class PostBox {
relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5
if let relay = pool.get_relay(relayer.relay) {
print("flushing event \(event.event.id) to \(relayer.relay)")
} else {
print("could not find relay when flushing: \(relayer.relay)")
}
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
}
}
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 +167,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
+25 -7
View File
@@ -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,
@@ -179,10 +180,11 @@ func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
filter.limit = 0
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
pool.send(.subscribe(sub), to: [url.relay.id])
pool.send(.subscribe(sub), to: [url.relay.id], skip_ephemeral: false)
}
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,30 @@ 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
}
print("damus-donation donating...")
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 {
+3 -4
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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)
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="gradient" translatesAutoresizingMaskIntoConstraints="NO" id="zoF-av-bOb">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="damus-home" translatesAutoresizingMaskIntoConstraints="NO" id="LOu-EK-R9r">
<rect key="frame" x="153.66666666666666" y="383" width="86" height="86"/>
<constraints>
<constraint firstAttribute="height" constant="86" id="KmA-28-Ngq"/>
<constraint firstAttribute="width" constant="86" id="ShD-nJ-gt9"/>
</constraints>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemPurpleColor"/>
<constraints>
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="Y10-Wq-VOp"/>
<constraint firstItem="zoF-av-bOb" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="Y5l-Ax-ViU"/>
<constraint firstItem="zoF-av-bOb" firstAttribute="leading" secondItem="5EZ-qb-Rvc" secondAttribute="leading" id="bvq-6J-kYc"/>
<constraint firstAttribute="bottom" secondItem="zoF-av-bOb" secondAttribute="bottom" id="dfj-BJ-nxB"/>
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerX" secondItem="5EZ-qb-Rvc" secondAttribute="centerX" id="mtD-6Q-d3P"/>
<constraint firstAttribute="right" secondItem="zoF-av-bOb" secondAttribute="right" id="xQW-SS-8nb"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-106.10687022900763" y="-29.577464788732396"/>
</scene>
</scenes>
<resources>
<image name="damus-home" width="43.333332061767578" height="43.333332061767578"/>
<image name="gradient" width="1125" height="2400"/>
<systemColor name="systemPurpleColor">
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+3 -2
View File
@@ -45,6 +45,7 @@ struct PostView: View {
@State var references: [ReferencedId] = []
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
@State var newCursorIndex: Int?
@State var postTextViewCanScroll: Bool = true
@State var mediaToUpload: MediaUpload? = nil
@@ -203,7 +204,7 @@ struct PostView: View {
var TextEntry: some View {
ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
focusWordAttributes = (word, range)
self.newCursorIndex = nil
})
@@ -335,7 +336,7 @@ struct PostView: View {
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, post: $post)
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
.frame(maxHeight: .infinity)
} else {
Divider()
+11 -2
View File
@@ -22,6 +22,7 @@ struct UserSearch: View {
let search: String
@Binding var focusWordAttributes: (String?, NSRange?)
@Binding var newCursorIndex: Int?
@Binding var postTextViewCanScroll: Bool
@Binding var post: NSMutableAttributedString
@@ -92,7 +93,14 @@ struct UserSearch: View {
.padding()
}
}
.onAppear() {
postTextViewCanScroll = false
}
.onDisappear() {
postTextViewCanScroll = true
}
}
}
struct UserSearch_Previews: PreviewProvider {
@@ -100,9 +108,10 @@ struct UserSearch_Previews: PreviewProvider {
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
@State static var word: (String?, NSRange?) = (nil, nil)
@State static var newCursorIndex: Int?
@State static var postTextViewCanScroll: Bool = false
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, post: $post)
UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
}
}
@@ -140,7 +149,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
}
}
}
+16
View File
@@ -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
}
}
}
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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
+1 -4
View File
@@ -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 {
+5 -1
View File
@@ -24,7 +24,11 @@ struct RelayDetailView: View {
}
func FieldText(_ str: String?) -> some View {
Text(str ?? "No data available")
if let s = str {
return Text(verbatim: s)
} else {
return Text("No data available", comment: "Text indicating that there is no data available to show for specific metadata about a relay server.")
}
}
var body: some View {
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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)
@@ -57,6 +57,7 @@ struct SideMenuView: View {
.font(.title2)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
}
}
@@ -144,6 +145,7 @@ struct SideMenuView: View {
.font(.title3)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
})
Spacer()
@@ -154,6 +156,7 @@ struct SideMenuView: View {
Label("", systemImage: "qrcode")
.font(.title)
.foregroundColor(textColor())
.dynamicTypeSize(.xSmall)
}).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state, pubkey: damus_state.pubkey)
}
@@ -188,6 +191,7 @@ struct SideMenuView: View {
.font(.title2)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
}
struct SideMenuLabelStyle: LabelStyle {
+3
View File
@@ -9,12 +9,14 @@ import SwiftUI
struct TextViewWrapper: UIViewRepresentable {
@Binding var attributedText: NSMutableAttributedString
@Binding var postTextViewCanScroll: Bool
let cursorIndex: Int?
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isScrollEnabled = postTextViewCanScroll
textView.showsVerticalScrollIndicator = false
TextViewWrapper.setTextProperties(textView)
return textView
@@ -29,6 +31,7 @@ struct TextViewWrapper: UIViewRepresentable {
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.isScrollEnabled = postTextViewCanScroll
uiView.attributedText = attributedText
TextViewWrapper.setTextProperties(uiView)
setCursorPosition(textView: uiView)
+7 -7
View File
@@ -17,7 +17,7 @@ struct ConnectWalletView: View {
var body: some View {
MainContent
.navigationTitle("Attach a Wallet")
.navigationTitle(NSLocalizedString("Attach a Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet."))
.navigationBarTitleDisplayMode(.large)
.padding()
.onChange(of: wallet_scan_result) { res in
@@ -39,7 +39,7 @@ struct ConnectWalletView: View {
func AreYouSure(nwc: WalletConnectURL) -> some View {
VStack {
Text("Are you sure you want to attach this wallet?")
Text("Are you sure you want to attach this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
.font(.title)
Text(nwc.relay.id)
@@ -52,11 +52,11 @@ struct ConnectWalletView: View {
.foregroundColor(.gray)
}
BigButton("Attach") {
BigButton(NSLocalizedString("Attach", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
model.connect(nwc)
}
BigButton("Cancel") {
BigButton(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.")) {
model.cancel()
}
}
@@ -72,7 +72,7 @@ struct ConnectWalletView: View {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
}
BigButton("Attach Wallet") {
BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
scanning = true
}
@@ -89,7 +89,7 @@ struct ConnectWalletView: View {
case .new(let nwc):
AreYouSure(nwc: nwc)
case .existing:
Text("Shouldn't happen")
Text(verbatim: "Shouldn't happen")
case .none:
ConnectWallet
}
@@ -99,6 +99,6 @@ struct ConnectWalletView: View {
struct ConnectWalletView_Previews: PreviewProvider {
static var previews: some View {
ConnectWalletView(model: WalletModel())
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()))
}
}
+158 -4
View File
@@ -8,23 +8,148 @@
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 {
Text("\(nwc.relay.id)")
SupportDamus
Spacer()
Text(verbatim: nwc.relay.id)
if let lud16 = nwc.lud16 {
Text("\(lud16)")
Text(verbatim: lud16)
}
BigButton("Disconnect Wallet") {
BigButton(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")) {
self.model.disconnect()
}
}
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))
.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", comment: "Text calling for the user to support Damus through zaps")
.font(.title.bold())
.foregroundColor(.white)
}
Text("Help build the future of decentralized communication on the web.", comment: "Text indicating the goal of developing Damus which the user can help with.")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.white)
Text("An additional percentage of each zap will be sent to support Damus development", comment: "Text indicating that they can contribute zaps 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))%", comment: "Percentage of additional zap that should be sent to support Damus development.")
.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", comment: "Text underneath the number of sats indicating that it's the amount used for zaps.")
.foregroundColor(.white)
}
Spacer()
Text(verbatim: "+")
.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(verbatim: 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)
}
.padding(25)
}
.frame(height: 370)
}
var body: some View {
switch model.connect_state {
case .new:
@@ -33,12 +158,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))
}
}
+1 -1
View File
@@ -136,7 +136,7 @@ struct CustomizeZapView: View {
VStack(alignment: .center, spacing: 0) {
TextField("", text: $custom_amount)
.placeholder(when: custom_amount.isEmpty, alignment: .center) {
Text(String("0"))
Text(verbatim: 0.formatted())
}
.accentColor(.clear)
.font(.system(size: 72, weight: .heavy))
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -42,11 +42,6 @@
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3" build-num="14E222b"/>
</header>
<body>
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<target>%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target>%@ %@</target>
@@ -83,6 +78,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.</target>
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%lld%%" xml:space="preserve">
<source>%lld%%</source>
<target>%lld%%</target>
<note>Percentage of additional zap that should be sent to support Damus development.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
<source>%lld/%lld</source>
<target>%lld/%lld</target>
@@ -174,6 +174,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Always show images</target>
<note>Setting to always show and never blur images</note>
</trans-unit>
<trans-unit id="An additional percentage of each zap will be sent to support Damus development" xml:space="preserve">
<source>An additional percentage of each zap will be sent to support Damus development</source>
<target>An additional percentage of each zap will be sent to support Damus development</target>
<note>Text indicating that they can contribute zaps to support Damus development.</note>
</trans-unit>
<trans-unit id="Animations" xml:space="preserve">
<source>Animations</source>
<target>Animations</target>
@@ -201,6 +206,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Are you lost?</target>
<note>Text asking the user if they are lost in the app.</note>
</trans-unit>
<trans-unit id="Are you sure you want to attach this wallet?" xml:space="preserve">
<source>Are you sure you want to attach this wallet?</source>
<target>Are you sure you want to attach this wallet?</target>
<note>Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Are you sure you want to delete all of your bookmarks?" xml:space="preserve">
<source>Are you sure you want to delete all of your bookmarks?</source>
<target>Are you sure you want to delete all of your bookmarks?</target>
@@ -216,6 +226,26 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Are you sure you want to upload this media?</target>
<note>Alert message asking if the user wants to upload media.</note>
</trans-unit>
<trans-unit id="Attach" xml:space="preserve">
<source>Attach</source>
<target>Attach</target>
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Attach Alby Wallet" xml:space="preserve">
<source>Attach Alby Wallet</source>
<target>Attach Alby Wallet</target>
<note>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.</note>
</trans-unit>
<trans-unit id="Attach Wallet" xml:space="preserve">
<source>Attach Wallet</source>
<target>Attach Wallet</target>
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Attach a Wallet" xml:space="preserve">
<source>Attach a Wallet</source>
<target>Attach a Wallet</target>
<note>Navigation title for attaching Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Automatically translate notes" xml:space="preserve">
<source>Automatically translate notes</source>
<target>Automatically translate notes</target>
@@ -264,7 +294,8 @@ Sentence composed of 2 variables to describe how many people are following a use
Button to cancel the upload.
Cancel deleting bookmarks.
Cancel deleting the user.
Cancel out of logging out the user.</note>
Cancel out of logging out the user.
Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.</note>
</trans-unit>
<trans-unit id="Choose from Library" xml:space="preserve">
<source>Choose from Library</source>
@@ -471,10 +502,10 @@ 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 id="Disconnect Wallet" xml:space="preserve">
<source>Disconnect Wallet</source>
<target>Disconnect Wallet</target>
<note>Text for button to disconnect from Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Display Name" xml:space="preserve">
<source>Display Name</source>
@@ -581,6 +612,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Get API Key with BTC/Lightning</target>
<note>Button to navigate to nokyctranslate website to get a translation API key.</note>
</trans-unit>
<trans-unit id="Help build the future of decentralized communication on the web." xml:space="preserve">
<source>Help build the future of decentralized communication on the web.</source>
<target>Help build the future of decentralized communication on the web.</target>
<note>Text indicating the goal of developing Damus which the user can help with.</note>
</trans-unit>
<trans-unit id="Hide" xml:space="preserve">
<source>Hide</source>
<target>Hide</target>
@@ -777,6 +813,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>No</target>
<note>Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key.</note>
</trans-unit>
<trans-unit id="No data available" xml:space="preserve">
<source>No data available</source>
<target>No data available</target>
<note>Text indicating that there is no data available to show for specific metadata about a relay server.</note>
</trans-unit>
<trans-unit id="No mute list found, create a new one? This will overwrite any previous mute lists." xml:space="preserve">
<source>No mute list found, create a new one? This will overwrite any previous mute lists.</source>
<target>No mute list found, create a new one? This will overwrite any previous mute lists.</target>
@@ -995,8 +1036,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 +1107,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>
@@ -1232,6 +1267,11 @@ Button text to indicate that the zap type is a private zap.</note>
<target>Software</target>
<note>Label to display relay software.</note>
</trans-unit>
<trans-unit id="Support Damus" xml:space="preserve">
<source>Support Damus</source>
<target>Support Damus</target>
<note>Text calling for the user to support Damus through zaps</note>
</trans-unit>
<trans-unit id="Supported NIPs" xml:space="preserve">
<source>Supported NIPs</source>
<target>Supported NIPs</target>
@@ -1358,6 +1398,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>
@@ -1415,7 +1460,8 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<trans-unit id="Wallet" xml:space="preserve">
<source>Wallet</source>
<target>Wallet</target>
<note>Sidebar menu label for Wallet view.
<note>Navigation title for Wallet view
Sidebar menu label for Wallet view.
Title for section in zap settings that controls the Lightning wallet selection.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
Binary file not shown.
Binary file not shown.
+18
View File
@@ -236,6 +236,24 @@
<string>Republicaciones</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>sat</string>
<key>many</key>
<string>sats</string>
<key>other</key>
<string>sats</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
+58 -10
View File
@@ -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>
@@ -111,7 +111,7 @@
<key>one</key>
<string>بازخورد</string>
<key>other</key>
<string>بازخوردها</string>
<string>واکنش ها</string>
</dict>
</dict>
<key>relays_count</key>
@@ -159,7 +159,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>reposted_your_post_3</key>
@@ -175,7 +175,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>reposted_your_profile_3</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>
@@ -239,7 +287,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>zapped_your_post_3</key>
@@ -255,7 +303,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>zapped_your_profile_3</key>
@@ -287,7 +335,7 @@
<key>one</key>
<string>Zap</string>
<key>other</key>
<string>Zaps</string>
<string>زپ</string>
</dict>
</dict>
</dict>
+18
View File
@@ -236,6 +236,24 @@
<string>Republications</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>sat</string>
<key>many</key>
<string>sats</string>
<key>other</key>
<string>sats</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -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), "🤙")
}
}
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
curl $(curl -s 'https://storage.googleapis.com/storage/v1/b/nostrdb-backups/o?prefix=ndjson' | jq -r '.items | last | .mediaLink') > nostr-directory.json
jq -rc '.data | {url: .profileImageUrl, pk: .hexPubKey, userName: .userName, twitterFollowers: .user.followers_count, nostrFollowers: .nFollowerCount}' nostr-directory.json | jq -cs 'sort_by(.twitterFollowers + .nostrFollowers) | .[]' | tail -n1000 | tac > popular_users.json
printf "saved popular_users.json\n" >&2