Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6858d7e72f
|
|||
| 476f52562a | |||
| f591ad2dff | |||
| dacade299d | |||
| cdacbcfdca | |||
| 41e036cff2 | |||
| eb901a4d84 | |||
| 9f15688699 | |||
| 6d055be3cd | |||
| 01c6e3e9ab | |||
| 4377cf28ef | |||
| 6f67c159ff | |||
| 7a85ae29ca | |||
| fafe3b4b3e | |||
| 69c7acea76 | |||
| 22d635d850 | |||
| fc9b9f2940 | |||
| 622a436589 | |||
| 9398877415 | |||
| 129d3ff101 | |||
| bb4fd75576 | |||
| 8586eed635 | |||
| 440e37c1d3 | |||
| 49283f2bb2 | |||
| 305ee03b0e | |||
| a88f5db10b | |||
| d39a3da3b7 | |||
| 40459e247e | |||
| fff4549933 | |||
| c4dfae9ede | |||
| bfda0d1b74 | |||
| 01b8e43a6e | |||
| aa4ecc2139 | |||
| 617dee3e6b | |||
| 510432bb98 | |||
| c4a9f2fdb2 | |||
| b1e0a62109 | |||
| 1fc5ceff3b | |||
| 16edc3fe13 | |||
| 6a88ca2777 | |||
| e3ccf95780 | |||
| 9bac83352b | |||
| 0803594553 | |||
| 8dad8e6703 | |||
| e30d38e69f | |||
| c13f29e98c | |||
| 5b901656f3 | |||
| 36acdf420e | |||
| 76a6dbc406 | |||
| 1b1d4bd6d1 | |||
| 14586b616c | |||
| 7baf7e66dc | |||
| 4263b9690f | |||
| 7f6a702412 | |||
| 6f35de65f9 | |||
| 65f3651896 | |||
| 94ce604b9d | |||
| b934d66f64 | |||
| 20b6627799 | |||
| 3e15f15a57 | |||
| 5c87b8e610 | |||
| 42234b1cf3 | |||
| 54ba64535d | |||
| 9cf53a9e93 | |||
| 3569da5687 | |||
| f1f3abfb98 | |||
| dec07df2c1 | |||
| 53734ea483 | |||
| b18a0c573e | |||
| f6f7d13f12 | |||
| 6ee0be40e9 | |||
| a64f898df7 | |||
| dd29e87146 | |||
| c71b0ee916 | |||
| 8e92e28faf | |||
| 5657512370 | |||
| 882f6e2534 | |||
| 2f60888fb1 | |||
| ba6792640d | |||
| 984c7b6932 | |||
| 0bbc2c6348 | |||
| c44c0d0863 | |||
| 50d55572be | |||
| caffa0398b | |||
| 92bbc9766d | |||
| 699f77d9e1 | |||
| 4c0166bd31 | |||
| 35b67dc08d | |||
| 1f5f1e28a4 | |||
| f30f93f65c | |||
| 7255481705 | |||
| 16fa701509 | |||
| 2c6999e15c | |||
| 981d500c25 | |||
| d02fc9142d | |||
| db59f74970 | |||
| bf3ca4a186 | |||
| 53c2b3a48d | |||
| 23a8d6fb6b | |||
| 042b7da315 | |||
| e62ba5826b | |||
| 1d11bb40b5 | |||
| 0338297bfe | |||
| 59cf8056bd | |||
| d34d417fcc | |||
| b665a40a11 | |||
| 5caa4a6e97 | |||
| c5d8e4a4a1 | |||
| 8b600a9774 | |||
| 286ae68fd6 | |||
| 6ab893a617 | |||
| 9bfb59c4cc | |||
| dcb94635ea | |||
| c464a26151 | |||
| 9104ddb051 | |||
| 1432087edf | |||
| ae2f7255a7 | |||
| d5b944170f | |||
| 9fb1cc5b57 | |||
| 2e512317e7 | |||
| f9eb669132 | |||
| 066b3cdde8 | |||
| 7f313dcbd4 | |||
| 1dabd88355 | |||
| 4f33641244 | |||
| 006a6ef16f | |||
| 7467a9d5b1 | |||
| 9f01cab2be | |||
| 502917012c | |||
| 916f7d789e | |||
| 21eda288c4 | |||
| 25e022d933 | |||
| e642913944 | |||
| 967785392f | |||
| 9e6fbeefcd | |||
| de58e52199 | |||
| 53e9269da6 | |||
| 85930df8e3 | |||
| cf3a9a576d | |||
| e397fc069b | |||
| 2529797dfb | |||
| bd2193251f | |||
| 1a2ac976a3 | |||
| d4faacb99f | |||
| a73271e3d4 | |||
| 624a7b4e88 | |||
| 5b9803d234 | |||
| 3098d4b4fa | |||
| 0178478199 | |||
| d489bcc586 | |||
| 453d540255 | |||
| 5ded564bdc | |||
| 3908192fe2 | |||
| 92020e551b | |||
| ccd52a09d8 | |||
| ced3c76996 | |||
| 29bba15230 | |||
| fb179ac1d4 | |||
| 7900865c02 | |||
| 0350809e82 | |||
| cddb88b890 | |||
| 14ba33674b | |||
| c0f4e3fe03 | |||
| dae2e8ef56 | |||
| b2d2fbee0d | |||
| cebd1f48ca | |||
| 55bbe8f855 | |||
| 39dce64131 | |||
| b556257edd | |||
| cdc4a7b7a4 | |||
| ef5a3030a6 | |||
| f0b8dcc5e9 | |||
| 72b60573de | |||
| 6e6c1eb7b6 | |||
| 07dfa3b1fb | |||
| 88306d00a3 | |||
| 616de2eebc | |||
| 709aab549b | |||
| 15ab9f7135 | |||
| d4aa8a5602 | |||
| a9b4cfd424 | |||
| 2b99f94d13 | |||
| 66e204eb91 | |||
| 7040235605 | |||
| f9d21ef901 | |||
| a08d0a5a19 | |||
| ff20cc4767 | |||
| aacb336002 | |||
| b40c595a7c | |||
| 80063af19a | |||
| df3b94a1fc | |||
| 06a66a3709 | |||
| 1463ce5e3a | |||
| 480921db20 | |||
| f0de8721c7 | |||
| d11cd76e6a | |||
| 815f4d4a96 | |||
| 4fecf72963 | |||
| 593d0e2abe | |||
| 2f8aa29e92 | |||
| e3c04465fc | |||
| 54d40f7ffd | |||
| 2053033b25 | |||
| 45801f3e6c | |||
| 2d44f2744b | |||
| 04e408bfea | |||
| b3c87bdc07 | |||
| b5dd90b36a | |||
| 6fa9149939 | |||
| 1e9e4a7f3a | |||
| c8e236b6d5 | |||
| e8d0f1db8d | |||
| 99b5dc94cb | |||
| e34351ca37 | |||
| 1a33d639ed | |||
| 5c1043b4e5 | |||
| 23b5763a6b | |||
| dd65209a20 | |||
| f0d07c3663 | |||
| b3119fa41e | |||
| 7ec8da6c73 | |||
| 9e659c49b5 | |||
| c72666b352 | |||
| 1854e10486 | |||
| 58e2fb40ef | |||
| af7ea7024f | |||
| 0263c11a94 | |||
| 6d43754e71 | |||
| 4da23390f8 | |||
| c74993366b | |||
| ad0e1f28b7 | |||
| 61051ee853 | |||
| dc7826c4e5 | |||
| 4eee715bcd | |||
| 08bea16be0 | |||
| 8f04b12a90 | |||
| 9cfed9f3aa | |||
| 123ca3b802 | |||
| 5e7b1f4ff3 | |||
| 12594e35c1 | |||
| ab92f7b561 | |||
| 11b9062865 | |||
| 5c5b55bf67 | |||
| dd6c082a8e | |||
| 2a4ee6c48c | |||
| fa520d48d3 | |||
| 160b293359 | |||
| 7d17b9b476 | |||
| d04f1c6867 | |||
| 5c87dd5bbb | |||
| 12febf9671 | |||
| 4033ad66ba | |||
| 2c0296cce3 | |||
| 080aaf2d1b | |||
| 0e55b08b6c | |||
| ff70cb7ebf | |||
| fe82134a75 | |||
| 60a0c21272 | |||
| 8242ca27d2 | |||
| c7baa153af | |||
| ff654c4e11 | |||
| deaf5f042a | |||
| 4f56ff3dfb | |||
| fd59407171 | |||
| 9b759247ee | |||
| cd7998b69d | |||
| bd4c29604f | |||
| bf1175f22c | |||
| 064888f78d | |||
| fc640b85ed | |||
| d5766253cf | |||
| 571ed39d52 | |||
| 16d81ed40f | |||
| 1135c19fea | |||
| 77331644cb | |||
| 8d14fdffb5 | |||
| 0c95071de7 | |||
| da78a217a3 | |||
| f53b824122 | |||
| 45ab394b09 | |||
| 47e7505573 | |||
| 0f1390f412 | |||
| 6bf5293701 | |||
| 3d6909bf62 | |||
| ecd8b64b8b | |||
| 0c627ae0a0 | |||
| 16c86c1d1c |
@@ -1,6 +1,8 @@
|
||||
xcuserdata
|
||||
/.direnv
|
||||
damus/TestingPrivate.swift
|
||||
damus.xcodeproj/xcshareddata/xcbaselines
|
||||
.DS_Store
|
||||
TODO.bak
|
||||
tags
|
||||
build-git-hash.txt
|
||||
|
||||
@@ -3,3 +3,4 @@ Ben Weeks <ben.weeks@knowall.ai> <ben.weeks@outlook.com>
|
||||
Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github.com>
|
||||
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
||||
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
||||
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
|
||||
|
||||
+184
@@ -1,3 +1,186 @@
|
||||
## [1.6-18] - 2023-09-21
|
||||
|
||||
### Added
|
||||
|
||||
- Add followed hashtags to your following list (Daniel D’Aquino)
|
||||
- Add "Do not show #nsfw tagged posts" setting (Daniel D’Aquino)
|
||||
- Hold tap to preview status URL (Jericho Hasselbush)
|
||||
- Finnish translations (etrikaj)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to nostrdb for @'s and user search (William Casarin)
|
||||
- Use nostrdb for profiles (William Casarin)
|
||||
- Updated relay view (ericholguin)
|
||||
- Increase size of the hitbox on note ellipsis button (Daniel D’Aquino)
|
||||
- Make carousel tab dots tappable (Bryan Montz)
|
||||
- Move the "Follow you" badge into the profile header (Grimless)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix text composer wrapping issue when mentioning npub (Daniel D’Aquino)
|
||||
- Make blurred videos viewable by allowing blur to disappear once tapped (Daniel D’Aquino)
|
||||
- Fix parsing issue with NIP-47 compliant NWC urls without double-slashes (Daniel D’Aquino)
|
||||
- Fix padding of username next to pfp on some views (William Casarin)
|
||||
- Fixes issue where username with multiple emojis would place cursor in strange position. (Jericho Hasselbush)
|
||||
- Fixed audio in video playing twice (Bryan Montz)
|
||||
- Fix crash when long pressing custom reactions (William Casarin)
|
||||
- Fix random crashom due to old profile database (William Casarin)
|
||||
|
||||
[1.6-18]: https://github.com/damus-io/damus/releases/tag/v1.6-18
|
||||
|
||||
## [1.6-17] - 2023-08-23
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for status URLs (William Casarin)
|
||||
- Click music statuses to display in spotify (William Casarin)
|
||||
- Add settings for disabling user statuses (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- clear statuses if they only contain whitespace (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix long status lines (William Casarin)
|
||||
- Fix status events not expiring locally (William Casarin)
|
||||
|
||||
[1.6-17]: https://github.com/damus-io/damus/releases/tag/v1.6-17
|
||||
|
||||
## [1.6-16] - 2023-08-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added live music statuses (William Casarin)
|
||||
- Added generic user statuses (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Avoid notification for zaps from muted profiles (tappu75e@duck.com)
|
||||
- Fix text editing issues on characters added right after mention link (Daniel D’Aquino)
|
||||
- Mute hellthreads everywhere (William Casarin)
|
||||
|
||||
|
||||
[1.6-16]: https://github.com/damus-io/damus/releases/tag/v1.6-16
|
||||
|
||||
## [1.6-13] - 2023-08-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bug where it would sometimes show -1 in replies (tappu75e@duck.com)
|
||||
- Fix images and links occasionally appearing with escaped slashes (Daniel D‘Aquino)
|
||||
- Fixed nostrscript not working on smaller phones (William Casarin)
|
||||
- Fix zaps sometimes not appearing (William Casarin)
|
||||
- Fixed issue where reposts would sometimes repost the wrong thing (William Casarin)
|
||||
- Fixed issue where sometimes there would be empty entries on your profile (William Casarin)
|
||||
|
||||
[1.6-13]: https://github.com/damus-io/damus/releases/tag/v1.6-13
|
||||
|
||||
|
||||
## [1.6-11]: "Bugfix Sunday" - 2023-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add close button to custom reactions (Suhail Saqan)
|
||||
- Add ability to change order of custom reactions (Suhail Saqan)
|
||||
- Adjustable font size (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Show renotes in Notes timeline (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure the person you're replying to is the first entry in the reply description (William Casarin)
|
||||
- Don't cutoff text in notifications (William Casarin)
|
||||
- Fix wikipedia url detection with parenthesis (William Casarin)
|
||||
- Fixed old notifications always appearing on first start (William Casarin)
|
||||
- Fix issue with slashes on relay urls causing relay connection problems (William Casarin)
|
||||
- Fix rare crash triggered by local notifications (William Casarin)
|
||||
- Fix crash when long-pressing reactions (William Casarin)
|
||||
- Fixed nostr reporting decoding (William Casarin)
|
||||
- Dismiss qr screen on scan (Suhail Saqan)
|
||||
- Show QRCameraView regardless of same user (Suhail Saqan)
|
||||
- Fix wiggle when long press reactions (Suhail Saqan)
|
||||
- Fix reaction button breaking scrolling (Suhail Saqan)
|
||||
- Fix crash when muting threads (Bryan Montz)
|
||||
|
||||
|
||||
[1.6-11]: https://github.com/damus-io/damus/releases/tag/v1.6-11
|
||||
|
||||
## [1.6-8]: "nostrdb prep" 2023-08-03
|
||||
|
||||
### Added
|
||||
|
||||
- Suggested Users to Follow (Joel Klabo)
|
||||
- Add support for multiple reactions (Suhail Saqan)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved memory usage and performance when processing events (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed disappearing text on iOS17 (cr0bar)
|
||||
- Fix UTF support for hashtags (Daniel D‘Aquino)
|
||||
- Fix compilation error on test target in UserSearchCacheTests (Daniel D‘Aquino)
|
||||
- Fix nav crashing and buggyness (William Casarin)
|
||||
- Allow relay logs to be opened in dev mode even if relay (Daniel D'Aquino)
|
||||
- endless connection attempt loop after user removes relay (Bryan Montz)
|
||||
|
||||
|
||||
[1.6-8]: https://github.com/damus-io/damus/releases/tag/v1.6-8
|
||||
|
||||
## 1.6 (7): "Less bad" - 2023-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Show nostr address username and support abbreviated _ usernames (William Casarin)
|
||||
- Re-add nip05 badges to profiles (William Casarin)
|
||||
- Add space when tagging users in posts if needed (William Casarin)
|
||||
- Added padding under word count on longform account (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't spam lnurls when validating zaps (William Casarin)
|
||||
- Eliminate nostr address validation bandwidth on startup (William Casarin)
|
||||
- Allow user to login to deleted profile (William Casarin)
|
||||
- Fix issue where typing cc@bob would produce brokenb ccnostr:bob mention (William Casarin)
|
||||
|
||||
|
||||
|
||||
[1.6-7]: https://github.com/damus-io/damus/releases/tag/v1.6-7
|
||||
|
||||
## [1.6-6] - 2023-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- New markdown renderer (William Casarin)
|
||||
- Added feedback when user adds a relay that is already on the list (Daniel D'Aquino)
|
||||
|
||||
### Changed
|
||||
|
||||
- Hide nsec when logging in (cr0bar)
|
||||
- Remove nip05 on events (William Casarin)
|
||||
- Rename NIP05 to "nostr address" (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where hashtags were leaking in DMs (William Casarin)
|
||||
- Fix issue with emojis next to hashtags and urls (William Casarin)
|
||||
- relay detail view is not immediately available after adding new relay (Bryan Montz)
|
||||
- Fix nostr:nostr:... bugs (William Casarin)
|
||||
|
||||
|
||||
[1.6-6]: https://github.com/damus-io/damus/releases/tag/v1.6-6
|
||||
|
||||
## [1.6-4] - 2023-07-13
|
||||
|
||||
### Added
|
||||
@@ -1376,3 +1559,4 @@
|
||||
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
@@ -4,5 +4,10 @@ all: nostrscript/primal.wasm
|
||||
nostrscript/%.wasm: nostrscript/%.ts nostrscript/nostr.ts Makefile
|
||||
asc $< --runtime stub --outFile $@ --optimize
|
||||
|
||||
tags:
|
||||
find damus-c -name '*.c' -or -name '*.h' | xargs ctags
|
||||
|
||||
clean:
|
||||
rm nostrscript/*.wasm
|
||||
|
||||
.PHONY: tags
|
||||
|
||||
+181
-29
@@ -1,6 +1,6 @@
|
||||
|
||||
#ifndef PROTOVERSE_CURSOR_H
|
||||
#define PROTOVERSE_CURSOR_H
|
||||
#ifndef JB55_CURSOR_H
|
||||
#define JB55_CURSOR_H
|
||||
|
||||
#include "typedefs.h"
|
||||
#include "varint.h"
|
||||
@@ -107,6 +107,36 @@ static inline int pull_byte(struct cursor *cursor, u8 *c)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int parse_byte(struct cursor *cursor, u8 *c)
|
||||
{
|
||||
if (unlikely(cursor->p >= cursor->end))
|
||||
return 0;
|
||||
|
||||
*c = *cursor->p;
|
||||
//cursor->p++;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
static inline int cursor_pull_c_str(struct cursor *cursor, const char **str)
|
||||
{
|
||||
*str = (const char*)cursor->p;
|
||||
@@ -303,6 +333,10 @@ static inline int cursor_pull_int(struct cursor *cursor, int *i)
|
||||
return cursor_pull(cursor, (u8*)i, sizeof(*i));
|
||||
}
|
||||
|
||||
static inline int cursor_push_u32(struct cursor *cursor, uint32_t i) {
|
||||
return cursor_push(cursor, (unsigned char*)&i, sizeof(i));
|
||||
}
|
||||
|
||||
static inline int cursor_push_u16(struct cursor *cursor, u16 i)
|
||||
{
|
||||
return cursor_push(cursor, (u8*)&i, sizeof(i));
|
||||
@@ -431,28 +465,165 @@ static inline int is_whitespace(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
static inline int is_boundary(char c) {
|
||||
return is_whitespace(c) || ispunct(c);
|
||||
static inline int is_underscore(char c) {
|
||||
return c == '_';
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
static inline int is_utf8_byte(u8 c) {
|
||||
return c & 0x80;
|
||||
}
|
||||
|
||||
static inline int parse_utf8_char(struct cursor *cursor, unsigned int *code_point, unsigned int *utf8_length)
|
||||
{
|
||||
u8 first_byte;
|
||||
if (!parse_byte(cursor, &first_byte))
|
||||
return 0; // Not enough data
|
||||
|
||||
// Determine the number of bytes in this UTF-8 character
|
||||
int remaining_bytes = 0;
|
||||
if (first_byte < 0x80) {
|
||||
*code_point = first_byte;
|
||||
return 1;
|
||||
} else if ((first_byte & 0xE0) == 0xC0) {
|
||||
remaining_bytes = 1;
|
||||
*utf8_length = remaining_bytes + 1;
|
||||
*code_point = first_byte & 0x1F;
|
||||
} else if ((first_byte & 0xF0) == 0xE0) {
|
||||
remaining_bytes = 2;
|
||||
*utf8_length = remaining_bytes + 1;
|
||||
*code_point = first_byte & 0x0F;
|
||||
} else if ((first_byte & 0xF8) == 0xF0) {
|
||||
remaining_bytes = 3;
|
||||
*utf8_length = remaining_bytes + 1;
|
||||
*code_point = first_byte & 0x07;
|
||||
} else {
|
||||
remaining_bytes = 0;
|
||||
*utf8_length = 1; // Assume 1 byte length for unrecognized UTF-8 characters
|
||||
// TODO: We need to gracefully handle unrecognized UTF-8 characters
|
||||
printf("Invalid UTF-8 byte: %x\n", *code_point);
|
||||
*code_point = ((first_byte & 0xF0) << 6); // Prevent testing as punctuation
|
||||
return 0; // Invalid first byte
|
||||
}
|
||||
|
||||
// Peek at remaining bytes
|
||||
for (int i = 0; i < remaining_bytes; ++i) {
|
||||
signed char next_byte;
|
||||
if ((next_byte = peek_char(cursor, i+1)) == -1) {
|
||||
*utf8_length = 1;
|
||||
return 0; // Not enough data
|
||||
}
|
||||
|
||||
// Debugging lines
|
||||
//printf("Cursor: %s\n", cursor->p);
|
||||
//printf("Codepoint: %x\n", *code_point);
|
||||
//printf("Codepoint <<6: %x\n", ((*code_point << 6) | (next_byte & 0x3F)));
|
||||
//printf("Remaining bytes: %x\n", remaining_bytes);
|
||||
//printf("First byte: %x\n", first_byte);
|
||||
//printf("Next byte: %x\n", next_byte);
|
||||
//printf("Bitwise AND result: %x\n", (next_byte & 0xC0));
|
||||
|
||||
if ((next_byte & 0xC0) != 0x80) {
|
||||
*utf8_length = 1;
|
||||
return 0; // Invalid byte in sequence
|
||||
}
|
||||
|
||||
*code_point = (*code_point << 6) | (next_byte & 0x3F);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given Unicode code point is a punctuation character
|
||||
*
|
||||
* @param codepoint The Unicode code point to check. @return true if the
|
||||
* code point is a punctuation character, false otherwise.
|
||||
*/
|
||||
static inline int is_punctuation(unsigned int codepoint) {
|
||||
|
||||
// Check for underscore (underscore is not treated as punctuation)
|
||||
if (is_underscore(codepoint))
|
||||
return 0;
|
||||
|
||||
// Check for ASCII punctuation
|
||||
if (ispunct(codepoint))
|
||||
return 1;
|
||||
|
||||
// Check for Unicode punctuation exceptions (punctuation allowed in hashtags)
|
||||
if (codepoint == 0x301C || codepoint == 0xFF5E) // Japanese Wave Dash / Tilde
|
||||
return 0;
|
||||
|
||||
// Check for Unicode punctuation
|
||||
// NOTE: We may need to adjust the codepoint ranges in the future,
|
||||
// to include/exclude certain types of Unicode characters in hashtags.
|
||||
// Unicode Blocks Reference: https://www.compart.com/en/unicode/block
|
||||
return (
|
||||
// Latin-1 Supplement No-Break Space (NBSP): U+00A0
|
||||
(codepoint == 0x00A0) ||
|
||||
|
||||
// Latin-1 Supplement Punctuation: U+00A1 to U+00BF
|
||||
(codepoint >= 0x00A1 && codepoint <= 0x00BF) ||
|
||||
|
||||
// General Punctuation: U+2000 to U+206F
|
||||
(codepoint >= 0x2000 && codepoint <= 0x206F) ||
|
||||
|
||||
// Currency Symbols: U+20A0 to U+20CF
|
||||
(codepoint >= 0x20A0 && codepoint <= 0x20CF) ||
|
||||
|
||||
// Supplemental Punctuation: U+2E00 to U+2E7F
|
||||
(codepoint >= 0x2E00 && codepoint <= 0x2E7F) ||
|
||||
|
||||
// CJK Symbols and Punctuation: U+3000 to U+303F
|
||||
(codepoint >= 0x3000 && codepoint <= 0x303F) ||
|
||||
|
||||
// Ideographic Description Characters: U+2FF0 to U+2FFF
|
||||
(codepoint >= 0x2FF0 && codepoint <= 0x2FFF)
|
||||
);
|
||||
}
|
||||
|
||||
static inline int is_right_boundary(int c) {
|
||||
return is_whitespace(c) || is_punctuation(c);
|
||||
}
|
||||
|
||||
static inline int is_left_boundary(char c) {
|
||||
return is_right_boundary(c) || is_utf8_byte(c);
|
||||
}
|
||||
|
||||
static inline int is_alphanumeric(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
static inline int consume_until_boundary(struct cursor *cur) {
|
||||
char c;
|
||||
unsigned int c;
|
||||
unsigned int char_length = 1;
|
||||
unsigned int *utf8_char_length = &char_length;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_boundary(c))
|
||||
*utf8_char_length = 1;
|
||||
|
||||
if (is_whitespace(c))
|
||||
return 1;
|
||||
|
||||
cur->p++;
|
||||
// Need to check for UTF-8 characters, which can be multiple bytes long
|
||||
if (is_utf8_byte(c)) {
|
||||
if (!parse_utf8_char(cur, &c, utf8_char_length)) {
|
||||
if (!is_right_boundary(c)){
|
||||
// TODO: We should work towards handling all UTF-8 characters.
|
||||
printf("Invalid UTF-8 code point: %x\n", c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_right_boundary(c))
|
||||
return 1;
|
||||
|
||||
// Need to use a variable character byte length for UTF-8 (2-4 bytes)
|
||||
if (cur->p + *utf8_char_length <= cur->end)
|
||||
cur->p += *utf8_char_length;
|
||||
else
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
@@ -492,23 +663,4 @@ static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end)
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
#include "nostr_bech32.h"
|
||||
#include "wasm.h"
|
||||
#include "nostrscript.h"
|
||||
#include "nostrdb.h"
|
||||
#include "lmdb.h"
|
||||
|
||||
|
||||
+80
-7
@@ -104,6 +104,69 @@ static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8
|
||||
return add_block(blocks, b);
|
||||
}
|
||||
|
||||
static int consume_url_fragment(struct cursor *cur)
|
||||
{
|
||||
int c;
|
||||
|
||||
if ((c = peek_char(cur, 0)) < 0)
|
||||
return 1;
|
||||
|
||||
if (c != '#' && c != '?') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
cur->p++;
|
||||
|
||||
return consume_until_whitespace(cur, 1);
|
||||
}
|
||||
|
||||
static int consume_url_path(struct cursor *cur)
|
||||
{
|
||||
int c;
|
||||
|
||||
if ((c = peek_char(cur, 0)) < 0)
|
||||
return 1;
|
||||
|
||||
if (c != '/') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (c == '?' || c == '#' || is_whitespace(c)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int consume_url_host(struct cursor *cur)
|
||||
{
|
||||
char c;
|
||||
int count = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
// TODO: handle IDNs
|
||||
if (is_alphanumeric(c) || c == '.' || c == '-')
|
||||
{
|
||||
count++;
|
||||
cur->p++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return count != 0;
|
||||
}
|
||||
|
||||
|
||||
// this means the end of the URL hostname is the end of the buffer and we finished
|
||||
return count != 0;
|
||||
}
|
||||
|
||||
static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start = cur->p;
|
||||
|
||||
@@ -121,15 +184,25 @@ static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!consume_until_whitespace(cur, 1)) {
|
||||
|
||||
if (!(consume_url_host(cur) &&
|
||||
consume_url_path(cur) &&
|
||||
consume_url_fragment(cur)))
|
||||
{
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// strip any unwanted characters
|
||||
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
|
||||
|
||||
|
||||
// smart parens
|
||||
if (start - 1 >= 0 &&
|
||||
start < cur->end &&
|
||||
*(start - 1) == '(' &&
|
||||
(cur->p - 1) < cur->end &&
|
||||
*(cur->p - 1) == ')')
|
||||
{
|
||||
cur->p--;
|
||||
}
|
||||
|
||||
block->type = BLOCK_URL;
|
||||
block->block.str.start = (const char *)start;
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
@@ -231,7 +304,7 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
||||
}
|
||||
|
||||
pre_mention = cur.p;
|
||||
if (cp == -1 || is_boundary(cp) || c == '#') {
|
||||
if (cp == -1 || is_left_boundary(cp) || c == '#') {
|
||||
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
|
||||
@@ -39,15 +39,6 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize)
|
||||
return slen == 0 && bufsize == 0;
|
||||
}
|
||||
|
||||
static char hexchar(unsigned int val)
|
||||
{
|
||||
if (val < 10)
|
||||
return '0' + val;
|
||||
if (val < 16)
|
||||
return 'a' + val - 10;
|
||||
abort();
|
||||
}
|
||||
|
||||
bool hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
@@ -70,4 +70,15 @@ static inline size_t hex_data_size(size_t strlen)
|
||||
{
|
||||
return strlen / 2;
|
||||
}
|
||||
|
||||
static inline char hexchar(unsigned int val)
|
||||
{
|
||||
if (val < 10)
|
||||
return '0' + val;
|
||||
if (val < 16)
|
||||
return 'a' + val - 10;
|
||||
abort();
|
||||
}
|
||||
|
||||
|
||||
#endif /* CCAN_HEX_H */
|
||||
|
||||
+2
-1
@@ -7100,7 +7100,8 @@ int wasm_interp_init(struct wasm_interp *interp, struct module *module)
|
||||
return 0;
|
||||
}
|
||||
|
||||
memory_pages_size = 64 * 256 * WASM_PAGE_SIZE; /* 1 gb virt */
|
||||
// keep total memory size small for now, iOS doesn't like like mallocs
|
||||
memory_pages_size = 8 * WASM_PAGE_SIZE;
|
||||
|
||||
memsize =
|
||||
stack_size +
|
||||
|
||||
+834
-77
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,14 @@
|
||||
"state" : {
|
||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/damus-io/swift-markdown-ui",
|
||||
"state" : {
|
||||
"revision" : "76bb7971da7fbf429de1c84f1244adf657242fee"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE3",
|
||||
"green" : "0xD7",
|
||||
"red" : "0xF7"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x20",
|
||||
"green" : "0x13",
|
||||
"red" : "0x61"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x63",
|
||||
"green" : "0x11",
|
||||
"red" : "0xF5"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x6E",
|
||||
"green" : "0x20",
|
||||
"red" : "0xF8"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xEE",
|
||||
"green" : "0xE8",
|
||||
"red" : "0xF7"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x35",
|
||||
"green" : "0x04",
|
||||
"red" : "0x8B"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x3D",
|
||||
"green" : "0x07",
|
||||
"red" : "0x9C"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x44",
|
||||
"green" : "0x06",
|
||||
"red" : "0xB2"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2D",
|
||||
"green" : "0x05",
|
||||
"red" : "0x75"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xD8",
|
||||
"green" : "0xC2",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFA",
|
||||
"green" : "0xFA",
|
||||
"red" : "0xF9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x24",
|
||||
"green" : "0x22",
|
||||
"red" : "0x20"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE3",
|
||||
"green" : "0xE1",
|
||||
"red" : "0xDD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2A",
|
||||
"green" : "0x26",
|
||||
"red" : "0x23"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x59",
|
||||
"green" : "0x53",
|
||||
"red" : "0x4A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x85",
|
||||
"green" : "0x7A",
|
||||
"red" : "0x6A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE4",
|
||||
"green" : "0xF1",
|
||||
"red" : "0xD6"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x38",
|
||||
"green" : "0x5C",
|
||||
"red" : "0x12"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x5A",
|
||||
"green" : "0xAB",
|
||||
"red" : "0x04"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x64",
|
||||
"green" : "0xBF",
|
||||
"red" : "0x03"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF0",
|
||||
"green" : "0xF7",
|
||||
"red" : "0xE8"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1F",
|
||||
"green" : "0x33",
|
||||
"red" : "0x0A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x34",
|
||||
"green" : "0x64",
|
||||
"red" : "0x02"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x3F",
|
||||
"green" : "0x79",
|
||||
"red" : "0x02"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1F",
|
||||
"green" : "0x3C",
|
||||
"red" : "0x01"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE4",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xAD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xD1",
|
||||
"green" : "0xEE",
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x12",
|
||||
"green" : "0x43",
|
||||
"red" : "0x5C"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1C",
|
||||
"green" : "0xAD",
|
||||
"red" : "0xF9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2C",
|
||||
"green" : "0xB5",
|
||||
"red" : "0xFC"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE1",
|
||||
"green" : "0xF4",
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x0A",
|
||||
"green" : "0x25",
|
||||
"red" : "0x33"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x06",
|
||||
"green" : "0x85",
|
||||
"red" : "0xC6"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x03",
|
||||
"green" : "0x93",
|
||||
"red" : "0xDD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x04",
|
||||
"green" : "0x6A",
|
||||
"red" : "0x9F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xCC",
|
||||
"green" : "0xF5",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// CarouselDotsView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 7/15/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CarouselDotsView: View {
|
||||
let maxCount: Int
|
||||
let maxVisibleCount: Int
|
||||
@Binding var selectedIndex: Int
|
||||
|
||||
var body: some View {
|
||||
if maxCount > 1 {
|
||||
HStack {
|
||||
let visibleRange = visibleRange()
|
||||
ForEach(0 ..< maxCount, id: \.self) { index in
|
||||
if visibleRange.contains(index) {
|
||||
Circle()
|
||||
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
|
||||
.frame(width: 10, height: 10)
|
||||
.onTapGesture {
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, CGFloat(8))
|
||||
.id(UUID())
|
||||
}
|
||||
}
|
||||
|
||||
private func visibleRange() -> ClosedRange<Int> {
|
||||
let visibleCount = min(maxCount, maxVisibleCount)
|
||||
|
||||
let half = Int(visibleCount / 2)
|
||||
|
||||
// Keep the selected dot in the middle of the visible dots when possible.
|
||||
var minVisibleIndex: Int
|
||||
var maxVisibleIndex: Int
|
||||
|
||||
if visibleCount % 2 == 0 {
|
||||
minVisibleIndex = max(0, selectedIndex - half)
|
||||
maxVisibleIndex = min(maxCount - 1, selectedIndex + half - 1)
|
||||
} else {
|
||||
minVisibleIndex = max(0, selectedIndex - half)
|
||||
maxVisibleIndex = min(maxCount - 1, selectedIndex + half)
|
||||
}
|
||||
|
||||
// Adjust min and max to be within the bounds of what is visibly allowed.
|
||||
if (maxVisibleIndex - minVisibleIndex + 1) < visibleCount {
|
||||
if minVisibleIndex == 0 {
|
||||
maxVisibleIndex = visibleCount - 1
|
||||
} else if maxVisibleIndex == maxCount - 1 {
|
||||
minVisibleIndex = maxVisibleIndex - visibleCount + 1
|
||||
}
|
||||
} else if (maxVisibleIndex - minVisibleIndex + 1) > visibleCount {
|
||||
minVisibleIndex = maxVisibleIndex - maxVisibleCount + 1
|
||||
}
|
||||
|
||||
return minVisibleIndex...maxVisibleIndex
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,23 @@ class DamusColors {
|
||||
static let purple = Color("DamusPurple")
|
||||
static let deepPurple = Color("DamusDeepPurple")
|
||||
static let blue = Color("DamusBlue")
|
||||
static let success = Color("DamusSuccessPrimary")
|
||||
static let successSecondary = Color("DamusSuccessSecondary")
|
||||
static let successTertiary = Color("DamusSuccessTertiary")
|
||||
static let successQuaternary = Color("DamusSuccessQuaternary")
|
||||
static let successBorder = Color("DamusSuccessBorder")
|
||||
static let warning = Color("DamusWarningPrimary")
|
||||
static let warningSecondary = Color("DamusWarningSecondary")
|
||||
static let warningTertiary = Color("DamusWarningTertiary")
|
||||
static let warningQuaternary = Color("DamusWarningQuaternary")
|
||||
static let warningBorder = Color("DamusWarningBorder")
|
||||
static let danger = Color("DamusDangerPrimary")
|
||||
static let dangerSecondary = Color("DamusDangerSecondary")
|
||||
static let dangerTertiary = Color("DamusDangerTertiary")
|
||||
static let dangerQuaternary = Color("DamusDangerQuaternary")
|
||||
static let dangerBorder = Color("DamusDangerBorder")
|
||||
static let neutral1 = Color("DamusNeutral1")
|
||||
static let neutral3 = Color("DamusNeutral3")
|
||||
static let neutral6 = Color("DamusNeutral6")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ import SwiftUI
|
||||
struct EndBlock: View {
|
||||
let height: CGFloat
|
||||
|
||||
init () {
|
||||
self.height = 10.0
|
||||
}
|
||||
|
||||
init (height: Float) {
|
||||
init(height: Float = 10) {
|
||||
self.height = CGFloat(height)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ struct GradientButtonStyle: ButtonStyle {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(PinkGradient)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? 0.8 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// DamusLightGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 9/8/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x2d, b: 0xc3)
|
||||
fileprivate let damus_grad_c2 = hex_col(r: 0x33, g: 0xc5, b: 0xbc)
|
||||
fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2]
|
||||
|
||||
struct DamusLightGradient: View {
|
||||
var body: some View {
|
||||
DamusLightGradient.gradient
|
||||
.opacity(0.5)
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: damus_grad, startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusLightGradient_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusLightGradient()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// GrayGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by klabo on 7/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
let GrayGradient = LinearGradient(gradient:
|
||||
Gradient(colors: [Color(#colorLiteral(red: 0.9764705882, green: 0.9803921569, blue: 0.9803921569, alpha: 1))]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing)
|
||||
|
||||
struct GrayGradientView: View {
|
||||
var body: some View {
|
||||
GrayGradient
|
||||
.edgesIgnoringSafeArea([.top, .bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct GrayGradient_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GrayGradientView()
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ enum ImageShape {
|
||||
struct ImageCarousel: View {
|
||||
var urls: [MediaUrl]
|
||||
|
||||
let evid: String
|
||||
let evid: NoteId
|
||||
|
||||
let state: DamusState
|
||||
|
||||
@@ -72,7 +72,7 @@ struct ImageCarousel: View {
|
||||
@State private var selectedIndex = 0
|
||||
@State private var video_size: CGSize? = nil
|
||||
|
||||
init(state: DamusState, evid: String, urls: [MediaUrl]) {
|
||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
@@ -105,17 +105,13 @@ struct ImageCarousel: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
||||
if self.image_fill == nil, let size = state.video.size_for_url(url) {
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func video_model(_ url: URL) -> VideoPlayerModel {
|
||||
return state.events.get_video_player_model(url: url)
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
Group {
|
||||
switch url {
|
||||
@@ -125,7 +121,7 @@ struct ImageCarousel: View {
|
||||
open_sheet = true
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
||||
DamusVideoPlayer(url: url, video_size: $video_size, controller: state.video)
|
||||
.onChange(of: video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
@@ -194,7 +190,7 @@ struct ImageCarousel: View {
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(video_controller: state.video, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
.frame(height: height)
|
||||
.onChange(of: selectedIndex) { value in
|
||||
@@ -209,9 +205,30 @@ struct ImageCarousel: View {
|
||||
.onTapGesture { }
|
||||
|
||||
// This is our custom carousel image indicator
|
||||
// A maximum of 18 should be visible. Any more than that and it starts to push the frame of the parent view
|
||||
// causing adjacent views to disort in dimensions.
|
||||
CarouselDotsView(maxCount: urls.count, maxVisibleCount: 18, selectedIndex: $selectedIndex)
|
||||
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Carousel
|
||||
struct CarouselDotsView<T>: View {
|
||||
let urls: [T]
|
||||
@Binding var selectedIndex: Int
|
||||
|
||||
var body: some View {
|
||||
if urls.count > 1 {
|
||||
HStack {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
|
||||
.frame(width: 10, height: 10)
|
||||
.onTapGesture {
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, CGFloat(8))
|
||||
.id(UUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +285,7 @@ public struct ImageFill {
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
|
||||
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
|
||||
struct InvoiceView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let our_pubkey: String
|
||||
let our_pubkey: Pubkey
|
||||
let invoice: Invoice
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var copied = false
|
||||
@@ -108,12 +108,12 @@ let test_invoice = Invoice(description: .description("this is a description"), a
|
||||
|
||||
struct InvoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InvoiceView(our_pubkey: "", invoice: test_invoice, settings: test_damus_state().settings)
|
||||
InvoiceView(our_pubkey: .empty, invoice: test_invoice, settings: test_damus_state.settings)
|
||||
.frame(width: 300, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet, sheet)
|
||||
notify(.present_sheet(sheet))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InvoicesView: View {
|
||||
let our_pubkey: String
|
||||
let our_pubkey: Pubkey
|
||||
var invoices: [Invoice]
|
||||
let settings: UserSettingsStore
|
||||
|
||||
@@ -29,7 +29,7 @@ struct InvoicesView: View {
|
||||
|
||||
struct InvoicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state().settings)
|
||||
InvoicesView(our_pubkey: test_note.pubkey, invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state.settings)
|
||||
.frame(width: 300)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ import SwiftUI
|
||||
|
||||
struct NIP05Badge: View {
|
||||
let nip05: NIP05
|
||||
let pubkey: String
|
||||
let pubkey: Pubkey
|
||||
let contacts: Contacts
|
||||
let show_domain: Bool
|
||||
let clickable: Bool
|
||||
|
||||
let profiles: Profiles
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
init (nip05: NIP05, pubkey: String, contacts: Contacts, show_domain: Bool, clickable: Bool) {
|
||||
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
|
||||
self.nip05 = nip05
|
||||
self.pubkey = pubkey
|
||||
self.contacts = contacts
|
||||
self.show_domain = show_domain
|
||||
self.clickable = clickable
|
||||
self.profiles = profiles
|
||||
}
|
||||
|
||||
var nip05_color: Bool {
|
||||
@@ -43,24 +43,36 @@ struct NIP05Badge: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var username_matches_nip05: Bool {
|
||||
guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return name.lowercased() == nip05.username.lowercased()
|
||||
}
|
||||
|
||||
var nip05_string: String {
|
||||
if nip05.username == "_" || username_matches_nip05 {
|
||||
return nip05.host
|
||||
} else {
|
||||
return "\(nip05.username)@\(nip05.host)"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
Seal
|
||||
|
||||
|
||||
if show_domain {
|
||||
if clickable {
|
||||
Text(nip05.host)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
}
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
}
|
||||
} else {
|
||||
Text(nip05.host)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,17 +90,21 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
|
||||
func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool {
|
||||
return contacts.is_friend_or_self(pubkey) ? true : false
|
||||
}
|
||||
|
||||
struct NIP05Badge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state()
|
||||
let test_state = test_damus_state
|
||||
VStack {
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, clickable: false)
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: "sdkfjsdf"), show_domain: true, clickable: false)
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// NeutralButtonStyle.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 9/1/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NeutralButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
return configuration.label
|
||||
.background(DamusColors.neutral1)
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||
)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Button(action: {
|
||||
print("dynamic size")
|
||||
}) {
|
||||
Text(verbatim: "Dynamic Size")
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
|
||||
|
||||
Button(action: {
|
||||
print("infinite width")
|
||||
}) {
|
||||
HStack {
|
||||
Text(verbatim: "Infinite Width")
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,13 @@ import SwiftUI
|
||||
|
||||
struct Reposted: View {
|
||||
let damus: DamusState
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
|
||||
let pubkey: Pubkey
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image("repost")
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
|
||||
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
@@ -26,7 +25,7 @@ struct Reposted: View {
|
||||
|
||||
struct Reposted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state()
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey, profile: make_test_profile())
|
||||
let test_state = test_damus_state
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,22 +25,11 @@ struct SearchHeaderView: View {
|
||||
|
||||
var Icon: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||
.frame(width: 54, height: 54)
|
||||
|
||||
switch described {
|
||||
case .hashtag:
|
||||
Text(verbatim: "#")
|
||||
.font(.largeTitle.bold())
|
||||
.foregroundStyle(PinkGradient)
|
||||
.mask(Text(verbatim: "#")
|
||||
.font(.largeTitle.bold()))
|
||||
|
||||
case .unknown:
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.title.bold())
|
||||
.foregroundStyle(PinkGradient)
|
||||
case .hashtag:
|
||||
SingleCharacterAvatar(character: "#")
|
||||
case .unknown:
|
||||
SystemIconAvatar(system_name: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,32 +38,6 @@ struct SearchHeaderView: View {
|
||||
Text(described.description)
|
||||
}
|
||||
|
||||
func unfollow(_ hashtag: String) {
|
||||
is_following = false
|
||||
handle_unfollow(state: state, unfollow: .t(hashtag))
|
||||
}
|
||||
|
||||
func follow(_ hashtag: String) {
|
||||
is_following = true
|
||||
handle_follow(state: state, follow: .t(hashtag))
|
||||
}
|
||||
|
||||
func FollowButton(_ ht: String) -> some View {
|
||||
return Button(action: { follow(ht) }) {
|
||||
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
||||
.font(.footnote.bold())
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
func UnfollowButton(_ ht: String) -> some View {
|
||||
return Button(action: { unfollow(ht) }) {
|
||||
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
||||
.font(.footnote.bold())
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 30) {
|
||||
Icon
|
||||
@@ -86,44 +49,128 @@ struct SearchHeaderView: View {
|
||||
|
||||
if state.is_privkey_user, case .hashtag(let ht) = described {
|
||||
if is_following {
|
||||
UnfollowButton(ht)
|
||||
HashtagUnfollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||
} else {
|
||||
FollowButton(ht)
|
||||
HashtagFollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.followed)) { notif in
|
||||
let ref = notif.object as! ReferencedId
|
||||
.onReceive(handle_notify(.followed)) { ref in
|
||||
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||
self.is_following = true
|
||||
}
|
||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||
let ref = notif.object as! ReferencedId
|
||||
.onReceive(handle_notify(.unfollowed)) { ref in
|
||||
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||
self.is_following = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool {
|
||||
guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht
|
||||
else { return false }
|
||||
struct SystemIconAvatar: View {
|
||||
let system_name: String
|
||||
|
||||
var body: some View {
|
||||
NonImageAvatar {
|
||||
Image(systemName: system_name)
|
||||
.font(.title.bold())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SingleCharacterAvatar: View {
|
||||
let character: String
|
||||
|
||||
var body: some View {
|
||||
NonImageAvatar {
|
||||
Text(verbatim: character)
|
||||
.font(.largeTitle.bold())
|
||||
.mask(Text(verbatim: character)
|
||||
.font(.largeTitle.bold()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NonImageAvatar<Content: View>: View {
|
||||
let content: Content
|
||||
|
||||
init(@ViewBuilder content: () -> Content) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||
.frame(width: 54, height: 54)
|
||||
|
||||
content
|
||||
.foregroundStyle(PinkGradient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagUnfollowButton: View {
|
||||
let damus_state: DamusState
|
||||
let hashtag: String
|
||||
@Binding var is_following: Bool
|
||||
|
||||
var body: some View {
|
||||
return Button(action: { unfollow(hashtag) }) {
|
||||
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
||||
.font(.footnote.bold())
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
func unfollow(_ hashtag: String) {
|
||||
is_following = false
|
||||
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag))
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagFollowButton: View {
|
||||
let damus_state: DamusState
|
||||
let hashtag: String
|
||||
@Binding var is_following: Bool
|
||||
|
||||
var body: some View {
|
||||
return Button(action: { follow(hashtag) }) {
|
||||
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
||||
.font(.footnote.bold())
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
func follow(_ hashtag: String) {
|
||||
is_following = true
|
||||
handle_follow(state: damus_state, follow: .hashtag(hashtag))
|
||||
}
|
||||
}
|
||||
|
||||
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
|
||||
guard case .hashtag(let follow_ht) = ref,
|
||||
case .hashtag(let search_ht) = desc,
|
||||
follow_ht == search_ht
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
|
||||
guard let contacts else { return false }
|
||||
return is_already_following(contacts: contacts, follow: .t(hashtag))
|
||||
return is_already_following(contacts: contacts, follow: .hashtag(hashtag))
|
||||
}
|
||||
|
||||
|
||||
struct SearchHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SearchHeaderView(state: test_damus_state(), described: .hashtag("damus"))
|
||||
SearchHeaderView(state: test_damus_state, described: .hashtag("damus"))
|
||||
|
||||
SearchHeaderView(state: test_damus_state(), described: .unknown)
|
||||
SearchHeaderView(state: test_damus_state, described: .unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ struct SelectableText: View {
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
.onAppear {
|
||||
self.selectedTextWidth = geo.size.width
|
||||
if geo.size.width == .zero {
|
||||
self.selectedTextHeight = 1000.0
|
||||
} else {
|
||||
self.selectedTextWidth = geo.size.width
|
||||
}
|
||||
}
|
||||
.onChange(of: geo.size) { newSize in
|
||||
self.selectedTextWidth = newSize.width
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// MusicController.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-21.
|
||||
//
|
||||
import SwiftUI
|
||||
import MediaPlayer
|
||||
|
||||
enum MusicState {
|
||||
case playback_state(MPMusicPlaybackState)
|
||||
case song(MPMediaItem?)
|
||||
}
|
||||
|
||||
class MusicController {
|
||||
let player: MPMusicPlayerController
|
||||
|
||||
let onChange: (MusicState) -> ()
|
||||
|
||||
init(onChange: @escaping (MusicState) -> ()) {
|
||||
player = .systemMusicPlayer
|
||||
|
||||
player.beginGeneratingPlaybackNotifications()
|
||||
|
||||
self.onChange = onChange
|
||||
|
||||
print("Playback State: \(player.playbackState)")
|
||||
print("Now Playing Item: \(player.nowPlayingItem?.title ?? "None")")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.songChanged(notification:)), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: player)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.playbackStatusChanged(notification:)), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: player)
|
||||
}
|
||||
|
||||
deinit {
|
||||
print("deinit musiccontroller")
|
||||
}
|
||||
|
||||
@objc
|
||||
func songChanged(notification: Notification) {
|
||||
onChange(.song(player.nowPlayingItem))
|
||||
}
|
||||
|
||||
@objc
|
||||
func playbackStatusChanged(notification: Notification) {
|
||||
onChange(.playback_state(player.playbackState))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// UserStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
struct Song {
|
||||
let started_playing: Date
|
||||
let content: String
|
||||
}
|
||||
|
||||
struct UserStatus {
|
||||
let type: UserStatusType
|
||||
let expires_at: Date?
|
||||
var content: String
|
||||
let created_at: UInt32
|
||||
var url: URL?
|
||||
|
||||
func to_note(keypair: FullKeypair) -> NostrEvent? {
|
||||
return make_user_status_note(status: self, keypair: keypair)
|
||||
}
|
||||
|
||||
init(type: UserStatusType, expires_at: Date?, content: String, created_at: UInt32, url: URL? = nil) {
|
||||
self.type = type
|
||||
self.expires_at = expires_at
|
||||
self.content = content
|
||||
self.created_at = created_at
|
||||
self.url = url
|
||||
}
|
||||
|
||||
func expired() -> Bool {
|
||||
guard let expires_at else { return false }
|
||||
return Date.now >= expires_at
|
||||
}
|
||||
|
||||
init?(ev: NostrEvent) {
|
||||
guard let tag = ev.referenced_params.just_one() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let str = tag.param.string()
|
||||
if str == "general" {
|
||||
self.type = .general
|
||||
} else if str == "music" {
|
||||
self.type = .music
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_char("r") }),
|
||||
tag.count >= 2,
|
||||
let url = URL(string: tag[1].string())
|
||||
{
|
||||
self.url = url
|
||||
} else {
|
||||
self.url = nil
|
||||
}
|
||||
|
||||
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_str("expiration") }),
|
||||
tag.count == 2,
|
||||
let expires = UInt32(tag[1].string())
|
||||
{
|
||||
self.expires_at = Date(timeIntervalSince1970: TimeInterval(expires))
|
||||
} else {
|
||||
self.expires_at = nil
|
||||
}
|
||||
|
||||
self.content = ev.content
|
||||
self.created_at = ev.created_at
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum UserStatusType: String {
|
||||
case music
|
||||
case general
|
||||
|
||||
}
|
||||
|
||||
class UserStatusModel: ObservableObject {
|
||||
@Published var general: UserStatus?
|
||||
@Published var music: UserStatus?
|
||||
|
||||
func update_status(_ s: UserStatus) {
|
||||
// whitespace = delete
|
||||
let del = s.content.allSatisfy({ c in c.isWhitespace })
|
||||
|
||||
switch s.type {
|
||||
case .music:
|
||||
if del {
|
||||
self.music = nil
|
||||
} else {
|
||||
self.music = s
|
||||
}
|
||||
case .general:
|
||||
if del {
|
||||
self.general = nil
|
||||
} else {
|
||||
self.general = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func try_expire() {
|
||||
if let general, general.expired() {
|
||||
self.general = nil
|
||||
}
|
||||
|
||||
if let music, music.expired() {
|
||||
self.music = nil
|
||||
}
|
||||
}
|
||||
|
||||
var _playing_enabled: Bool
|
||||
var playing_enabled: Bool {
|
||||
set {
|
||||
var new_val = newValue
|
||||
|
||||
if newValue {
|
||||
MPMediaLibrary.requestAuthorization { astatus in
|
||||
switch astatus {
|
||||
case .notDetermined: new_val = false
|
||||
case .denied: new_val = false
|
||||
case .restricted: new_val = false
|
||||
case .authorized: new_val = true
|
||||
@unknown default:
|
||||
new_val = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if new_val != playing_enabled {
|
||||
_playing_enabled = new_val
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
get {
|
||||
return _playing_enabled
|
||||
}
|
||||
}
|
||||
|
||||
init(playing: UserStatus? = nil, status: UserStatus? = nil) {
|
||||
self.general = status
|
||||
self.music = playing
|
||||
self._playing_enabled = false
|
||||
self.playing_enabled = false
|
||||
}
|
||||
|
||||
static var current_track: String? {
|
||||
let player = MPMusicPlayerController.systemMusicPlayer
|
||||
guard let nowPlayingItem = player.nowPlayingItem else { return nil }
|
||||
return nowPlayingItem.title
|
||||
}
|
||||
}
|
||||
|
||||
func make_user_status_note(status: UserStatus, keypair: FullKeypair, expiry: Date? = nil) -> NostrEvent?
|
||||
{
|
||||
var tags: [[String]] = [ ["d", status.type.rawValue] ]
|
||||
|
||||
if let expiry {
|
||||
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||
} else if let expiry = status.expires_at {
|
||||
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||
}
|
||||
|
||||
if let url = status.url {
|
||||
tags.append(["r", url.absoluteString])
|
||||
}
|
||||
|
||||
let kind = NostrKind.status.rawValue
|
||||
guard let ev = NostrEvent(content: status.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// UserStatusSheet.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum StatusDuration: CustomStringConvertible, CaseIterable {
|
||||
case never
|
||||
case thirty_mins
|
||||
case hour
|
||||
case four_hours
|
||||
case day
|
||||
case week
|
||||
|
||||
var timeInterval: TimeInterval? {
|
||||
switch self {
|
||||
case .never:
|
||||
return nil
|
||||
case .thirty_mins:
|
||||
return 60 * 30
|
||||
case .hour:
|
||||
return 60 * 60
|
||||
case .four_hours:
|
||||
return 60 * 60 * 4
|
||||
case .day:
|
||||
return 60 * 60 * 24
|
||||
case .week:
|
||||
return 60 * 60 * 24 * 7
|
||||
}
|
||||
}
|
||||
|
||||
var expiration: Date? {
|
||||
guard let timeInterval else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Date.now.addingTimeInterval(timeInterval)
|
||||
}
|
||||
|
||||
var description: String {
|
||||
guard let timeInterval else {
|
||||
return NSLocalizedString("Never", comment: "Profile status duration setting of never expiring.")
|
||||
}
|
||||
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
formatter.allowedUnits = [.minute, .hour, .day, .weekOfMonth]
|
||||
return formatter.string(from: timeInterval) ?? "\(timeInterval) seconds"
|
||||
}
|
||||
}
|
||||
|
||||
struct UserStatusSheet: View {
|
||||
let postbox: PostBox
|
||||
let keypair: Keypair
|
||||
|
||||
@State var duration: StatusDuration = .never
|
||||
|
||||
@ObservedObject var status: UserStatusModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var status_binding: Binding<String> {
|
||||
Binding(get: {
|
||||
status.general?.content ?? ""
|
||||
}, set: { v in
|
||||
if let general = status.general {
|
||||
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: general.url)
|
||||
} else {
|
||||
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var url_binding: Binding<String> {
|
||||
Binding(get: {
|
||||
status.general?.url?.absoluteString ?? ""
|
||||
}, set: { v in
|
||||
if let general = status.general {
|
||||
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: general.content, created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
|
||||
} else {
|
||||
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: "", created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Set Status", comment: "Title of view that allows the user to set their profile status (e.g. working, studying, coding)")
|
||||
.font(.largeTitle)
|
||||
|
||||
TextField(text: status_binding, label: {
|
||||
Text("📋 Working", comment: "Placeholder as an example of what the user could set as their profile status.")
|
||||
})
|
||||
|
||||
HStack {
|
||||
Image("link")
|
||||
|
||||
TextField(text: url_binding, label: {
|
||||
Text("https://example.com", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.")
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.")
|
||||
|
||||
Spacer()
|
||||
|
||||
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
|
||||
ForEach(StatusDuration.allCases, id: \.self) { d in
|
||||
Text(verbatim: d.description)
|
||||
.tag(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle(isOn: $status.playing_enabled, label: {
|
||||
Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.")
|
||||
})
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.")
|
||||
})
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
guard let status = self.status.general,
|
||||
let kp = keypair.to_full(),
|
||||
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
postbox.send(ev)
|
||||
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Save", comment: "Save button text for saving profile status settings.")
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
}
|
||||
.padding([.top], 30)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct UserStatusSheet_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserStatusSheet(postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// UserStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MediaPlayer
|
||||
import WebKit
|
||||
|
||||
struct UserStatusView: View {
|
||||
@ObservedObject var status: UserStatusModel
|
||||
|
||||
var show_general: Bool
|
||||
var show_music: Bool
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
func Status(st: UserStatus, prefix: String = "") -> some View {
|
||||
HStack {
|
||||
Text(verbatim: "\(prefix)\(st.content)")
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout.italic())
|
||||
if st.url != nil {
|
||||
Image("link")
|
||||
.resizable()
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if let url = st.url {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
.contextMenu(
|
||||
menuItems: {
|
||||
if let url = st.url {
|
||||
Button(url.absoluteString, action: { openURL(url) }) }
|
||||
}, preview: {
|
||||
if let url = st.url {
|
||||
URLPreview(url: url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if show_general, let general = status.general {
|
||||
Status(st: general)
|
||||
}
|
||||
|
||||
if show_music, let playing = status.music {
|
||||
Status(st: playing, prefix: "🎵")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct URLPreview: UIViewRepresentable {
|
||||
var url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
return WKWebView()
|
||||
}
|
||||
|
||||
func updateUIView(_ wkView: WKWebView, context: Context) {
|
||||
let request = URLRequest(url: url)
|
||||
wkView.load(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct UserStatusView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserStatusView(status: UserStatus(type: .music, expires_at: nil, content: "Track - Artist", created_at: 0, url: URL(string: "spotify:search:abc")), show_general: true, show_music: true)
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -42,7 +42,7 @@ struct TranslateView: View {
|
||||
.translate_button_style()
|
||||
}
|
||||
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated) -> some View {
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
|
||||
Text(translatedFromLanguageString)
|
||||
@@ -54,7 +54,7 @@ struct TranslateView: View {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
} else {
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(self.size))
|
||||
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ struct TranslateView: View {
|
||||
guard let note_language = translations_model.note_language else {
|
||||
return
|
||||
}
|
||||
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings, note_lang: note_language)
|
||||
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language)
|
||||
DispatchQueue.main.async {
|
||||
self.translations_model.state = res
|
||||
}
|
||||
@@ -98,7 +98,7 @@ struct TranslateView: View {
|
||||
Text("")
|
||||
case .translated(let translated):
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
|
||||
case .not_needed:
|
||||
Text("")
|
||||
}
|
||||
@@ -120,16 +120,16 @@ extension View {
|
||||
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state()
|
||||
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
||||
let ds = test_damus_state
|
||||
TranslateView(damus_state: ds, event: test_note, size: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
||||
|
||||
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(settings)
|
||||
let originalContent = event.get_content(privkey)
|
||||
let originalContent = event.get_content(keypair)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||
|
||||
guard let translated_note else {
|
||||
@@ -143,7 +143,7 @@ func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, set
|
||||
}
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = event.get_blocks(content: translated_note)
|
||||
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
||||
|
||||
// and cache it
|
||||
|
||||
@@ -9,8 +9,8 @@ import SwiftUI
|
||||
|
||||
struct UserViewRow: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
let pubkey: Pubkey
|
||||
|
||||
var body: some View {
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.contentShape(Rectangle())
|
||||
@@ -20,12 +20,12 @@ struct UserViewRow: View {
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let pubkey: Pubkey
|
||||
let spacer: Bool
|
||||
|
||||
@State var about_text: Text? = nil
|
||||
|
||||
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
|
||||
init(damus_state: DamusState, pubkey: Pubkey, spacer: Bool = true) {
|
||||
self.damus_state = damus_state
|
||||
self.pubkey = pubkey
|
||||
self.spacer = spacer
|
||||
@@ -37,8 +37,7 @@ struct UserView: View {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
|
||||
ProfileName(pubkey: pubkey, damus: damus_state, show_nip5_domain: false)
|
||||
if let about_text {
|
||||
about_text
|
||||
.lineLimit(3)
|
||||
@@ -56,6 +55,6 @@ struct UserView: View {
|
||||
|
||||
struct UserView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserView(damus_state: test_damus_state(), pubkey: "pk")
|
||||
UserView(damus_state: test_damus_state, pubkey: test_note.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +141,10 @@ struct ZapButton: View {
|
||||
|
||||
struct ZapButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||
let zaps = ZapsDataModel([.pending(pending_zap)])
|
||||
|
||||
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||
ZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,90 +183,74 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
damus_state.add_zap(zap: .pending(pending_zap))
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
Task { @MainActor in
|
||||
guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping(ev))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping(ev))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
switch pending_zap_state {
|
||||
case .nwc(let nwc_state):
|
||||
// don't both continuing, user has canceled
|
||||
if case .cancel_fetching_invoice = nwc_state.state {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.canceled)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
return
|
||||
}
|
||||
|
||||
var flusher: OnFlush? = nil
|
||||
|
||||
// donations are only enabled on one-tap zaps and off appstore
|
||||
if !damus_state.settings.nozaps && !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 { @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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||
|
||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, 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)")
|
||||
|
||||
let typ = ZappingEventType.failed(.send_failed)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping, ev)
|
||||
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
|
||||
}
|
||||
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
|
||||
notify(.zapping, ev)
|
||||
|
||||
case .external(let pending_ext):
|
||||
pending_ext.state = .done
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
||||
notify(.zapping, ev)
|
||||
switch pending_zap_state {
|
||||
case .nwc(let nwc_state):
|
||||
// don't both continuing, user has canceled
|
||||
if case .cancel_fetching_invoice = nwc_state.state {
|
||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||
let typ = ZappingEventType.failed(.canceled)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping(ev))
|
||||
return
|
||||
}
|
||||
|
||||
var flusher: OnFlush? = nil
|
||||
|
||||
// donations are only enabled on one-tap zaps and off appstore
|
||||
if !damus_state.settings.nozaps && !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 { @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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||
|
||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, 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)")
|
||||
|
||||
let typ = ZappingEventType.failed(.send_failed)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||
notify(.zapping(ev))
|
||||
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
|
||||
}
|
||||
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
|
||||
notify(.zapping(ev))
|
||||
|
||||
case .external(let pending_ext):
|
||||
pending_ext.state = .done
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
||||
notify(.zapping(ev))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// ContentParsing.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-07-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NoteContent {
|
||||
case note(NostrEvent)
|
||||
case content(String, TagsSequence?)
|
||||
|
||||
init(note: NostrEvent, keypair: Keypair) {
|
||||
if note.known_kind == .dm {
|
||||
self = .content(note.get_content(keypair), note.tags)
|
||||
} else {
|
||||
self = .note(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks {
|
||||
var out: [Block] = []
|
||||
|
||||
var i = 0
|
||||
while (i < bs.num_blocks) {
|
||||
let block = bs.blocks[i]
|
||||
|
||||
if let converted = Block(block, tags: tags) {
|
||||
out.append(converted)
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
let words = Int(bs.words)
|
||||
blocks_free(&bs)
|
||||
|
||||
return Blocks(words: words, blocks: out)
|
||||
|
||||
}
|
||||
|
||||
func parse_note_content(content: NoteContent) -> Blocks {
|
||||
var bs = note_blocks()
|
||||
bs.num_blocks = 0;
|
||||
|
||||
blocks_init(&bs)
|
||||
|
||||
switch content {
|
||||
case .content(let s, let tags):
|
||||
return s.withCString { cptr in
|
||||
damus_parse_content(&bs, cptr)
|
||||
return parsed_blocks_finish(bs: &bs, tags: tags)
|
||||
}
|
||||
case .note(let note):
|
||||
damus_parse_content(&bs, note.content_raw)
|
||||
return parsed_blocks_finish(bs: &bs, tags: note.tags)
|
||||
}
|
||||
}
|
||||
|
||||
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
|
||||
if tags.count == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
/// build a set of indices for each event mention
|
||||
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||
|
||||
/// simpler case with no mentions
|
||||
if mention_indices.count == 0 {
|
||||
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
|
||||
}
|
||||
|
||||
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
|
||||
}
|
||||
|
||||
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
|
||||
|
||||
var count = 0
|
||||
var evrefs: [EventRef] = []
|
||||
var first: Bool = true
|
||||
var first_ref: NoteRef? = nil
|
||||
|
||||
for ref in ev_tags {
|
||||
if first {
|
||||
first_ref = ref
|
||||
evrefs.append(.thread_id(ref))
|
||||
first = false
|
||||
} else {
|
||||
|
||||
evrefs.append(.reply(ref))
|
||||
}
|
||||
count += 1
|
||||
}
|
||||
|
||||
if let first_ref, count == 1 {
|
||||
let r = first_ref
|
||||
return [.reply_to_root(r)]
|
||||
}
|
||||
|
||||
return evrefs
|
||||
}
|
||||
|
||||
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
|
||||
var mentions: [EventRef] = []
|
||||
var ev_refs: [NoteRef] = []
|
||||
var i: Int = 0
|
||||
|
||||
for tag in tags {
|
||||
if let note_id = NoteRef.from_tag(tag: tag) {
|
||||
if mention_indices.contains(i) {
|
||||
mentions.append(.mention(.noteref(note_id, index: i)))
|
||||
} else {
|
||||
ev_refs.append(note_id)
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||
replies.append(contentsOf: mentions)
|
||||
return replies
|
||||
}
|
||||
+228
-211
@@ -7,12 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
let timestamp: Int64
|
||||
let event: NostrEvent
|
||||
}
|
||||
import MediaPlayer
|
||||
|
||||
struct ZapSheet {
|
||||
let target: ZapTarget
|
||||
@@ -30,6 +25,8 @@ enum Sheets: Identifiable {
|
||||
case zap(ZapSheet)
|
||||
case select_wallet(SelectWallet)
|
||||
case filter
|
||||
case user_status
|
||||
case suggestedUsers
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||
@@ -42,25 +39,13 @@ enum Sheets: Identifiable {
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post(let action): return "post-" + (action.ev?.id ?? "")
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .zap(let sheet): return "zap-" + sheet.target.id
|
||||
case .user_status: return "user_status"
|
||||
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
|
||||
case .event(let ev): return "event-" + ev.id.hex()
|
||||
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
|
||||
case .select_wallet: return "select-wallet"
|
||||
case .filter: return "filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FilterState : Int {
|
||||
case posts_and_replies = 1
|
||||
case posts = 0
|
||||
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
case .posts:
|
||||
return !ev.is_reply(nil)
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
case .suggestedUsers: return "suggested-users"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,11 +53,11 @@ enum FilterState : Int {
|
||||
struct ContentView: View {
|
||||
let keypair: Keypair
|
||||
|
||||
var pubkey: String {
|
||||
var pubkey: Pubkey {
|
||||
return keypair.pubkey
|
||||
}
|
||||
|
||||
var privkey: String? {
|
||||
var privkey: Privkey? {
|
||||
return keypair.privkey
|
||||
}
|
||||
|
||||
@@ -81,8 +66,7 @@ struct ContentView: View {
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var damus_state: DamusState? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var muting: String? = nil
|
||||
@State var muting: Pubkey? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@@ -90,9 +74,9 @@ struct ContentView: View {
|
||||
@State private var isSideBarOpened = false
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
|
||||
@AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
|
||||
let sub_id = UUID().description
|
||||
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
// connect retry timer
|
||||
@@ -102,7 +86,13 @@ struct ContentView: View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state!.settings)
|
||||
filters.append(fstate.filter)
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
var PostingTimelineView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
@@ -110,10 +100,10 @@ struct ContentView: View {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: FilterState.posts.filter)
|
||||
contentTimelineView(filter: content_filter(.posts))
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
contentTimelineView(filter: FilterState.posts_and_replies.filter)
|
||||
contentTimelineView(filter: content_filter(.posts_and_replies))
|
||||
.tag(FilterState.posts_and_replies)
|
||||
.id(FilterState.posts_and_replies)
|
||||
}
|
||||
@@ -203,8 +193,8 @@ struct ContentView: View {
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let damus_state {
|
||||
if let sec = damus_state.keypair.privkey {
|
||||
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
|
||||
if let keypair = damus_state.keypair.to_full() {
|
||||
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -230,9 +220,9 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: Route.Script(script: model))
|
||||
}
|
||||
|
||||
func open_profile(id: String) {
|
||||
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: id)
|
||||
func open_profile(pubkey: Pubkey) {
|
||||
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
}
|
||||
|
||||
@@ -285,7 +275,7 @@ struct ContentView: View {
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
|
||||
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
navigationCoordinator.popToRoot()
|
||||
@@ -303,6 +293,10 @@ struct ContentView: View {
|
||||
self.connect()
|
||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
setup_notifications()
|
||||
if !hasSeenSuggestedUsers {
|
||||
active_sheet = .suggestedUsers
|
||||
hasSeenSuggestedUsers = true
|
||||
}
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
switch item {
|
||||
@@ -310,6 +304,8 @@ struct ContentView: View {
|
||||
MaybeReportView(target: target)
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .user_status:
|
||||
UserStatusSheet(postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .zap(let zapsheet):
|
||||
@@ -325,6 +321,8 @@ struct ContentView: View {
|
||||
} else {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
}
|
||||
case .suggestedUsers:
|
||||
SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
@@ -335,106 +333,97 @@ struct ContentView: View {
|
||||
|
||||
switch res {
|
||||
case .filter(let filt): self.open_search(filt: filt)
|
||||
case .profile(let id): self.open_profile(id: id)
|
||||
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||
case .event(let ev): self.open_event(ev: ev)
|
||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||
case .script(let data): self.open_script(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.compose)) { notif in
|
||||
let action = notif.object as! PostAction
|
||||
.onReceive(handle_notify(.compose)) { action in
|
||||
self.active_sheet = .post(action)
|
||||
}
|
||||
.onReceive(timer) { n in
|
||||
self.damus_state?.postbox.try_flushing_events()
|
||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||
}
|
||||
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||
self.is_deleted_account = true
|
||||
}
|
||||
.onReceive(handle_notify(.report)) { notif in
|
||||
let target = notif.object as! ReportTarget
|
||||
.onReceive(handle_notify(.report)) { target in
|
||||
self.active_sheet = .report(target)
|
||||
}
|
||||
.onReceive(handle_notify(.mute)) { notif in
|
||||
let pubkey = notif.object as! String
|
||||
.onReceive(handle_notify(.mute)) { pubkey in
|
||||
self.muting = pubkey
|
||||
self.confirm_mute = true
|
||||
}
|
||||
.onReceive(handle_notify(.attached_wallet)) { notif in
|
||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||
// update the lightning address on our profile when we attach a
|
||||
// wallet with an associated
|
||||
let nwc = notif.object as! WalletConnectURL
|
||||
guard let ds = self.damus_state,
|
||||
let lud16 = nwc.lud16,
|
||||
let keypair = ds.keypair.to_full(),
|
||||
let profile = ds.profiles.lookup(id: ds.pubkey),
|
||||
lud16 != profile.lud16
|
||||
let keypair = ds.keypair.to_full()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||
|
||||
guard let profile = profile_txn.unsafeUnownedValue,
|
||||
lud16 != profile.lud16 else {
|
||||
return
|
||||
}
|
||||
|
||||
// clear zapper cache for old lud16
|
||||
if profile.lud16 != nil {
|
||||
// TODO: should this be somewhere else, where we process profile events!?
|
||||
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
||||
}
|
||||
|
||||
profile.lud16 = lud16
|
||||
let ev = make_metadata_event(keypair: keypair, metadata: profile)
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||
|
||||
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
||||
let ev = obj.object as! NostrEvent
|
||||
guard let ds = self.damus_state else {
|
||||
return
|
||||
}
|
||||
.onReceive(handle_notify(.broadcast)) { ev in
|
||||
guard let ds = self.damus_state else { return }
|
||||
|
||||
ds.postbox.send(ev)
|
||||
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
ds.postbox.send(profile.event)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.unfollow)) { notif in
|
||||
.onReceive(handle_notify(.unfollow)) { target in
|
||||
guard let state = self.damus_state else { return }
|
||||
_ = handle_unfollow_notif(state: state, notif: notif)
|
||||
_ = handle_unfollow(state: state, unfollow: target.follow_ref)
|
||||
}
|
||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||
let unfollow = notif.object as! ReferencedId
|
||||
.onReceive(handle_notify(.unfollowed)) { unfollow in
|
||||
home.resubscribe(.unfollowing(unfollow))
|
||||
}
|
||||
.onReceive(handle_notify(.follow)) { notif in
|
||||
.onReceive(handle_notify(.follow)) { target in
|
||||
guard let state = self.damus_state else { return }
|
||||
guard handle_follow_notif(state: state, notif: notif) else { return }
|
||||
handle_follow_notif(state: state, target: target)
|
||||
}
|
||||
.onReceive(handle_notify(.followed)) { notif in
|
||||
.onReceive(handle_notify(.followed)) { _ in
|
||||
home.resubscribe(.following)
|
||||
}
|
||||
.onReceive(handle_notify(.post)) { notif in
|
||||
.onReceive(handle_notify(.post)) { post in
|
||||
guard let state = self.damus_state,
|
||||
let keypair = state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, notif: notif) {
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
|
||||
self.active_sheet = nil
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
.onReceive(handle_notify(.new_mutes)) { _ in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.mute_thread)) { notif in
|
||||
.onReceive(handle_notify(.mute_thread)) { _ in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
.onReceive(handle_notify(.unmute_thread)) { _ in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.present_sheet)) { notif in
|
||||
let sheet = notif.object as! Sheets
|
||||
.onReceive(handle_notify(.present_sheet)) { sheet in
|
||||
self.active_sheet = sheet
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
.onReceive(handle_notify(.zapping)) { zap_ev in
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
@@ -469,57 +458,49 @@ struct ContentView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { notif in
|
||||
|
||||
guard let local = notif.object as? LossyLocalNotification,
|
||||
let damus_state else {
|
||||
return
|
||||
.onReceive(handle_notify(.local_notification)) { local in
|
||||
guard let damus_state else { return }
|
||||
|
||||
switch local.mention {
|
||||
case .pubkey(let pubkey):
|
||||
open_profile(pubkey: pubkey)
|
||||
|
||||
case .note(let noteId):
|
||||
guard let target = damus_state.events.lookup(noteId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like, .zap, .mention, .repost:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
// Handled separately above.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if local.type == .profile_zap {
|
||||
open_profile(id: local.event_id)
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like: fallthrough
|
||||
case .zap: fallthrough
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
// Handled separately above.
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
|
||||
let hide = notif.object as! Bool
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||
home.filter_events()
|
||||
|
||||
guard let damus_state,
|
||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
|
||||
let keypair = damus_state.keypair.to_full()
|
||||
|
||||
guard let ds = damus_state else { return }
|
||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||
|
||||
guard let profile = profile_txn.unsafeUnownedValue,
|
||||
let keypair = ds.keypair.to_full()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
profile.reactions = !hide
|
||||
let profile_ev = make_metadata_event(keypair: keypair, metadata: profile)
|
||||
damus_state.postbox.send(profile_ev)
|
||||
}
|
||||
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||
is_deleted_account = false
|
||||
notify(.logout, ())
|
||||
}
|
||||
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||
|
||||
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(profile_ev)
|
||||
}
|
||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||
@@ -527,11 +508,12 @@ struct ContentView: View {
|
||||
}
|
||||
}, message: {
|
||||
if let pubkey = self.muting {
|
||||
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
let name = damus_state!.profiles.lookup(id: pubkey).map { profile in
|
||||
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
}.value
|
||||
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||
} else {
|
||||
Text("User has been muted", comment: "Alert message that informs a user was d.")
|
||||
Text("User has been muted", comment: "Alert message that informs a user was muted.")
|
||||
}
|
||||
})
|
||||
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
||||
@@ -544,7 +526,7 @@ struct ContentView: View {
|
||||
guard let ds = damus_state,
|
||||
let keypair = ds.keypair.to_full(),
|
||||
let pubkey = muting,
|
||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey)
|
||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -571,24 +553,25 @@ struct ContentView: View {
|
||||
if ds.contacts.mutelist == nil {
|
||||
confirm_overwrite_mutelist = true
|
||||
} else {
|
||||
guard let keypair = ds.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
guard let pubkey = muting else {
|
||||
guard let keypair = ds.keypair.to_full(),
|
||||
let pubkey = muting
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
|
||||
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state?.contacts.set_mutelist(ev)
|
||||
ds.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
}, message: {
|
||||
if let pubkey = muting {
|
||||
let profile = damus_state?.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in
|
||||
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||
}).value ?? "unknown"
|
||||
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||
} else {
|
||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||
@@ -598,12 +581,12 @@ struct ContentView: View {
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
self.isSideBarOpened = false
|
||||
|
||||
self.popToRoot()
|
||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||
|
||||
|
||||
notify(.switched_timeline(timeline))
|
||||
|
||||
if timeline == self.selected_timeline {
|
||||
NotificationCenter.default.post(name: .scroll_to_top, object: nil)
|
||||
notify(.scroll_to_top)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -611,7 +594,10 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func connect() {
|
||||
let pool = RelayPool()
|
||||
// nostrdb
|
||||
let ndb = Ndb()!
|
||||
|
||||
let pool = RelayPool(ndb: ndb)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
@@ -636,13 +622,14 @@ struct ContentView: View {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
|
||||
|
||||
let user_search_cache = UserSearchCache()
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
profiles: Profiles(user_search_cache: user_search_cache),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
@@ -651,7 +638,7 @@ struct ContentView: View {
|
||||
relay_filters: relay_filters,
|
||||
relay_model_cache: model_cache,
|
||||
drafts: Drafts(),
|
||||
events: EventCache(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
@@ -659,23 +646,48 @@ struct ContentView: View {
|
||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
user_search_cache: user_search_cache
|
||||
music: MusicController(onChange: music_changed),
|
||||
video: VideoController(),
|
||||
ndb: ndb
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
pool.connect()
|
||||
}
|
||||
|
||||
|
||||
func music_changed(_ state: MusicState) {
|
||||
guard let damus_state else { return }
|
||||
switch state {
|
||||
case .playback_state:
|
||||
break
|
||||
case .song(let song):
|
||||
guard let song, let kp = damus_state.keypair.to_full() else { return }
|
||||
|
||||
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
|
||||
|
||||
let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")"
|
||||
let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
let url = encodedDesc.flatMap { enc in
|
||||
URL(string: "spotify:search:\(enc)")
|
||||
}
|
||||
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url)
|
||||
|
||||
pdata.status.music = music
|
||||
|
||||
guard let ev = music.to_note(keypair: kp) else { return }
|
||||
damus_state.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(keypair: Keypair(pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", privkey: nil))
|
||||
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil))
|
||||
}
|
||||
}
|
||||
|
||||
func get_since_time(last_event: NostrEvent?) -> Int64? {
|
||||
func get_since_time(last_event: NostrEvent?) -> UInt32? {
|
||||
if let last_event = last_event {
|
||||
return last_event.created_at - 60 * 10
|
||||
}
|
||||
@@ -695,7 +707,7 @@ extension UINavigationController: UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
struct LastNotification {
|
||||
let id: String
|
||||
let id: NoteId
|
||||
let created_at: Int64
|
||||
}
|
||||
|
||||
@@ -704,29 +716,32 @@ func get_last_event(_ timeline: Timeline) -> LastNotification? {
|
||||
let last = UserDefaults.standard.string(forKey: "last_\(str)")
|
||||
let last_created = UserDefaults.standard.string(forKey: "last_\(str)_time")
|
||||
.flatMap { Int64($0) }
|
||||
|
||||
return last.flatMap { id in
|
||||
last_created.map { created in
|
||||
return LastNotification(id: id, created_at: created)
|
||||
}
|
||||
|
||||
guard let last,
|
||||
let note_id = NoteId(hex: last),
|
||||
let last_created
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return LastNotification(id: note_id, created_at: last_created)
|
||||
}
|
||||
|
||||
func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
|
||||
let str = timeline.rawValue
|
||||
UserDefaults.standard.set(ev.id, forKey: "last_\(str)")
|
||||
UserDefaults.standard.set(ev.id.hex(), forKey: "last_\(str)")
|
||||
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
||||
}
|
||||
|
||||
func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
||||
|
||||
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
||||
|
||||
return filters.map { filter in
|
||||
let kinds = filter.kinds ?? []
|
||||
let initial: Int64? = nil
|
||||
let initial: UInt32? = nil
|
||||
let earliest = kinds.reduce(initial) { earliest, kind in
|
||||
let last = last_of_kind[kind.rawValue]
|
||||
let since: Int64? = get_since_time(last_event: last)
|
||||
|
||||
let since: UInt32? = get_since_time(last_event: last)
|
||||
|
||||
if earliest == nil {
|
||||
if since == nil {
|
||||
return nil
|
||||
@@ -753,7 +768,6 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
|
||||
|
||||
|
||||
func setup_notifications() {
|
||||
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
@@ -772,36 +786,42 @@ struct FindEvent {
|
||||
let type: FindEventType
|
||||
let find_from: [String]?
|
||||
|
||||
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
|
||||
static func profile(pubkey: Pubkey, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
||||
}
|
||||
|
||||
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
|
||||
static func event(evid: NoteId, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .event(evid), find_from: find_from)
|
||||
}
|
||||
}
|
||||
|
||||
enum FindEventType {
|
||||
case profile(String)
|
||||
case event(String)
|
||||
case profile(Pubkey)
|
||||
case event(NoteId)
|
||||
}
|
||||
|
||||
enum FoundEvent {
|
||||
case profile(Profile, NostrEvent)
|
||||
case profile(Pubkey)
|
||||
case invalid_profile(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
||||
|
||||
return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
|
||||
}
|
||||
|
||||
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
|
||||
|
||||
var filter: NostrFilter? = nil
|
||||
let find_from = query_.find_from
|
||||
let query = query_.type
|
||||
|
||||
switch query {
|
||||
case .profile(let pubkey):
|
||||
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
|
||||
callback(.profile(profile.profile, profile.event))
|
||||
if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue,
|
||||
record.profile != nil
|
||||
{
|
||||
callback(.profile(pubkey))
|
||||
return
|
||||
}
|
||||
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
||||
@@ -815,7 +835,6 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
||||
filter = NostrFilter(ids: [evid], limit: 1)
|
||||
}
|
||||
|
||||
let subid = UUID().description
|
||||
var attempts: Int = 0
|
||||
var has_event = false
|
||||
guard let filter else { return }
|
||||
@@ -839,14 +858,11 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
||||
switch query {
|
||||
case .profile:
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
|
||||
guard let profile else {
|
||||
callback(.invalid_profile(ev))
|
||||
return
|
||||
}
|
||||
callback(.profile(profile, ev))
|
||||
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
|
||||
callback(.invalid_profile(ev))
|
||||
return
|
||||
}
|
||||
callback(.profile(ev.pubkey))
|
||||
}
|
||||
case .event:
|
||||
callback(.event(ev))
|
||||
@@ -859,7 +875,7 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
case .notice(_):
|
||||
case .notice:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -883,7 +899,7 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
||||
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
||||
guard let keypair = state.keypair.to_full() else {
|
||||
return false
|
||||
}
|
||||
@@ -895,32 +911,23 @@ func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
notify(.unfollowed, unfollow)
|
||||
notify(.unfollowed(unfollow))
|
||||
|
||||
state.contacts.event = ev
|
||||
|
||||
if unfollow.key == "p" {
|
||||
state.contacts.remove_friend(unfollow.ref_id)
|
||||
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
|
||||
switch unfollow {
|
||||
case .pubkey(let pk):
|
||||
state.contacts.remove_friend(pk)
|
||||
case .hashtag:
|
||||
// nothing to handle here really
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handle_unfollow_notif(state: DamusState, notif: Notification) -> ReferencedId? {
|
||||
let target = notif.object as! FollowTarget
|
||||
let pk = target.pubkey
|
||||
|
||||
let ref = ReferencedId.p(pk)
|
||||
if handle_unfollow(state: state, unfollow: ref) {
|
||||
return ref
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
||||
func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
||||
guard let keypair = state.keypair.to_full() else {
|
||||
return false
|
||||
}
|
||||
@@ -930,41 +937,45 @@ func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
notify(.followed, follow)
|
||||
notify(.followed(follow))
|
||||
|
||||
state.contacts.event = ev
|
||||
if follow.key == "p" {
|
||||
state.contacts.add_friend_pubkey(follow.ref_id)
|
||||
switch follow {
|
||||
case .pubkey(let pubkey):
|
||||
state.contacts.add_friend_pubkey(pubkey)
|
||||
case .hashtag:
|
||||
// nothing to do
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_follow_notif(state: DamusState, notif: Notification) -> Bool {
|
||||
let fnotify = notif.object as! FollowTarget
|
||||
switch fnotify {
|
||||
func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
|
||||
switch target {
|
||||
case .pubkey(let pk):
|
||||
state.contacts.add_friend_pubkey(pk)
|
||||
case .contact(let ev):
|
||||
state.contacts.add_friend_contact(ev)
|
||||
}
|
||||
|
||||
return handle_follow(state: state, follow: .p(fnotify.pubkey))
|
||||
return handle_follow(state: state, follow: target.follow_ref)
|
||||
}
|
||||
|
||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
|
||||
let post_res = notif.object as! NostrPostResult
|
||||
switch post_res {
|
||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool {
|
||||
switch post {
|
||||
case .post(let post):
|
||||
//let post = tup.0
|
||||
//let to_relays = tup.1
|
||||
print("post \(post.content)")
|
||||
let new_ev = post_to_event(post: post, privkey: keypair.privkey, pubkey: keypair.pubkey)
|
||||
guard let new_ev = post_to_event(post: post, keypair: keypair) else {
|
||||
return false
|
||||
}
|
||||
postbox.send(new_ev)
|
||||
for eref in new_ev.referenced_ids.prefix(3) {
|
||||
// also broadcast at most 3 referenced events
|
||||
if let ev = events.lookup(eref.ref_id) {
|
||||
if let ev = events.lookup(eref) {
|
||||
postbox.send(ev)
|
||||
}
|
||||
}
|
||||
@@ -977,7 +988,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
||||
|
||||
|
||||
enum OpenResult {
|
||||
case profile(String)
|
||||
case profile(Pubkey)
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
@@ -997,13 +1008,19 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
||||
|
||||
switch link {
|
||||
case .ref(let ref):
|
||||
if ref.key == "p" {
|
||||
result(.profile(ref.ref_id))
|
||||
} else if ref.key == "e" {
|
||||
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
result(.profile(pk))
|
||||
case .event(let noteid):
|
||||
find_event(state: state, query: .event(evid: noteid)) { res in
|
||||
guard let res, case .event(let ev) = res else { return }
|
||||
result(.event(ev))
|
||||
}
|
||||
case .hashtag(let ht):
|
||||
result(.filter(.filter_hashtag([ht.string()])))
|
||||
case .param, .quote:
|
||||
// doesn't really make sense here
|
||||
break
|
||||
}
|
||||
case .filter(let filt):
|
||||
result(.filter(filt))
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Damus needs access to your media library for playback statuses</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||
</dict>
|
||||
|
||||
@@ -28,19 +28,7 @@ class ActionBarModel: ObservableObject {
|
||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
}
|
||||
|
||||
init() {
|
||||
self.our_like = nil
|
||||
self.our_boost = nil
|
||||
self.our_reply = nil
|
||||
self.our_zap = nil
|
||||
self.likes = 0
|
||||
self.boosts = 0
|
||||
self.zaps = 0
|
||||
self.zap_total = 0
|
||||
self.replies = 0
|
||||
}
|
||||
|
||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) {
|
||||
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) {
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.zaps = zaps
|
||||
@@ -52,7 +40,7 @@ class ActionBarModel: ObservableObject {
|
||||
self.our_reply = our_reply
|
||||
}
|
||||
|
||||
func update(damus: DamusState, evid: String) {
|
||||
func update(damus: DamusState, evid: NoteId) {
|
||||
self.likes = damus.likes.counts[evid] ?? 0
|
||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func get_bookmarks_key(pubkey: String) -> String {
|
||||
fileprivate func get_bookmarks_key(pubkey: Pubkey) -> String {
|
||||
pk_setting_key(pubkey, key: "bookmarks")
|
||||
}
|
||||
|
||||
func load_bookmarks(pubkey: String) -> [NostrEvent] {
|
||||
func load_bookmarks(pubkey: Pubkey) -> [NostrEvent] {
|
||||
let key = get_bookmarks_key(pubkey: pubkey)
|
||||
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
|
||||
event_from_json(dat: $0)
|
||||
}
|
||||
}
|
||||
|
||||
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||
func save_bookmarks(pubkey: Pubkey, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||
let uniq_bookmarks = uniq(value)
|
||||
|
||||
if uniq_bookmarks != current_value {
|
||||
@@ -32,8 +32,8 @@ func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEv
|
||||
|
||||
class BookmarksManager: ObservableObject {
|
||||
|
||||
private let pubkey: String
|
||||
|
||||
private let pubkey: Pubkey
|
||||
|
||||
private var _bookmarks: [NostrEvent]
|
||||
var bookmarks: [NostrEvent] {
|
||||
get {
|
||||
@@ -47,7 +47,7 @@ class BookmarksManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
init(pubkey: String) {
|
||||
init(pubkey: Pubkey) {
|
||||
self._bookmarks = load_bookmarks(pubkey: pubkey)
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ImageResizer.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Suhail Saqan on 8/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum ImageResizingError: Error {
|
||||
case cannotRetrieveFromURL
|
||||
case cannotRetrieveFromData
|
||||
}
|
||||
|
||||
public struct ImageResizer {
|
||||
public var targetWidth: CGFloat
|
||||
|
||||
public init(targetWidth: CGFloat) {
|
||||
self.targetWidth = targetWidth
|
||||
}
|
||||
|
||||
public func resize(at url: URL) -> UIImage? {
|
||||
guard let image = UIImage(contentsOfFile: url.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.resize(image: image)
|
||||
}
|
||||
|
||||
public func resize(image: UIImage) -> UIImage {
|
||||
let originalSize = image.size
|
||||
let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width)
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
return renderer.image { (context) in
|
||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
-82
@@ -9,56 +9,56 @@ import Foundation
|
||||
|
||||
|
||||
class Contacts {
|
||||
private var friends: Set<String> = Set()
|
||||
private var friend_of_friends: Set<String> = Set()
|
||||
private var friends: Set<Pubkey> = Set()
|
||||
private var friend_of_friends: Set<Pubkey> = Set()
|
||||
/// Tracks which friends are friends of a given pubkey.
|
||||
private var pubkey_to_our_friends = [String : Set<String>]()
|
||||
private var muted: Set<String> = Set()
|
||||
|
||||
let our_pubkey: String
|
||||
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||
private var muted: Set<Pubkey> = Set()
|
||||
|
||||
let our_pubkey: Pubkey
|
||||
var event: NostrEvent?
|
||||
var mutelist: NostrEvent?
|
||||
|
||||
init(our_pubkey: String) {
|
||||
init(our_pubkey: Pubkey) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
func is_muted(_ pk: String) -> Bool {
|
||||
func is_muted(_ pk: Pubkey) -> Bool {
|
||||
return muted.contains(pk)
|
||||
}
|
||||
|
||||
func set_mutelist(_ ev: NostrEvent) {
|
||||
let oldlist = self.mutelist
|
||||
self.mutelist = ev
|
||||
|
||||
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
|
||||
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
||||
|
||||
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
|
||||
let new = Set(ev.referenced_pubkeys)
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
var new_mutes = Array<String>()
|
||||
var new_unmutes = Array<String>()
|
||||
|
||||
var new_mutes = Set<Pubkey>()
|
||||
var new_unmutes = Set<Pubkey>()
|
||||
|
||||
for d in diff {
|
||||
if new.contains(d) {
|
||||
new_mutes.append(d)
|
||||
new_mutes.insert(d)
|
||||
} else {
|
||||
new_unmutes.append(d)
|
||||
new_unmutes.insert(d)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: set local mutelist here
|
||||
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
||||
|
||||
self.muted = Set(ev.referenced_pubkeys)
|
||||
|
||||
if new_mutes.count > 0 {
|
||||
notify(.new_mutes, new_mutes)
|
||||
notify(.new_mutes(new_mutes))
|
||||
}
|
||||
|
||||
if new_unmutes.count > 0 {
|
||||
notify(.new_unmutes, new_unmutes)
|
||||
notify(.new_unmutes(new_unmutes))
|
||||
}
|
||||
}
|
||||
|
||||
func remove_friend(_ pubkey: String) {
|
||||
func remove_friend(_ pubkey: Pubkey) {
|
||||
friends.remove(pubkey)
|
||||
|
||||
pubkey_to_our_friends.forEach {
|
||||
@@ -66,107 +66,105 @@ class Contacts {
|
||||
}
|
||||
}
|
||||
|
||||
func get_friend_list() -> Set<String> {
|
||||
func get_friend_list() -> Set<Pubkey> {
|
||||
return friends
|
||||
}
|
||||
|
||||
func get_followed_hashtags() -> Set<String> {
|
||||
guard let ev = self.event else { return Set() }
|
||||
return ev.tags.reduce(into: Set<String>(), { htags, tag in
|
||||
if tag.count >= 2 && tag[0] == "t" && tag[1] != "" {
|
||||
htags.insert(tag[1])
|
||||
}
|
||||
})
|
||||
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
||||
}
|
||||
|
||||
func follows(hashtag: Hashtag) -> Bool {
|
||||
guard let ev = self.event else { return false }
|
||||
return ev.referenced_hashtags.first(where: { $0 == hashtag }) != nil
|
||||
}
|
||||
|
||||
func add_friend_pubkey(_ pubkey: String) {
|
||||
func add_friend_pubkey(_ pubkey: Pubkey) {
|
||||
friends.insert(pubkey)
|
||||
}
|
||||
|
||||
func add_friend_contact(_ contact: NostrEvent) {
|
||||
friends.insert(contact.pubkey)
|
||||
for tag in contact.tags {
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
friend_of_friends.insert(tag[1])
|
||||
for pk in contact.referenced_pubkeys {
|
||||
friend_of_friends.insert(pk)
|
||||
|
||||
// Exclude themself and us.
|
||||
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
|
||||
if pubkey_to_our_friends[tag[1]] == nil {
|
||||
pubkey_to_our_friends[tag[1]] = Set<String>()
|
||||
}
|
||||
|
||||
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
|
||||
// Exclude themself and us.
|
||||
if contact.pubkey != our_pubkey && contact.pubkey != pk {
|
||||
if pubkey_to_our_friends[pk] == nil {
|
||||
pubkey_to_our_friends[pk] = Set<Pubkey>()
|
||||
}
|
||||
|
||||
pubkey_to_our_friends[pk]?.insert(contact.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func is_friend_of_friend(_ pubkey: String) -> Bool {
|
||||
func is_friend_of_friend(_ pubkey: Pubkey) -> Bool {
|
||||
return friend_of_friends.contains(pubkey)
|
||||
}
|
||||
|
||||
func is_in_friendosphere(_ pubkey: String) -> Bool {
|
||||
func is_in_friendosphere(_ pubkey: Pubkey) -> Bool {
|
||||
return friends.contains(pubkey) || friend_of_friends.contains(pubkey)
|
||||
}
|
||||
|
||||
func is_friend(_ pubkey: String) -> Bool {
|
||||
func is_friend(_ pubkey: Pubkey) -> Bool {
|
||||
return friends.contains(pubkey)
|
||||
}
|
||||
|
||||
func is_friend_or_self(_ pubkey: String) -> Bool {
|
||||
func is_friend_or_self(_ pubkey: Pubkey) -> Bool {
|
||||
return pubkey == our_pubkey || is_friend(pubkey)
|
||||
}
|
||||
|
||||
func follow_state(_ pubkey: String) -> FollowState {
|
||||
func follow_state(_ pubkey: Pubkey) -> FollowState {
|
||||
return is_friend(pubkey) ? .follows : .unfollows
|
||||
}
|
||||
|
||||
/// Gets the list of pubkeys of our friends who follow the given pubkey.
|
||||
func get_friended_followers(_ pubkey: String) -> [String] {
|
||||
func get_friended_followers(_ pubkey: Pubkey) -> [Pubkey] {
|
||||
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
||||
}
|
||||
}
|
||||
|
||||
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? {
|
||||
guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: keypair.pubkey, follow: follow) else {
|
||||
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: keypair.privkey)
|
||||
|
||||
box.send(ev)
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? {
|
||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||
guard let cs = our_contacts else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let ev = unfollow_reference_event(our_contacts: cs, our_pubkey: keypair.pubkey, unfollow: unfollow)
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: keypair.privkey)
|
||||
guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
postbox.send(ev)
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
func unfollow_reference_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: ReferencedId) -> NostrEvent {
|
||||
let tags = our_contacts.tags.filter { tag in
|
||||
if tag.count >= 2 && tag[0] == unfollow.key && tag[1] == unfollow.ref_id {
|
||||
return false
|
||||
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
|
||||
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
|
||||
return
|
||||
}
|
||||
return true
|
||||
|
||||
ts.append(tag.strings())
|
||||
}
|
||||
|
||||
|
||||
let kind = NostrKind.contacts.rawValue
|
||||
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
|
||||
|
||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
|
||||
}
|
||||
|
||||
func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: ReferencedId) -> NostrEvent? {
|
||||
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||
guard let cs = our_contacts else {
|
||||
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
|
||||
// we should only create contacts during profile creation
|
||||
@@ -174,7 +172,7 @@ func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: Re
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ev = follow_with_existing_contacts(our_pubkey: our_pubkey, our_contacts: cs, follow: follow) else {
|
||||
guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -186,23 +184,18 @@ func decode_json_relays(_ content: String) -> [String: RelayInfo]? {
|
||||
return decode_json(content)
|
||||
}
|
||||
|
||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], privkey: String, relay: String) -> NostrEvent? {
|
||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: String) -> NostrEvent?{
|
||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||
|
||||
relays.removeValue(forKey: relay)
|
||||
|
||||
print("remove_relay \(relays)")
|
||||
guard let content = encode_json(relays) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
|
||||
new_ev.calculate_id()
|
||||
new_ev.sign(privkey: privkey)
|
||||
return new_ev
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||
}
|
||||
|
||||
func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
|
||||
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
|
||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||
|
||||
guard relays.index(forKey: relay) == nil else {
|
||||
@@ -215,10 +208,7 @@ func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor
|
||||
return nil
|
||||
}
|
||||
|
||||
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
|
||||
new_ev.calculate_id()
|
||||
new_ev.sign(privkey: privkey)
|
||||
return new_ev
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||
}
|
||||
|
||||
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] {
|
||||
@@ -228,20 +218,31 @@ func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: R
|
||||
return relay_info
|
||||
}
|
||||
|
||||
func is_already_following(contacts: NostrEvent, follow: ReferencedId) -> Bool {
|
||||
return contacts.references(id: follow.ref_id, key: follow.key)
|
||||
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||
return contacts.references.contains { ref in
|
||||
switch (ref, follow) {
|
||||
case let (.hashtag(ht), .hashtag(follow_ht)):
|
||||
return ht.string() == follow_ht
|
||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||
return pk == follow_pk
|
||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||
(.event, _), (.quote, _), (.param, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
|
||||
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
|
||||
// don't update if we're already following
|
||||
if is_already_following(contacts: our_contacts, follow: follow) {
|
||||
return nil
|
||||
}
|
||||
|
||||
let kind = NostrKind.contacts.rawValue
|
||||
var tags = our_contacts.tags
|
||||
tags.append(refid_to_tag(follow))
|
||||
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
|
||||
|
||||
var tags = our_contacts.tags.strings()
|
||||
tags.append(follow.tag)
|
||||
|
||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||
}
|
||||
|
||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ContentFilters.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-09-18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||
enum FilterState : Int {
|
||||
case posts_and_replies = 1
|
||||
case posts = 0
|
||||
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
case .posts:
|
||||
return ev.known_kind == .boost || !ev.is_reply(.empty)
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple filter to determine whether to show posts with #nsfw tags
|
||||
func nsfw_tag_filter(ev: NostrEvent) -> Bool {
|
||||
return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil
|
||||
}
|
||||
|
||||
/// Generic filter with various tweakable settings
|
||||
struct ContentFilters {
|
||||
var filters: [(NostrEvent) -> Bool]
|
||||
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
for filter in filters {
|
||||
if !filter(ev) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentFilters {
|
||||
static func defaults(_ settings: UserSettingsStore) -> [(NostrEvent) -> Bool] {
|
||||
var filters = Array<(NostrEvent) -> Bool>()
|
||||
if settings.hide_nsfw_tagged_content {
|
||||
filters.append(nsfw_tag_filter)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,10 @@ class CreateAccountModel: ObservableObject {
|
||||
@Published var real_name: String = ""
|
||||
@Published var nick_name: String = ""
|
||||
@Published var about: String = ""
|
||||
@Published var pubkey: String = ""
|
||||
@Published var privkey: String = ""
|
||||
@Published var pubkey: Pubkey = .empty
|
||||
@Published var privkey: Privkey = .empty
|
||||
@Published var profile_image: URL? = nil
|
||||
|
||||
var pubkey_bech32: String {
|
||||
return bech32_pubkey(self.pubkey) ?? ""
|
||||
}
|
||||
|
||||
var privkey_bech32: String {
|
||||
return bech32_privkey(self.privkey) ?? ""
|
||||
}
|
||||
|
||||
var rendered_name: String {
|
||||
if real_name.isEmpty {
|
||||
return nick_name
|
||||
@@ -35,17 +27,11 @@ class CreateAccountModel: ObservableObject {
|
||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||
}
|
||||
|
||||
init() {
|
||||
init(real: String = "", nick: String = "", about: String = "") {
|
||||
let keypair = generate_new_keypair()
|
||||
self.pubkey = keypair.pubkey
|
||||
self.privkey = keypair.privkey!
|
||||
}
|
||||
|
||||
init(real: String, nick: String, about: String) {
|
||||
let keypair = generate_new_keypair()
|
||||
self.pubkey = keypair.pubkey
|
||||
self.privkey = keypair.privkey!
|
||||
|
||||
self.privkey = keypair.privkey
|
||||
|
||||
self.real_name = real
|
||||
self.nick_name = nick
|
||||
self.about = about
|
||||
|
||||
@@ -31,8 +31,10 @@ struct DamusState {
|
||||
let muted_threads: MutedThreadsManager
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let user_search_cache: UserSearchCache
|
||||
|
||||
let music: MusicController?
|
||||
let video: VideoController
|
||||
let ndb: Ndb
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
// store generic zap mapping
|
||||
@@ -42,15 +44,15 @@ struct DamusState {
|
||||
// thread zaps
|
||||
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
|
||||
// [nozaps]: thread zaps are only available outside of the app store
|
||||
replies.count_replies(ev)
|
||||
events.add_replies(ev: ev)
|
||||
replies.count_replies(ev, keypair: self.keypair)
|
||||
events.add_replies(ev: ev, keypair: self.keypair)
|
||||
}
|
||||
|
||||
// associate with events as well
|
||||
return stored
|
||||
}
|
||||
|
||||
var pubkey: String {
|
||||
var pubkey: Pubkey {
|
||||
return keypair.pubkey
|
||||
}
|
||||
|
||||
@@ -59,6 +61,36 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
let user_search_cache = UserSearchCache()
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(user_search_cache: user_search_cache), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_model_cache: RelayModelCache(), 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()), nav: NavigationCoordinator(), user_search_cache: user_search_cache) }
|
||||
let empty_pub: Pubkey = .empty
|
||||
let empty_sec: Privkey = .empty
|
||||
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||
|
||||
return DamusState.init(
|
||||
pool: RelayPool(ndb: .empty),
|
||||
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
contacts: Contacts(our_pubkey: empty_pub),
|
||||
profiles: Profiles(ndb: .empty),
|
||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: empty_pub),
|
||||
lnurls: LNUrls(),
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: RelayFilters(our_pubkey: empty_pub),
|
||||
relay_model_cache: RelayModelCache(),
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: .empty),
|
||||
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||
bootstrap_relays: [],
|
||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||
muted_threads: MutedThreadsManager(keypair: kp),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
music: nil,
|
||||
video: VideoController(),
|
||||
ndb: .empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ class DirectMessageModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
let pubkey: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
@Published var draft: String = ""
|
||||
|
||||
let pubkey: Pubkey
|
||||
|
||||
var is_request = false
|
||||
var our_pubkey: Pubkey
|
||||
|
||||
func determine_is_request() -> Bool {
|
||||
for event in events {
|
||||
if event.pubkey == our_pubkey {
|
||||
@@ -31,19 +31,9 @@ class DirectMessageModel: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
|
||||
init(events: [NostrEvent] = [], our_pubkey: Pubkey, pubkey: Pubkey) {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
init(our_pubkey: String, pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ class DirectMessagesModel: ObservableObject {
|
||||
@Published var dms: [DirectMessageModel] = []
|
||||
@Published var loading: Bool = false
|
||||
@Published var open_dm: Bool = false
|
||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
|
||||
let our_pubkey: String
|
||||
|
||||
init(our_pubkey: String) {
|
||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: .empty, pubkey: .empty)
|
||||
let our_pubkey: Pubkey
|
||||
|
||||
init(our_pubkey: Pubkey) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ class DirectMessagesModel: ObservableObject {
|
||||
self.active_model = model
|
||||
}
|
||||
|
||||
func set_active_dm(_ pubkey: String) {
|
||||
func set_active_dm(_ pubkey: Pubkey) {
|
||||
for model in self.dms where model.pubkey == pubkey {
|
||||
self.set_active_dm_model(model)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||
func lookup_or_create(_ pubkey: Pubkey) -> DirectMessageModel {
|
||||
if let dm = lookup(pubkey) {
|
||||
return dm
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class DirectMessagesModel: ObservableObject {
|
||||
return new
|
||||
}
|
||||
|
||||
func lookup(_ pubkey: String) -> DirectMessageModel? {
|
||||
func lookup(_ pubkey: Pubkey) -> DirectMessageModel? {
|
||||
for dm in dms {
|
||||
if pubkey == dm.pubkey {
|
||||
return dm
|
||||
|
||||
@@ -11,12 +11,7 @@ class DraftArtifacts {
|
||||
var content: NSMutableAttributedString
|
||||
var media: [UploadedMedia]
|
||||
|
||||
init() {
|
||||
self.content = NSMutableAttributedString(string: "")
|
||||
self.media = []
|
||||
}
|
||||
|
||||
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
|
||||
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = []) {
|
||||
self.content = content
|
||||
self.media = media
|
||||
}
|
||||
|
||||
+25
-31
@@ -7,20 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EventRef {
|
||||
case mention(Mention)
|
||||
case thread_id(ReferencedId)
|
||||
case reply(ReferencedId)
|
||||
case reply_to_root(ReferencedId)
|
||||
|
||||
var is_mention: Mention? {
|
||||
if case .mention(let m) = self {
|
||||
return m
|
||||
}
|
||||
enum EventRef: Equatable {
|
||||
case mention(Mention<NoteRef>)
|
||||
case thread_id(NoteRef)
|
||||
case reply(NoteRef)
|
||||
case reply_to_root(NoteRef)
|
||||
|
||||
var is_mention: NoteRef? {
|
||||
if case .mention(let m) = self { return m.ref }
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_direct_reply: ReferencedId? {
|
||||
var is_direct_reply: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
@@ -33,7 +31,7 @@ enum EventRef {
|
||||
}
|
||||
}
|
||||
|
||||
var is_thread_id: ReferencedId? {
|
||||
var is_thread_id: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
@@ -46,7 +44,7 @@ enum EventRef {
|
||||
}
|
||||
}
|
||||
|
||||
var is_reply: ReferencedId? {
|
||||
var is_reply: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
@@ -64,10 +62,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
return blocks.reduce(into: []) { acc, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if m.type == type {
|
||||
if let idx = m.index {
|
||||
acc.insert(idx)
|
||||
}
|
||||
if m.ref.key == type, let idx = m.index {
|
||||
acc.insert(idx)
|
||||
}
|
||||
case .relay:
|
||||
return
|
||||
@@ -83,7 +79,7 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
}
|
||||
}
|
||||
|
||||
func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
|
||||
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
|
||||
if refs.count == 0 {
|
||||
return []
|
||||
}
|
||||
@@ -105,16 +101,15 @@ func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
|
||||
return evrefs
|
||||
}
|
||||
|
||||
func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>) -> [EventRef] {
|
||||
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
|
||||
var mentions: [EventRef] = []
|
||||
var ev_refs: [ReferencedId] = []
|
||||
var ev_refs: [NoteRef] = []
|
||||
var i: Int = 0
|
||||
|
||||
|
||||
for tag in tags {
|
||||
if tag.count >= 2 && tag[0] == "e" {
|
||||
let ref = tag_to_refid(tag)!
|
||||
if let ref = NoteRef.from_tag(tag: tag) {
|
||||
if mention_indices.contains(i) {
|
||||
let mention = Mention(index: i, type: .event, ref: ref)
|
||||
let mention = Mention<NoteRef>(index: i, ref: ref)
|
||||
mentions.append(.mention(mention))
|
||||
} else {
|
||||
ev_refs.append(ref)
|
||||
@@ -128,26 +123,25 @@ func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>
|
||||
return replies
|
||||
}
|
||||
|
||||
func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
|
||||
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
|
||||
if tags.count == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
/// build a set of indices for each event mention
|
||||
let mention_indices = build_mention_indices(blocks, type: .event)
|
||||
|
||||
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||
|
||||
/// simpler case with no mentions
|
||||
if mention_indices.count == 0 {
|
||||
let ev_refs = get_referenced_ids(tags: tags, key: "e")
|
||||
return interp_event_refs_without_mentions(ev_refs)
|
||||
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
||||
}
|
||||
|
||||
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
|
||||
}
|
||||
|
||||
|
||||
func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
return ev.event_refs(privkey).contains { evref in
|
||||
func event_is_reply(_ refs: [EventRef]) -> Bool {
|
||||
return refs.contains { evref in
|
||||
return evref.is_reply != nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import Foundation
|
||||
|
||||
class EventsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
let target: String
|
||||
let target: NoteId
|
||||
let kind: NostrKind
|
||||
let sub_id = UUID().uuidString
|
||||
let profiles_id = UUID().uuidString
|
||||
|
||||
@Published var events: [NostrEvent] = []
|
||||
|
||||
init(state: DamusState, target: String, kind: NostrKind) {
|
||||
init(state: DamusState, target: NoteId, kind: NostrKind) {
|
||||
self.state = state
|
||||
self.target = target
|
||||
self.kind = kind
|
||||
@@ -41,14 +41,11 @@ class EventsModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func handle_event(relay_id: String, ev: NostrEvent) {
|
||||
guard ev.kind == kind.rawValue else {
|
||||
guard ev.kind == kind.rawValue,
|
||||
ev.referenced_ids.last == target else {
|
||||
return
|
||||
}
|
||||
|
||||
guard last_etag(tags: ev.tags) == target else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
@@ -62,11 +59,11 @@ class EventsModel: ObservableObject {
|
||||
switch nev {
|
||||
case .event(_, let ev):
|
||||
handle_event(relay_id: relay_id, ev: ev)
|
||||
case .notice(_):
|
||||
case .notice:
|
||||
break
|
||||
case .ok:
|
||||
break
|
||||
case .eose(_):
|
||||
case .eose:
|
||||
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
enum FollowTarget {
|
||||
case pubkey(String)
|
||||
case pubkey(Pubkey)
|
||||
case contact(NostrEvent)
|
||||
|
||||
var pubkey: String {
|
||||
|
||||
var follow_ref: FollowRef {
|
||||
FollowRef.pubkey(pubkey)
|
||||
}
|
||||
|
||||
var pubkey: Pubkey {
|
||||
switch self {
|
||||
case .pubkey(let pk):
|
||||
return pk
|
||||
case .contact(let ev):
|
||||
return ev.pubkey
|
||||
case .pubkey(let pk): return pk
|
||||
case .contact(let ev): return ev.pubkey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import Foundation
|
||||
|
||||
class FollowersModel: ObservableObject {
|
||||
let damus_state: DamusState
|
||||
let target: String
|
||||
|
||||
@Published var contacts: [String]? = nil
|
||||
var has_contact: Set<String> = Set()
|
||||
|
||||
let target: Pubkey
|
||||
|
||||
@Published var contacts: [Pubkey]? = nil
|
||||
var has_contact: Set<Pubkey> = Set()
|
||||
|
||||
let sub_id: String = UUID().description
|
||||
let profiles_id: String = UUID().description
|
||||
|
||||
@@ -24,20 +24,19 @@ class FollowersModel: ObservableObject {
|
||||
return contacts.count
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, target: String) {
|
||||
init(damus_state: DamusState, target: Pubkey) {
|
||||
self.damus_state = damus_state
|
||||
self.target = target
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
NostrFilter(kinds: [.contacts],
|
||||
pubkeys: [target])
|
||||
NostrFilter(kinds: [.contacts], pubkeys: [target])
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
let filter = get_filter()
|
||||
let filters = [filter]
|
||||
print_filters(relay_id: "following", filters: [filters])
|
||||
//print_filters(relay_id: "following", filters: [filters])
|
||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
}
|
||||
|
||||
@@ -78,10 +77,7 @@ class FollowersModel: ObservableObject {
|
||||
|
||||
if ev.known_kind == .contacts {
|
||||
handle_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
case .notice(let msg):
|
||||
print("followingmodel notice: \(msg)")
|
||||
|
||||
|
||||
@@ -11,18 +11,20 @@ class FollowingModel {
|
||||
let damus_state: DamusState
|
||||
var needs_sub: Bool = true
|
||||
|
||||
let contacts: [String]
|
||||
|
||||
let contacts: [Pubkey]
|
||||
let hashtags: [Hashtag]
|
||||
|
||||
let sub_id: String = UUID().description
|
||||
|
||||
init(damus_state: DamusState, contacts: [String]) {
|
||||
init(damus_state: DamusState, contacts: [Pubkey], hashtags: [Hashtag]) {
|
||||
self.damus_state = damus_state
|
||||
self.contacts = contacts
|
||||
self.hashtags = hashtags
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var f = NostrFilter(kinds: [.metadata])
|
||||
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
|
||||
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in
|
||||
// don't fetch profiles we already have
|
||||
if damus_state.profiles.has_fresh_profile(id: pk) {
|
||||
return
|
||||
@@ -39,7 +41,7 @@ class FollowingModel {
|
||||
return
|
||||
}
|
||||
let filters = [filter]
|
||||
print_filters(relay_id: "following", filters: [filters])
|
||||
//print_filters(relay_id: "following", filters: [filters])
|
||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
}
|
||||
|
||||
@@ -52,22 +54,6 @@ class FollowingModel {
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||
switch ev {
|
||||
case .ws_event:
|
||||
break
|
||||
case .nostr_event(let nev):
|
||||
switch nev {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
if ev.kind == 0 {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
case .notice(let msg):
|
||||
print("followingmodel notice: \(msg)")
|
||||
case .eose:
|
||||
break
|
||||
}
|
||||
}
|
||||
// don't need to do anything here really
|
||||
}
|
||||
}
|
||||
|
||||
+188
-230
@@ -25,20 +25,17 @@ struct NewEventsBits: OptionSet {
|
||||
|
||||
enum Resubscribe {
|
||||
case following
|
||||
case unfollowing(ReferencedId)
|
||||
case unfollowing(FollowRef)
|
||||
}
|
||||
|
||||
enum HomeResubFilter {
|
||||
case pubkey(String)
|
||||
case pubkey(Pubkey)
|
||||
case hashtag(String)
|
||||
|
||||
init?(from: ReferencedId) {
|
||||
if from.key == "p" {
|
||||
self = .pubkey(from.ref_id)
|
||||
return
|
||||
} else if from.key == "t" {
|
||||
self = .hashtag(from.ref_id)
|
||||
return
|
||||
init?(from: FollowRef) {
|
||||
switch from {
|
||||
case .hashtag(let ht): self = .hashtag(ht.string())
|
||||
case .pubkey(let pk): self = .pubkey(pk)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -52,7 +49,9 @@ enum HomeResubFilter {
|
||||
if contacts.is_friend(ev.pubkey) {
|
||||
return false
|
||||
}
|
||||
return ev.references(id: ht, key: "t")
|
||||
return ev.referenced_hashtags.contains(where: { ref_ht in
|
||||
ht == ref_ht.hashtag
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,10 +62,10 @@ class HomeModel {
|
||||
|
||||
var damus_state: DamusState
|
||||
|
||||
var has_event: [String: Set<String>] = [:]
|
||||
var deleted_events: Set<String> = Set()
|
||||
var channels: [String: NostrEvent] = [:]
|
||||
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
||||
// NDBTODO: let's get rid of this entirely, let nostrdb handle it
|
||||
var has_event: [String: Set<NoteId>] = [:]
|
||||
var deleted_events: Set<NoteId> = Set()
|
||||
var last_event_of_kind: [String: [UInt32: NostrEvent]] = [:]
|
||||
var done_init: Bool = false
|
||||
var incoming_dms: [NostrEvent] = []
|
||||
let dm_debouncer = Debouncer(interval: 0.5)
|
||||
@@ -109,7 +108,7 @@ class HomeModel {
|
||||
return damus_state.dms
|
||||
}
|
||||
|
||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
|
||||
if !has_event.keys.contains(sub_id) {
|
||||
has_event[sub_id] = Set()
|
||||
return false
|
||||
@@ -150,6 +149,7 @@ class HomeModel {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||
return
|
||||
@@ -165,14 +165,13 @@ class HomeModel {
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case .chat: fallthrough
|
||||
case .longform: fallthrough
|
||||
case .text:
|
||||
case .chat, .longform, .text:
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
case .contacts:
|
||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||
case .metadata:
|
||||
handle_metadata_event(ev)
|
||||
// profile metadata processing is handled by nostrdb
|
||||
break
|
||||
case .list:
|
||||
handle_list_event(ev)
|
||||
case .boost:
|
||||
@@ -183,10 +182,6 @@ class HomeModel {
|
||||
handle_dm(ev)
|
||||
case .delete:
|
||||
handle_delete_event(ev)
|
||||
case .channel_create:
|
||||
handle_channel_create(ev)
|
||||
case .channel_meta:
|
||||
break
|
||||
case .zap:
|
||||
handle_zap_event(ev)
|
||||
case .zap_request:
|
||||
@@ -195,9 +190,40 @@ class HomeModel {
|
||||
break
|
||||
case .nwc_response:
|
||||
handle_nwc_response(ev, relay: relay_id)
|
||||
case .http_auth:
|
||||
break
|
||||
case .status:
|
||||
handle_status_event(ev)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func handle_status_event(_ ev: NostrEvent) {
|
||||
guard let st = UserStatus(ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
// don't process expired events
|
||||
if let expires = st.expires_at, Date.now >= expires {
|
||||
return
|
||||
}
|
||||
|
||||
let pdata = damus_state.profiles.profile_data(ev.pubkey)
|
||||
|
||||
// don't use old events
|
||||
if st.type == .music,
|
||||
let music = pdata.status.music,
|
||||
ev.created_at < music.created_at {
|
||||
return
|
||||
} else if st.type == .general,
|
||||
let general = pdata.status.general,
|
||||
ev.created_at < general.created_at {
|
||||
return
|
||||
}
|
||||
|
||||
pdata.status.update_status(st)
|
||||
}
|
||||
|
||||
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
|
||||
Task { @MainActor in
|
||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||
@@ -225,12 +251,13 @@ class HomeModel {
|
||||
nwc_success(state: self.damus_state, resp: resp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func handle_zap_event(_ ev: NostrEvent) {
|
||||
process_zap_event(damus_state: damus_state, ev: ev) { zapres in
|
||||
guard case .done(let zap) = zapres else { return }
|
||||
|
||||
guard zap.target.pubkey == self.damus_state.keypair.pubkey else {
|
||||
guard case .done(let zap) = zapres,
|
||||
zap.target.pubkey == self.damus_state.keypair.pubkey,
|
||||
should_show_event(keypair: self.damus_state.keypair, hellthreads: self.damus_state.muted_threads, contacts: self.damus_state.contacts, ev: zap.request.ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -262,10 +289,6 @@ class HomeModel {
|
||||
|
||||
}
|
||||
|
||||
func handle_channel_create(_ ev: NostrEvent) {
|
||||
self.channels[ev.id] = ev
|
||||
}
|
||||
|
||||
func filter_events() {
|
||||
events.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
@@ -280,7 +303,7 @@ class HomeModel {
|
||||
return false
|
||||
}
|
||||
|
||||
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
|
||||
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,13 +324,12 @@ class HomeModel {
|
||||
}
|
||||
|
||||
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
|
||||
var boost_ev_id = ev.last_refid()?.ref_id
|
||||
var boost_ev_id = ev.last_refid()
|
||||
|
||||
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
|
||||
Task.init {
|
||||
|
||||
Task {
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
@@ -318,7 +340,6 @@ class HomeModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
guard let e = boost_ev_id else {
|
||||
@@ -330,8 +351,8 @@ class HomeModel {
|
||||
break
|
||||
case .success(let n):
|
||||
let boosted = Counted(event: ev, id: e, total: n)
|
||||
notify(.boosted, boosted)
|
||||
notify(.update_stats, e)
|
||||
notify(.reposted(boosted))
|
||||
notify(.update_stats(note_id: e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,18 +366,18 @@ class HomeModel {
|
||||
return
|
||||
}
|
||||
|
||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||
switch damus_state.likes.add_event(ev, target: e) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
handle_notification(ev: ev)
|
||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||
notify(.liked, liked)
|
||||
notify(.update_stats, e.ref_id)
|
||||
let liked = Counted(event: ev, id: e, total: n)
|
||||
notify(.liked(liked))
|
||||
notify(.update_stats(note_id: e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
|
||||
switch conn_event {
|
||||
case .ws_event(let ev):
|
||||
@@ -496,7 +517,7 @@ class HomeModel {
|
||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
|
||||
}
|
||||
|
||||
func get_last_of_kind(relay_id: String?) -> [Int: NostrEvent] {
|
||||
func get_last_of_kind(relay_id: String?) -> [UInt32: NostrEvent] {
|
||||
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||
}
|
||||
|
||||
@@ -504,13 +525,13 @@ class HomeModel {
|
||||
pool.send(.unsubscribe(home_subid))
|
||||
}
|
||||
|
||||
func get_friends() -> [String] {
|
||||
func get_friends() -> [Pubkey] {
|
||||
var friends = damus_state.contacts.get_friend_list()
|
||||
friends.insert(damus_state.pubkey)
|
||||
return Array(friends)
|
||||
}
|
||||
|
||||
func subscribe_to_home_filters(friends fs: [String]? = nil, relay_id: String? = nil) {
|
||||
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: String? = nil) {
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text, .longform, .boost
|
||||
@@ -519,6 +540,11 @@ class HomeModel {
|
||||
home_filter_kinds.append(.like)
|
||||
}
|
||||
|
||||
// only pull status data if we care for it
|
||||
if damus_state.settings.show_music_statuses || damus_state.settings.show_general_statuses {
|
||||
home_filter_kinds.append(.status)
|
||||
}
|
||||
|
||||
let friends = fs ?? get_friends()
|
||||
var home_filter = NostrFilter(kinds: home_filter_kinds)
|
||||
// include our pubkey as well even if we're not technically a friend
|
||||
@@ -553,22 +579,14 @@ class HomeModel {
|
||||
}
|
||||
}
|
||||
|
||||
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
|
||||
guard ev.referenced_params.contains(where: { p in p.param.matches_str("mute") }) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard name.ref_id == "mute" else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
damus_state.contacts.set_mutelist(ev)
|
||||
}
|
||||
|
||||
func handle_metadata_event(_ ev: NostrEvent) {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||
func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? {
|
||||
guard let m = last_event_of_kind[relay_id] else {
|
||||
last_event_of_kind[relay_id] = [:]
|
||||
return nil
|
||||
@@ -579,15 +597,9 @@ class HomeModel {
|
||||
|
||||
func handle_notification(ev: NostrEvent) {
|
||||
// don't show notifications from ourselves
|
||||
guard ev.pubkey != damus_state.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
|
||||
guard ev.pubkey != damus_state.pubkey,
|
||||
event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey),
|
||||
should_show_event(keypair: self.damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -625,13 +637,13 @@ class HomeModel {
|
||||
|
||||
|
||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: will we need to process this in other places like zap request contents, etc?
|
||||
process_image_metadatas(cache: damus_state.events, ev: ev)
|
||||
damus_state.replies.count_replies(ev)
|
||||
damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair)
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if sub_id == home_subid {
|
||||
@@ -645,14 +657,14 @@ class HomeModel {
|
||||
notification_status.new_events = notifs
|
||||
|
||||
if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
|
||||
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
let convo = ev.decrypted(keypair: self.damus_state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_dm(_ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -699,38 +711,29 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
|
||||
|
||||
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let contacts = state.contacts
|
||||
var new_refs = Set<ReferencedId>()
|
||||
// our contacts
|
||||
for tag in ev.tags {
|
||||
guard let ref = tag_to_refid(tag) else { continue }
|
||||
new_refs.insert(ref)
|
||||
}
|
||||
|
||||
var old_refs = Set<ReferencedId>()
|
||||
// find removed contacts
|
||||
if let old_ev = m_old_ev {
|
||||
for tag in old_ev.tags {
|
||||
guard let ref = tag_to_refid(tag) else { continue }
|
||||
old_refs.insert(ref)
|
||||
}
|
||||
}
|
||||
|
||||
let new_refs = Set<FollowRef>(ev.referenced_follows)
|
||||
let old_refs = m_old_ev.map({ old_ev in Set(old_ev.referenced_follows) }) ?? Set()
|
||||
|
||||
let diff = new_refs.symmetricDifference(old_refs)
|
||||
for ref in diff {
|
||||
if new_refs.contains(ref) {
|
||||
notify(.followed, ref)
|
||||
if ref.key == "p" {
|
||||
contacts.add_friend_pubkey(ref.ref_id)
|
||||
notify(.followed(ref))
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
contacts.add_friend_pubkey(pk)
|
||||
case .hashtag:
|
||||
// I guess I could cache followed hashtags here... whatever
|
||||
break
|
||||
}
|
||||
} else {
|
||||
notify(.unfollowed, ref)
|
||||
if ref.key == "p" {
|
||||
contacts.remove_friend(ref.ref_id)
|
||||
notify(.unfollowed(ref))
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
contacts.remove_friend(pk)
|
||||
case .hashtag: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.user_search_cache.updateOwnContactsPetnames(id: contacts.our_pubkey, oldEvent: m_old_ev, newEvent: ev)
|
||||
}
|
||||
|
||||
|
||||
@@ -758,6 +761,7 @@ func abbrev_ids_field(_ n: String, _ ids: [String]?) -> String {
|
||||
return "\(n): \(abbrev_ids(ids))"
|
||||
}
|
||||
|
||||
/*
|
||||
func print_filter(_ f: NostrFilter) {
|
||||
let fmt = [
|
||||
abbrev_ids_field("ids", f.ids),
|
||||
@@ -783,66 +787,10 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
||||
}
|
||||
print("-----")
|
||||
}
|
||||
*/
|
||||
|
||||
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
|
||||
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||
notify(.deleted_account, ())
|
||||
return
|
||||
}
|
||||
|
||||
var old_nip05: String? = nil
|
||||
let mprof = profiles.lookup_with_timestamp(id: ev.pubkey)
|
||||
|
||||
if let mprof {
|
||||
old_nip05 = mprof.profile.nip05
|
||||
if mprof.event.created_at > ev.created_at {
|
||||
// skip if we already have an newer profile
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
|
||||
profiles.add(id: ev.pubkey, profile: tprof)
|
||||
|
||||
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
|
||||
if validated != nil {
|
||||
print("validated nip05 for '\(nip05)'")
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
profiles.set_validated(ev.pubkey, nip05: validated)
|
||||
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load pfps asap
|
||||
|
||||
var changed = false
|
||||
|
||||
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
||||
if URL(string: picture) != nil {
|
||||
changed = true
|
||||
}
|
||||
|
||||
let banner = tprof.profile.banner ?? ""
|
||||
if URL(string: banner) != nil {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this, let nostrdb handle all validation
|
||||
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
||||
guard ev.id==calculate_event_id(ev: ev) else {
|
||||
return
|
||||
}
|
||||
let validated = events.is_event_valid(ev.id)
|
||||
|
||||
switch validated {
|
||||
@@ -862,32 +810,13 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
|
||||
case .ok:
|
||||
callback()
|
||||
|
||||
case .bad_id: fallthrough
|
||||
case .bad_sig:
|
||||
case .bad_id, .bad_sig:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
|
||||
guard_valid_event(events: events, ev: ev) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
profile.cache_lnurl()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
completion?(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func robohash(_ pk: String) -> String {
|
||||
return "https://robohash.org/" + pk
|
||||
func robohash(_ pk: Pubkey) -> String {
|
||||
return "https://robohash.org/" + pk.hex()
|
||||
}
|
||||
|
||||
func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
@@ -954,7 +883,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
if changed {
|
||||
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
||||
state.pool.connect()
|
||||
notify(.relays_changed, ())
|
||||
notify(.relays_changed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,7 +941,7 @@ func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
|
||||
func handle_incoming_dm(ev: NostrEvent, our_pubkey: Pubkey, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
|
||||
var inserted = false
|
||||
var found = false
|
||||
|
||||
@@ -1022,7 +951,7 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
var the_pk = ev.pubkey
|
||||
if ours {
|
||||
if let ref_pk = ev.referenced_pubkeys.first {
|
||||
the_pk = ref_pk.ref_id
|
||||
the_pk = ref_pk
|
||||
} else {
|
||||
// self dm!?
|
||||
print("TODO: handle self dm?")
|
||||
@@ -1056,7 +985,7 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
|
||||
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: Pubkey, evs: [NostrEvent]) -> NewEventsBits? {
|
||||
var inserted = false
|
||||
|
||||
var new_events: NewEventsBits? = nil
|
||||
@@ -1136,21 +1065,20 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
|
||||
|
||||
|
||||
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
|
||||
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
|
||||
for tag in ev.tags {
|
||||
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
|
||||
return ev.referenced_pubkeys.contains(our_pubkey)
|
||||
}
|
||||
|
||||
|
||||
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
if contacts.is_muted(ev.pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if hellthreads.isMutedThread(ev, keypair: keypair) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ev.should_show_event
|
||||
}
|
||||
|
||||
@@ -1178,10 +1106,13 @@ func zap_notification_title(_ zap: Zap) -> String {
|
||||
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
||||
let src = zap.request.ev
|
||||
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
|
||||
let profile = profiles.lookup(id: pk)
|
||||
|
||||
let name = profiles.lookup(id: pk).map { profile in
|
||||
Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
|
||||
}.value
|
||||
|
||||
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
||||
let formattedSats = format_msats_abbrev(zap.invoice.amount)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
|
||||
|
||||
if src.content.isEmpty {
|
||||
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||||
@@ -1192,13 +1123,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
|
||||
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: Pubkey) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
|
||||
content.userInfo = LossyLocalNotification(type: .profile_zap, mention: .pubkey(profile_id)).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
@@ -1213,13 +1144,13 @@ func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: NoteId) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
|
||||
content.userInfo = LossyLocalNotification(type: .zap, mention: .note(evId)).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
@@ -1234,11 +1165,11 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
|
||||
}
|
||||
}
|
||||
|
||||
func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, privkey: String?) -> String {
|
||||
|
||||
let prefix_len = 50
|
||||
let artifacts = cache.get_cache_data(ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, privkey: privkey)
|
||||
|
||||
func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String {
|
||||
|
||||
let prefix_len = 300
|
||||
let artifacts = cache.get_cache_data(ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, keypair: keypair)
|
||||
|
||||
// special case for longform events
|
||||
if ev.known_kind == .longform {
|
||||
let longform = LongformEvent(event: ev)
|
||||
@@ -1246,7 +1177,7 @@ func render_notification_content_preview(cache: EventCache, ev: NostrEvent, prof
|
||||
}
|
||||
|
||||
switch artifacts {
|
||||
case .parts:
|
||||
case .longform:
|
||||
// we should never hit this until we have more note types built out of parts
|
||||
// since we handle this case above in known_kind == .longform
|
||||
return String(ev.content.prefix(prefix_len))
|
||||
@@ -1268,7 +1199,7 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
}
|
||||
|
||||
// Don't show notifications from muted threads.
|
||||
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
|
||||
if damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1277,22 +1208,29 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
let blocks = ev.blocks(damus_state.keypair.privkey).blocks
|
||||
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
if type == .text, damus_state.settings.mention_notification {
|
||||
let blocks = ev.blocks(damus_state.keypair).blocks
|
||||
for case .mention(let mention) in blocks {
|
||||
guard case .pubkey(let pk) = mention.ref, pk == damus_state.keypair.pubkey else {
|
||||
continue
|
||||
}
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
|
||||
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify )
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
} else if type == .boost,
|
||||
damus_state.settings.repost_notification,
|
||||
let inner_ev = ev.get_inner_event(cache: damus_state.events)
|
||||
{
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
} else if type == .like && damus_state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.last?.ref_id,
|
||||
} else if type == .like,
|
||||
damus_state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.last,
|
||||
let liked_event = damus_state.events.lookup(evid)
|
||||
{
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, keypair: damus_state.keypair)
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
@@ -1348,22 +1286,35 @@ enum ProcessZapResult {
|
||||
case failed
|
||||
}
|
||||
|
||||
extension Sequence {
|
||||
func just_one() -> Element? {
|
||||
var got_one = false
|
||||
var the_x: Element? = nil
|
||||
for x in self {
|
||||
guard !got_one else {
|
||||
return nil
|
||||
}
|
||||
the_x = x
|
||||
got_one = true
|
||||
}
|
||||
return the_x
|
||||
}
|
||||
}
|
||||
|
||||
// securely get the zap target's pubkey. this can be faked so we need to be
|
||||
// careful
|
||||
func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> String? {
|
||||
let etags = ev.referenced_ids
|
||||
func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? {
|
||||
let etags = Array(ev.referenced_ids)
|
||||
|
||||
guard let etag = etags.first else {
|
||||
// no etags, ptag-only case
|
||||
|
||||
let ptags = ev.referenced_pubkeys
|
||||
|
||||
// ensure that there is only 1 ptag to stop fake profile zap attacks
|
||||
guard ptags.count == 1 else {
|
||||
guard let a = ev.referenced_pubkeys.just_one() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ptags.first?.id
|
||||
// TODO: just return data here
|
||||
return a
|
||||
}
|
||||
|
||||
// we have an e-tag
|
||||
@@ -1374,9 +1325,20 @@ func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> String? {
|
||||
}
|
||||
|
||||
// we can't trust the p tag on note zaps because they can be faked
|
||||
return events.lookup(etag.id)?.pubkey
|
||||
guard let pk = events.lookup(etag)?.pubkey else {
|
||||
// We don't have the event in cache so we can't check the pubkey.
|
||||
|
||||
// We could return this as an invalid zap but that wouldn't be correct
|
||||
// all of the time, and may reject valid zaps. What we need is a new
|
||||
// unvalidated zap state, but for now we simply leak a bit of correctness...
|
||||
|
||||
return ev.referenced_pubkeys.just_one()
|
||||
}
|
||||
|
||||
return pk
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
|
||||
// These are zap notifications
|
||||
guard let ptag = get_zap_target_pubkey(ev: ev, events: damus_state.events) else {
|
||||
@@ -1400,24 +1362,20 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
guard let lnurl = damus_state.profiles.lookup_with_timestamp(ptag)
|
||||
.map({ pr in pr?.lnurl }).value else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
|
||||
Task { [lnurl] in
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurls: damus_state.lnurls, pubkey: ptag, lnurl: lnurl) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.profiles.zappers[ptag] = zapper
|
||||
damus_state.profiles.profile_data(ptag).zapper = zapper
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
@@ -1430,7 +1388,7 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc
|
||||
|
||||
}
|
||||
|
||||
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? {
|
||||
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? {
|
||||
let our_keypair = damus_state.keypair
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
|
||||
@@ -47,8 +47,8 @@ enum MediaUpload {
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
@Published var progress: Double? = nil
|
||||
|
||||
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
|
||||
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
|
||||
func start(media: MediaUpload, uploader: MediaUploader, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self, keypair: keypair)
|
||||
DispatchQueue.main.async {
|
||||
self.progress = nil
|
||||
}
|
||||
|
||||
@@ -28,16 +28,10 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
|
||||
self = libreTranslateServer
|
||||
}
|
||||
|
||||
case argosopentech
|
||||
case terraprint
|
||||
case custom
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .argosopentech:
|
||||
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||
case .terraprint:
|
||||
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
|
||||
case .custom:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ enum CountResult {
|
||||
}
|
||||
|
||||
class EventCounter {
|
||||
var counts: [String: Int] = [:]
|
||||
var user_events: [String: Set<String>] = [:]
|
||||
var our_events: [String: NostrEvent] = [:]
|
||||
var our_pubkey: String
|
||||
|
||||
init (our_pubkey: String) {
|
||||
var counts: [NoteId: Int] = [:]
|
||||
var user_events: [Pubkey: Set<NoteId>] = [:]
|
||||
var our_events: [NoteId: NostrEvent] = [:]
|
||||
var our_pubkey: Pubkey
|
||||
|
||||
init(our_pubkey: Pubkey) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent, target: String) -> CountResult {
|
||||
func add_event(_ ev: NostrEvent, target: NoteId) -> CountResult {
|
||||
let pubkey = ev.pubkey
|
||||
|
||||
if self.user_events[pubkey] == nil {
|
||||
|
||||
@@ -9,6 +9,6 @@ import Foundation
|
||||
|
||||
struct Counted {
|
||||
let event: NostrEvent
|
||||
let id: String
|
||||
let id: NoteId
|
||||
let total: Int
|
||||
}
|
||||
|
||||
+88
-307
@@ -7,31 +7,93 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MentionType {
|
||||
case pubkey
|
||||
case event
|
||||
enum MentionType: AsciiCharacter, TagKey {
|
||||
case p
|
||||
case e
|
||||
|
||||
var ref: String {
|
||||
var keychar: AsciiCharacter {
|
||||
self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
case pubkey(Pubkey) // TODO: handle nprofile
|
||||
case note(NoteId)
|
||||
|
||||
var key: MentionType {
|
||||
switch self {
|
||||
case .pubkey:
|
||||
return "p"
|
||||
case .event:
|
||||
return "e"
|
||||
case .pubkey: return .p
|
||||
case .note: return .e
|
||||
}
|
||||
}
|
||||
|
||||
var bech32: String {
|
||||
switch self {
|
||||
case .pubkey(let pubkey): return bech32_pubkey(pubkey)
|
||||
case .note(let noteId): return bech32_note_id(noteId)
|
||||
}
|
||||
}
|
||||
|
||||
static func from_bech32(str: String) -> MentionRef? {
|
||||
switch Bech32Object.parse(str) {
|
||||
case .note(let noteid): return .note(noteid)
|
||||
case .npub(let pubkey): return .pubkey(pubkey)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var pubkey: Pubkey? {
|
||||
switch self {
|
||||
case .pubkey(let pubkey): return pubkey
|
||||
case .note: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var tag: [String] {
|
||||
switch self {
|
||||
case .pubkey(let pubkey): return ["p", pubkey.hex()]
|
||||
case .note(let noteId): return ["e", noteId.hex()]
|
||||
}
|
||||
}
|
||||
|
||||
static func from_tag(tag: TagSequence) -> MentionRef? {
|
||||
guard tag.count >= 2 else { return nil }
|
||||
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard let t0 = i.next(),
|
||||
let chr = t0.single_char,
|
||||
let mention_type = MentionType(rawValue: chr),
|
||||
let id = i.next()?.id()
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch mention_type {
|
||||
case .p: return .pubkey(Pubkey(id))
|
||||
case .e: return .note(NoteId(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Mention: Equatable {
|
||||
struct Mention<T: Equatable>: Equatable {
|
||||
let index: Int?
|
||||
let type: MentionType
|
||||
let ref: ReferencedId
|
||||
let ref: T
|
||||
|
||||
static func note(_ id: String) -> Mention {
|
||||
return Mention(index: nil, type: .event, ref: .e(id))
|
||||
static func any(_ mention_id: MentionRef, index: Int? = nil) -> Mention<MentionRef> {
|
||||
return Mention<MentionRef>(index: index, ref: mention_id)
|
||||
}
|
||||
|
||||
static func pubkey(_ pubkey: String) -> Mention {
|
||||
return Mention(index: nil, type: .pubkey, ref: .p(pubkey))
|
||||
static func noteref(_ id: NoteRef, index: Int? = nil) -> Mention<NoteRef> {
|
||||
return Mention<NoteRef>(index: index, ref: id)
|
||||
}
|
||||
|
||||
static func note(_ id: NoteId, index: Int? = nil) -> Mention<NoteId> {
|
||||
return Mention<NoteId>(index: index, ref: id)
|
||||
}
|
||||
|
||||
static func pubkey(_ pubkey: Pubkey, index: Int? = nil) -> Mention<Pubkey> {
|
||||
return Mention<Pubkey>(index: index, ref: pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,178 +123,11 @@ struct LightningInvoice<T> {
|
||||
}
|
||||
}
|
||||
|
||||
enum Block: Equatable {
|
||||
static func == (lhs: Block, rhs: Block) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.text(let a), .text(let b)):
|
||||
return a == b
|
||||
case (.mention(let a), .mention(let b)):
|
||||
return a == b
|
||||
case (.hashtag(let a), .hashtag(let b)):
|
||||
return a == b
|
||||
case (.url(let a), .url(let b)):
|
||||
return a == b
|
||||
case (.invoice(let a), .invoice(let b)):
|
||||
return a.string == b.string
|
||||
case (_, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case text(String)
|
||||
case mention(Mention)
|
||||
case hashtag(String)
|
||||
case url(URL)
|
||||
case invoice(Invoice)
|
||||
case relay(String)
|
||||
|
||||
var is_invoice: Invoice? {
|
||||
if case .invoice(let invoice) = self {
|
||||
return invoice
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_hashtag: String? {
|
||||
if case .hashtag(let htag) = self {
|
||||
return htag
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_url: URL? {
|
||||
if case .url(let url) = self {
|
||||
return url
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_text: String? {
|
||||
if case .text(let txt) = self {
|
||||
return txt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_note_mention: Bool {
|
||||
guard case .mention(let mention) = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
return mention.type == .event
|
||||
}
|
||||
|
||||
var is_mention: Mention? {
|
||||
if case .mention(let m) = self {
|
||||
return m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func render_blocks(blocks: [Block]) -> String {
|
||||
return blocks.reduce("") { str, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if let idx = m.index {
|
||||
return str + "#[\(idx)]"
|
||||
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
|
||||
return str + "nostr:\(pk)"
|
||||
} else if let note_id = bech32_note_id(m.ref.ref_id) {
|
||||
return str + "nostr:\(note_id)"
|
||||
} else {
|
||||
return str + m.ref.ref_id
|
||||
}
|
||||
case .relay(let relay):
|
||||
return str + relay
|
||||
case .text(let txt):
|
||||
return str + txt
|
||||
case .hashtag(let htag):
|
||||
return str + "#" + htag
|
||||
case .url(let url):
|
||||
return str + url.absoluteString
|
||||
case .invoice(let inv):
|
||||
return str + inv.string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Blocks {
|
||||
struct Blocks: Equatable {
|
||||
let words: Int
|
||||
let blocks: [Block]
|
||||
}
|
||||
|
||||
func parse_mentions(content: String, tags: [[String]]) -> Blocks {
|
||||
var out: [Block] = []
|
||||
|
||||
var bs = note_blocks()
|
||||
bs.num_blocks = 0;
|
||||
|
||||
blocks_init(&bs)
|
||||
|
||||
let bytes = content.utf8CString
|
||||
let _ = bytes.withUnsafeBufferPointer { p in
|
||||
damus_parse_content(&bs, p.baseAddress)
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i < bs.num_blocks) {
|
||||
let block = bs.blocks[i]
|
||||
|
||||
if let converted = convert_block(block, tags: tags) {
|
||||
out.append(converted)
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
let words = Int(bs.words)
|
||||
blocks_free(&bs)
|
||||
|
||||
return Blocks(words: words, blocks: out)
|
||||
}
|
||||
|
||||
func strblock_to_string(_ s: str_block_t) -> String? {
|
||||
let len = s.end - s.start
|
||||
let bytes = Data(bytes: s.start, count: len)
|
||||
return String(bytes: bytes, encoding: .utf8)
|
||||
}
|
||||
|
||||
func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
|
||||
if b.type == BLOCK_HASHTAG {
|
||||
guard let str = strblock_to_string(b.block.str) else {
|
||||
return nil
|
||||
}
|
||||
return .hashtag(str)
|
||||
} else if b.type == BLOCK_TEXT {
|
||||
guard let str = strblock_to_string(b.block.str) else {
|
||||
return nil
|
||||
}
|
||||
return .text(str)
|
||||
} else if b.type == BLOCK_MENTION_INDEX {
|
||||
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
|
||||
} else if b.type == BLOCK_URL {
|
||||
return convert_url_block(b.block.str)
|
||||
} else if b.type == BLOCK_INVOICE {
|
||||
return convert_invoice_block(b.block.invoice)
|
||||
} else if b.type == BLOCK_MENTION_BECH32 {
|
||||
return convert_mention_bech32_block(b.block.mention_bech32)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_url_block(_ b: str_block) -> Block? {
|
||||
guard let str = strblock_to_string(b) else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: str) else {
|
||||
return .text(str)
|
||||
}
|
||||
return .url(url)
|
||||
}
|
||||
|
||||
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? {
|
||||
guard p != nil else {
|
||||
return nil
|
||||
@@ -295,87 +190,6 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
|
||||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
}
|
||||
|
||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
guard let invstr = strblock_to_string(b.invstr) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard var b11 = maybe_pointee(b.bolt11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let description = convert_invoice_description(b11: b11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
||||
let payment_hash = Data(bytes: &b11.payment_hash, count: 32)
|
||||
let created_at = b11.timestamp
|
||||
|
||||
tal_free(b.bolt11)
|
||||
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||
}
|
||||
|
||||
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
|
||||
{
|
||||
switch b.bech32.type {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
let note = b.bech32.data.note;
|
||||
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
let nevent = b.bech32.data.nevent;
|
||||
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nevent.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nevent.relays.relays.0)
|
||||
}
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NPUB:
|
||||
let npub = b.bech32.data.npub
|
||||
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NSEC:
|
||||
let nsec = b.bech32.data.nsec
|
||||
let nsec_bytes = Data(bytes: nsec.nsec, count: 32)
|
||||
let pubkey = privkey_to_pubkey_raw(sec: nsec_bytes.bytes) ?? hex_encode(nsec_bytes)
|
||||
return .mention(.pubkey(pubkey))
|
||||
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
let nprofile = b.bech32.data.nprofile
|
||||
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nprofile.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nprofile.relays.relays.0)
|
||||
}
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
let nrelay = b.bech32.data.nrelay
|
||||
guard let relay_str = strblock_to_string(nrelay.relay) else {
|
||||
return nil
|
||||
}
|
||||
return .relay(relay_str)
|
||||
|
||||
case NOSTR_BECH32_NADDR:
|
||||
// TODO: wtf do I do with this
|
||||
guard let naddr = strblock_to_string(b.str) else {
|
||||
return nil
|
||||
}
|
||||
return .text("nostr:" + naddr)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
if let desc = b11.description {
|
||||
return .description(String(cString: desc))
|
||||
@@ -388,26 +202,6 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
{
|
||||
let ind = Int(ind)
|
||||
|
||||
if ind < 0 || (ind + 1 > tags.count) || tags[ind].count < 2 {
|
||||
return .text("#[\(ind)]")
|
||||
}
|
||||
|
||||
let tag = tags[ind]
|
||||
guard let mention_type = parse_mention_type(tag[0]) else {
|
||||
return .text("#[\(ind)]")
|
||||
}
|
||||
|
||||
guard let ref = tag_to_refid(tag) else {
|
||||
return .text("#[\(ind)]")
|
||||
}
|
||||
|
||||
return .mention(Mention(index: ind, type: mention_type, ref: ref))
|
||||
}
|
||||
|
||||
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
||||
var i: Int = 0
|
||||
for tag in tags {
|
||||
@@ -427,30 +221,18 @@ struct PostTags {
|
||||
let tags: [[String]]
|
||||
}
|
||||
|
||||
func parse_mention_type(_ c: String) -> MentionType? {
|
||||
if c == "e" {
|
||||
return .event
|
||||
} else if c == "p" {
|
||||
return .pubkey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]], silent_mentions: Bool) -> PostTags {
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||
var new_tags = tags
|
||||
|
||||
for post_block in post_blocks {
|
||||
switch post_block {
|
||||
case .mention(let mention):
|
||||
let mention_type = mention.type
|
||||
|
||||
if silent_mentions || mention_type == .event {
|
||||
if case .note = mention.ref {
|
||||
continue
|
||||
}
|
||||
|
||||
new_tags.append(refid_to_tag(mention.ref))
|
||||
new_tags.append(mention.ref.tag)
|
||||
case .hashtag(let hashtag):
|
||||
new_tags.append(["t", hashtag.lowercased()])
|
||||
case .text: break
|
||||
@@ -465,14 +247,13 @@ func make_post_tags(post_blocks: [Block], tags: [[String]], silent_mentions: Boo
|
||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||
}
|
||||
|
||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||
let tags = post.references.map(refid_to_tag) + post.tags
|
||||
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
|
||||
let tags = post.references.map({ r in r.tag }) + post.tags
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
|
||||
new_ev.calculate_id()
|
||||
new_ev.sign(privkey: privkey)
|
||||
return new_ev
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let content = post_tags.blocks
|
||||
.map(\.asString)
|
||||
.joined(separator: "")
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
|
||||
fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
|
||||
pk_setting_key(pubkey, key: "muted_threads")
|
||||
}
|
||||
|
||||
func loadMutedThreads(pubkey: String) -> [String] {
|
||||
func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||
return UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
return xs.reduce(into: [NoteId]()) { ids, k in
|
||||
guard let note_id = hex_decode(k) else { return }
|
||||
ids.append(NoteId(Data(note_id)))
|
||||
}
|
||||
}
|
||||
|
||||
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
|
||||
func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool {
|
||||
let uniqueMutedThreads = Array(Set(value))
|
||||
|
||||
if uniqueMutedThreads != currentValue {
|
||||
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
|
||||
let ids = uniqueMutedThreads.map { note_id in return note_id.hex() }
|
||||
UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -31,9 +36,9 @@ class MutedThreadsManager: ObservableObject {
|
||||
|
||||
private let keypair: Keypair
|
||||
|
||||
private var _mutedThreadsSet: Set<String>
|
||||
private var _mutedThreads: [String]
|
||||
var mutedThreads: [String] {
|
||||
private var _mutedThreadsSet: Set<NoteId>
|
||||
private var _mutedThreads: [NoteId]
|
||||
var mutedThreads: [NoteId] {
|
||||
get {
|
||||
return _mutedThreads
|
||||
}
|
||||
@@ -51,20 +56,20 @@ class MutedThreadsManager: ObservableObject {
|
||||
self.keypair = keypair
|
||||
}
|
||||
|
||||
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
|
||||
func isMutedThread(_ ev: NostrEvent, keypair: Keypair) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(keypair: keypair))
|
||||
}
|
||||
|
||||
func updateMutedThread(_ ev: NostrEvent) {
|
||||
let threadId = ev.thread_id(privkey: nil)
|
||||
if isMutedThread(ev, privkey: keypair.privkey) {
|
||||
let threadId = ev.thread_id(keypair: keypair)
|
||||
if isMutedThread(ev, keypair: keypair) {
|
||||
mutedThreads = mutedThreads.filter { $0 != threadId }
|
||||
_mutedThreadsSet.remove(threadId)
|
||||
notify(.unmute_thread, ev)
|
||||
notify(.unmute_thread(ev))
|
||||
} else {
|
||||
mutedThreads.append(threadId)
|
||||
_mutedThreadsSet.insert(threadId)
|
||||
notify(.mute_thread, ev)
|
||||
notify(.mute_thread(ev))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
class EventGroup {
|
||||
var events: [NostrEvent]
|
||||
|
||||
var last_event_at: Int64 {
|
||||
var last_event_at: UInt32 {
|
||||
guard let first = self.events.first else {
|
||||
return 0
|
||||
}
|
||||
@@ -18,11 +18,7 @@ class EventGroup {
|
||||
return first.created_at
|
||||
}
|
||||
|
||||
init() {
|
||||
self.events = []
|
||||
}
|
||||
|
||||
init(events: [NostrEvent]) {
|
||||
init(events: [NostrEvent] = []) {
|
||||
self.events = events
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
import Foundation
|
||||
|
||||
class ZapGroup {
|
||||
var zaps: [Zapping]
|
||||
var msat_total: Int64
|
||||
var zappers: Set<String>
|
||||
|
||||
var last_event_at: Int64 {
|
||||
var zaps: [Zapping] = []
|
||||
var msat_total: Int64 = 0
|
||||
var zappers = Set<Pubkey>()
|
||||
|
||||
var last_event_at: UInt32 {
|
||||
guard let first = zaps.first else {
|
||||
return 0
|
||||
}
|
||||
@@ -46,12 +46,6 @@ class ZapGroup {
|
||||
return grp
|
||||
}
|
||||
|
||||
init() {
|
||||
self.zaps = []
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert(_ zap: Zapping) -> Bool {
|
||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import Foundation
|
||||
|
||||
enum NotificationItem {
|
||||
case repost(String, EventGroup)
|
||||
case reaction(String, EventGroup)
|
||||
case repost(NoteId, EventGroup)
|
||||
case reaction(NoteId, EventGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
case event_zap(String, ZapGroup)
|
||||
case event_zap(NoteId, ZapGroup)
|
||||
case reply(NostrEvent)
|
||||
|
||||
var is_reply: NostrEvent? {
|
||||
@@ -35,23 +35,8 @@ enum NotificationItem {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .repost(let evid, _):
|
||||
return "repost_" + evid
|
||||
case .reaction(let evid, _):
|
||||
return "reaction_" + evid
|
||||
case .profile_zap:
|
||||
return "profile_zap"
|
||||
case .event_zap(let evid, _):
|
||||
return "event_zap_" + evid
|
||||
case .reply(let ev):
|
||||
return "reply_" + ev.id
|
||||
}
|
||||
}
|
||||
|
||||
var last_event_at: Int64 {
|
||||
|
||||
var last_event_at: UInt32 {
|
||||
switch self {
|
||||
case .reaction(_, let evgrp):
|
||||
return evgrp.last_event_at
|
||||
@@ -99,42 +84,28 @@ enum NotificationItem {
|
||||
}
|
||||
|
||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
var incoming_zaps: [Zapping]
|
||||
var incoming_events: [NostrEvent]
|
||||
var should_queue: Bool
|
||||
var incoming_zaps: [Zapping] = []
|
||||
var incoming_events: [NostrEvent] = []
|
||||
var should_queue: Bool = true
|
||||
|
||||
// mappings from events to
|
||||
var zaps: [String: ZapGroup]
|
||||
var profile_zaps: ZapGroup
|
||||
var reactions: [String: EventGroup]
|
||||
var reposts: [String: EventGroup]
|
||||
var replies: [NostrEvent]
|
||||
var has_reply: Set<String>
|
||||
var has_ev: Set<String>
|
||||
|
||||
@Published var notifications: [NotificationItem]
|
||||
|
||||
init() {
|
||||
self.zaps = [:]
|
||||
self.reactions = [:]
|
||||
self.reposts = [:]
|
||||
self.replies = []
|
||||
self.has_reply = Set()
|
||||
self.should_queue = true
|
||||
self.incoming_zaps = []
|
||||
self.incoming_events = []
|
||||
self.profile_zaps = ZapGroup()
|
||||
self.notifications = []
|
||||
self.has_ev = Set()
|
||||
}
|
||||
var zaps: [NoteId: ZapGroup] = [:]
|
||||
var profile_zaps = ZapGroup()
|
||||
var reactions: [NoteId: EventGroup] = [:]
|
||||
var reposts: [NoteId: EventGroup] = [:]
|
||||
var replies: [NostrEvent] = []
|
||||
var has_reply = Set<NoteId>()
|
||||
var has_ev = Set<NoteId>()
|
||||
|
||||
@Published var notifications: [NotificationItem] = []
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
self.should_queue = val
|
||||
}
|
||||
|
||||
func uniq_pubkeys() -> [String] {
|
||||
var pks = Set<String>()
|
||||
|
||||
func uniq_pubkeys() -> [Pubkey] {
|
||||
var pks = Set<Pubkey>()
|
||||
|
||||
for ev in incoming_events {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
@@ -222,12 +193,10 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
private func insert_reaction(_ ev: NostrEvent) -> Bool {
|
||||
guard let ref_id = ev.referenced_ids.last else {
|
||||
guard let id = ev.referenced_ids.last else {
|
||||
return false
|
||||
}
|
||||
|
||||
let id = ref_id.id
|
||||
|
||||
|
||||
if let evgrp = self.reactions[id] {
|
||||
return evgrp.insert(ev)
|
||||
} else {
|
||||
|
||||
+4
-91
@@ -10,10 +10,10 @@ import Foundation
|
||||
struct NostrPost {
|
||||
let kind: NostrKind
|
||||
let content: String
|
||||
let references: [ReferencedId]
|
||||
let references: [RefId]
|
||||
let tags: [[String]]
|
||||
|
||||
init (content: String, references: [ReferencedId], kind: NostrKind = .text, tags: [[String]] = []) {
|
||||
|
||||
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
|
||||
self.content = content
|
||||
self.references = references
|
||||
self.kind = kind
|
||||
@@ -21,96 +21,9 @@ struct NostrPost {
|
||||
}
|
||||
}
|
||||
|
||||
func parse_post_mention_type(_ p: Parser) -> MentionType? {
|
||||
if parse_char(p, "@") {
|
||||
return .pubkey
|
||||
}
|
||||
|
||||
if parse_char(p, "&") {
|
||||
return .event
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse_post_reference(_ p: Parser) -> ReferencedId? {
|
||||
let start = p.pos
|
||||
|
||||
guard let typ = parse_post_mention_type(p) else {
|
||||
return parse_nostr_ref_uri(p)
|
||||
}
|
||||
|
||||
if let ref = parse_post_mention(p, mention_type: typ) {
|
||||
return ref
|
||||
}
|
||||
|
||||
p.pos = start
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func is_bech32_char(_ c: Character) -> Bool {
|
||||
let contains = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".contains(c)
|
||||
return contains
|
||||
}
|
||||
|
||||
func parse_post_mention(_ p: Parser, mention_type: MentionType) -> ReferencedId? {
|
||||
if let id = parse_hexstr(p, len: 64) {
|
||||
return ReferencedId(ref_id: id, relay_id: nil, key: mention_type.ref)
|
||||
} else if let bech32_ref = parse_post_bech32_mention(p) {
|
||||
return bech32_ref
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with our C parser
|
||||
func parse_post_bech32_mention(_ p: Parser) -> ReferencedId? {
|
||||
let start = p.pos
|
||||
if parse_str(p, "note") {
|
||||
} else if parse_str(p, "npub") {
|
||||
} else if parse_str(p, "nsec") {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !parse_char(p, "1") {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
guard consume_until(p, match: { c in !is_bech32_char(c) }, end_ok: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let end = p.pos
|
||||
|
||||
let sliced = String(substring(p.str, start: start, end: end))
|
||||
guard let decoded = try? bech32_decode(sliced) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
let hex = hex_encode(decoded.data)
|
||||
switch decoded.hrp {
|
||||
case "note":
|
||||
return ReferencedId(ref_id: hex, relay_id: nil, key: "e")
|
||||
case "npub":
|
||||
return ReferencedId(ref_id: hex, relay_id: nil, key: "p")
|
||||
case "nsec":
|
||||
guard let pubkey = privkey_to_pubkey(privkey: hex) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
return ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
|
||||
default:
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of tags
|
||||
func parse_post_blocks(content: String) -> [Block] {
|
||||
return parse_mentions(content: content, tags: []).blocks
|
||||
return parse_note_content(content: .content(content, nil)).blocks
|
||||
}
|
||||
|
||||
|
||||
@@ -6,34 +6,3 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PostBlock {
|
||||
case text(String)
|
||||
case ref(ReferencedId)
|
||||
case hashtag(String)
|
||||
|
||||
var is_text: String? {
|
||||
if case .text(let txt) = self {
|
||||
return txt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_hashtag: String? {
|
||||
if case .hashtag(let ht) = self {
|
||||
return ht
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_ref: ReferencedId? {
|
||||
if case .ref(let ref) = self {
|
||||
return ref
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func parse_post_textblock(str: String, from: Int, to: Int) -> PostBlock {
|
||||
return .text(String(substring(str, start: from, end: to)))
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
@Published var progress: Int = 0
|
||||
|
||||
var events: EventHolder
|
||||
let pubkey: String
|
||||
let pubkey: Pubkey
|
||||
let damus: DamusState
|
||||
|
||||
var seen_event: Set<String> = Set()
|
||||
var seen_event: Set<NoteId> = Set()
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
|
||||
init(pubkey: String, damus: DamusState) {
|
||||
init(pubkey: Pubkey, damus: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
self.damus = damus
|
||||
self.events = EventHolder(on_queue: { ev in
|
||||
@@ -29,22 +29,12 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
})
|
||||
}
|
||||
|
||||
func follows(pubkey: String) -> Bool {
|
||||
func follows(pubkey: Pubkey) -> Bool {
|
||||
guard let contacts = self.contacts else {
|
||||
return false
|
||||
}
|
||||
|
||||
for tag in contacts.tags {
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
continue
|
||||
}
|
||||
|
||||
if tag[1] == pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
return contacts.referenced_pubkeys.contains(pubkey)
|
||||
}
|
||||
|
||||
func get_follow_target() -> FollowTarget {
|
||||
@@ -78,7 +68,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
text_filter.limit = 500
|
||||
|
||||
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
|
||||
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
|
||||
}
|
||||
@@ -112,8 +102,6 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
} else if ev.known_kind == .contacts {
|
||||
handle_profile_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
@@ -131,8 +119,9 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
break
|
||||
case .event(_, let ev):
|
||||
add_event(ev)
|
||||
case .notice(let notice):
|
||||
notify(.notice, notice)
|
||||
case .notice:
|
||||
break
|
||||
//notify(.notice, notice)
|
||||
case .eose:
|
||||
if resp.subid == sub_id {
|
||||
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
|
||||
@@ -145,10 +134,10 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
|
||||
|
||||
func count_pubkeys(_ tags: [[String]]) -> Int {
|
||||
func count_pubkeys(_ tags: Tags) -> Int {
|
||||
var c: Int = 0
|
||||
for tag in tags {
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
if tag.count >= 2 && tag[0].matches_char("p") {
|
||||
c += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,16 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
struct ProfileUpdate {
|
||||
let pubkey: String
|
||||
let profile: Profile
|
||||
enum ProfileUpdate {
|
||||
case manual(pubkey: Pubkey, profile: Profile)
|
||||
case remote(pubkey: Pubkey)
|
||||
|
||||
var pubkey: Pubkey {
|
||||
switch self {
|
||||
case .manual(let pubkey, _):
|
||||
return pubkey
|
||||
case .remote(let pubkey):
|
||||
return pubkey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
final class ReactionsModel: EventsModel {
|
||||
|
||||
init(state: DamusState, target: String) {
|
||||
init(state: DamusState, target: NoteId) {
|
||||
super.init(state: state, target: target, kind: .like)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-10
@@ -8,25 +8,32 @@
|
||||
import Foundation
|
||||
|
||||
struct ReplyDesc {
|
||||
let pubkeys: [String]
|
||||
let pubkeys: [Pubkey]
|
||||
let others: Int
|
||||
}
|
||||
|
||||
func make_reply_description(_ tags: [[String]]) -> ReplyDesc {
|
||||
func make_reply_description(_ event: NostrEvent, replying_to: NostrEvent?) -> ReplyDesc {
|
||||
var c = 0
|
||||
var ns: [String] = []
|
||||
var i = tags.count - 1
|
||||
|
||||
while i >= 0 {
|
||||
let tag = tags[i]
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
var ns: [Pubkey] = []
|
||||
var i = event.tags.count
|
||||
|
||||
if let replying_to {
|
||||
ns.append(replying_to.pubkey)
|
||||
}
|
||||
|
||||
for tag in event.tags {
|
||||
if let pk = Pubkey.from_tag(tag: tag) {
|
||||
c += 1
|
||||
if ns.count < 2 {
|
||||
ns.append(tag[1])
|
||||
if let replying_to, pk == replying_to.pubkey {
|
||||
continue
|
||||
} else {
|
||||
ns.append(pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
|
||||
|
||||
return ReplyDesc(pubkeys: ns, others: c)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@
|
||||
import Foundation
|
||||
|
||||
class ReplyMap {
|
||||
var replies: [String: Set<String>] = [:]
|
||||
|
||||
func lookup(_ id: String) -> Set<String>? {
|
||||
var replies: [NoteId: Set<NoteId>] = [:]
|
||||
|
||||
func lookup(_ id: NoteId) -> Set<NoteId>? {
|
||||
return replies[id]
|
||||
}
|
||||
|
||||
private func ensure_set(id: String) {
|
||||
private func ensure_set(id: NoteId) {
|
||||
if replies[id] == nil {
|
||||
replies[id] = Set()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add(id: String, reply_id: String) -> Bool {
|
||||
func add(id: NoteId, reply_id: NoteId) -> Bool {
|
||||
ensure_set(id: id)
|
||||
if (replies[id]!).contains(reply_id) {
|
||||
return false
|
||||
|
||||
@@ -31,41 +31,12 @@ enum ReportType: String, CustomStringConvertible, CaseIterable {
|
||||
}
|
||||
|
||||
struct ReportNoteTarget {
|
||||
let pubkey: String
|
||||
let note_id: String
|
||||
let pubkey: Pubkey
|
||||
let note_id: NoteId
|
||||
}
|
||||
|
||||
enum ReportTarget {
|
||||
case user(String)
|
||||
case user(Pubkey)
|
||||
case note(ReportNoteTarget)
|
||||
}
|
||||
|
||||
struct Report {
|
||||
let type: ReportType
|
||||
let target: ReportTarget
|
||||
let message: String
|
||||
}
|
||||
|
||||
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
|
||||
switch target {
|
||||
case .user(let pubkey):
|
||||
return [["p", pubkey, type.rawValue]]
|
||||
case .note(let notet):
|
||||
return [["e", notet.note_id, type.rawValue], ["p", notet.pubkey]]
|
||||
}
|
||||
}
|
||||
|
||||
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
|
||||
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let kind = 1984
|
||||
let tags = create_report_tags(target: report.target, type: report.type)
|
||||
let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags)
|
||||
|
||||
ev.id = calculate_event_id(ev: ev)
|
||||
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
final class RepostsModel: EventsModel {
|
||||
|
||||
init(state: DamusState, target: String) {
|
||||
init(state: DamusState, target: NoteId) {
|
||||
super.init(state: state, target: target, kind: .boost)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class SearchHomeModel: ObservableObject {
|
||||
var events: EventHolder
|
||||
@Published var loading: Bool = false
|
||||
|
||||
var seen_pubkey: Set<String> = Set()
|
||||
var seen_pubkey: Set<Pubkey> = Set()
|
||||
let damus_state: DamusState
|
||||
let base_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
@@ -30,12 +30,12 @@ class SearchHomeModel: ObservableObject {
|
||||
func get_base_filter() -> NostrFilter {
|
||||
var filter = NostrFilter(kinds: [.text, .chat])
|
||||
filter.limit = self.limit
|
||||
filter.until = Int64(Date.now.timeIntervalSince1970)
|
||||
filter.until = UInt32(Date.now.timeIntervalSince1970)
|
||||
return filter
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
|
||||
events.filter { should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: $0) }
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ class SearchHomeModel: ObservableObject {
|
||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
|
||||
if ev.is_textlike && should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair)
|
||||
{
|
||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
@@ -91,7 +92,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [Pubkey] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
|
||||
@@ -100,13 +101,13 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] {
|
||||
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] {
|
||||
var pubkeys = Set<Pubkey>()
|
||||
|
||||
for ev in events {
|
||||
// lookup profiles from boosted events
|
||||
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
|
||||
@@ -123,7 +124,7 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
|
||||
|
||||
enum PubkeysToLoad {
|
||||
case from_events([NostrEvent])
|
||||
case from_keys([String])
|
||||
case from_keys([Pubkey])
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
@@ -138,21 +139,13 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
|
||||
authors: authors)
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in
|
||||
let (sid, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: conn_ev) { sub_id, ev in
|
||||
guard sub_id == profiles_subid else {
|
||||
return
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
guard done && sid == profiles_subid else {
|
||||
guard case .nostr_event(let ev) = conn_ev,
|
||||
case .eose = ev,
|
||||
sub_id == profiles_subid
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
print("done loading \(authors.count) profiles from \(relay_id)")
|
||||
damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ class SearchModel: ObservableObject {
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
self.events.filter { should_show_event(contacts: state.contacts, ev: $0) }
|
||||
self.events.filter {
|
||||
should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: $0)
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ class SearchModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: state.contacts, ev: ev) else {
|
||||
guard should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,10 +70,6 @@ class SearchModel: ObservableObject {
|
||||
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
if ev.is_textlike && ev.should_show_event {
|
||||
self.add_event(ev)
|
||||
} else if ev.known_kind == .channel_create {
|
||||
// unimplemented
|
||||
} else if ev.known_kind == .channel_meta {
|
||||
// unimplemented
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,16 +87,16 @@ class SearchModel: ObservableObject {
|
||||
|
||||
func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
|
||||
for tag in ev.tags {
|
||||
if tag_is_hashtag(tag) && hashtags.contains(tag[1]) {
|
||||
if tag_is_hashtag(tag) && hashtags.contains(tag[1].string()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tag_is_hashtag(_ tag: [String]) -> Bool {
|
||||
func tag_is_hashtag(_ tag: Tag) -> Bool {
|
||||
// "hashtag" is deprecated, will remove in the future
|
||||
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
|
||||
return tag.count >= 2 && tag[0].matches_char("t")
|
||||
}
|
||||
|
||||
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
|
||||
|
||||
@@ -12,12 +12,7 @@ class SignalModel: ObservableObject {
|
||||
@Published var signal: Int
|
||||
@Published var max_signal: Int
|
||||
|
||||
init() {
|
||||
self.signal = 0
|
||||
self.max_signal = 0
|
||||
}
|
||||
|
||||
init(signal: Int, max_signal: Int) {
|
||||
init(signal: Int = 0, max_signal: Int = 0) {
|
||||
self.signal = signal
|
||||
self.max_signal = max_signal
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class ThreadModel: ObservableObject {
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
self.original_event = event
|
||||
add_event(event)
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
}
|
||||
|
||||
var is_original: Bool {
|
||||
@@ -46,10 +46,10 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func set_active_event(_ ev: NostrEvent) -> Bool {
|
||||
func set_active_event(_ ev: NostrEvent, keypair: Keypair) -> Bool {
|
||||
self.event = ev
|
||||
add_event(ev)
|
||||
|
||||
add_event(ev, keypair: keypair)
|
||||
|
||||
//self.objectWillChange.send()
|
||||
return false
|
||||
}
|
||||
@@ -59,8 +59,8 @@ class ThreadModel: ObservableObject {
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
let thread_id = event.thread_id(keypair: .empty)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [.text]
|
||||
ref_events.limit = 1000
|
||||
@@ -85,19 +85,20 @@ class ThreadModel: ObservableObject {
|
||||
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent) {
|
||||
func add_event(_ ev: NostrEvent, keypair: Keypair) {
|
||||
if event_map.contains(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
let the_ev = damus_state.events.upsert(ev)
|
||||
damus_state.replies.count_replies(the_ev)
|
||||
damus_state.events.add_replies(ev: the_ev)
|
||||
|
||||
damus_state.replies.count_replies(ev, keypair: keypair)
|
||||
damus_state.events.add_replies(ev: ev, keypair: keypair)
|
||||
|
||||
event_map.insert(ev)
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||
|
||||
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||
@@ -105,14 +106,12 @@ class ThreadModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
} else if ev.known_kind == .zap {
|
||||
if ev.known_kind == .zap {
|
||||
process_zap_event(damus_state: damus_state, ev: ev) { zap in
|
||||
|
||||
}
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev)
|
||||
self.add_event(ev, keypair: damus_state.keypair)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +127,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
|
||||
func get_top_zap(events: EventCache, evid: String) -> Zapping? {
|
||||
func get_top_zap(events: EventCache, evid: NoteId) -> Zapping? {
|
||||
return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in
|
||||
!zap.request.marked_hidden
|
||||
})
|
||||
|
||||
@@ -10,16 +10,20 @@ import Foundation
|
||||
/// Cache of searchable users by name, display_name, NIP-05 identifier, or own contact list petname.
|
||||
/// Optimized for fast searches of substrings by using a Trie.
|
||||
/// Optimal for performing user searches that could be initiated by typing quickly on a keyboard into a text input field.
|
||||
class UserSearchCache {
|
||||
private let trie = Trie<String>()
|
||||
|
||||
func search(key: String) -> [String] {
|
||||
// TODO: replace with lmdb (the b tree should handle this just fine ?)
|
||||
// we just need a name to profile index
|
||||
class UserSearchCache {
|
||||
private let trie = Trie<Pubkey>()
|
||||
|
||||
func search(key: String) -> [Pubkey] {
|
||||
let results = trie.find(key: key)
|
||||
return results
|
||||
}
|
||||
|
||||
/// Computes the differences between an old profile, if it exists, and a new profile, and updates the user search cache accordingly.
|
||||
func updateProfile(id: String, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) {
|
||||
@MainActor
|
||||
func updateProfile(id: Pubkey, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) {
|
||||
// Remove searchable keys tied to the old profile if they differ from the new profile
|
||||
// to keep the trie clean without empty nodes while avoiding excessive graph searching.
|
||||
if let oldProfile {
|
||||
@@ -38,7 +42,8 @@ class UserSearchCache {
|
||||
}
|
||||
|
||||
/// Adds a profile to the user search cache.
|
||||
private func addProfile(id: String, profiles: Profiles, profile: Profile) {
|
||||
@MainActor
|
||||
private func addProfile(id: Pubkey, profiles: Profiles, profile: Profile) {
|
||||
// Searchable by name.
|
||||
if let name = profile.name {
|
||||
trie.insert(key: name.lowercased(), value: id)
|
||||
@@ -56,36 +61,40 @@ class UserSearchCache {
|
||||
}
|
||||
|
||||
/// Computes the diffences between an old contacts event and a new contacts event for our own user, and updates the search cache accordingly.
|
||||
func updateOwnContactsPetnames(id: String, oldEvent: NostrEvent?, newEvent: NostrEvent) {
|
||||
func updateOwnContactsPetnames(id: Pubkey, oldEvent: NostrEvent?, newEvent: NostrEvent) {
|
||||
guard newEvent.known_kind == .contacts && newEvent.pubkey == id else {
|
||||
return
|
||||
}
|
||||
|
||||
var petnames: [String: String] = [:]
|
||||
|
||||
// Gets all petnames from our new contacts list.
|
||||
newEvent.tags.forEach { tag in
|
||||
guard tag.count >= 4 && tag[0] == "p" else {
|
||||
var petnames: [Pubkey: String] = [:]
|
||||
for tag in newEvent.tags {
|
||||
guard tag.count > 3,
|
||||
let chr = tag[0].single_char, chr == "p",
|
||||
let id = tag[1].id()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let petname = tag[3]
|
||||
let pubkey = Pubkey(id)
|
||||
|
||||
petnames[pubkey] = petname
|
||||
petnames[pubkey] = tag[3].string()
|
||||
}
|
||||
|
||||
// Compute the diff with the old contacts list, if it exists,
|
||||
// mark the ones that are the same to not be removed from the user search cache,
|
||||
// and remove the old ones that are different from the user search cache.
|
||||
if let oldEvent, oldEvent.known_kind == .contacts && oldEvent.pubkey == id {
|
||||
oldEvent.tags.forEach { tag in
|
||||
guard tag.count >= 4 && tag[0] == "p" else {
|
||||
if let oldEvent, oldEvent.known_kind == .contacts, oldEvent.pubkey == id {
|
||||
for tag in oldEvent.tags {
|
||||
guard tag.count >= 4,
|
||||
tag[0].matches_char("p"),
|
||||
let id = tag[1].id()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let oldPetname = tag[3]
|
||||
let pubkey = Pubkey(id)
|
||||
|
||||
let oldPetname = tag[3].string()
|
||||
|
||||
if let newPetname = petnames[pubkey] {
|
||||
if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame {
|
||||
|
||||
@@ -10,6 +10,31 @@ import UIKit
|
||||
|
||||
let fallback_zap_amount = 1000
|
||||
|
||||
func setting_property_key(key: String) -> String {
|
||||
return pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
|
||||
}
|
||||
|
||||
func setting_get_property_value<T>(key: String, scoped_key: String, default_value: T) -> T {
|
||||
if let loaded = UserDefaults.standard.object(forKey: scoped_key) as? T {
|
||||
return loaded
|
||||
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
|
||||
// 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.
|
||||
UserDefaults.standard.set(loaded, forKey: scoped_key)
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
return loaded
|
||||
} else {
|
||||
return default_value
|
||||
}
|
||||
}
|
||||
|
||||
func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
|
||||
guard old_value != new_value else { return nil }
|
||||
UserDefaults.standard.set(new_value, forKey: scoped_key)
|
||||
UserSettingsStore.shared?.objectWillChange.send()
|
||||
return new_value
|
||||
}
|
||||
|
||||
@propertyWrapper struct Setting<T: Equatable> {
|
||||
private let key: String
|
||||
private var value: T
|
||||
@@ -18,29 +43,17 @@ let fallback_zap_amount = 1000
|
||||
if T.self == Bool.self {
|
||||
UserSettingsStore.bool_options.insert(key)
|
||||
}
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
let scoped_key = setting_property_key(key: key)
|
||||
|
||||
self.value = setting_get_property_value(key: key, scoped_key: scoped_key, default_value: default_value)
|
||||
self.key = scoped_key
|
||||
}
|
||||
|
||||
|
||||
var wrappedValue: T {
|
||||
get { return value }
|
||||
set {
|
||||
guard self.value != newValue else {
|
||||
return
|
||||
}
|
||||
self.value = newValue
|
||||
UserDefaults.standard.set(newValue, forKey: key)
|
||||
UserSettingsStore.shared!.objectWillChange.send()
|
||||
guard let new_val = setting_set_property_value(scoped_key: key, old_value: value, new_value: newValue) else { return }
|
||||
self.value = new_val
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +63,7 @@ let fallback_zap_amount = 1000
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
|
||||
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) {
|
||||
@@ -78,7 +91,7 @@ let fallback_zap_amount = 1000
|
||||
}
|
||||
|
||||
class UserSettingsStore: ObservableObject {
|
||||
static var pubkey: String? = nil
|
||||
static var pubkey: Pubkey? = nil
|
||||
static var shared: UserSettingsStore? = nil
|
||||
static var bool_options = Set<String>()
|
||||
|
||||
@@ -96,6 +109,9 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "always_show_images", default_value: false)
|
||||
var always_show_images: Bool
|
||||
|
||||
@Setting(key: "hide_nsfw_tagged_content", default_value: false)
|
||||
var hide_nsfw_tagged_content: Bool
|
||||
|
||||
@Setting(key: "zap_vibration", default_value: true)
|
||||
var zap_vibration: Bool
|
||||
@@ -114,7 +130,10 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "repost_notification", default_value: true)
|
||||
var repost_notification: Bool
|
||||
|
||||
|
||||
@Setting(key: "font_size", default_value: 1.0)
|
||||
var font_size: Double
|
||||
|
||||
@Setting(key: "dm_notification", default_value: true)
|
||||
var dm_notification: Bool
|
||||
|
||||
@@ -143,6 +162,12 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "auto_translate", default_value: true)
|
||||
var auto_translate: Bool
|
||||
|
||||
@Setting(key: "show_general_statuses", default_value: true)
|
||||
var show_general_statuses: Bool
|
||||
|
||||
@Setting(key: "show_music_statuses", default_value: true)
|
||||
var show_music_statuses: Bool
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@@ -160,6 +185,12 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "developer_mode", default_value: false)
|
||||
var developer_mode: Bool
|
||||
|
||||
@Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
|
||||
var emoji_reactions: [String]
|
||||
|
||||
@Setting(key: "default_emoji_reaction", default_value: "🤙")
|
||||
var default_emoji_reaction: String
|
||||
|
||||
// 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.
|
||||
@@ -190,7 +221,7 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "libretranslate_server", default_value: .terraprint)
|
||||
@StringSetting(key: "libretranslate_server", default_value: .custom)
|
||||
var libretranslate_server: LibreTranslateServer
|
||||
|
||||
@Setting(key: "libretranslate_url", default_value: "")
|
||||
@@ -242,6 +273,6 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: String, key: String) -> String {
|
||||
return "\(pubkey)_\(key)"
|
||||
func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
|
||||
return "\(pubkey.hex())_\(key)"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum WalletConnectState {
|
||||
class WalletModel: ObservableObject {
|
||||
var settings: UserSettingsStore
|
||||
private(set) var previous_state: WalletConnectState
|
||||
var inital_percent: Int
|
||||
var initial_percent: Int
|
||||
|
||||
@Published private(set) var connect_state: WalletConnectState
|
||||
|
||||
@@ -24,7 +24,7 @@ class WalletModel: ObservableObject {
|
||||
self.connect_state = state
|
||||
self.previous_state = .none
|
||||
self.settings = settings
|
||||
self.inital_percent = settings.donation_percent
|
||||
self.initial_percent = settings.donation_percent
|
||||
}
|
||||
|
||||
init(settings: UserSettingsStore) {
|
||||
@@ -37,7 +37,7 @@ class WalletModel: ObservableObject {
|
||||
self.previous_state = .none
|
||||
self.connect_state = .none
|
||||
}
|
||||
self.inital_percent = settings.donation_percent
|
||||
self.initial_percent = settings.donation_percent
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
@@ -57,7 +57,7 @@ class WalletModel: ObservableObject {
|
||||
|
||||
func connect(_ nwc: WalletConnectURL) {
|
||||
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||
notify(.attached_wallet, nwc)
|
||||
notify(.attached_wallet(nwc))
|
||||
self.connect_state = .existing(nwc)
|
||||
self.previous_state = .existing(nwc)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ class ZapsModel: ObservableObject {
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: zaps_subid)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
|
||||
guard case .nostr_event(let resp) = conn_ev else {
|
||||
return
|
||||
@@ -56,15 +57,10 @@ class ZapsModel: ObservableObject {
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
case .event(_, let ev):
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
|
||||
guard ev.kind == 9735,
|
||||
let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey),
|
||||
let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
//
|
||||
// PersistedProfile.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 4/30/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PersistedProfile)
|
||||
final class PersistedProfile: NSManagedObject {
|
||||
@NSManaged var id: String?
|
||||
@NSManaged var name: String?
|
||||
@NSManaged var display_name: String?
|
||||
@NSManaged var about: String?
|
||||
@NSManaged var picture: String?
|
||||
@NSManaged var banner: String?
|
||||
@NSManaged var website: String?
|
||||
@NSManaged var lud06: String?
|
||||
@NSManaged var lud16: String?
|
||||
@NSManaged var nip05: String?
|
||||
@NSManaged var damus_donation: Int16
|
||||
@NSManaged var last_update: Date? // The date that the profile was last updated by the user
|
||||
@NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
|
||||
|
||||
func copyValues(from profile: Profile) {
|
||||
name = profile.name
|
||||
display_name = profile.display_name
|
||||
about = profile.about
|
||||
picture = profile.picture
|
||||
banner = profile.banner
|
||||
website = profile.website
|
||||
lud06 = profile.lud06
|
||||
lud16 = profile.lud16
|
||||
nip05 = profile.nip05
|
||||
damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// Id.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-07-26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TagRef<T>: Hashable, Equatable, Encodable {
|
||||
let elem: TagElem
|
||||
|
||||
init(_ elem: TagElem) {
|
||||
self.elem = elem
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(elem.string())
|
||||
}
|
||||
}
|
||||
|
||||
protocol TagKey {
|
||||
var keychar: AsciiCharacter { get }
|
||||
}
|
||||
|
||||
protocol TagKeys {
|
||||
associatedtype TagKeys: TagKey
|
||||
var key: TagKeys { get }
|
||||
}
|
||||
|
||||
protocol TagConvertible {
|
||||
var tag: [String] { get }
|
||||
static func from_tag(tag: TagSequence) -> Self?
|
||||
}
|
||||
|
||||
struct QuoteId: IdType, TagKey {
|
||||
let id: Data
|
||||
|
||||
init(_ data: Data) {
|
||||
self.id = data
|
||||
}
|
||||
|
||||
var keychar: AsciiCharacter { "q" }
|
||||
}
|
||||
|
||||
|
||||
struct Privkey: IdType {
|
||||
let id: Data
|
||||
|
||||
var nsec: String {
|
||||
bech32_privkey(self)
|
||||
}
|
||||
|
||||
init?(hex: String) {
|
||||
guard let id = hex_decode_id(hex) else {
|
||||
return nil
|
||||
}
|
||||
self.init(id)
|
||||
}
|
||||
|
||||
init(_ data: Data) {
|
||||
self.id = data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct Hashtag: TagConvertible, Hashable {
|
||||
let hashtag: String
|
||||
|
||||
static func from_tag(tag: TagSequence) -> Hashtag? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard tag.count >= 2,
|
||||
let t0 = i.next(),
|
||||
let chr = t0.single_char,
|
||||
chr == "t",
|
||||
let t1 = i.next() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Hashtag(hashtag: t1.string())
|
||||
}
|
||||
|
||||
var tag: [String] { ["t", self.hashtag] }
|
||||
var keychar: AsciiCharacter { "t" }
|
||||
}
|
||||
|
||||
struct ReplaceableParam: TagConvertible {
|
||||
let param: TagElem
|
||||
|
||||
static func from_tag(tag: TagSequence) -> ReplaceableParam? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard tag.count >= 2,
|
||||
let t0 = i.next(),
|
||||
let chr = t0.single_char,
|
||||
chr == "d",
|
||||
let t1 = i.next() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ReplaceableParam(param: t1)
|
||||
}
|
||||
|
||||
var tag: [String] { [self.keychar.description, self.param.string()] }
|
||||
var keychar: AsciiCharacter { "d" }
|
||||
}
|
||||
|
||||
struct Signature: Hashable, Equatable {
|
||||
let data: Data
|
||||
|
||||
init(_ p: Data) {
|
||||
self.data = p
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user