Compare commits
1 Commits
hide-last-
...
hide-futur
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ee970ea9e |
@@ -1649,9 +1649,6 @@
|
||||
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
|
||||
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
|
||||
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
@@ -2557,7 +2554,6 @@
|
||||
D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
|
||||
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosDeterministicAccountClient.swift; sourceTree = "<group>"; };
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
|
||||
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
|
||||
@@ -3367,7 +3363,6 @@
|
||||
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
|
||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
|
||||
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -4864,7 +4859,6 @@
|
||||
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */,
|
||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||
4C1253582A76C9060004F4B8 /* PresentSheetNotify.swift in Sources */,
|
||||
D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */,
|
||||
4C363A962827096D006E126D /* PostBlock.swift in Sources */,
|
||||
4CA9275F2A2902B20098A105 /* LongformPreview.swift in Sources */,
|
||||
@@ -5253,7 +5247,6 @@
|
||||
82D6FB652CD99F7900C925F4 /* CollectionExtension.swift in Sources */,
|
||||
82D6FB662CD99F7900C925F4 /* ZapDataModel.swift in Sources */,
|
||||
82D6FB672CD99F7900C925F4 /* Zaps+.swift in Sources */,
|
||||
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
82D6FB682CD99F7900C925F4 /* WalletConnect+.swift in Sources */,
|
||||
82D6FB692CD99F7900C925F4 /* DamusPurpleNotificationManagement.swift in Sources */,
|
||||
82D6FB6A2CD99F7900C925F4 /* DamusPurple.swift in Sources */,
|
||||
@@ -6003,7 +5996,6 @@
|
||||
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */,
|
||||
D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */,
|
||||
D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */,
|
||||
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
D703D7802C670C2500A400EA /* NIP05.swift in Sources */,
|
||||
D703D7AA2C670E5D00A400EA /* verifier.c in Sources */,
|
||||
D73E5E1D2C6A9680007EB227 /* PreviewCache.swift in Sources */,
|
||||
|
||||
@@ -160,7 +160,7 @@ func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, set
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, can_hide_last_previewable_refs: true)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
||||
|
||||
// and cache it
|
||||
return .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
|
||||
@@ -73,130 +73,85 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -
|
||||
return .longform(LongformContent(ev.content))
|
||||
}
|
||||
|
||||
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
|
||||
return .separated(render_blocks(blocks: blocks, profiles: profiles))
|
||||
}
|
||||
|
||||
func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
|
||||
func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSeparated {
|
||||
var invoices: [Invoice] = []
|
||||
var urls: [UrlType] = []
|
||||
let blocks = bs.blocks
|
||||
|
||||
var end_mention_count = 0
|
||||
var end_url_count = 0
|
||||
|
||||
// Search backwards until we find the beginning index of the chain of previewables that reach the end of the content.
|
||||
var hide_text_index = blocks.endIndex
|
||||
if can_hide_last_previewable_refs {
|
||||
outerLoop: for (i, block) in blocks.enumerated().reversed() {
|
||||
if block.is_previewable {
|
||||
switch block {
|
||||
case .mention:
|
||||
end_mention_count += 1
|
||||
|
||||
// If there is more than one previewable mention,
|
||||
// do not hide anything because we allow rich rendering of only one mention currently.
|
||||
// This should be fixed in the future to show events inline instead.
|
||||
if end_mention_count > 1 {
|
||||
hide_text_index = blocks.endIndex
|
||||
break outerLoop
|
||||
}
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
if case .link = url_type {
|
||||
end_url_count += 1
|
||||
|
||||
// If there is more than one link, do not hide anything because we allow rich rendering of only
|
||||
// one link.
|
||||
if end_url_count > 1 {
|
||||
hide_text_index = blocks.endIndex
|
||||
break outerLoop
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
hide_text_index = i
|
||||
} else if case .text(let txt) = block, txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
hide_text_index = i
|
||||
} else {
|
||||
break
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({
|
||||
if case .mention(let mention) = $0,
|
||||
case .note = mention.ref {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
.count == 1
|
||||
|
||||
var ind: Int = -1
|
||||
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
|
||||
ind = ind + 1
|
||||
|
||||
// Add the rendered previewable blocks to their type-specific lists.
|
||||
switch block {
|
||||
case .invoice(let invoice):
|
||||
invoices.append(invoice)
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
urls.append(url_type)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if can_hide_last_previewable_refs {
|
||||
// If there are previewable blocks that occur before the consecutive sequence of them at the end of the content,
|
||||
// we should not hide the text representation of any previewable block to avoid altering the format of the note.
|
||||
if ind < hide_text_index && block.is_previewable {
|
||||
hide_text_index = blocks.endIndex
|
||||
}
|
||||
|
||||
// No need to show the text representation of the block if the only previewables are the sequence of them
|
||||
// found at the end of the content.
|
||||
// This is to save unnecessary use of screen space.
|
||||
if ind >= hide_text_index {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if case .note = m.ref, one_note_ref {
|
||||
return str
|
||||
}
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref))
|
||||
|
||||
case .relay(let relay):
|
||||
return str + CompatibleText(stringLiteral: relay)
|
||||
|
||||
case .hashtag(let htag):
|
||||
return str + hashtag_str(htag)
|
||||
case .invoice(let invoice):
|
||||
return str + invoice_str(invoice)
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
case .url(let url):
|
||||
return str + url_str(url)
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
return str
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
func reduce_text_block(ind: Int, hide_text_index: Int, txt: String) -> String {
|
||||
func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> String {
|
||||
var trimmed = txt
|
||||
|
||||
// Trim leading whitespaces.
|
||||
if ind == 0 {
|
||||
trimmed = trim_prefix(trimmed)
|
||||
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
// Trim trailing whitespaces if the following blocks will be hidden or if this is the last block.
|
||||
if ind == hide_text_index - 1 {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next,
|
||||
case .note = m.ref,
|
||||
one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func invoice_str(_ invoice: Invoice) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: abbrev_identifier(invoice.string))
|
||||
attributedString.link = URL(string: "damus:lightning:\(invoice.string)")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
@@ -206,16 +161,17 @@ func url_str(_ url: URL) -> CompatibleText {
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
let fileExtension = url.lastPathComponent.lowercased().components(separatedBy: ".").last
|
||||
|
||||
switch fileExtension {
|
||||
case "png", "jpg", "jpeg", "gif", "webp":
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
|
||||
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
|
||||
return .media(.image(url))
|
||||
case "mp4", "mov", "m3u8":
|
||||
return .media(.video(url))
|
||||
default:
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") {
|
||||
return .media(.video(url))
|
||||
}
|
||||
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
|
||||
@@ -238,11 +194,11 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText
|
||||
let display_str: String = {
|
||||
switch m.ref {
|
||||
case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles)
|
||||
case .note: return abbrev_identifier(bech32String)
|
||||
case .nevent: return abbrev_identifier(bech32String)
|
||||
case .note: return abbrev_pubkey(bech32String)
|
||||
case .nevent: return abbrev_pubkey(bech32String)
|
||||
case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles)
|
||||
case .nrelay(let url): return url
|
||||
case .naddr: return abbrev_identifier(bech32String)
|
||||
case .naddr: return abbrev_pubkey(bech32String)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -43,18 +43,6 @@ struct DamusURLHandler {
|
||||
return .route(.Script(script: model))
|
||||
case .purple(let purple_url):
|
||||
return await damus_state.purple.handle(purple_url: purple_url)
|
||||
case .invoice(let invoice):
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
return .sheet(.select_wallet(invoice: invoice.string))
|
||||
} else {
|
||||
do {
|
||||
try open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: invoice.string)
|
||||
return .no_action
|
||||
}
|
||||
catch {
|
||||
return .sheet(.select_wallet(invoice: invoice.string))
|
||||
}
|
||||
}
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
@@ -103,11 +91,6 @@ struct DamusURLHandler {
|
||||
return .filter(filt)
|
||||
case .script(let script):
|
||||
return .script(script)
|
||||
case .invoice(let bolt11):
|
||||
if let invoice = decode_bolt11(bolt11) {
|
||||
return .invoice(invoice)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -120,6 +103,5 @@ struct DamusURLHandler {
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
case invoice(Invoice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ enum NostrLink: Equatable {
|
||||
case ref(RefId)
|
||||
case filter(NostrFilter)
|
||||
case script([UInt8])
|
||||
case invoice(String)
|
||||
}
|
||||
|
||||
func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
|
||||
@@ -94,15 +93,8 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
return
|
||||
}
|
||||
|
||||
if parts.count >= 2 {
|
||||
switch parts[0] {
|
||||
case "t":
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
case "lightning":
|
||||
return .invoice(parts[1])
|
||||
default:
|
||||
break
|
||||
}
|
||||
if parts.count >= 2 && parts[0] == "t" {
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
}
|
||||
|
||||
guard parts.count == 1 else {
|
||||
|
||||
@@ -37,23 +37,7 @@ enum Block: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var is_previewable: Bool {
|
||||
switch self {
|
||||
case .mention(let m):
|
||||
switch m.ref {
|
||||
case .note, .nevent: return true
|
||||
default: return false
|
||||
}
|
||||
case .invoice:
|
||||
return true
|
||||
case .url:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case text(String)
|
||||
case mention(Mention<MentionRef>)
|
||||
case hashtag(String)
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
//
|
||||
// CoinosDeterministicClient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-04-14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Implements a client that can talk to the Coinos API server with a deterministic account derived from the user's private key.
|
||||
///
|
||||
/// This is NOT a general-purpose Coinos client, and only works with the user's own deterministic "one-click setup" Coinos wallet account.
|
||||
class CoinosDeterministicAccountClient {
|
||||
// MARK: - State
|
||||
|
||||
/// The user's normal keypair for using Nostr
|
||||
private let userKeypair: FullKeypair
|
||||
/// The JWT authentication token with Coinos
|
||||
private var jwtAuthToken: String? = nil
|
||||
|
||||
|
||||
// MARK: - Computed properties for a deterministic wallet
|
||||
|
||||
/// A deterministic keypair for the NWC connection derived from the user's private key
|
||||
private var nwcKeypair: FullKeypair? {
|
||||
let nwcPrivateKey: Privkey = Privkey(sha256(self.userKeypair.privkey.id)) // SHA256 is an irreversible operation, user's nsec should not be deriveable from this new private key
|
||||
return FullKeypair(privkey: nwcPrivateKey)
|
||||
}
|
||||
|
||||
/// A deterministic username for a Coinos account
|
||||
private var username: String? {
|
||||
// Derive from private key because deriving from a pubkey would mean that anyone could compute the username and take that username before our user
|
||||
// Add some prefix so that we can ensure this will NOT match the password nor the NWC keypair
|
||||
guard let fullText = sha256Hex(text: "coinos_username:" + self.userKeypair.privkey.hex()) else { return nil }
|
||||
// There is very little risk of a birthday attack on getting only the first 16 characters, because:
|
||||
// 1. before this user creates an account, no one else knows the private key in order to know the expected username and create an account before them
|
||||
// 2. after the account is created and username is revealed, finding collisions is pointless as duplicate usernames will be rejected by Coinos
|
||||
//
|
||||
// In terms of the risk of an accidental collision due to the birthday problem, 16 characters should be enough to pragmatically avoid any collision.
|
||||
// According to `https://en.wikipedia.org/wiki/Birthday_problem#Probability_table`,
|
||||
// even if we have 610 million Damus users connected to Coinos, the probability of even a single collision is still as low as 1%.
|
||||
return String(fullText.prefix(16))
|
||||
}
|
||||
|
||||
/// A deterministic password for a Coinos account
|
||||
private var password: String? {
|
||||
// Add some prefix so that we can ensure this will NOT match the user nor the NWC private key
|
||||
return sha256Hex(text: "coinos_password:" + self.userKeypair.privkey.hex())
|
||||
}
|
||||
|
||||
/// A deterministic NWC app connection name
|
||||
private var nwcConnectionName: String { return "Damus" }
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initializes the client with the user's keypair
|
||||
init(userKeypair: FullKeypair) {
|
||||
self.userKeypair = userKeypair
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Authentication and registration
|
||||
|
||||
/// Tries to login to the user's deterministic account. If it cannot be found, it will register for one and log into that.
|
||||
func loginOrRegister() async throws {
|
||||
do {
|
||||
// Check if client has an account
|
||||
try await self.login()
|
||||
}
|
||||
catch {
|
||||
guard let error = error as? CoinosDeterministicAccountClient.ClientError, error == .unauthorized else { throw error }
|
||||
// Client does not seem to have an account, create one
|
||||
try await self.register()
|
||||
try await self.login()
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers for a Coinos account using deterministic account details.
|
||||
///
|
||||
/// It succeeds if it returns without throwing errors.
|
||||
func register() async throws {
|
||||
guard let username, let password else { throw ClientError.errorFormingRequest }
|
||||
let registerPayload = RegisterRequest(user: UserCredentials(username: username, password: password))
|
||||
let jsonData = try JSONEncoder().encode(registerPayload)
|
||||
|
||||
let url = URL(string: "https://coinos.io/api/register")!
|
||||
let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||
return
|
||||
} else {
|
||||
throw ClientError.unexpectedHTTPResponse(status_code: (response as? HTTPURLResponse)?.statusCode ?? -1, response: data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs into the deterministic account, if an auth token is not present
|
||||
func loginIfNeeded() async throws {
|
||||
if self.jwtAuthToken == nil { try await self.login() }
|
||||
}
|
||||
|
||||
/// Logs into to our deterministic account.
|
||||
///
|
||||
/// Succeeds if it returns without returning errors.
|
||||
///
|
||||
/// Mutating function, will update the client's internal state.
|
||||
func login() async throws {
|
||||
self.jwtAuthToken = try await sendLoginRequest().token
|
||||
}
|
||||
|
||||
/// Sends the login request and return the response
|
||||
///
|
||||
/// Does NOT update the internal login state.
|
||||
private func sendLoginRequest() async throws -> AuthResponse {
|
||||
guard let url = URL(string: "https://coinos.io/api/login") else { throw ClientError.errorFormingRequest }
|
||||
guard let username, let password else { throw ClientError.errorFormingRequest }
|
||||
let credentials = UserCredentials(username: username, password: password)
|
||||
let jsonData = try JSONEncoder().encode(credentials)
|
||||
|
||||
let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200: return try JSONDecoder().decode(AuthResponse.self, from: data)
|
||||
case 401: throw ClientError.unauthorized
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Managing NWC connections
|
||||
|
||||
/// Creates a new NWC connection
|
||||
///
|
||||
/// Note: Account must exist before calling this endpoint
|
||||
func createNWCConnection() async throws -> WalletConnectURL {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
guard let urlEndpoint = URL(string: "https://coinos.io/api/app") else { throw ClientError.errorFormingRequest }
|
||||
|
||||
try await self.loginIfNeeded()
|
||||
|
||||
let config = try defaultWalletConnectionConfig()
|
||||
let configData = try encode_json_data(config)
|
||||
|
||||
let (data, response) = try await self.makeAuthenticatedRequest(
|
||||
method: .post,
|
||||
url: urlEndpoint,
|
||||
payload: configData,
|
||||
payload_type: .json
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
guard let nwc = try await self.getNWCUrl() else { throw ClientError.errorProcessingResponse }
|
||||
return nwc
|
||||
case 401: throw ClientError.unauthorized
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
/// Returns the default wallet connection config
|
||||
private func defaultWalletConnectionConfig() throws -> NewWalletConnectionConfig {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
return NewWalletConnectionConfig(
|
||||
name: self.nwcConnectionName,
|
||||
secret: nwcKeypair.privkey.hex(),
|
||||
pubkey: nwcKeypair.pubkey.hex(),
|
||||
max_amount: 30000, // 30K sats per week maximum
|
||||
budget_renewal: .weekly
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the NWC URL for the deterministic NWC app connection
|
||||
///
|
||||
/// Account must already exist before calling this
|
||||
///
|
||||
/// Returns `nil` if no NWC url is found, (e.g. if app connection has not been configured yet)
|
||||
func getNWCUrl() async throws -> WalletConnectURL? {
|
||||
guard let connectionConfig = try await self.getNWCAppConnectionConfig(), let nwc = connectionConfig.nwc else { return nil }
|
||||
return WalletConnectURL(str: nwc)
|
||||
}
|
||||
|
||||
/// Gets the deterministic NWC app connection configuration details, if it exists
|
||||
///
|
||||
/// Account must already exist before calling this
|
||||
///
|
||||
/// Returns `nil` if no connection is found, (e.g. if app connection has not been configured yet)
|
||||
func getNWCAppConnectionConfig() async throws -> WalletConnectionConfig? {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
guard let url = URL(string: "https://coinos.io/api/app/" + nwcKeypair.pubkey.hex()) else { throw ClientError.errorFormingRequest }
|
||||
|
||||
try await self.loginIfNeeded()
|
||||
|
||||
let (data, response) = try await self.makeAuthenticatedRequest(
|
||||
method: .get,
|
||||
url: url,
|
||||
payload: nil,
|
||||
payload_type: nil
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200: return try JSONDecoder().decode(WalletConnectionConfig.self, from: data)
|
||||
case 401: throw ClientError.unauthorized
|
||||
case 404: return nil
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lower level request convenience functions
|
||||
|
||||
/// Makes a request without any authorization
|
||||
func makeRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.httpBody = payload
|
||||
|
||||
if let payload_type {
|
||||
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
return try await URLSession.shared.data(for: request)
|
||||
}
|
||||
|
||||
/// Makes an authenticated request with our JWT auth token.
|
||||
///
|
||||
/// Client must be logged-in before calling this, otherwise an error will be thrown.
|
||||
func makeAuthenticatedRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) {
|
||||
guard let jwtAuthToken else { throw ClientError.errorFormingRequest }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.httpBody = payload
|
||||
|
||||
request.setValue("Bearer " + jwtAuthToken, forHTTPHeaderField: "Authorization")
|
||||
if let payload_type {
|
||||
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
return try await URLSession.shared.data(for: request)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper structures
|
||||
|
||||
/// Payload for registering for a new Coinos account
|
||||
struct RegisterRequest: Codable {
|
||||
/// New user credentials
|
||||
let user: UserCredentials
|
||||
}
|
||||
|
||||
/// Payload for user credentials (sign-up and login)
|
||||
struct UserCredentials: Codable {
|
||||
/// The username
|
||||
let username: String
|
||||
/// The user password
|
||||
let password: String
|
||||
}
|
||||
|
||||
/// A successful response to a login auth endpoint
|
||||
struct AuthResponse: Codable {
|
||||
/// The JWT token to be applied to any authenticated API calls
|
||||
let token: String
|
||||
}
|
||||
|
||||
/// Used by the client to define new NWC configurations
|
||||
struct NewWalletConnectionConfig: Codable {
|
||||
/// The name of the connection
|
||||
let name: String
|
||||
/// 32 Hex-encoded bytes containing a shared private key secret
|
||||
let secret: String
|
||||
/// 32 Hex-encoded bytes containing the pubkey for the secret
|
||||
let pubkey: String
|
||||
/// Max amount that can be spent in each renewal period (measured in sats)
|
||||
let max_amount: UInt64
|
||||
/// The period of time it takes for the budget limits to reset
|
||||
let budget_renewal: BudgetRenewalPeriod
|
||||
}
|
||||
|
||||
/// The NWC connection configuration details
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - All items defined as optionals because the Coinos API may change in the future, so this may help increase future compatibility.
|
||||
struct WalletConnectionConfig: Codable {
|
||||
/// The name of the connection
|
||||
let name: String?
|
||||
/// 32 Hex-encoded bytes containing a shared private key secret
|
||||
let secret: String?
|
||||
/// 32 Hex-encoded bytes containing the pubkey for the secret
|
||||
let pubkey: String?
|
||||
/// Max amount that can be spent in every renewal period (measured in sats)
|
||||
let max_amount: UInt64?
|
||||
/// The NWC url generated by the server
|
||||
let nwc: String?
|
||||
/// Budget renewal information
|
||||
let budget_renewal: BudgetRenewalPeriod?
|
||||
}
|
||||
|
||||
/// A period of time it takes for budget limits to be reset
|
||||
enum BudgetRenewalPeriod: String, Codable {
|
||||
/// Resets once a week
|
||||
case weekly
|
||||
}
|
||||
|
||||
/// A client error occured
|
||||
enum ClientError: Error, Equatable {
|
||||
/// Received an unexpected HTTP response
|
||||
///
|
||||
/// Could be for a variety of reasons.
|
||||
case unexpectedHTTPResponse(status_code: Int, response: Data)
|
||||
/// Error forming the request, generally due to missing or inconsistent internal data
|
||||
///
|
||||
/// Probably caused by a programming error.
|
||||
case errorFormingRequest
|
||||
/// The client could not process the response from the server
|
||||
///
|
||||
/// Might be a sign of an incompatibility bug
|
||||
case errorProcessingResponse
|
||||
/// The action performed is not authorized
|
||||
/// Generally thrown if user does not exist, credentials do not match what Coinos has on file, or programming error
|
||||
case unauthorized
|
||||
/// Client not logged in on a call that expected login
|
||||
case notLoggedIn
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a SHA256 hash digest from a piece of UTF-8 text, and returns the result as a "hex" string
|
||||
///
|
||||
/// When working only with strings, this can be more convenient than transforming text to data, and data back to text
|
||||
fileprivate func sha256Hex(text: String) -> String? {
|
||||
guard let data = text.data(using: .utf8) else { return nil }
|
||||
return sha256(data).toHexString()
|
||||
}
|
||||
@@ -80,9 +80,9 @@ func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) ->
|
||||
}
|
||||
|
||||
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
|
||||
return abbrev_identifier(String(pubkey.npub.dropFirst(4)))
|
||||
return abbrev_pubkey(String(pubkey.npub.dropFirst(4)))
|
||||
}
|
||||
|
||||
func abbrev_identifier(_ pubkey: String, amount: Int = 8) -> String {
|
||||
func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String {
|
||||
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ func remove_nostr_uri_prefix(_ s: String) -> String {
|
||||
return uri
|
||||
}
|
||||
|
||||
func abbreviateURL(_ url: URL, maxLength: Int = MAX_CHAR_URL) -> String {
|
||||
func abbreviateURL(_ url: URL) -> String {
|
||||
let urlString = url.absoluteString
|
||||
|
||||
if urlString.count > maxLength {
|
||||
return String(urlString.prefix(maxLength)) + "…"
|
||||
if urlString.count > MAX_CHAR_URL {
|
||||
return String(urlString.prefix(MAX_CHAR_URL)) + "..."
|
||||
}
|
||||
return urlString
|
||||
}
|
||||
|
||||
@@ -122,7 +122,10 @@ struct LongformPreviewBody: View {
|
||||
} else if blur_images || (blur_images && !state.settings.media_previews) {
|
||||
ZStack {
|
||||
titleImage(url: url)
|
||||
BlurOverlayView(blur_images: $blur_images, artifacts: nil, size: nil, damus_state: nil, parentView: .longFormView)
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ struct Blur: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NoteContentView: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
@@ -167,7 +166,10 @@ struct NoteContentView: View {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) { dismiss in
|
||||
fullscreen_preview(dismiss: dismiss)
|
||||
}
|
||||
BlurOverlayView(blur_images: $blur_images, artifacts: artifacts, size: size, damus_state: damus_state, parentView: .noteContentView)
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,64 +384,6 @@ func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat
|
||||
return height
|
||||
}
|
||||
|
||||
struct BlurOverlayView: View {
|
||||
@Binding var blur_images: Bool
|
||||
let artifacts: NoteArtifactsSeparated?
|
||||
let size: EventViewKind?
|
||||
let damus_state: DamusState?
|
||||
let parentView: ParentViewType
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
Color.black
|
||||
.opacity(0.54)
|
||||
|
||||
Blur()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Image(systemName: "eye.slash")
|
||||
.foregroundStyle(.white)
|
||||
.bold()
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
Text(NSLocalizedString("Media from someone you \n don't follow", comment: "Label on the image blur mask"))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.white)
|
||||
.font(.title2)
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
Button(NSLocalizedString("Tap to load", comment: "Label for button that allows user to dismiss media content warning and unblur the image")) {
|
||||
blur_images = false
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
|
||||
if parentView == .noteContentView,
|
||||
let artifacts = artifacts,
|
||||
let size = size,
|
||||
let damus_state = damus_state
|
||||
{
|
||||
switch artifacts.media[0] {
|
||||
case .image(let url), .video(let url):
|
||||
Text(abbreviateURL(url, maxLength: 30))
|
||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size * 0.8))
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(EdgeInsets(top: 20, leading: 10, bottom: 5, trailing: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
|
||||
enum ParentViewType {
|
||||
case noteContentView, longFormView
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state
|
||||
@@ -457,7 +401,7 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
.previewDisplayName("Super short note")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: true, size: .normal, options: [])
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Note with image")
|
||||
|
||||
@@ -490,3 +434,4 @@ func separate_images(ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? {
|
||||
let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
|
||||
return mediaUrls.isEmpty ? nil : mediaUrls
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ struct PubkeyView: View {
|
||||
let bech32 = pubkey.npub
|
||||
|
||||
HStack {
|
||||
Text(verbatim: "\(abbrev_identifier(bech32, amount: sidemenu ? 12 : 16))")
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: sidemenu ? 12 : 16))")
|
||||
.font(sidemenu ? .system(size: 10) : .footnote)
|
||||
.foregroundColor(keyColor())
|
||||
.padding(5)
|
||||
|
||||
@@ -16,9 +16,7 @@ struct ConnectWalletView: View {
|
||||
@State var error: String? = nil
|
||||
@State var wallet_scan_result: WalletScanResult = .scanning
|
||||
@State var show_introduction: Bool = true
|
||||
@State var show_coinos_options: Bool = false
|
||||
var nav: NavigationCoordinator
|
||||
let userKeypair: Keypair
|
||||
|
||||
var body: some View {
|
||||
MainContent
|
||||
@@ -148,14 +146,9 @@ struct ConnectWalletView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 5) {
|
||||
CoinosButton() {
|
||||
self.show_coinos_options = true
|
||||
}
|
||||
Text("Coinos is a service operated by a third-party. We have no access to your Coinos wallet.", comment: "Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
CoinosButton() {
|
||||
show_introduction = false
|
||||
openURL(URL(string:"https://coinos.io/settings/nostr")!)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -168,110 +161,6 @@ struct ConnectWalletView: View {
|
||||
.padding(2) // Avoids border clipping on the sides
|
||||
)
|
||||
.padding(.top, 20)
|
||||
.sheet(isPresented: $show_coinos_options, content: {
|
||||
CoinosConnectionOptionsSheet
|
||||
})
|
||||
}
|
||||
|
||||
var CoinosConnectionOptionsSheet: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("How would you like to connect to your Coinos wallet?", comment: "Question for the user when connecting a Coinos wallet.")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 10)
|
||||
.lineLimit(2)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 5) {
|
||||
Button(
|
||||
action: { self.oneClickSetup() },
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "wand.and.sparkles")
|
||||
Text("One-click setup", comment: "Button label for users to do a one-click Coinos wallet setup.")
|
||||
}
|
||||
// I have to hide this on npub logins, because otherwise SwiftUI will start truncating text
|
||||
if self.userKeypair.privkey != nil {
|
||||
Text("Also click here if you had a one-click setup before.", comment: "Button description hint for users who may want to do a one-click setup.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.opacity(self.userKeypair.privkey == nil ? 0.5 : 1.0)
|
||||
.disabled(self.userKeypair.privkey == nil)
|
||||
|
||||
if self.userKeypair.privkey == nil {
|
||||
Text("You must be logged in with your nsec to use this option.", comment: "Warning text for users who cannot create a Coinos account via the one-click setup without being logged in with their nsec.")
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Your profile will not be shared with Coinos.", comment: "Label text for users to reassure them that their nsec is not shared with a third party.")
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
action: {
|
||||
show_introduction = false
|
||||
show_coinos_options = false
|
||||
openURL(URL(string:"https://coinos.io/settings/nostr")!)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.right")
|
||||
Text("Connect via the website", comment: "Button label for users who are setting up a Coinos wallet and would like to connect via the website")
|
||||
}
|
||||
Text("Click here if you have a Coinos username and password.", comment: "Button description hint for users who may want to connect via the website.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.presentationDetents([.height(300)])
|
||||
}
|
||||
|
||||
func oneClickSetup() {
|
||||
Task {
|
||||
show_coinos_options = false
|
||||
do {
|
||||
guard let fullKeypair = self.userKeypair.to_full() else {
|
||||
throw CoinosDeterministicAccountClient.ClientError.errorFormingRequest
|
||||
}
|
||||
let client = CoinosDeterministicAccountClient(userKeypair: fullKeypair)
|
||||
try await client.loginOrRegister()
|
||||
let nwcURL = try await client.createNWCConnection()
|
||||
model.connect(nwcURL) // Connect directly, to make it a true one-click setup
|
||||
}
|
||||
catch {
|
||||
present_sheet(.error(.init(
|
||||
user_visible_description: NSLocalizedString("Something went wrong when performing the one-click Coinos wallet setup.", comment: "Error label when user tries the one-click Coinos wallet setup but fails for some generic reason."),
|
||||
tip: NSLocalizedString("Check your internet connection and try again. If the error persists, contact support.", comment: "Error tip when user tries to create the one-click Coinos wallet setup but fails for a generic reason."),
|
||||
technical_info: error.localizedDescription
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ManualSetup: some View {
|
||||
@@ -381,7 +270,7 @@ struct ConnectWalletView: View {
|
||||
|
||||
struct ConnectWalletView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init(), userKeypair: test_keypair)
|
||||
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init())
|
||||
.previewDisplayName("Main Wallet Connect View")
|
||||
ConnectWalletView.AreYouSure(nwc: get_test_nwc(), show_introduction: .constant(false), model: WalletModel(settings: test_damus_state.settings))
|
||||
.previewDisplayName("Are you sure screen")
|
||||
|
||||
@@ -14,8 +14,6 @@ struct NWCSettings: View {
|
||||
@ObservedObject var model: WalletModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
func donation_binding() -> Binding<Double> {
|
||||
return Binding(get: {
|
||||
@@ -138,7 +136,6 @@ struct NWCSettings: View {
|
||||
|
||||
Button(action: {
|
||||
self.model.disconnect()
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
let WALLET_WARNING_THRESHOLD: UInt64 = 100000
|
||||
|
||||
struct WalletView: View {
|
||||
let damus_state: DamusState
|
||||
@State var show_settings: Bool = false
|
||||
@@ -24,27 +22,6 @@ struct WalletView: View {
|
||||
func MainWalletView(nwc: WalletConnectURL) -> some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 35) {
|
||||
if let balance = model.balance, balance > WALLET_WARNING_THRESHOLD {
|
||||
VStack(spacing: 10) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
Text("Safety Reminder", comment: "Heading for a safety reminder that appears when the user has too many funds, recommending them to learn about safeguarding their funds.")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
}
|
||||
.foregroundStyle(.damusWarningTertiary)
|
||||
|
||||
Text("If your wallet balance is getting high, it's important to understand how to keep your funds secure. Please consider learning the best practices to ensure your assets remain safe. [Click here](https://damus.io/docs/wallet/high-balance-safety-reminder/) to learn more.", comment: "Text reminding the user has a high balance, recommending them to learn about self-custody")
|
||||
.foregroundStyle(.damusWarningSecondary)
|
||||
.opacity(0.8)
|
||||
}
|
||||
.padding()
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(.damusWarningBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
VStack(spacing: 5) {
|
||||
|
||||
BalanceView(balance: model.balance)
|
||||
@@ -62,9 +39,9 @@ struct WalletView: View {
|
||||
var body: some View {
|
||||
switch model.connect_state {
|
||||
case .new:
|
||||
ConnectWalletView(model: model, nav: damus_state.nav, userKeypair: self.damus_state.keypair)
|
||||
ConnectWalletView(model: model, nav: damus_state.nav)
|
||||
case .none:
|
||||
ConnectWalletView(model: model, nav: damus_state.nav, userKeypair: self.damus_state.keypair)
|
||||
ConnectWalletView(model: model, nav: damus_state.nav)
|
||||
case .existing(let nwc):
|
||||
MainWalletView(nwc: nwc)
|
||||
.toolbar {
|
||||
|
||||
@@ -10,292 +10,28 @@ import SwiftUI
|
||||
@testable import damus
|
||||
|
||||
class NoteContentViewTests: XCTestCase {
|
||||
func testRenderBlocksWithNonLatinHashtags() throws {
|
||||
func testRenderBlocksWithNonLatinHashtags() {
|
||||
let content = "Damusはかっこいいです #cool #かっこいい"
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair, tags: [["t", "かっこいい"]]))
|
||||
let note = NostrEvent(content: content, keypair: test_keypair, tags: [["t", "かっこいい"]])!
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let text: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let text: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
|
||||
let attributedText: AttributedString = text.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
print(runArray.description)
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:t:cool", "Latin-character hashtag is missing. Runs description :\(runArray.description)")
|
||||
XCTAssertEqual(runArray[3].link?.absoluteString.removingPercentEncoding, "damus:t:かっこいい", "Non-latin-character hashtag is missing. Runs description :\(runArray.description)")
|
||||
XCTAssertEqual(runArray[3].link?.absoluteString.removingPercentEncoding!, "damus:t:かっこいい", "Non-latin-character hashtag is missing. Runs description :\(runArray.description)")
|
||||
}
|
||||
|
||||
func testRenderBlocksWithLeadingAndTrailingWhitespacesTrimmed() throws {
|
||||
let content = " \n\n Hello, \nworld! \n\n "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
let text = attributedText.description
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
|
||||
XCTAssertEqual(runArray.count, 1)
|
||||
XCTAssertTrue(text.contains("Hello, \nworld!"))
|
||||
XCTAssertFalse(text.contains(content))
|
||||
}
|
||||
|
||||
func testRenderBlocksWithMediaBlockInMiddleRendered() throws {
|
||||
let content = " Check this out: https://damus.io/image.png Isn't this cool? "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 3)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("https://damus.io/image.png "))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "https://damus.io/image.png")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Isn't this cool?"))
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/image.png")
|
||||
}
|
||||
|
||||
func testRenderBlocksWithInvoiceInMiddleAbbreviated() throws {
|
||||
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
|
||||
let content = " Donations appreciated: \(invoiceString) Pura Vida "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 3)
|
||||
XCTAssertTrue(runArray[0].description.contains("Donations appreciated: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("lnbc100n:qpsql29r"))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:lightning:\(invoiceString)")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
|
||||
}
|
||||
|
||||
func testRenderBlocksWithNoteIdInMiddleAreRendered() throws {
|
||||
let noteId = test_note.id.bech32
|
||||
let content = " Check this out: nostr:\(noteId) Pura Vida "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 3)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("note1qqq:qqn2l0z3"))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:nostr:\(noteId)")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
|
||||
}
|
||||
|
||||
func testRenderBlocksWithNeventInMiddleAreRendered() throws {
|
||||
let nevent = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
|
||||
let content = " Check this out: nostr:\(nevent) Pura Vida "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 3)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("nevent1q:t5nxnepm"))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:nostr:\(nevent)")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
|
||||
}
|
||||
|
||||
func testRenderBlocksWithPreviewableBlocksAtEndAreHidden() throws {
|
||||
let noteId = test_note.id.bech32
|
||||
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
|
||||
let content = " Check this out. \nhttps://hidden.tld/\nhttps://damus.io/hidden1.png\n\(invoiceString)\nhttps://damus.io/hidden2.png\nnostr:\(noteId) "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 1)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out."))
|
||||
XCTAssertFalse(runArray[0].description.contains("https://hidden.tld/"))
|
||||
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden1.png"))
|
||||
XCTAssertFalse(runArray[0].description.contains("lnbc100n:qpsql29r"))
|
||||
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden2.png"))
|
||||
XCTAssertFalse(runArray[0].description.contains("note1qqq:qqn2l0z3"))
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/hidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/hidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/hidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/hidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.links.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.links[0].absoluteString, "https://hidden.tld/")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
|
||||
}
|
||||
|
||||
func testRenderBlocksWithMultipleLinksAtEndAreNotHidden() throws {
|
||||
let noteId = test_note.id.bech32
|
||||
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
|
||||
let content = " Check this out. \nhttps://nothidden1.tld/\nhttps://nothidden2.tld/\nhttps://damus.io/nothidden1.png\n\(invoiceString)\nhttps://damus.io/nothidden2.png\nnostr:\(noteId) "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 12)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out."))
|
||||
XCTAssertTrue(runArray[1].description.contains("https://nothidden1.tld/"))
|
||||
XCTAssertTrue(runArray[3].description.contains("https://nothidden2.tld/"))
|
||||
XCTAssertTrue(runArray[5].description.contains("https://damus.io/nothidden1.png"))
|
||||
XCTAssertTrue(runArray[7].description.contains("lnbc100n:qpsql29r"))
|
||||
XCTAssertTrue(runArray[9].description.contains("https://damus.io/nothidden2.png"))
|
||||
XCTAssertTrue(runArray[11].description.contains("note1qqq:qqn2l0z3"))
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/nothidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/nothidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.links.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.links[0].absoluteString, "https://nothidden1.tld/")
|
||||
XCTAssertEqual(noteArtifactsSeparated.links[1].absoluteString, "https://nothidden2.tld/")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
|
||||
}
|
||||
|
||||
func testRenderBlocksWithMultipleEventsAtEndAreNotHidden() throws {
|
||||
let noteId = test_note.id.bech32
|
||||
let nevent = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
|
||||
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
|
||||
let content = " Check this out. \nnostr:\(noteId)\nnostr:\(nevent)\nhttps://damus.io/nothidden1.png\n\(invoiceString)\nhttps://damus.io/nothidden2.png "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 10)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out."))
|
||||
XCTAssertTrue(runArray[1].description.contains("note1qqq:qqn2l0z3"))
|
||||
XCTAssertTrue(runArray[3].description.contains("nevent1q:t5nxnepm"))
|
||||
XCTAssertTrue(runArray[5].description.contains("https://damus.io/nothidden1.png"))
|
||||
XCTAssertTrue(runArray[7].description.contains("lnbc100n:qpsql29r"))
|
||||
XCTAssertTrue(runArray[9].description.contains("https://damus.io/nothidden2.png"))
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/nothidden1.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/nothidden2.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.links.count, 0)
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
|
||||
}
|
||||
|
||||
func testRenderBlocksWithPreviewableBlocksAtEndAreNotHiddenWhenMediaBlockPrecedesThem() throws {
|
||||
let content = " Check this out: https://damus.io/image.png Isn't this cool? \nhttps://damus.io/nothidden.png "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 4)
|
||||
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("https://damus.io/image.png "))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "https://damus.io/image.png")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Isn't this cool?"))
|
||||
XCTAssertTrue(runArray[3].description.contains("https://damus.io/nothidden.png"))
|
||||
XCTAssertEqual(runArray[3].link?.absoluteString, "https://damus.io/nothidden.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/image.png")
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden.png")
|
||||
}
|
||||
|
||||
func testRenderBlocksWithPreviewableBlocksAtEndAreNotHiddenWhenInvoicePrecedesThem() throws {
|
||||
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
|
||||
let content = " Donations appreciated: \(invoiceString) Pura Vida \nhttps://damus.io/nothidden.png "
|
||||
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
|
||||
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
|
||||
|
||||
let testState = test_damus_state
|
||||
|
||||
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
|
||||
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
|
||||
|
||||
let runs: AttributedString.Runs = attributedText.runs
|
||||
let runArray: [AttributedString.Runs.Run] = Array(runs)
|
||||
XCTAssertEqual(runArray.count, 4)
|
||||
XCTAssertTrue(runArray[0].description.contains("Donations appreciated: "))
|
||||
XCTAssertTrue(runArray[1].description.contains("lnbc100n:qpsql29r"))
|
||||
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:lightning:\(invoiceString)")
|
||||
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
|
||||
XCTAssertTrue(runArray[3].description.contains("https://damus.io/nothidden.png"))
|
||||
XCTAssertEqual(runArray[3].link?.absoluteString, "https://damus.io/nothidden.png")
|
||||
|
||||
XCTAssertEqual(noteArtifactsSeparated.images.count, 1)
|
||||
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden.png")
|
||||
}
|
||||
|
||||
|
||||
/// Based on https://github.com/damus-io/damus/issues/1468
|
||||
/// Tests whether a note content view correctly parses an image block when url in JSON content contains optional escaped slashes
|
||||
func testParseImageBlockInContentWithEscapedSlashes() throws {
|
||||
func testParseImageBlockInContentWithEscapedSlashes() {
|
||||
let testJSONWithEscapedSlashes = "{\"tags\":[],\"pubkey\":\"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9\",\"content\":\"https:\\/\\/cdn.nostr.build\\/i\\/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg\",\"created_at\":1691864981,\"kind\":1,\"sig\":\"fc0033aa3d4df50b692a5b346fa816fdded698de2045e36e0642a021391468c44ca69c2471adc7e92088131872d4aaa1e90ea6e1ad97f3cc748f4aed96dfae18\",\"id\":\"e8f6eca3b161abba034dac9a02bb6930ecde9fd2fb5d6c5f22a05526e11382cb\"}"
|
||||
let testNote = try XCTUnwrap(NostrEvent.owned_from_json(json: testJSONWithEscapedSlashes))
|
||||
let testNote = NostrEvent.owned_from_json(json: testJSONWithEscapedSlashes)!
|
||||
let parsed = parse_note_content(content: .init(note: testNote, keypair: test_keypair))
|
||||
|
||||
XCTAssertTrue((parsed.blocks[0].asURL != nil), "NoteContentView does not correctly parse an image block when url in JSON content contains optional escaped slashes.")
|
||||
@@ -333,9 +69,9 @@ class NoteContentViewTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testMentionStr_Note_ContainsFullBech32() {
|
||||
let compatibleText = createCompatibleText(test_note.id.bech32)
|
||||
let compatableText = createCompatibleText(test_note.id.bech32)
|
||||
|
||||
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: test_note.id.bech32)
|
||||
assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_note.id.bech32)
|
||||
}
|
||||
|
||||
func testMentionStr_Nevent_ContainsAbbreviated() {
|
||||
|
||||
@@ -36,9 +36,10 @@ class damusTests: XCTestCase {
|
||||
XCTAssertEqual(bytes.count, 32)
|
||||
}
|
||||
|
||||
func testTrimSuffix() {
|
||||
func testTrimmingFunctions() {
|
||||
let txt = " bobs "
|
||||
|
||||
XCTAssertEqual(trim_prefix(txt), "bobs ")
|
||||
XCTAssertEqual(trim_suffix(txt), " bobs")
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,6 @@ struct ShareExtensionView: View {
|
||||
return
|
||||
}
|
||||
self.state = DamusState(keypair: keypair)
|
||||
self.state?.nostrNetwork.connect()
|
||||
})
|
||||
.onChange(of: self.highlighter_state) {
|
||||
if case .cancelled = highlighter_state {
|
||||
|
||||
@@ -250,7 +250,6 @@ struct ShareExtensionView: View {
|
||||
return false
|
||||
}
|
||||
state = DamusState(keypair: keypair)
|
||||
state?.nostrNetwork.connect()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user