diff --git a/README.md b/README.md index 7d563ed..2a2c806 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,75 @@ damus implements the following [Nostr Implementation Possibilities][nips] [nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md [nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md +## Getting Started on Damus + +### Damus iOS +1) Get the Damus app on Testflight: https://testflight.apple.com/join/CLwjLxWl + +#### βš™οΈ Settings (gear icon, top right) +- Relays: You can add more relays to send your notes to by tapping the "+". + - Find more relays to add: https://nostr.info/relays/ +- Public Key (pubkey): Your public, personal address and how people can find and tag you + - Secret Key: Your *private* key unique to you. Never share your private key publically and share with other clients at your own risk! + - Save your keys somewhere safe + - Log out + +#### 🏠 Personal Feed (home icon, bottom navigation) +- Feed from everyone you follow +- Can post notes by tapping the blue + button + +#### Notes (under 🏠 Personal Feed) +- Sending a Note is easy and it goes to both your 🏠 Personal and πŸ” Global Feeds +- To tag a user you must grab their pubkey: + 1. Search their username in the search bar at the top of the πŸ” Global Feed and click their profile + 2. Tap the πŸ”‘ icon which will copy their pubkey to your clipboard + 3. Go back to your 🏠 Personal Feed and tap the blue + button to compose your Note + 4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`) +- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note. +- Currently you can't delete your Notes in the iOS app +- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in πŸ” Global Feed +- Engaging with Notes + - πŸ’¬ Replying to a Note: Tap the chat icon underneath the note. This will show up in the users’ notifications and in your 🏠 Personal and πŸ” Global Feeds + - β™Ί Reposts: Tap the repost icon which will show up in your 🏠 Personal and πŸ” Global Feeds + - β™‘ Likes: Tap the heart icon. Users will not get a notification, and cannot see who liked their note (currently, web clients can see your pfp only) +- Formatting Notes (may not format as intended in other web clients) + - Italics: 1 asterisk `*italic*` + - Bold: 2 asterisk `**bold**` + - Strikethrough: 2 tildes `~~strikethrough~~` + - Code: 1 back-tick ``code`` + +#### πŸ’¬ Encrypted DMs (chat app, bottom navigation) +- Tap the chat icon and you'll notice there's nothing to see at first. Go to a user profile and tap the πŸ’¬ chat icon next to the follow button to begin a DM + +#### πŸ” Global Feed (magnify glass, bottom navigation) +- View the Global Feed from all the relays you've added in βš™οΈ Settings. Currently you can only search hashtags and user names and pubkeys + +#### πŸ”” Notifications +- All your notifications except πŸ’¬ DMs + +#### πŸ‘€ Change Your Profile (PFP) and Bio +- Currently you can't change your pfp on the Damus app (coming soon!). Here's how to do it on other clients (do at your own risk) +1. Get the [Alby](https://getalby.com/) or [nos2x](https://chrome.google.com/webstore/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp) browser extension (Chrome, Brave) +2. Go to https://damus.io/key to convert your nsec key (secret key in βš™οΈ Settings) into a hex version + i. For Alby, right-click the extension, select Options and scroll to the Nostr section to enter your secret hex key + ii. For nos2x, right-click the extension, select Options, then and add the relay `wss://relay.damus.io` and select both read and write, click Save, then enter your secret hex key and click save +3. Visit https://metadata.nostr.com and your profile data should auto-populate from the extension. If not click the extension or refresh the page +4. Add your image using a hosting site like imgbb.com + +#### ⚑️ Request Sats + (Sats or Satoshis are the smallest denomination of bitcoin) + +**Alby (browser extension)** +- Get the [Alby](https://getalby.com/) browser extension and create your Alby address [yourname]@getalby.com +- Convert your Damus secret key from nsec to hex at https://damus.io/key then go to Settings in Alby and under the Nostr section at the bottom of the page add your private hex key +- Click the Alby extension > click Receive > enter the amount of Sats > click Get Invoice > click Copy > then paste into Damus +- Note: On Damus Web it will appear as a string of characters but on Damus iOS it will appear as a clickable image + +**Zeus (mobile app)** +- Download [Zeus](https://zeusln.app/) app (iOS, Google, APK) +- Tap Get Started button > tap Connect a node > click on + sign (top right) > select Indhub > press Scan Lndhub QR > (from the Alby browser extension… click your account on the top left > click Manage Accounts > click 3-dot menu to right of your account and click Export Account to get a QR code then go back to Zeus app) > scan the QR Code and tap Save Node Config button +- To create an invoice tap Lightning > tap Receive > type in amount > tap Create Invoice > tap Copy Invoice > paste into a new Damus note + ## Contributing Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well. diff --git a/damus-c/damus.c b/damus-c/damus.c index 43eb326..48a2fbe 100644 --- a/damus-c/damus.c +++ b/damus-c/damus.c @@ -211,6 +211,9 @@ static int parse_invoice(struct cursor *cur, struct block *block) { const u8 *start, *end; char *fail; struct bolt11 *bolt11; + // optional + parse_str(cur, "lightning:"); + start = cur->p; if (!parse_str(cur, "lnbc")) diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 93d7402..2a30c20 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -1019,9 +1019,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; @@ -1039,7 +1039,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 0.1.6; + MARKETING_VERSION = 0.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1058,9 +1058,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = damus/Info.plist; @@ -1078,7 +1078,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 0.1.6; + MARKETING_VERSION = 0.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift index 43c2b2e..0f8f9cc 100644 --- a/damus/Components/ImageCarousel.swift +++ b/damus/Components/ImageCarousel.swift @@ -17,10 +17,15 @@ struct ImageViewer: View { VStack{ Text(url.lastPathComponent) - KFImage(url) + KFAnimatedImage(url) + .configure { view in + view.framePreloadCount = 3 + } + .cacheOriginalImage() .loadDiskFileSynchronously() .scaleFactor(UIScreen.main.scale) .fade(duration: 0.1) + .aspectRatio(contentMode: .fit) .tabItem { Text(url.absoluteString) } @@ -41,10 +46,15 @@ struct ImageCarousel: View { var body: some View { TabView { ForEach(urls, id: \.absoluteString) { url in - KFImage(url) + KFAnimatedImage(url) + .configure { view in + view.framePreloadCount = 3 + } + .cacheOriginalImage() .loadDiskFileSynchronously() .scaleFactor(UIScreen.main.scale) .fade(duration: 0.1) + .aspectRatio(contentMode: .fit) .tabItem { Text(url.absoluteString) } diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift index a3027cf..dfa66a3 100644 --- a/damus/Util/Keys.swift +++ b/damus/Util/Keys.swift @@ -69,7 +69,6 @@ func generate_new_keypair() -> Keypair { let key = try! secp256k1.Signing.PrivateKey() let privkey = hex_encode(key.rawRepresentation) let pubkey = hex_encode(Data(key.publicKey.xonly.bytes)) - print("generating privkey:\(privkey) pubkey:\(pubkey)") return Keypair(pubkey: pubkey, privkey: privkey) } diff --git a/damus/Views/AddRelayView.swift b/damus/Views/AddRelayView.swift index 5b62100..b16447f 100644 --- a/damus/Views/AddRelayView.swift +++ b/damus/Views/AddRelayView.swift @@ -18,6 +18,7 @@ struct AddRelayView: View { Form { Section("Add Relay") { TextField("wss://some.relay.com", text: $relay) + .autocorrectionDisabled(true) .textInputAutocapitalization(.never) } } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 97d2e19..bc14d92 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -15,8 +15,7 @@ enum NostrPostResult { let POST_PLACEHOLDER = "Type your post here..." struct PostView: View { - @State var post: String = POST_PLACEHOLDER - @State var new: Bool = true + @State var post: String = "" let replying_to: NostrEvent? @FocusState var focus: Bool @@ -50,7 +49,7 @@ struct PostView: View { } var is_post_empty: Bool { - return post == POST_PLACEHOLDER || post.allSatisfy { $0.isWhitespace } + return post.allSatisfy { $0.isWhitespace } } var body: some View { @@ -71,18 +70,17 @@ struct PostView: View { } .padding([.top, .bottom], 4) - - TextEditor(text: $post) - .foregroundColor(self.post == POST_PLACEHOLDER ? .gray : .primary) - .focused($focus) - .textInputAutocapitalization(.sentences) - .onTapGesture { - handle_post_placeholder() - } - .onChange(of: post) { value in - handle_post_placeholder() + ZStack(alignment: .topLeading) { + TextEditor(text: $post) + .focused($focus) + .textInputAutocapitalization(.sentences) + if post.isEmpty { + Text(POST_PLACEHOLDER) + .padding(.top, 8) + .padding(.leading, 10) + .foregroundColor(Color(uiColor: .placeholderText)) } - + } } .onAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -91,14 +89,5 @@ struct PostView: View { } .padding() } - - func handle_post_placeholder() { - guard new else { - return - } - - new = false - post = post.replacingOccurrences(of: POST_PLACEHOLDER, with: "") - } } diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift index d1f9ce1..d7c0f55 100644 --- a/damus/Views/ProfilePicView.swift +++ b/damus/Views/ProfilePicView.swift @@ -56,16 +56,19 @@ struct ProfilePicView: View { Group { let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) let url = URL(string: pic) - let processor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size)) - KFImage.url(url) + KFAnimatedImage(url) + .configure { view in + view.framePreloadCount = 1 + } .placeholder { _ in Placeholder } - .setProcessor(processor) + .cacheOriginalImage() .scaleFactor(UIScreen.main.scale) .loadDiskFileSynchronously() .fade(duration: 0.1) + .frame(width: size, height: size) .clipShape(Circle()) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index 9d67ada..2e30a76 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -88,6 +88,7 @@ struct ProfileView: View { @StateObject var followers: FollowersModel @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme //@EnvironmentObject var profile: ProfileModel diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift index 0fe98d4..74da505 100644 --- a/damus/Views/SearchHomeView.swift +++ b/damus/Views/SearchHomeView.swift @@ -19,6 +19,7 @@ struct SearchHomeView: View { TextField("", text: $search) .padding(8) .padding(.leading, 35) + .autocorrectionDisabled(true) .textInputAutocapitalization(.never) Label("", systemImage: "xmark.square") .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0)) diff --git a/damusTests/InvoiceTests.swift b/damusTests/InvoiceTests.swift index d2d726e..aa5b88d 100644 --- a/damusTests/InvoiceTests.swift +++ b/damusTests/InvoiceTests.swift @@ -34,6 +34,24 @@ final class InvoiceTests: XCTestCase { XCTAssertEqual(invoice.string, invstr) } + func testParseInvoiceWithPrefix() throws { + let invstr = "lightning:lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" + let parsed = parse_mentions(content: invstr, tags: []) + + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed.count, 1) + XCTAssertNotNil(parsed[0].is_invoice) + } + + func testParseInvoiceWithPrefixCapitalized() throws { + let invstr = "LIGHTNING:LNBC100N1P357SL0SP5T9N56WDZTUN39LGDQLR30XQWKSG3K69Q4Q2RKR52APLUJW0ESN0QPP5MRQGLJK62Z20Q4NVGR6LZCYN6FHYLZCCWDVU4K77APG3ZMRKUJJQDPZW35XJUEQD9EJQCFQV3JHXCMJD9C8G6T0DCXQYJW5QCQPJRZJQT56H4GVP5YX36U2UZQA6QWCSK3E2DUUNFXPPZJ9VHYPC3WFE2WSWZ607UQQ3XQQQSQQQQQQQQQQQLQQYG9QYYSGQAGX5H20AEULJ3GDWX3KXS8U9F4MCAKDKWUAKASAMM9562FFYR9EN8YG20LG0YGNR9ZPWP68524KMDA0T5XP2WYTEX35PU8HAPYJAJXQPSQL29R" + let parsed = parse_mentions(content: invstr, tags: []) + + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed.count, 1) + XCTAssertNotNil(parsed[0].is_invoice) + } + func testParseInvoice() throws { let invstr = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" let parsed = parse_mentions(content: invstr, tags: [])