Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
78851d5a91
|
@@ -5,7 +5,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "ci"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
@@ -3,4 +3,3 @@ xcuserdata
|
||||
damus/TestingPrivate.swift
|
||||
.DS_Store
|
||||
TODO.bak
|
||||
tags
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Terry Yiu <git@tyiu.xyz> <963907+tyiu@users.noreply.github.com>
|
||||
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>
|
||||
@@ -1,149 +1,3 @@
|
||||
## [1.6-4] - 2023-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add the ability to follow hashtags (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove note size restriction for longform events (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hide users and hashtags from home timeline when you unfollow (William Casarin)
|
||||
- Fixed a bug where following a user might not work due to poor connectivity (William Casarin)
|
||||
- Icon color for developer mode setting is incorrect in low-light mode (Bryan Montz)
|
||||
- Fixed nav bar color on login, eula, and account creation (ericholguin)
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove following Damus Will by default (William Casarin)
|
||||
|
||||
[1.6-4]: https://github.com/damus-io/damus/releases/tag/v1.6-4
|
||||
|
||||
## [1.6-3] - 2023-07-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Start at top when reading longform events (William Casarin)
|
||||
- Allow reposting and quote reposting multiple times (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show longform previews in notifications instead of the entire post (William Casarin)
|
||||
- Fix padding on longform events (William Casarin)
|
||||
- Fix action bar appearing on quoted longform previews (William Casarin)
|
||||
|
||||
|
||||
[1.6-3]: https://github.com/damus-io/damus/releases/tag/v1.6-3
|
||||
## [1.6-2] - 2023-07-11
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for multilingual hashtags (cr0bar)
|
||||
- Add r tag when mentioning a url (William Casarin)
|
||||
- Add initial longform note support (William Casarin)
|
||||
- Enable banner image editing (Joel Klabo)
|
||||
- Add relay log in developer mode (Bryan Montz)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix lag when creating large posts (William Casarin)
|
||||
- Fix npub mentions failing to parse in some cases (William Casarin)
|
||||
- Fix PostView initial string to skip mentioning self when on own profile (Terry Yiu)
|
||||
- Fix freezing bug when tapping Developer settings menu (Terry Yiu)
|
||||
- Fix potential fake profile zap attacks (William Casarin)
|
||||
- Fix issue where malicious zappers can send fake zaps to another user's posts (William Casarin)
|
||||
- Fix profile post button mentions (cr0bar)
|
||||
- Fix icons on settings view (cr0bar)
|
||||
- Fix Invalid Zap bug in reposts (William Casarin)
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove old @ and & hex key mentions (William Casarin)
|
||||
|
||||
|
||||
[1.6-2]: https://github.com/damus-io/damus/releases/tag/v1.6-2
|
||||
## [1.6] - 2023-07-04
|
||||
|
||||
### Added
|
||||
|
||||
- Speed up user search (Terry Yiu)
|
||||
- Add post button to profile pages (William Casarin)
|
||||
- Add post button when logged in with private key and on own profile view (Terry Yiu)
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop iOS15 support (Scott Penrose)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Load more content on profile view (William Casarin)
|
||||
- Fix reports to conform to NIP-56 (Terry Yiu)
|
||||
- Fix profile navigation bugs from muted users list and relay list views (Terry Yiu)
|
||||
- Fix navigation to translation settings view (Terry Yiu)
|
||||
- Fixed all navigation issues (Scott Penrose)
|
||||
- Disable post button when media upload in progress (Terry Yiu)
|
||||
- Fix taps on mentions in note drafts to not redirect to other Nostr clients (Terry Yiu)
|
||||
- Fix missing profile zap notification text (Terry Yiu)
|
||||
|
||||
|
||||
[1.6]: https://github.com/damus-io/damus/releases/tag/v1.6
|
||||
|
||||
## [1.5-5] - 2023-06-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove note zaps to fit apples appstore guidelines
|
||||
- Fix zap sheet popping (William Casarin)
|
||||
- Fix CustomizeZapView from randomly disappearing (William Casarin)
|
||||
- Fix "zapped your profile" strings to say "zapped you" (Terry Yiu)
|
||||
- Fix reconnect loop issues on iOS17 (William Casarin)
|
||||
- Fix some more thread jankiness (William Casarin)
|
||||
- Fix spelling of Nostr to use Titlecase instead of lowercase (Terry Yiu)
|
||||
- Rename all usages of the term Post as a noun to Note to conform to the Nostr spec (Terry Yiu)
|
||||
- Fix text cutoff on login with npub (gladiusKatana)
|
||||
- Fix hangs due to video player (William Casarin)
|
||||
|
||||
|
||||
[1.5-5]: https://github.com/damus-io/damus/releases/tag/v1.5-5
|
||||
|
||||
## [1.5-2] - 2023-05-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add new full-bleed video player (William Casarin)
|
||||
- Add ability to show multiple posts per user in Universe (Ben Weeks)
|
||||
- Custom iconography added for other areas of the app. (Ben Weeks)
|
||||
- Custom iconography for the left navigation. (Ben Weeks)
|
||||
- Custom iconography for the tab buttons. (Ben Weeks)
|
||||
- Added dots under image carousel (Ben Weeks)
|
||||
- Add profile caching (Bryan Montz)
|
||||
- Add mention parsing and fine-grained text selection on description in ProfileView (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesign phase 1 (text, icons)
|
||||
- Updated UI to use custom font (Ben Weeks)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix side menu bug in landscape (OlegAba)
|
||||
- Use "Follow me on nostr" text when looking at someone else's QR code (Ben Weeks)
|
||||
- Fix issue where cursor dissapears when typing long message (gladiusKatana)
|
||||
- Attempt fix for randomly broken animated gifs (William Casarin)
|
||||
- Fix cursor jumping when pressing return (gladius)
|
||||
- Fix side menu label size so that translations in longer languages fit without wrapping (Terry Yiu)
|
||||
- Fix reaction notification title to be consistent with ReactionView (Terry Yiu)
|
||||
- Fix nostr URL scheme to open properly even if there's already a different view open (Terry Yiu)
|
||||
- Fix crash related to preloading events (Bryan Montz)
|
||||
|
||||
|
||||
## v1.4.3 - 2023-05-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
all: nostrscript/primal.wasm
|
||||
|
||||
nostrscript/%.wasm: nostrscript/%.ts nostrscript/nostr.ts Makefile
|
||||
asc $< --runtime stub --outFile $@ --optimize
|
||||
|
||||
clean:
|
||||
rm nostrscript/*.wasm
|
||||
@@ -94,23 +94,11 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
|
||||
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
|
||||
|
||||
### Mailing lists
|
||||
### Code
|
||||
|
||||
We have a few mailing lists that anyone can join to get involved in damus development:
|
||||
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
|
||||
|
||||
- [dev][dev-list] - development discussions
|
||||
- [patches][patches-list] - code submission and review
|
||||
- [product][product-list] - product discussions
|
||||
- [design][design-list] - design discussions
|
||||
|
||||
[dev-list]: https://damus.io/list/dev
|
||||
[patches-list]: https://damus.io/list/patches
|
||||
[product-list]: https://damus.io/list/product
|
||||
[design-list]: https://damus.io/list/design
|
||||
|
||||
### Contributing
|
||||
|
||||
See [docs/CONTRIBUTING.md](./docs/CONTRIBUTING.md)
|
||||
[git-send-email]: http://git-send-email.io
|
||||
|
||||
### Privacy
|
||||
Your internet protocol (IP) address is exposed to the relays you connect to, and third party media hosters (e.g. nostr.build, imgur.com, giphy.com, youtube.com etc.) that render on Damus. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
|
||||
|
||||
@@ -35,7 +35,7 @@ typedef struct mention_bech32_block {
|
||||
struct nostr_bech32 bech32;
|
||||
} mention_bech32_block_t;
|
||||
|
||||
typedef struct note_block {
|
||||
typedef struct block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
@@ -45,13 +45,12 @@ typedef struct note_block {
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct note_blocks {
|
||||
int words;
|
||||
typedef struct blocks {
|
||||
int num_blocks;
|
||||
struct note_block *blocks;
|
||||
struct block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct note_blocks *blocks);
|
||||
void blocks_free(struct note_blocks *blocks);
|
||||
void blocks_init(struct blocks *blocks);
|
||||
void blocks_free(struct blocks *blocks);
|
||||
|
||||
#endif /* block_h */
|
||||
|
||||
@@ -1,438 +1,30 @@
|
||||
//
|
||||
// cursor.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef PROTOVERSE_CURSOR_H
|
||||
#define PROTOVERSE_CURSOR_H
|
||||
#ifndef cursor_h
|
||||
#define cursor_h
|
||||
|
||||
#include "typedefs.h"
|
||||
#include "varint.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#define unlikely(x) __builtin_expect((x),0)
|
||||
#define likely(x) __builtin_expect((x),1)
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct cursor {
|
||||
unsigned char *start;
|
||||
unsigned char *p;
|
||||
unsigned char *end;
|
||||
const u8 *p;
|
||||
const u8 *start;
|
||||
const u8 *end;
|
||||
};
|
||||
|
||||
struct array {
|
||||
struct cursor cur;
|
||||
unsigned int elem_size;
|
||||
};
|
||||
|
||||
static inline void reset_cursor(struct cursor *cursor)
|
||||
{
|
||||
cursor->p = cursor->start;
|
||||
}
|
||||
|
||||
static inline void wipe_cursor(struct cursor *cursor)
|
||||
{
|
||||
reset_cursor(cursor);
|
||||
memset(cursor->start, 0, cursor->end - cursor->start);
|
||||
}
|
||||
|
||||
static inline void make_cursor(u8 *start, u8 *end, struct cursor *cursor)
|
||||
{
|
||||
cursor->start = start;
|
||||
cursor->p = start;
|
||||
cursor->end = end;
|
||||
}
|
||||
|
||||
static inline void make_array(struct array *a, u8* start, u8 *end, unsigned int elem_size)
|
||||
{
|
||||
make_cursor(start, end, &a->cur);
|
||||
a->elem_size = elem_size;
|
||||
}
|
||||
|
||||
static inline int cursor_eof(struct cursor *c)
|
||||
{
|
||||
return c->p == c->end;
|
||||
}
|
||||
|
||||
static inline void *cursor_malloc(struct cursor *mem, unsigned long size)
|
||||
{
|
||||
void *ret;
|
||||
|
||||
if (mem->p + size > mem->end) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mem->p;
|
||||
mem->p += size;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline void *cursor_alloc(struct cursor *mem, unsigned long size)
|
||||
{
|
||||
void *ret;
|
||||
if (!(ret = cursor_malloc(mem, size))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(ret, 0, size);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline int cursor_slice(struct cursor *mem, struct cursor *slice, size_t size)
|
||||
{
|
||||
u8 *p;
|
||||
if (!(p = cursor_alloc(mem, size))) {
|
||||
return 0;
|
||||
}
|
||||
make_cursor(p, mem->p, slice);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static inline void copy_cursor(struct cursor *src, struct cursor *dest)
|
||||
{
|
||||
dest->start = src->start;
|
||||
dest->p = src->p;
|
||||
dest->end = src->end;
|
||||
}
|
||||
|
||||
static inline int pull_byte(struct cursor *cursor, u8 *c)
|
||||
{
|
||||
if (unlikely(cursor->p >= cursor->end))
|
||||
return 0;
|
||||
|
||||
*c = *cursor->p;
|
||||
cursor->p++;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_pull_c_str(struct cursor *cursor, const char **str)
|
||||
{
|
||||
*str = (const char*)cursor->p;
|
||||
|
||||
for (; cursor->p < cursor->end; cursor->p++) {
|
||||
if (*cursor->p == 0) {
|
||||
cursor->p++;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static inline int cursor_push_byte(struct cursor *cursor, u8 c)
|
||||
{
|
||||
if (unlikely(cursor->p + 1 > cursor->end)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*cursor->p = c;
|
||||
cursor->p++;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_pull(struct cursor *cursor, u8 *data, int len)
|
||||
{
|
||||
if (unlikely(cursor->p + len > cursor->end)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(data, cursor->p, len);
|
||||
cursor->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int pull_data_into_cursor(struct cursor *cursor,
|
||||
struct cursor *dest,
|
||||
unsigned char **data,
|
||||
int len)
|
||||
{
|
||||
int ok;
|
||||
|
||||
if (unlikely(dest->p + len > dest->end)) {
|
||||
printf("not enough room in dest buffer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ok = cursor_pull(cursor, dest->p, len);
|
||||
if (!ok) return 0;
|
||||
|
||||
*data = dest->p;
|
||||
dest->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_dropn(struct cursor *cur, int size, int n)
|
||||
{
|
||||
if (n == 0)
|
||||
return 1;
|
||||
|
||||
if (unlikely(cur->p - size*n < cur->start)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p -= size*n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_drop(struct cursor *cur, int size)
|
||||
{
|
||||
return cursor_dropn(cur, size, 1);
|
||||
}
|
||||
|
||||
static inline unsigned char *cursor_topn(struct cursor *cur, int len, int n)
|
||||
{
|
||||
n += 1;
|
||||
if (unlikely(cur->p - len*n < cur->start)) {
|
||||
return NULL;
|
||||
}
|
||||
return cur->p - len*n;
|
||||
}
|
||||
|
||||
static inline unsigned char *cursor_top(struct cursor *cur, int len)
|
||||
{
|
||||
if (unlikely(cur->p - len < cur->start)) {
|
||||
return NULL;
|
||||
}
|
||||
return cur->p - len;
|
||||
}
|
||||
|
||||
static inline int cursor_top_int(struct cursor *cur, int *i)
|
||||
{
|
||||
u8 *p;
|
||||
if (unlikely(!(p = cursor_top(cur, sizeof(*i))))) {
|
||||
return 0;
|
||||
}
|
||||
*i = *((int*)p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_pop(struct cursor *cur, u8 *data, int len)
|
||||
{
|
||||
if (unlikely(cur->p - len < cur->start)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p -= len;
|
||||
memcpy(data, cur->p, len);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_push(struct cursor *cursor, u8 *data, int len)
|
||||
{
|
||||
if (unlikely(cursor->p + len >= cursor->end)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cursor->p != data)
|
||||
memcpy(cursor->p, data, len);
|
||||
|
||||
cursor->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_push_int(struct cursor *cursor, int i)
|
||||
{
|
||||
return cursor_push(cursor, (u8*)&i, sizeof(i));
|
||||
}
|
||||
|
||||
static inline size_t cursor_count(struct cursor *cursor, size_t elem_size)
|
||||
{
|
||||
return (cursor->p - cursor->start)/elem_size;
|
||||
}
|
||||
|
||||
/* TODO: push_varint */
|
||||
static inline int push_varint(struct cursor *cursor, int n)
|
||||
{
|
||||
int ok, len;
|
||||
unsigned char b;
|
||||
len = 0;
|
||||
|
||||
while (1) {
|
||||
b = (n & 0xFF) | 0x80;
|
||||
n >>= 7;
|
||||
if (n == 0) {
|
||||
b &= 0x7F;
|
||||
ok = cursor_push_byte(cursor, b);
|
||||
len++;
|
||||
if (!ok) return 0;
|
||||
break;
|
||||
}
|
||||
|
||||
ok = cursor_push_byte(cursor, b);
|
||||
len++;
|
||||
if (!ok) return 0;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/* TODO: pull_varint */
|
||||
static inline int pull_varint(struct cursor *cursor, int *n)
|
||||
{
|
||||
int ok, i;
|
||||
unsigned char b;
|
||||
*n = 0;
|
||||
|
||||
for (i = 0;; i++) {
|
||||
ok = pull_byte(cursor, &b);
|
||||
if (!ok) return 0;
|
||||
|
||||
*n |= ((int)b & 0x7F) << (i * 7);
|
||||
|
||||
/* is_last */
|
||||
if ((b & 0x80) == 0) {
|
||||
return i+1;
|
||||
}
|
||||
|
||||
if (i == 4) return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int cursor_pull_int(struct cursor *cursor, int *i)
|
||||
{
|
||||
return cursor_pull(cursor, (u8*)i, sizeof(*i));
|
||||
}
|
||||
|
||||
static inline int cursor_push_u16(struct cursor *cursor, u16 i)
|
||||
{
|
||||
return cursor_push(cursor, (u8*)&i, sizeof(i));
|
||||
}
|
||||
|
||||
static inline void *index_cursor(struct cursor *cursor, unsigned int index, int elem_size)
|
||||
{
|
||||
u8 *p;
|
||||
p = &cursor->start[elem_size * index];
|
||||
|
||||
if (unlikely(p >= cursor->end))
|
||||
return NULL;
|
||||
|
||||
return (void*)p;
|
||||
}
|
||||
|
||||
|
||||
static inline int push_sized_str(struct cursor *cursor, const char *str, int len)
|
||||
{
|
||||
return cursor_push(cursor, (u8*)str, len);
|
||||
}
|
||||
|
||||
static inline int cursor_push_str(struct cursor *cursor, const char *str)
|
||||
{
|
||||
return cursor_push(cursor, (u8*)str, (int)strlen(str));
|
||||
}
|
||||
|
||||
static inline int cursor_push_c_str(struct cursor *cursor, const char *str)
|
||||
{
|
||||
return cursor_push_str(cursor, str) && cursor_push_byte(cursor, 0);
|
||||
}
|
||||
|
||||
/* TODO: push varint size */
|
||||
static inline int push_prefixed_str(struct cursor *cursor, const char *str)
|
||||
{
|
||||
int ok, len;
|
||||
len = (int)strlen(str);
|
||||
ok = push_varint(cursor, len);
|
||||
if (!ok) return 0;
|
||||
return push_sized_str(cursor, str, len);
|
||||
}
|
||||
|
||||
static inline int pull_prefixed_str(struct cursor *cursor, struct cursor *dest_buf, const char **str)
|
||||
{
|
||||
int len, ok;
|
||||
|
||||
ok = pull_varint(cursor, &len);
|
||||
if (!ok) return 0;
|
||||
|
||||
if (unlikely(dest_buf->p + len > dest_buf->end)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ok = pull_data_into_cursor(cursor, dest_buf, (unsigned char**)str, len);
|
||||
if (!ok) return 0;
|
||||
|
||||
ok = cursor_push_byte(dest_buf, 0);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int cursor_remaining_capacity(struct cursor *cursor)
|
||||
{
|
||||
return (int)(cursor->end - cursor->p);
|
||||
}
|
||||
|
||||
|
||||
#define max(a,b) ((a) > (b) ? (a) : (b))
|
||||
static inline void cursor_print_around(struct cursor *cur, int range)
|
||||
{
|
||||
unsigned char *c;
|
||||
|
||||
printf("[%ld/%ld]\n", cur->p - cur->start, cur->end - cur->start);
|
||||
|
||||
c = max(cur->p - range, cur->start);
|
||||
for (; c < cur->end && c < (cur->p + range); c++) {
|
||||
printf("%02x", *c);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
c = max(cur->p - range, cur->start);
|
||||
for (; c < cur->end && c < (cur->p + range); c++) {
|
||||
if (c == cur->p) {
|
||||
printf("^");
|
||||
continue;
|
||||
}
|
||||
printf(" ");
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
#undef max
|
||||
|
||||
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
|
||||
if (cur->p + count > cur->end)
|
||||
return 0;
|
||||
|
||||
*bytes = cur->p;
|
||||
cur->p += count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int parse_str(struct cursor *cur, const char *str) {
|
||||
int i;
|
||||
char c, cs;
|
||||
unsigned long len;
|
||||
|
||||
len = strlen(str);
|
||||
|
||||
if (cur->p + len >= cur->end)
|
||||
return 0;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
c = tolower(cur->p[i]);
|
||||
cs = tolower(str[i]);
|
||||
|
||||
if (c != cs)
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
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);
|
||||
return !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
@@ -443,6 +35,13 @@ static inline int is_alphanumeric(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
c->end = content + len;
|
||||
c->p = content;
|
||||
}
|
||||
|
||||
static inline int consume_until_boundary(struct cursor *cur) {
|
||||
char c;
|
||||
|
||||
@@ -511,4 +110,62 @@ static inline int peek_char(struct cursor *cur, int ind) {
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
#endif
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static inline int pull_byte(struct cursor *cur, u8 *byte) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
*byte = *cur->p;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
|
||||
if (cur->p + count > cur->end)
|
||||
return 0;
|
||||
|
||||
*bytes = cur->p;
|
||||
cur->p += count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int parse_str(struct cursor *cur, const char *str) {
|
||||
int i;
|
||||
char c, cs;
|
||||
unsigned long len;
|
||||
|
||||
len = strlen(str);
|
||||
|
||||
if (cur->p + len >= cur->end)
|
||||
return 0;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
c = tolower(cur->p[i]);
|
||||
cs = tolower(str[i]);
|
||||
|
||||
if (c != cs)
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
#endif /* cursor_h */
|
||||
|
||||
@@ -5,7 +5,3 @@
|
||||
#include "damus.h"
|
||||
#include "bolt11.h"
|
||||
#include "amount.h"
|
||||
#include "nostr_bech32.h"
|
||||
#include "wasm.h"
|
||||
#include "nostrscript.h"
|
||||
|
||||
|
||||
@@ -12,25 +12,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_index(struct cursor *cur, struct note_block *block) {
|
||||
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
||||
int d1, d2, d3, ind;
|
||||
u8 *start = cur->p;
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "#["))
|
||||
return 0;
|
||||
@@ -59,9 +43,9 @@ static int parse_mention_index(struct cursor *cur, struct note_block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_hashtag(struct cursor *cur, struct note_block *block) {
|
||||
static int parse_hashtag(struct cursor *cur, struct block *block) {
|
||||
int c;
|
||||
u8 *start = cur->p;
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_char(cur, '#'))
|
||||
return 0;
|
||||
@@ -81,7 +65,7 @@ static int parse_hashtag(struct cursor *cur, struct note_block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_block(struct note_blocks *blocks, struct note_block block)
|
||||
static int add_block(struct blocks *blocks, struct block block)
|
||||
{
|
||||
if (blocks->num_blocks + 1 >= MAX_BLOCKS)
|
||||
return 0;
|
||||
@@ -90,9 +74,9 @@ static int add_block(struct note_blocks *blocks, struct note_block block)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8 *end)
|
||||
static int add_text_block(struct blocks *blocks, const u8 *start, const u8 *end)
|
||||
{
|
||||
struct note_block b;
|
||||
struct block b;
|
||||
|
||||
if (start == end)
|
||||
return 1;
|
||||
@@ -104,8 +88,8 @@ static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8
|
||||
return add_block(blocks, b);
|
||||
}
|
||||
|
||||
static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start = cur->p;
|
||||
static int parse_url(struct cursor *cur, struct block *block) {
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "http"))
|
||||
return 0;
|
||||
@@ -137,8 +121,8 @@ static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_invoice(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start, *end;
|
||||
static int parse_invoice(struct cursor *cur, struct block *block) {
|
||||
const u8 *start, *end;
|
||||
char *fail;
|
||||
struct bolt11 *bolt11;
|
||||
// optional
|
||||
@@ -177,12 +161,12 @@ static int parse_invoice(struct cursor *cur, struct note_block *block) {
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_bech32(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start = cur->p;
|
||||
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "nostr:"))
|
||||
return 0;
|
||||
|
||||
parse_char(cur, '@');
|
||||
parse_str(cur, "nostr:");
|
||||
|
||||
block->block.str.start = (const char *)cur->p;
|
||||
|
||||
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
|
||||
@@ -197,7 +181,7 @@ static int parse_mention_bech32(struct cursor *cur, struct note_block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_text_then_block(struct cursor *cur, struct note_blocks *blocks, struct note_block block, u8 **start, const u8 *pre_mention)
|
||||
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
|
||||
{
|
||||
if (!add_text_block(blocks, *start, pre_mention))
|
||||
return 0;
|
||||
@@ -210,28 +194,22 @@ static int add_text_then_block(struct cursor *cur, struct note_blocks *blocks, s
|
||||
return 1;
|
||||
}
|
||||
|
||||
int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
||||
int damus_parse_content(struct blocks *blocks, const char *content) {
|
||||
int cp, c;
|
||||
struct cursor cur;
|
||||
struct note_block block;
|
||||
u8 *start, *pre_mention;
|
||||
struct block block;
|
||||
const u8 *start, *pre_mention;
|
||||
|
||||
blocks->words = 0;
|
||||
blocks->num_blocks = 0;
|
||||
make_cursor((u8*)content, (u8*)content + strlen(content), &cur);
|
||||
make_cursor(&cur, (const u8*)content, strlen(content));
|
||||
|
||||
start = cur.p;
|
||||
while (cur.p < cur.end && blocks->num_blocks < MAX_BLOCKS) {
|
||||
cp = peek_char(&cur, -1);
|
||||
c = peek_char(&cur, 0);
|
||||
|
||||
// new word
|
||||
if (is_whitespace(cp) && !is_whitespace(c)) {
|
||||
blocks->words++;
|
||||
}
|
||||
|
||||
pre_mention = cur.p;
|
||||
if (cp == -1 || is_boundary(cp) || c == '#') {
|
||||
if (cp == -1 || is_whitespace(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;
|
||||
@@ -244,7 +222,7 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if ((c == 'n' || c == '@') && parse_mention_bech32(&cur, &block)) {
|
||||
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
@@ -262,12 +240,12 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void blocks_init(struct note_blocks *blocks) {
|
||||
blocks->blocks = malloc(sizeof(struct note_block) * MAX_BLOCKS);
|
||||
void blocks_init(struct blocks *blocks) {
|
||||
blocks->blocks = malloc(sizeof(struct block) * MAX_BLOCKS);
|
||||
blocks->num_blocks = 0;
|
||||
}
|
||||
|
||||
void blocks_free(struct note_blocks *blocks) {
|
||||
void blocks_free(struct blocks *blocks) {
|
||||
if (!blocks->blocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
#define damus_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "nostr_bech32.h"
|
||||
#include "block.h"
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
int damus_parse_content(struct note_blocks *blocks, const char *content);
|
||||
int damus_parse_content(struct blocks *blocks, const char *content);
|
||||
|
||||
#endif /* damus_h */
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_DEBUG_H
|
||||
#define PROTOVERSE_DEBUG_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define unusual(...) fprintf(stderr, "UNUSUAL: " __VA_ARGS__)
|
||||
|
||||
#ifdef DEBUG
|
||||
#define debug(...) printf(__VA_ARGS__)
|
||||
#else
|
||||
#define debug(...)
|
||||
#endif
|
||||
|
||||
#endif /* PROTOVERSE_DEBUG_H */
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
#include "error.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
int note_error_(struct errors *errs_, struct cursor *p, const char *fmt, ...)
|
||||
{
|
||||
static char buf[512];
|
||||
struct error err;
|
||||
struct cursor *errs;
|
||||
va_list ap;
|
||||
|
||||
errs = &errs_->cur;
|
||||
|
||||
if (errs_->enabled == 0)
|
||||
return 0;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsprintf(buf, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
err.msg = buf;
|
||||
err.pos = p ? (int)(p->p - p->start) : 0;
|
||||
|
||||
if (!cursor_push_error(errs, &err)) {
|
||||
fprintf(stderr, "arena OOM when recording error, ");
|
||||
fprintf(stderr, "errs->p at %ld, remaining %ld, strlen %ld\n",
|
||||
errs->p - errs->start, errs->end - errs->p, strlen(buf));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_ERROR_H
|
||||
#define PROTOVERSE_ERROR_H
|
||||
|
||||
#include "cursor.h"
|
||||
|
||||
struct error {
|
||||
int pos;
|
||||
const char *msg;
|
||||
};
|
||||
|
||||
struct errors {
|
||||
struct cursor cur;
|
||||
int enabled;
|
||||
};
|
||||
|
||||
#define note_error(errs, p, fmt, ...) note_error_(errs, p, "%s: " fmt, __FUNCTION__, ##__VA_ARGS__)
|
||||
|
||||
static inline int cursor_push_error(struct cursor *cur, struct error *err)
|
||||
{
|
||||
return cursor_push_int(cur, err->pos) &&
|
||||
cursor_push_c_str(cur, err->msg);
|
||||
}
|
||||
|
||||
static inline int cursor_pull_error(struct cursor *cur, struct error *err)
|
||||
{
|
||||
return cursor_pull_int(cur, &err->pos) &&
|
||||
cursor_pull_c_str(cur, &err->msg);
|
||||
}
|
||||
|
||||
int note_error_(struct errors *errs, struct cursor *p, const char *fmt, ...);
|
||||
|
||||
#endif /* PROTOVERSE_ERROR_H */
|
||||
@@ -26,7 +26,7 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize);
|
||||
/**
|
||||
* hex_encode - Create a nul-terminated hex string
|
||||
* @buf: the buffer to read the data from
|
||||
* @bufsize: the length of buf
|
||||
* @bufsize: the length of @buf
|
||||
* @dest: the string to fill
|
||||
* @destsize: the max size of the string
|
||||
*
|
||||
|
||||
@@ -52,13 +52,9 @@
|
||||
*/
|
||||
#define unlikely(cond) __builtin_expect(!!(cond), 0)
|
||||
#else
|
||||
#ifndef likely
|
||||
#define likely(cond) (!!(cond))
|
||||
#endif
|
||||
#ifndef unlikely
|
||||
#define unlikely(cond) (!!(cond))
|
||||
#endif
|
||||
#endif
|
||||
#else /* CCAN_LIKELY_DEBUG versions */
|
||||
#include <ccan/str/str.h>
|
||||
|
||||
|
||||
@@ -91,9 +91,6 @@ static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *t
|
||||
} else if (strcmp(prefix, "npub") == 0) {
|
||||
*type = NOSTR_BECH32_NPUB;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nsec") == 0) {
|
||||
*type = NOSTR_BECH32_NSEC;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nprofile") == 0) {
|
||||
*type = NOSTR_BECH32_NPROFILE;
|
||||
return 1;
|
||||
@@ -119,10 +116,6 @@ static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub)
|
||||
return pull_bytes(cur, 32, &npub->pubkey);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nsec(struct cursor *cur, struct bech32_nsec *nsec) {
|
||||
return pull_bytes(cur, 32, &nsec->nsec);
|
||||
}
|
||||
|
||||
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
|
||||
struct nostr_tlv *tlv;
|
||||
struct str_block *str;
|
||||
@@ -225,7 +218,7 @@ static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *n
|
||||
}
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
u8 *start, *end;
|
||||
const u8 *start, *end;
|
||||
|
||||
start = cur->p;
|
||||
|
||||
@@ -264,7 +257,7 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
}
|
||||
|
||||
struct cursor bcur;
|
||||
make_cursor(obj->buffer, obj->buffer + obj->buflen, &bcur);
|
||||
make_cursor(&bcur, obj->buffer, obj->buflen);
|
||||
|
||||
switch (obj->type) {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
@@ -275,10 +268,6 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NSEC:
|
||||
if (!parse_nostr_bech32_nsec(&bcur, &obj->data.nsec))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
|
||||
goto fail;
|
||||
|
||||
@@ -26,7 +26,6 @@ enum nostr_bech32_type {
|
||||
NOSTR_BECH32_NEVENT = 4,
|
||||
NOSTR_BECH32_NRELAY = 5,
|
||||
NOSTR_BECH32_NADDR = 6,
|
||||
NOSTR_BECH32_NSEC = 7,
|
||||
};
|
||||
|
||||
struct bech32_note {
|
||||
@@ -37,10 +36,6 @@ struct bech32_npub {
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nsec {
|
||||
const u8 *nsec;
|
||||
};
|
||||
|
||||
struct bech32_nevent {
|
||||
struct relays relays;
|
||||
const u8 *event_id;
|
||||
@@ -70,7 +65,6 @@ typedef struct nostr_bech32 {
|
||||
union {
|
||||
struct bech32_note note;
|
||||
struct bech32_npub npub;
|
||||
struct bech32_nsec nsec;
|
||||
struct bech32_nevent nevent;
|
||||
struct bech32_nprofile nprofile;
|
||||
struct bech32_naddr naddr;
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// nostrscript.c
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-02.
|
||||
//
|
||||
|
||||
#include "nostrscript.h"
|
||||
#include "wasm.h"
|
||||
#include "array_size.h"
|
||||
|
||||
// function to check if the character is in surrogate pair range
|
||||
static INLINE int is_surrogate(uint16_t uc) {
|
||||
return (uc - 0xd800u) < 2048u;
|
||||
}
|
||||
|
||||
// function to convert utf16 to utf8
|
||||
static int utf16_to_utf8(u16 utf16, u8 *utf8) {
|
||||
if (utf16 < 0x80) { // 1-byte sequence
|
||||
utf8[0] = (uint8_t) utf16;
|
||||
return 1;
|
||||
}
|
||||
else if (utf16 < 0x800) { // 2-byte sequence
|
||||
utf8[0] = (uint8_t) (0xc0 | (utf16 >> 6));
|
||||
utf8[1] = (uint8_t) (0x80 | (utf16 & 0x3f));
|
||||
return 2;
|
||||
}
|
||||
else if (!is_surrogate(utf16)) { // 3-byte sequence
|
||||
utf8[0] = (uint8_t) (0xe0 | (utf16 >> 12));
|
||||
utf8[1] = (uint8_t) (0x80 | ((utf16 >> 6) & 0x3f));
|
||||
utf8[2] = (uint8_t) (0x80 | (utf16 & 0x3f));
|
||||
return 3;
|
||||
}
|
||||
else { // surrogate pair, return error
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int nostr_cmd(struct wasm_interp *interp) {
|
||||
struct val *params = NULL;
|
||||
const char *val = NULL;
|
||||
int len, cmd, ival;
|
||||
|
||||
if (!get_params(interp, ¶ms, 3) || params == NULL)
|
||||
return interp_error(interp, "get params");
|
||||
|
||||
// command
|
||||
cmd = params[0].num.i32;
|
||||
|
||||
// value
|
||||
|
||||
ival = params[1].num.i32;
|
||||
if (!mem_ptr_str(interp, ival, &val))
|
||||
val = 0;
|
||||
|
||||
// length
|
||||
len = params[2].num.i32;
|
||||
|
||||
intptr_t iptr = ival;
|
||||
return nscript_nostr_cmd(interp, cmd, val ? (void*)val : (void*)iptr, len);
|
||||
}
|
||||
|
||||
static int print_utf16_str(u16 *chars) {
|
||||
u16 *p = chars;
|
||||
int c;
|
||||
|
||||
while (*p) {
|
||||
if (utf16_to_utf8(*p, (u8*)&c) == -1)
|
||||
return 0;
|
||||
|
||||
printf("%c", c);
|
||||
|
||||
p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int nostr_log(struct wasm_interp *interp) {
|
||||
struct val *vals;
|
||||
const char *str;
|
||||
struct callframe *callframe;
|
||||
|
||||
if (!get_params(interp, &vals, 1))
|
||||
return interp_error(interp, "nostr_log get params");
|
||||
|
||||
if (!mem_ptr_str(interp, vals[0].num.i32, &str))
|
||||
return interp_error(interp, "nostr_log log param");
|
||||
|
||||
if (!(callframe = top_callframes(&interp->callframes, 2)))
|
||||
return interp_error(interp, "nostr_log callframe");
|
||||
|
||||
printf("nostr_log:%s: ", callframe->func->name);
|
||||
|
||||
print_utf16_str((u16*)str);
|
||||
printf("\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int nostr_set_bool(struct wasm_interp *interp) {
|
||||
struct val *params = NULL;
|
||||
const u16 *setting;
|
||||
u32 val, len;
|
||||
|
||||
if (!get_params(interp, ¶ms, 3) || params == NULL)
|
||||
return 0;
|
||||
|
||||
if (!mem_ptr_str(interp, params[0].num.i32, (const char**)&setting))
|
||||
return 0;
|
||||
|
||||
len = params[1].num.i32;
|
||||
val = params[2].num.i32 > 0 ? 1 : 0;
|
||||
|
||||
return nscript_set_bool(interp, setting, len, val);
|
||||
}
|
||||
|
||||
static int nostr_pool_send_to(struct wasm_interp *interp) {
|
||||
struct val *params = NULL;
|
||||
const u16 *req, *to;
|
||||
int req_len, to_len;
|
||||
|
||||
if (!get_params(interp, ¶ms, 4) || params == NULL)
|
||||
return 0;
|
||||
|
||||
if (!mem_ptr_str(interp, params[0].num.i32, (const char**)&req))
|
||||
return 0;
|
||||
|
||||
req_len = params[1].num.i32;
|
||||
|
||||
if (!mem_ptr_str(interp, params[2].num.i32, (const char**)&to))
|
||||
return 0;
|
||||
|
||||
to_len = params[3].num.i32;
|
||||
|
||||
return nscript_pool_send_to(interp, req, req_len, to, to_len);
|
||||
}
|
||||
|
||||
static int nscript_abort(struct wasm_interp *interp) {
|
||||
struct val *params = NULL;
|
||||
const char *msg = "", *filename;
|
||||
int line, col;
|
||||
|
||||
if (!get_params(interp, ¶ms, 4) || params == NULL)
|
||||
return interp_error(interp, "get params");
|
||||
|
||||
if (params[0].ref.addr != 0 && !mem_ptr_str(interp, params[0].ref.addr, &msg))
|
||||
return interp_error(interp, "abort msg");
|
||||
|
||||
if (!mem_ptr_str(interp, params[1].ref.addr, &filename))
|
||||
return interp_error(interp, "abort filename");
|
||||
|
||||
line = params[2].num.i32;
|
||||
col = params[3].num.i32;
|
||||
|
||||
printf("nscript_abort:");
|
||||
print_utf16_str((u16*)filename);
|
||||
printf(":%d:%d: ", line, col);
|
||||
print_utf16_str((u16*)msg);
|
||||
printf("\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct builtin nscript_builtins[] = {
|
||||
{ .name = "null", .fn = 0 },
|
||||
{ .name = "nostr_log", .fn = nostr_log },
|
||||
{ .name = "nostr_cmd", .fn = nostr_cmd },
|
||||
{ .name = "nostr_pool_send_to", .fn = nostr_pool_send_to },
|
||||
{ .name = "nostr_set_bool", .fn = nostr_set_bool },
|
||||
{ .name = "abort", .fn = nscript_abort },
|
||||
};
|
||||
|
||||
int nscript_load(struct wasm_parser *p, struct wasm_interp *interp, unsigned char *wasm, unsigned long len) {
|
||||
wasm_parser_init(p, wasm, len, len * 16, nscript_builtins, ARRAY_SIZE(nscript_builtins));
|
||||
|
||||
if (!parse_wasm(p)) {
|
||||
wasm_parser_free(p);
|
||||
return NSCRIPT_PARSE_ERR;
|
||||
}
|
||||
|
||||
if (!wasm_interp_init(interp, &p->module)) {
|
||||
print_error_backtrace(&interp->errors);
|
||||
wasm_parser_free(p);
|
||||
return NSCRIPT_INIT_ERR;
|
||||
}
|
||||
|
||||
//setup_wasi(&interp, argc, argv, env);
|
||||
//wasm_parser_free(&p);
|
||||
|
||||
return NSCRIPT_LOADED;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// nostrscript.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-02.
|
||||
//
|
||||
|
||||
#ifndef nostrscript_h
|
||||
#define nostrscript_h
|
||||
|
||||
#define NSCRIPT_LOADED 1
|
||||
#define NSCRIPT_PARSE_ERR 2
|
||||
#define NSCRIPT_INIT_ERR 3
|
||||
|
||||
#include <stdio.h>
|
||||
#include "wasm.h"
|
||||
|
||||
int nscript_load(struct wasm_parser *p, struct wasm_interp *interp, unsigned char *wasm, unsigned long len);
|
||||
int nscript_nostr_cmd(struct wasm_interp *interp, int, void*, int);
|
||||
int nscript_pool_send_to(struct wasm_interp *interp, const u16*, int, const u16 *, int);
|
||||
int nscript_set_bool(struct wasm_interp *interp, const u16*, int, int);
|
||||
|
||||
|
||||
#endif /* nostrscript_h */
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
#ifndef CURSOR_PARSER
|
||||
#define CURSOR_PARSER
|
||||
|
||||
#include "cursor.h"
|
||||
|
||||
static int consume_bytes(struct cursor *cursor, const unsigned char *match, int len)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (cursor->p + len > cursor->end) {
|
||||
fprintf(stderr, "consume_bytes overflow\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
if (cursor->p[i] != match[i])
|
||||
return 0;
|
||||
}
|
||||
|
||||
cursor->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int consume_byte(struct cursor *cursor, unsigned char match)
|
||||
{
|
||||
if (unlikely(cursor->p >= cursor->end))
|
||||
return 0;
|
||||
if (*cursor->p != match)
|
||||
return 0;
|
||||
cursor->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int consume_u32(struct cursor *cursor, unsigned int match)
|
||||
{
|
||||
return consume_bytes(cursor, (unsigned char*)&match, sizeof(match));
|
||||
}
|
||||
|
||||
#endif /* CURSOR_PARSER */
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_TYPEDEFS_H
|
||||
#define PROTOVERSE_TYPEDEFS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned short u16;
|
||||
typedef uint64_t u64;
|
||||
typedef int64_t s64;
|
||||
|
||||
|
||||
#endif /* PROTOVERSE_TYPEDEFS_H */
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_VARINT_H
|
||||
#define PROTOVERSE_VARINT_H
|
||||
|
||||
#define VARINT_MAX_LEN 9
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
size_t varint_put(unsigned char buf[VARINT_MAX_LEN], uint64_t v);
|
||||
size_t varint_size(uint64_t v);
|
||||
size_t varint_get(const unsigned char *p, size_t max, int64_t *val);
|
||||
|
||||
#endif /* PROTOVERSE_VARINT_H */
|
||||
@@ -1,850 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_WASM_H
|
||||
#define PROTOVERSE_WASM_H
|
||||
|
||||
static const unsigned char WASM_MAGIC[] = {0,'a','s','m'};
|
||||
|
||||
#define WASM_VERSION 0x01
|
||||
#define MAX_U32_LEB128_BYTES 5
|
||||
#define MAX_U64_LEB128_BYTES 10
|
||||
#define MAX_CUSTOM_SECTIONS 32
|
||||
#define MAX_BUILTINS 64
|
||||
#define BUILTIN_SUSPEND 42
|
||||
|
||||
#define FUNC_TYPE_TAG 0x60
|
||||
|
||||
|
||||
#include "cursor.h"
|
||||
#include "error.h"
|
||||
|
||||
#ifdef NOINLINE
|
||||
#define INLINE __attribute__((noinline))
|
||||
#else
|
||||
#define INLINE inline
|
||||
#endif
|
||||
|
||||
|
||||
#define interp_error(p, fmt, ...) note_error(&((p)->errors), interp_codeptr(p), fmt, ##__VA_ARGS__)
|
||||
#define parse_err(p, fmt, ...) note_error(&((p)->errs), &(p)->cur, fmt, ##__VA_ARGS__)
|
||||
|
||||
enum valtype {
|
||||
val_i32 = 0x7F,
|
||||
val_i64 = 0x7E,
|
||||
val_f32 = 0x7D,
|
||||
val_f64 = 0x7C,
|
||||
val_ref_null = 0xD0,
|
||||
val_ref_func = 0x70,
|
||||
val_ref_extern = 0x6F,
|
||||
};
|
||||
|
||||
enum const_instr {
|
||||
ci_const_i32 = 0x41,
|
||||
ci_const_i64 = 0x42,
|
||||
ci_const_f32 = 0x43,
|
||||
ci_const_f64 = 0x44,
|
||||
ci_ref_null = 0xD0,
|
||||
ci_ref_func = 0xD2,
|
||||
ci_global_get = 0x23,
|
||||
ci_end = 0x0B,
|
||||
};
|
||||
|
||||
enum limit_type {
|
||||
limit_min = 0x00,
|
||||
limit_min_max = 0x01,
|
||||
};
|
||||
|
||||
struct limits {
|
||||
u32 min;
|
||||
u32 max;
|
||||
enum limit_type type;
|
||||
};
|
||||
|
||||
enum section_tag {
|
||||
section_custom,
|
||||
section_type,
|
||||
section_import,
|
||||
section_function,
|
||||
section_table,
|
||||
section_memory,
|
||||
section_global,
|
||||
section_export,
|
||||
section_start,
|
||||
section_element,
|
||||
section_code,
|
||||
section_data,
|
||||
section_data_count,
|
||||
section_name,
|
||||
num_sections,
|
||||
};
|
||||
|
||||
enum name_subsection_tag {
|
||||
name_subsection_module,
|
||||
name_subsection_funcs,
|
||||
name_subsection_locals,
|
||||
num_name_subsections,
|
||||
};
|
||||
|
||||
enum reftype {
|
||||
funcref = 0x70,
|
||||
externref = 0x6F,
|
||||
};
|
||||
|
||||
struct resulttype {
|
||||
unsigned char *valtypes; /* enum valtype */
|
||||
u32 num_valtypes;
|
||||
};
|
||||
|
||||
struct functype {
|
||||
struct resulttype params;
|
||||
struct resulttype result;
|
||||
};
|
||||
|
||||
struct table {
|
||||
enum reftype reftype;
|
||||
struct limits limits;
|
||||
};
|
||||
|
||||
struct tablesec {
|
||||
struct table *tables;
|
||||
u32 num_tables;
|
||||
};
|
||||
|
||||
enum elem_mode {
|
||||
elem_mode_passive,
|
||||
elem_mode_active,
|
||||
elem_mode_declarative,
|
||||
};
|
||||
|
||||
struct expr {
|
||||
u8 *code;
|
||||
u32 code_len;
|
||||
};
|
||||
|
||||
struct refval {
|
||||
u32 addr;
|
||||
};
|
||||
|
||||
struct table_inst {
|
||||
struct refval *refs;
|
||||
enum reftype reftype;
|
||||
u32 num_refs;
|
||||
};
|
||||
|
||||
struct numval {
|
||||
union {
|
||||
int i32;
|
||||
u32 u32;
|
||||
int64_t i64;
|
||||
uint64_t u64;
|
||||
float f32;
|
||||
double f64;
|
||||
};
|
||||
};
|
||||
|
||||
struct val {
|
||||
enum valtype type;
|
||||
union {
|
||||
struct numval num;
|
||||
struct refval ref;
|
||||
};
|
||||
};
|
||||
|
||||
struct elem_inst {
|
||||
struct val val;
|
||||
u16 elem;
|
||||
u16 init;
|
||||
};
|
||||
|
||||
struct elem {
|
||||
struct expr offset;
|
||||
u32 tableidx;
|
||||
struct expr *inits;
|
||||
u32 num_inits;
|
||||
enum elem_mode mode;
|
||||
enum reftype reftype;
|
||||
struct val val;
|
||||
};
|
||||
|
||||
struct customsec {
|
||||
const char *name;
|
||||
unsigned char *data;
|
||||
u32 data_len;
|
||||
};
|
||||
|
||||
struct elemsec {
|
||||
struct elem *elements;
|
||||
u32 num_elements;
|
||||
};
|
||||
|
||||
struct memsec {
|
||||
struct limits *mems; /* memtype */
|
||||
u32 num_mems;
|
||||
};
|
||||
|
||||
struct funcsec {
|
||||
u32 *type_indices;
|
||||
u32 num_indices;
|
||||
};
|
||||
|
||||
enum mut {
|
||||
mut_const,
|
||||
mut_var,
|
||||
};
|
||||
|
||||
struct globaltype {
|
||||
enum valtype valtype;
|
||||
enum mut mut;
|
||||
};
|
||||
|
||||
struct globalsec {
|
||||
struct global *globals;
|
||||
u32 num_globals;
|
||||
};
|
||||
|
||||
struct typesec {
|
||||
struct functype *functypes;
|
||||
u32 num_functypes;
|
||||
};
|
||||
|
||||
enum import_type {
|
||||
import_func,
|
||||
import_table,
|
||||
import_mem,
|
||||
import_global,
|
||||
};
|
||||
|
||||
struct importdesc {
|
||||
enum import_type type;
|
||||
union {
|
||||
u32 typeidx;
|
||||
struct limits tabletype;
|
||||
struct limits memtype;
|
||||
struct globaltype globaltype;
|
||||
};
|
||||
};
|
||||
|
||||
struct import {
|
||||
const char *module_name;
|
||||
const char *name;
|
||||
struct importdesc desc;
|
||||
int resolved_builtin;
|
||||
};
|
||||
|
||||
struct importsec {
|
||||
struct import *imports;
|
||||
u32 num_imports;
|
||||
};
|
||||
|
||||
struct global {
|
||||
struct globaltype type;
|
||||
struct expr init;
|
||||
struct val val;
|
||||
};
|
||||
|
||||
struct local_def {
|
||||
u32 num_types;
|
||||
enum valtype type;
|
||||
};
|
||||
|
||||
/* "code" */
|
||||
struct wasm_func {
|
||||
struct expr code;
|
||||
struct local_def *local_defs;
|
||||
u32 num_local_defs;
|
||||
};
|
||||
|
||||
enum func_type {
|
||||
func_type_wasm,
|
||||
func_type_builtin,
|
||||
};
|
||||
|
||||
struct func {
|
||||
union {
|
||||
struct wasm_func *wasm_func;
|
||||
struct builtin *builtin;
|
||||
};
|
||||
u32 num_locals;
|
||||
struct functype *functype;
|
||||
enum func_type type;
|
||||
const char *name;
|
||||
u32 idx;
|
||||
};
|
||||
|
||||
struct codesec {
|
||||
struct wasm_func *funcs;
|
||||
u32 num_funcs;
|
||||
};
|
||||
|
||||
enum exportdesc {
|
||||
export_func,
|
||||
export_table,
|
||||
export_mem,
|
||||
export_global,
|
||||
};
|
||||
|
||||
struct wexport {
|
||||
const char *name;
|
||||
u32 index;
|
||||
enum exportdesc desc;
|
||||
};
|
||||
|
||||
struct exportsec {
|
||||
struct wexport *exports;
|
||||
u32 num_exports;
|
||||
};
|
||||
|
||||
struct nameassoc {
|
||||
u32 index;
|
||||
const char *name;
|
||||
};
|
||||
|
||||
struct namemap {
|
||||
struct nameassoc *names;
|
||||
u32 num_names;
|
||||
};
|
||||
|
||||
struct namesec {
|
||||
const char *module_name;
|
||||
struct namemap func_names;
|
||||
int parsed;
|
||||
};
|
||||
|
||||
struct wsection {
|
||||
enum section_tag tag;
|
||||
};
|
||||
|
||||
enum bulk_tag {
|
||||
i_memory_copy = 10,
|
||||
i_memory_fill = 11,
|
||||
i_table_init = 12,
|
||||
i_elem_drop = 13,
|
||||
i_table_copy = 14,
|
||||
i_table_grow = 15,
|
||||
i_table_size = 16,
|
||||
i_table_fill = 17,
|
||||
};
|
||||
|
||||
enum instr_tag {
|
||||
/* control instructions */
|
||||
i_unreachable = 0x00,
|
||||
i_nop = 0x01,
|
||||
i_block = 0x02,
|
||||
i_loop = 0x03,
|
||||
i_if = 0x04,
|
||||
i_else = 0x05,
|
||||
i_end = 0x0B,
|
||||
i_br = 0x0C,
|
||||
i_br_if = 0x0D,
|
||||
i_br_table = 0x0E,
|
||||
i_return = 0x0F,
|
||||
i_call = 0x10,
|
||||
i_call_indirect = 0x11,
|
||||
|
||||
/* parametric instructions */
|
||||
i_drop = 0x1A,
|
||||
i_select = 0x1B,
|
||||
i_selects = 0x1C,
|
||||
|
||||
/* variable instructions */
|
||||
i_local_get = 0x20,
|
||||
i_local_set = 0x21,
|
||||
i_local_tee = 0x22,
|
||||
i_global_get = 0x23,
|
||||
i_global_set = 0x24,
|
||||
i_table_get = 0x25,
|
||||
i_table_set = 0x26,
|
||||
|
||||
/* memory instructions */
|
||||
i_i32_load = 0x28,
|
||||
i_i64_load = 0x29,
|
||||
i_f32_load = 0x2A,
|
||||
i_f64_load = 0x2B,
|
||||
i_i32_load8_s = 0x2C,
|
||||
i_i32_load8_u = 0x2D,
|
||||
i_i32_load16_s = 0x2E,
|
||||
i_i32_load16_u = 0x2F,
|
||||
i_i64_load8_s = 0x30,
|
||||
i_i64_load8_u = 0x31,
|
||||
i_i64_load16_s = 0x32,
|
||||
i_i64_load16_u = 0x33,
|
||||
i_i64_load32_s = 0x34,
|
||||
i_i64_load32_u = 0x35,
|
||||
i_i32_store = 0x36,
|
||||
i_i64_store = 0x37,
|
||||
i_f32_store = 0x38,
|
||||
i_f64_store = 0x39,
|
||||
i_i32_store8 = 0x3A,
|
||||
i_i32_store16 = 0x3B,
|
||||
i_i64_store8 = 0x3C,
|
||||
i_i64_store16 = 0x3D,
|
||||
i_i64_store32 = 0x3E,
|
||||
i_memory_size = 0x3F,
|
||||
i_memory_grow = 0x40,
|
||||
|
||||
/* numeric instructions */
|
||||
i_i32_const = 0x41,
|
||||
i_i64_const = 0x42,
|
||||
i_f32_const = 0x43,
|
||||
i_f64_const = 0x44,
|
||||
|
||||
i_i32_eqz = 0x45,
|
||||
i_i32_eq = 0x46,
|
||||
i_i32_ne = 0x47,
|
||||
i_i32_lt_s = 0x48,
|
||||
i_i32_lt_u = 0x49,
|
||||
i_i32_gt_s = 0x4A,
|
||||
i_i32_gt_u = 0x4B,
|
||||
i_i32_le_s = 0x4C,
|
||||
i_i32_le_u = 0x4D,
|
||||
i_i32_ge_s = 0x4E,
|
||||
i_i32_ge_u = 0x4F,
|
||||
|
||||
i_i64_eqz = 0x50,
|
||||
i_i64_eq = 0x51,
|
||||
i_i64_ne = 0x52,
|
||||
i_i64_lt_s = 0x53,
|
||||
i_i64_lt_u = 0x54,
|
||||
i_i64_gt_s = 0x55,
|
||||
i_i64_gt_u = 0x56,
|
||||
i_i64_le_s = 0x57,
|
||||
i_i64_le_u = 0x58,
|
||||
i_i64_ge_s = 0x59,
|
||||
i_i64_ge_u = 0x5A,
|
||||
|
||||
i_f32_eq = 0x5B,
|
||||
i_f32_ne = 0x5C,
|
||||
i_f32_lt = 0x5D,
|
||||
i_f32_gt = 0x5E,
|
||||
i_f32_le = 0x5F,
|
||||
i_f32_ge = 0x60,
|
||||
|
||||
i_f64_eq = 0x61,
|
||||
i_f64_ne = 0x62,
|
||||
i_f64_lt = 0x63,
|
||||
i_f64_gt = 0x64,
|
||||
i_f64_le = 0x65,
|
||||
i_f64_ge = 0x66,
|
||||
|
||||
i_i32_clz = 0x67,
|
||||
i_i32_ctz = 0x68,
|
||||
i_i32_popcnt = 0x69,
|
||||
|
||||
i_i32_add = 0x6A,
|
||||
i_i32_sub = 0x6B,
|
||||
i_i32_mul = 0x6C,
|
||||
i_i32_div_s = 0x6D,
|
||||
i_i32_div_u = 0x6E,
|
||||
i_i32_rem_s = 0x6F,
|
||||
i_i32_rem_u = 0x70,
|
||||
i_i32_and = 0x71,
|
||||
i_i32_or = 0x72,
|
||||
i_i32_xor = 0x73,
|
||||
i_i32_shl = 0x74,
|
||||
i_i32_shr_s = 0x75,
|
||||
i_i32_shr_u = 0x76,
|
||||
i_i32_rotl = 0x77,
|
||||
i_i32_rotr = 0x78,
|
||||
|
||||
i_i64_clz = 0x79,
|
||||
i_i64_ctz = 0x7A,
|
||||
i_i64_popcnt = 0x7B,
|
||||
i_i64_add = 0x7C,
|
||||
i_i64_sub = 0x7D,
|
||||
i_i64_mul = 0x7E,
|
||||
i_i64_div_s = 0x7F,
|
||||
i_i64_div_u = 0x80,
|
||||
i_i64_rem_s = 0x81,
|
||||
i_i64_rem_u = 0x82,
|
||||
i_i64_and = 0x83,
|
||||
i_i64_or = 0x84,
|
||||
i_i64_xor = 0x85,
|
||||
i_i64_shl = 0x86,
|
||||
i_i64_shr_s = 0x87,
|
||||
i_i64_shr_u = 0x88,
|
||||
i_i64_rotl = 0x89,
|
||||
i_i64_rotr = 0x8A,
|
||||
|
||||
i_f32_abs = 0x8b,
|
||||
i_f32_neg = 0x8c,
|
||||
i_f32_ceil = 0x8d,
|
||||
i_f32_floor = 0x8e,
|
||||
i_f32_trunc = 0x8f,
|
||||
i_f32_nearest = 0x90,
|
||||
i_f32_sqrt = 0x91,
|
||||
i_f32_add = 0x92,
|
||||
i_f32_sub = 0x93,
|
||||
i_f32_mul = 0x94,
|
||||
i_f32_div = 0x95,
|
||||
i_f32_min = 0x96,
|
||||
i_f32_max = 0x97,
|
||||
i_f32_copysign = 0x98,
|
||||
|
||||
i_f64_abs = 0x99,
|
||||
i_f64_neg = 0x9a,
|
||||
i_f64_ceil = 0x9b,
|
||||
i_f64_floor = 0x9c,
|
||||
i_f64_trunc = 0x9d,
|
||||
i_f64_nearest = 0x9e,
|
||||
i_f64_sqrt = 0x9f,
|
||||
i_f64_add = 0xa0,
|
||||
i_f64_sub = 0xa1,
|
||||
i_f64_mul = 0xa2,
|
||||
i_f64_div = 0xa3,
|
||||
i_f64_min = 0xa4,
|
||||
i_f64_max = 0xa5,
|
||||
i_f64_copysign = 0xa6,
|
||||
|
||||
i_i32_wrap_i64 = 0xa7,
|
||||
i_i32_trunc_f32_s = 0xa8,
|
||||
i_i32_trunc_f32_u = 0xa9,
|
||||
i_i32_trunc_f64_s = 0xaa,
|
||||
i_i32_trunc_f64_u = 0xab,
|
||||
i_i64_extend_i32_s = 0xac,
|
||||
i_i64_extend_i32_u = 0xad,
|
||||
i_i64_trunc_f32_s = 0xae,
|
||||
i_i64_trunc_f32_u = 0xaf,
|
||||
i_i64_trunc_f64_s = 0xb0,
|
||||
i_i64_trunc_f64_u = 0xb1,
|
||||
i_f32_convert_i32_s = 0xb2,
|
||||
i_f32_convert_i32_u = 0xb3,
|
||||
i_f32_convert_i64_s = 0xb4,
|
||||
i_f32_convert_i64_u = 0xb5,
|
||||
i_f32_demote_f64 = 0xb6,
|
||||
i_f64_convert_i32_s = 0xb7,
|
||||
i_f64_convert_i32_u = 0xb8,
|
||||
i_f64_convert_i64_s = 0xb9,
|
||||
i_f64_convert_i64_u = 0xba,
|
||||
i_f64_promote_f32 = 0xbb,
|
||||
|
||||
i_i32_reinterpret_f32 = 0xbc,
|
||||
i_i64_reinterpret_f64 = 0xbd,
|
||||
i_f32_reinterpret_i32 = 0xbe,
|
||||
i_f64_reinterpret_i64 = 0xbf,
|
||||
|
||||
i_i32_extend8_s = 0xc0,
|
||||
i_i32_extend16_s = 0xc1,
|
||||
i_i64_extend8_s = 0xc2,
|
||||
i_i64_extend16_s = 0xc3,
|
||||
i_i64_extend32_s = 0xc4,
|
||||
|
||||
i_ref_null = 0xD0,
|
||||
i_ref_is_null = 0xD1,
|
||||
i_ref_func = 0xD2,
|
||||
|
||||
i_bulk_op = 0xFC,
|
||||
/* TODO: more instrs */
|
||||
|
||||
};
|
||||
|
||||
enum blocktype_tag {
|
||||
blocktype_empty,
|
||||
blocktype_valtype,
|
||||
blocktype_index,
|
||||
};
|
||||
|
||||
struct blocktype {
|
||||
enum blocktype_tag tag;
|
||||
union {
|
||||
enum valtype valtype;
|
||||
int type_index;
|
||||
};
|
||||
};
|
||||
|
||||
struct instrs {
|
||||
unsigned char *data;
|
||||
u32 len;
|
||||
};
|
||||
|
||||
struct block {
|
||||
struct blocktype type;
|
||||
struct expr instrs;
|
||||
};
|
||||
|
||||
struct memarg {
|
||||
u32 offset;
|
||||
u32 align;
|
||||
};
|
||||
|
||||
struct br_table {
|
||||
u32 num_label_indices;
|
||||
u32 label_indices[512];
|
||||
u32 default_label;
|
||||
};
|
||||
|
||||
struct call_indirect {
|
||||
u32 tableidx;
|
||||
u32 typeidx;
|
||||
};
|
||||
|
||||
struct table_init {
|
||||
u32 tableidx;
|
||||
u32 elemidx;
|
||||
};
|
||||
|
||||
struct table_copy {
|
||||
u32 from;
|
||||
u32 to;
|
||||
};
|
||||
|
||||
struct bulk_op {
|
||||
enum bulk_tag tag;
|
||||
union {
|
||||
struct table_init table_init;
|
||||
struct table_copy table_copy;
|
||||
u32 idx;
|
||||
};
|
||||
};
|
||||
|
||||
struct select_instr {
|
||||
u8 *valtypes;
|
||||
u32 num_valtypes;
|
||||
};
|
||||
|
||||
struct instr {
|
||||
enum instr_tag tag;
|
||||
int pos;
|
||||
union {
|
||||
struct br_table br_table;
|
||||
struct bulk_op bulk_op;
|
||||
struct call_indirect call_indirect;
|
||||
struct memarg memarg;
|
||||
struct select_instr select;
|
||||
struct block block;
|
||||
struct expr else_block;
|
||||
double f64;
|
||||
float f32;
|
||||
int i32;
|
||||
u32 u32;
|
||||
int64_t i64;
|
||||
u64 u64;
|
||||
unsigned char memidx;
|
||||
enum reftype reftype;
|
||||
};
|
||||
};
|
||||
|
||||
enum datamode {
|
||||
datamode_active,
|
||||
datamode_passive,
|
||||
};
|
||||
|
||||
struct wdata_active {
|
||||
u32 mem_index;
|
||||
struct expr offset_expr;
|
||||
};
|
||||
|
||||
struct wdata {
|
||||
struct wdata_active active;
|
||||
u8 *bytes;
|
||||
u32 bytes_len;
|
||||
enum datamode mode;
|
||||
};
|
||||
|
||||
struct datasec {
|
||||
struct wdata *datas;
|
||||
u32 num_datas;
|
||||
};
|
||||
|
||||
struct startsec {
|
||||
u32 start_fn;
|
||||
};
|
||||
|
||||
struct module {
|
||||
unsigned int parsed;
|
||||
unsigned int custom_sections;
|
||||
|
||||
struct func *funcs;
|
||||
|
||||
u32 num_funcs;
|
||||
|
||||
struct customsec custom_section[MAX_CUSTOM_SECTIONS];
|
||||
struct typesec type_section;
|
||||
struct funcsec func_section;
|
||||
struct importsec import_section;
|
||||
struct exportsec export_section;
|
||||
struct codesec code_section;
|
||||
struct tablesec table_section;
|
||||
struct memsec memory_section;
|
||||
struct globalsec global_section;
|
||||
struct startsec start_section;
|
||||
struct elemsec element_section;
|
||||
struct datasec data_section;
|
||||
struct namesec name_section;
|
||||
};
|
||||
|
||||
// make sure the struct is packed so that
|
||||
struct label {
|
||||
u32 instr_pos; // resolved status is stored in HOB of pos
|
||||
u32 jump;
|
||||
};
|
||||
|
||||
struct callframe {
|
||||
struct cursor code;
|
||||
struct val *locals;
|
||||
struct func *func;
|
||||
u16 prev_stack_items;
|
||||
};
|
||||
|
||||
struct resolver {
|
||||
u16 label;
|
||||
u8 end_tag;
|
||||
u8 start_tag;
|
||||
};
|
||||
|
||||
struct global_inst {
|
||||
struct val val;
|
||||
};
|
||||
|
||||
struct module_inst {
|
||||
struct table_inst *tables;
|
||||
struct global_inst *globals;
|
||||
struct elem_inst *elements;
|
||||
|
||||
u32 num_tables;
|
||||
u32 num_globals;
|
||||
u32 num_elements;
|
||||
|
||||
int start_fn;
|
||||
unsigned char *globals_init;
|
||||
};
|
||||
|
||||
struct wasi {
|
||||
int argc;
|
||||
const char **argv;
|
||||
|
||||
int environc;
|
||||
const char **environ;
|
||||
};
|
||||
|
||||
struct wasm_interp;
|
||||
|
||||
struct builtin {
|
||||
const char *name;
|
||||
int (*fn)(struct wasm_interp *);
|
||||
int (*prepare_args)(struct wasm_interp *);
|
||||
};
|
||||
|
||||
struct wasm_interp {
|
||||
struct module *module;
|
||||
struct module_inst module_inst;
|
||||
struct wasi wasi;
|
||||
void *context;
|
||||
|
||||
struct builtin builtins[MAX_BUILTINS];
|
||||
int num_builtins;
|
||||
|
||||
int prev_resolvers, quitting;
|
||||
|
||||
struct errors errors; /* struct error */
|
||||
size_t ops;
|
||||
|
||||
struct cursor callframes; /* struct callframe */
|
||||
struct cursor stack; /* struct val */
|
||||
struct cursor mem; /* u8/mixed */
|
||||
|
||||
struct cursor memory; /* memory pages (65536 blocks) */
|
||||
|
||||
struct cursor locals; /* struct val */
|
||||
struct cursor labels; /* struct labels */
|
||||
struct cursor num_labels;
|
||||
|
||||
// resolve stack for the current function. every time a control
|
||||
// instruction is encountered, the label index is pushed. When an
|
||||
// instruction is popped, we can resolve the label
|
||||
struct cursor resolver_stack; /* struct resolver */
|
||||
struct cursor resolver_offsets; /* int */
|
||||
};
|
||||
|
||||
struct wasm_parser {
|
||||
struct module module;
|
||||
struct builtin *builtins;
|
||||
u32 num_builtins;
|
||||
struct cursor cur;
|
||||
struct cursor mem;
|
||||
struct errors errs;
|
||||
};
|
||||
|
||||
|
||||
int run_wasm(unsigned char *wasm, unsigned long len, int argc, const char **argv, char **env, int *retval);
|
||||
int parse_wasm(struct wasm_parser *p);
|
||||
int wasm_interp_init(struct wasm_interp *interp, struct module *module);
|
||||
void wasm_parser_free(struct wasm_parser *parser);
|
||||
void wasm_parser_init(struct wasm_parser *p, u8 *wasm, size_t wasm_len, size_t arena_size, struct builtin *, int num_builtins);
|
||||
void wasm_interp_free(struct wasm_interp *interp);
|
||||
int interp_wasm_module(struct wasm_interp *interp, int *retval);
|
||||
int interp_wasm_module_resume(struct wasm_interp *interp, int *retval);
|
||||
void print_error_backtrace(struct errors *errors);
|
||||
void setup_wasi(struct wasm_interp *interp, int argc, const char **argv, char **env);
|
||||
void print_callstack(struct wasm_interp *interp);
|
||||
|
||||
// builtin helpers
|
||||
int get_params(struct wasm_interp *interp, struct val** vals, u32 num_vals);
|
||||
int get_var_params(struct wasm_interp *interp, struct val** vals, u32 *num_vals);
|
||||
u8 *interp_mem_ptr(struct wasm_interp *interp, u32 ptr, int size);
|
||||
|
||||
static INLINE struct callframe *top_callframe(struct cursor *cur)
|
||||
{
|
||||
return (struct callframe*)cursor_top(cur, sizeof(struct callframe));
|
||||
}
|
||||
|
||||
|
||||
static INLINE struct cursor *interp_codeptr(struct wasm_interp *interp)
|
||||
{
|
||||
struct callframe *frame;
|
||||
if (unlikely(!(frame = top_callframe(&interp->callframes))))
|
||||
return 0;
|
||||
return &frame->code;
|
||||
}
|
||||
|
||||
|
||||
static INLINE int mem_ptr_str(struct wasm_interp *interp, u32 ptr,
|
||||
const char **str)
|
||||
{
|
||||
// still technically unsafe if the string runs over the end of memory...
|
||||
if (!(*str = (const char*)interp_mem_ptr(interp, ptr, 1))) {
|
||||
return interp_error(interp, "int memptr");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static INLINE int mem_ptr_i32(struct wasm_interp *interp, u32 ptr, int **i)
|
||||
{
|
||||
if (!(*i = (int*)interp_mem_ptr(interp, ptr, sizeof(int))))
|
||||
return interp_error(interp, "int memptr");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static INLINE int cursor_pushval(struct cursor *cur, struct val *val)
|
||||
{
|
||||
return cursor_push(cur, (u8*)val, sizeof(*val));
|
||||
}
|
||||
|
||||
static INLINE int cursor_push_i32(struct cursor *stack, int i)
|
||||
{
|
||||
struct val val;
|
||||
val.type = val_i32;
|
||||
val.num.i32 = i;
|
||||
|
||||
return cursor_pushval(stack, &val);
|
||||
}
|
||||
|
||||
static INLINE int stack_push_i32(struct wasm_interp *interp, int i)
|
||||
{
|
||||
return cursor_push_i32(&interp->stack, i);
|
||||
}
|
||||
|
||||
static INLINE struct callframe *top_callframes(struct cursor *cur, int top)
|
||||
{
|
||||
return (struct callframe*)cursor_topn(cur, sizeof(struct callframe), top);
|
||||
}
|
||||
|
||||
static INLINE int was_section_parsed(struct module *module,
|
||||
enum section_tag section)
|
||||
{
|
||||
if (section == section_custom)
|
||||
return module->custom_sections > 0;
|
||||
|
||||
return module->parsed & (1 << section);
|
||||
}
|
||||
|
||||
|
||||
#endif /* PROTOVERSE_WASM_H */
|
||||
@@ -1,14 +1,5 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "gsplayer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wxxsw/GSPlayer",
|
||||
"state" : {
|
||||
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
|
||||
"version" : "0.2.26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<svg width="430" height="813" viewBox="0 0 430 813" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_1069_29012)">
|
||||
<path d="M44.0811 256.851L186.315 111L276.203 223.574L244.02 388.295L69.9751 697.084L100.678 498.338L44.0811 256.851Z" fill="url(#paint0_linear_1069_29012)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_1069_29012)">
|
||||
<path d="M116.509 587.348L206.677 479.401L230.746 273.265L266.666 231.183L367.424 396.975L281.292 659.008L266.665 801.413L66.889 763.694L116.509 587.348Z" fill="url(#paint1_linear_1069_29012)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1069_29012" x="-66.6248" y="0.294121" width="453.534" height="807.496" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1069_29012" x="-43.8172" y="120.477" width="521.947" height="791.642" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1069_29012" x1="230.179" y1="166.577" x2="-67.7956" y2="310.108" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D34CD9"/>
|
||||
<stop offset="1" stop-color="#4E4DF4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1069_29012" x1="139.483" y1="462.902" x2="377.854" y2="565.47" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0DE8FF"/>
|
||||
<stop offset="1" stop-color="#641AAE"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "lightbulb.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.2" width="48" height="48" rx="24" fill="url(#paint0_linear_1843_42349)"/>
|
||||
<path d="M19.9993 36.0001H27.9993M28.8095 28.0001C31.5199 26.3669 33.3327 23.3952 33.3327 20.0001C33.3327 14.8454 29.154 10.6667 23.9993 10.6667C18.8447 10.6667 14.666 14.8454 14.666 20.0001C14.666 23.3952 16.4788 26.3669 19.1892 28.0001M28.8095 28.0001C28.5475 28.1579 28.2772 28.3032 27.9993 28.4352V31.3334C27.9993 31.7016 27.7009 32.0001 27.3327 32.0001H20.666C20.2978 32.0001 19.9993 31.7016 19.9993 31.3334V28.4352C19.7215 28.3032 19.4512 28.1579 19.1892 28.0001M28.8095 28.0001H19.1892" stroke="url(#paint1_linear_1843_42349)" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1843_42349" x1="5.41935" y1="0.774194" x2="37.9355" y2="47.2258" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9AD1C"/>
|
||||
<stop offset="1" stop-color="#DF7E0C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1843_42349" x1="16.7735" y1="11.0754" x2="35.0141" y2="30.2759" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F9AD1C"/>
|
||||
<stop offset="1" stop-color="#DF7E0C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "header.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nostr-logo.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "eula-bg.svg",
|
||||
"filename" : "shaka-full.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.073975 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
1.295334 8.661732 m
|
||||
3.613694 8.367855 l
|
||||
4.475733 8.733568 5.268113 9.771931 5.474915 10.327032 c
|
||||
6.083156 11.959681 5.507567 14.604573 5.474915 15.061715 c
|
||||
5.448792 15.427428 6.008246 15.693006 6.291239 15.780080 c
|
||||
7.571236 15.858447 8.508359 14.876789 8.642253 13.984165 c
|
||||
8.740212 13.331103 8.576948 11.752880 8.381030 10.849482 c
|
||||
8.979668 10.936556 10.980525 10.901726 11.868687 10.849482 c
|
||||
12.756847 10.797236 13.474895 10.196423 14.193260 9.412750 c
|
||||
14.767952 8.237244 13.953805 7.725680 13.474895 7.616838 c
|
||||
13.834077 7.257654 l
|
||||
14.781013 5.918882 13.649043 5.178749 13.115711 5.004600 c
|
||||
13.474895 4.743376 l
|
||||
14.487136 3.763786 13.246323 2.751544 13.017752 2.882155 c
|
||||
11.058574 3.176033 l
|
||||
15.499378 1.673996 l
|
||||
16.054478 0.400530 15.074889 0.073999 14.781013 0.073999 c
|
||||
8.576947 1.673996 l
|
||||
6.291239 1.673996 5.311650 1.869914 4.299407 2.163791 c
|
||||
4.157911 2.131138 3.659409 1.987464 2.797370 1.673996 c
|
||||
1.935332 1.360527 1.219143 2.087601 0.968804 2.490320 c
|
||||
-0.285071 4.083785 -0.467927 7.257655 1.295334 8.661732 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1149
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 15.666626 15.710510 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001239 00000 n
|
||||
0000001262 00000 n
|
||||
0000001435 00000 n
|
||||
0000001509 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1568
|
||||
%%EOF
|
||||
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gradient.jpg",
|
||||
"filename" : "shaka-line.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.474731 -0.563965 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
3.613694 9.332577 m
|
||||
3.553993 8.861599 l
|
||||
3.637261 8.851044 3.721838 8.862753 3.799107 8.895533 c
|
||||
3.613694 9.332577 l
|
||||
h
|
||||
1.295334 9.626453 m
|
||||
1.355035 10.097433 l
|
||||
1.227973 10.113539 1.099794 10.077623 0.999601 9.997839 c
|
||||
1.295334 9.626453 l
|
||||
h
|
||||
0.968804 3.455042 m
|
||||
1.372000 3.705677 l
|
||||
1.362764 3.720535 1.352713 3.734872 1.341894 3.748621 c
|
||||
0.968804 3.455042 l
|
||||
h
|
||||
4.299407 3.128512 m
|
||||
4.431771 3.584435 l
|
||||
4.353942 3.607030 4.271623 3.609325 4.192656 3.591103 c
|
||||
4.299407 3.128512 l
|
||||
h
|
||||
8.576947 2.638718 m
|
||||
8.695503 3.098424 l
|
||||
8.656776 3.108411 8.616942 3.113465 8.576947 3.113465 c
|
||||
8.576947 2.638718 l
|
||||
h
|
||||
14.781013 1.038721 m
|
||||
14.662457 0.579016 l
|
||||
14.701184 0.569027 14.741018 0.563974 14.781013 0.563974 c
|
||||
14.781013 1.038721 l
|
||||
h
|
||||
15.499378 2.638718 m
|
||||
15.934578 2.828420 l
|
||||
15.881091 2.951125 15.778289 3.045548 15.651489 3.088437 c
|
||||
15.499378 2.638718 l
|
||||
h
|
||||
11.058574 4.140755 m
|
||||
11.128998 4.610250 l
|
||||
10.885809 4.646729 10.655017 4.491467 10.597156 4.252461 c
|
||||
10.539293 4.013455 10.673516 3.769826 10.906463 3.691035 c
|
||||
11.058574 4.140755 l
|
||||
h
|
||||
13.017752 3.846877 m
|
||||
13.253292 4.259073 l
|
||||
13.202273 4.288227 13.146286 4.307655 13.088176 4.316372 c
|
||||
13.017752 3.846877 l
|
||||
h
|
||||
13.474895 5.708097 m
|
||||
13.805044 6.049252 l
|
||||
13.789093 6.064689 13.772079 6.078987 13.754128 6.092043 c
|
||||
13.474895 5.708097 l
|
||||
h
|
||||
13.115711 5.969321 m
|
||||
12.968349 6.420619 l
|
||||
12.798800 6.365256 12.674588 6.219535 12.646772 6.043359 c
|
||||
12.618958 5.867183 12.692234 5.690281 12.836478 5.585376 c
|
||||
13.115711 5.969321 l
|
||||
h
|
||||
13.834077 8.222376 m
|
||||
14.221668 8.496526 l
|
||||
14.206144 8.518474 14.188784 8.539063 14.169774 8.558073 c
|
||||
13.834077 8.222376 l
|
||||
h
|
||||
13.474895 8.581559 m
|
||||
13.369680 9.044500 l
|
||||
13.201114 9.006190 13.066693 8.879284 13.018762 8.713197 c
|
||||
12.970830 8.547110 13.016963 8.368095 13.139197 8.245862 c
|
||||
13.474895 8.581559 l
|
||||
h
|
||||
14.193260 10.377472 m
|
||||
14.619765 10.585986 l
|
||||
14.599768 10.626891 14.573989 10.664707 14.543221 10.698271 c
|
||||
14.193260 10.377472 l
|
||||
h
|
||||
8.381030 11.814203 m
|
||||
7.917068 11.914822 l
|
||||
7.884080 11.762714 7.927746 11.604099 8.033934 11.490305 c
|
||||
8.140121 11.376513 8.295343 11.321997 8.449365 11.344399 c
|
||||
8.381030 11.814203 l
|
||||
h
|
||||
8.642253 14.948887 m
|
||||
9.111748 15.019311 l
|
||||
8.642253 14.948887 l
|
||||
h
|
||||
6.291239 16.744801 m
|
||||
6.262227 17.218662 l
|
||||
6.224693 17.216364 6.187564 17.209614 6.151623 17.198555 c
|
||||
6.291239 16.744801 l
|
||||
h
|
||||
5.474915 16.026436 m
|
||||
5.948456 16.060261 l
|
||||
5.474915 16.026436 l
|
||||
h
|
||||
5.474915 11.291754 m
|
||||
5.030037 11.457493 l
|
||||
5.474915 11.291754 l
|
||||
h
|
||||
3.673396 9.803555 m
|
||||
1.355035 10.097433 l
|
||||
1.235632 9.155476 l
|
||||
3.553993 8.861599 l
|
||||
3.673396 9.803555 l
|
||||
h
|
||||
0.999601 9.997839 m
|
||||
-0.029049 9.178730 -0.454726 7.875908 -0.474048 6.619066 c
|
||||
-0.493367 5.362488 -0.110331 4.058727 0.595713 3.161463 c
|
||||
1.341894 3.748621 l
|
||||
0.794064 4.444821 0.458734 5.524729 0.475334 6.604470 c
|
||||
0.491930 7.683949 0.856455 8.670100 1.591066 9.255068 c
|
||||
0.999601 9.997839 l
|
||||
h
|
||||
0.565608 3.204407 m
|
||||
0.721970 2.952868 1.013515 2.611341 1.407507 2.372385 c
|
||||
1.811404 2.127421 2.357187 1.973489 2.959612 2.192553 c
|
||||
2.635129 3.084882 l
|
||||
2.375515 2.990478 2.132184 3.043347 1.899893 3.184233 c
|
||||
1.657696 3.331126 1.465977 3.554496 1.372000 3.705677 c
|
||||
0.565608 3.204407 l
|
||||
h
|
||||
2.959612 2.192553 m
|
||||
3.816493 2.504146 4.293336 2.639887 4.406158 2.665923 c
|
||||
4.192656 3.591103 l
|
||||
4.022485 3.551832 3.502325 3.400227 2.635129 3.084882 c
|
||||
2.959612 2.192553 l
|
||||
h
|
||||
4.167043 2.672591 m
|
||||
5.229115 2.364247 6.254152 2.163970 8.576947 2.163970 c
|
||||
8.576947 3.113465 l
|
||||
6.328326 3.113465 5.394184 3.305025 4.431771 3.584435 c
|
||||
4.167043 2.672591 l
|
||||
h
|
||||
8.458392 2.179011 m
|
||||
14.662457 0.579016 l
|
||||
14.899569 1.498427 l
|
||||
8.695503 3.098424 l
|
||||
8.458392 2.179011 l
|
||||
h
|
||||
14.781013 0.563974 m
|
||||
15.036198 0.563974 15.495326 0.684875 15.814721 1.047266 c
|
||||
16.180891 1.462728 16.264221 2.072176 15.934578 2.828420 c
|
||||
15.064179 2.449016 l
|
||||
15.289635 1.931793 15.160722 1.741243 15.102402 1.675073 c
|
||||
15.055794 1.622190 14.990156 1.579316 14.916806 1.549556 c
|
||||
14.881134 1.535082 14.847747 1.525430 14.820526 1.519657 c
|
||||
14.791491 1.513498 14.777695 1.513469 14.781013 1.513469 c
|
||||
14.781013 0.563974 l
|
||||
h
|
||||
15.651489 3.088437 m
|
||||
11.210685 4.590474 l
|
||||
10.906463 3.691035 l
|
||||
15.347267 2.188998 l
|
||||
15.651489 3.088437 l
|
||||
h
|
||||
10.988150 3.671260 m
|
||||
12.947328 3.377382 l
|
||||
13.088176 4.316372 l
|
||||
11.128998 4.610250 l
|
||||
10.988150 3.671260 l
|
||||
h
|
||||
12.782211 3.434681 m
|
||||
12.991495 3.315090 13.204453 3.370091 13.288217 3.396689 c
|
||||
13.400116 3.432221 13.506123 3.490767 13.598186 3.554502 c
|
||||
13.783985 3.683133 13.977411 3.877748 14.120350 4.119644 c
|
||||
14.264680 4.363894 14.369576 4.678114 14.335162 5.031647 c
|
||||
14.300108 5.391746 14.125634 5.739002 13.805044 6.049252 c
|
||||
13.144745 5.366943 l
|
||||
13.330275 5.187398 13.380290 5.040778 13.390134 4.939653 c
|
||||
13.400617 4.831963 13.370820 4.717613 13.302905 4.602680 c
|
||||
13.233600 4.485394 13.137231 4.390213 13.057724 4.335170 c
|
||||
13.017135 4.307070 12.996612 4.300308 13.000857 4.301657 c
|
||||
13.003194 4.302399 13.024761 4.309311 13.061064 4.310122 c
|
||||
13.095938 4.310902 13.170414 4.306433 13.253292 4.259073 c
|
||||
12.782211 3.434681 l
|
||||
h
|
||||
13.754128 6.092043 m
|
||||
13.394944 6.353267 l
|
||||
12.836478 5.585376 l
|
||||
13.195662 5.324152 l
|
||||
13.754128 6.092043 l
|
||||
h
|
||||
13.263074 5.518023 m
|
||||
13.593105 5.625790 14.123367 5.907292 14.433812 6.409482 c
|
||||
14.595931 6.671733 14.696482 6.993351 14.669847 7.364054 c
|
||||
14.643518 7.730516 14.495621 8.109214 14.221668 8.496526 c
|
||||
13.446486 7.948226 l
|
||||
13.646002 7.666152 13.711709 7.450294 13.722795 7.296009 c
|
||||
13.733575 7.145966 13.695351 7.020646 13.626177 6.908748 c
|
||||
13.474038 6.662641 13.171650 6.487002 12.968349 6.420619 c
|
||||
13.263074 5.518023 l
|
||||
h
|
||||
14.169774 8.558073 m
|
||||
13.810592 8.917255 l
|
||||
13.139197 8.245862 l
|
||||
13.498380 7.886679 l
|
||||
14.169774 8.558073 l
|
||||
h
|
||||
13.580109 8.118617 m
|
||||
13.896242 8.190466 14.344993 8.395787 14.624650 8.816864 c
|
||||
14.929440 9.275781 14.963785 9.882310 14.619765 10.585986 c
|
||||
13.766754 10.168959 l
|
||||
13.997427 9.697128 13.912044 9.460121 13.833706 9.342171 c
|
||||
13.730235 9.186377 13.532457 9.081495 13.369680 9.044500 c
|
||||
13.580109 8.118617 l
|
||||
h
|
||||
14.543221 10.698271 m
|
||||
13.820906 11.486253 12.989320 12.223852 11.896564 12.288132 c
|
||||
11.840808 11.340275 l
|
||||
12.524374 11.300065 13.128883 10.836036 13.843298 10.056674 c
|
||||
14.543221 10.698271 l
|
||||
h
|
||||
11.896564 12.288132 m
|
||||
11.441970 12.314873 10.711069 12.336796 10.019300 12.341186 c
|
||||
9.341933 12.345484 8.654247 12.333687 8.312695 12.284006 c
|
||||
8.449365 11.344399 l
|
||||
8.706450 11.381794 9.318512 11.396118 10.013274 11.391710 c
|
||||
10.693633 11.387392 11.407242 11.365778 11.840808 11.340275 c
|
||||
11.896564 12.288132 l
|
||||
h
|
||||
8.844993 11.713585 m
|
||||
8.948084 12.188952 9.040332 12.829445 9.094679 13.432834 c
|
||||
9.147870 14.023395 9.169946 14.631327 9.111748 15.019311 c
|
||||
8.172758 14.878462 l
|
||||
8.212520 14.613384 8.201942 14.105675 8.149012 13.518009 c
|
||||
8.097237 12.943172 8.009893 12.342852 7.917068 11.914822 c
|
||||
8.844993 11.713585 l
|
||||
h
|
||||
9.111748 15.019311 m
|
||||
8.944062 16.137217 7.805658 17.313158 6.262227 17.218662 c
|
||||
6.320251 16.270941 l
|
||||
7.336813 16.333179 8.072657 15.545805 8.172758 14.878462 c
|
||||
9.111748 15.019311 l
|
||||
h
|
||||
6.151623 17.198555 m
|
||||
5.976391 17.144638 5.715709 17.036982 5.490986 16.876261 c
|
||||
5.292936 16.734617 4.969444 16.439627 5.001374 15.992612 c
|
||||
5.948456 16.060261 l
|
||||
5.951383 16.019283 5.934667 15.999795 5.943361 16.012491 c
|
||||
5.954769 16.029152 5.984430 16.061831 6.043331 16.103956 c
|
||||
6.162553 16.189222 6.323094 16.257891 6.430855 16.291048 c
|
||||
6.151623 17.198555 l
|
||||
h
|
||||
5.001374 15.992612 m
|
||||
5.011176 15.855374 5.059216 15.566318 5.104405 15.255149 c
|
||||
5.152757 14.922197 5.207128 14.509316 5.241940 14.062993 c
|
||||
5.312967 13.152368 5.295928 12.171200 5.030037 11.457493 c
|
||||
5.919792 11.126015 l
|
||||
6.262142 12.044956 6.261431 13.202559 6.188560 14.136827 c
|
||||
6.151423 14.612950 6.093790 15.049047 6.044043 15.391605 c
|
||||
5.991133 15.755945 5.954979 15.968927 5.948456 16.060261 c
|
||||
5.001374 15.992612 l
|
||||
h
|
||||
5.030037 11.457493 m
|
||||
4.953650 11.252455 4.742510 10.903708 4.434547 10.555828 c
|
||||
4.127778 10.209298 3.769400 9.914337 3.428282 9.769621 c
|
||||
3.799107 8.895533 l
|
||||
4.320028 9.116529 4.788858 9.523607 5.145489 9.926461 c
|
||||
5.500926 10.327968 5.789377 10.775953 5.919792 11.126015 c
|
||||
5.030037 11.457493 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
7995
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 16.615845 16.660034 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000008085 00000 n
|
||||
0000008108 00000 n
|
||||
0000008281 00000 n
|
||||
0000008355 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
8414
|
||||
%%EOF
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// GradientButtonStyle.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GradientButtonStyle: ButtonStyle {
|
||||
let padding: CGFloat
|
||||
|
||||
init(padding: CGFloat = 16.0) {
|
||||
self.padding = padding
|
||||
}
|
||||
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
return configuration.label
|
||||
.padding(padding)
|
||||
.foregroundColor(Color.white)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(PinkGradient)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? 0.8 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct GradientButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Button(action: {
|
||||
print("dynamic size")
|
||||
}) {
|
||||
Text(verbatim: "Dynamic Size")
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
|
||||
|
||||
Button(action: {
|
||||
print("infinite width")
|
||||
}) {
|
||||
HStack {
|
||||
Text(verbatim: "Infinite Width")
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// DamusBackground.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-07-12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct DamusBackground: View {
|
||||
let maxHeight: CGFloat
|
||||
|
||||
init(maxHeight: CGFloat = 250.0) {
|
||||
self.maxHeight = maxHeight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Image("login-header")
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusBackground_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusBackground()
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,9 @@ fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2, damus_grad_c3]
|
||||
|
||||
struct DamusGradient: View {
|
||||
var body: some View {
|
||||
DamusGradient.gradient
|
||||
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusGradient_Previews: PreviewProvider {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// DamusLogoGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/24/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let damus_logo_grad_c1 = hex_col(r: 0x30, g: 0xb3, b: 0xf1)
|
||||
fileprivate let damus_logo_grad_c2 = hex_col(r: 0xc5, g: 0x39, b: 0xf9)
|
||||
fileprivate let damus_logo_grad = [damus_logo_grad_c1, damus_logo_grad_c2]
|
||||
|
||||
struct DamusLogoGradient: View {
|
||||
var body: some View {
|
||||
DamusLogoGradient.gradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
|
||||
static var gradient: LinearGradient {
|
||||
LinearGradient(colors: damus_logo_grad, startPoint: .leading, endPoint: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusLogoGradient_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusLogoGradient()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// GoldSupportGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
|
||||
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
|
||||
|
||||
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
|
||||
|
||||
let GoldGradient: LinearGradient =
|
||||
LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
|
||||
struct GoldGradientView: View {
|
||||
var body: some View {
|
||||
GoldGradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct GoldGradientView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GoldGradientView()
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// PinkGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x4c, b: 0xd9)
|
||||
fileprivate let damus_grad_c2 = hex_col(r: 0xf8, g: 0x69, b: 0xb6)
|
||||
fileprivate let pink_grad = [damus_grad_c1, damus_grad_c2]
|
||||
|
||||
let PinkGradient = LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
|
||||
|
||||
struct PinkGradientView: View {
|
||||
var body: some View {
|
||||
PinkGradient
|
||||
.edgesIgnoringSafeArea([.top,.bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct PinkGradientView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PinkGradientView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct IconLabel: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(img_name)
|
||||
Image(systemName: img_name)
|
||||
.foregroundColor(img_color)
|
||||
.frame(width: 20)
|
||||
.padding([.trailing], 20)
|
||||
|
||||
@@ -52,10 +52,8 @@ enum ImageShape {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Carousel
|
||||
@MainActor
|
||||
struct ImageCarousel: View {
|
||||
var urls: [MediaUrl]
|
||||
var urls: [URL]
|
||||
|
||||
let evid: String
|
||||
|
||||
@@ -64,19 +62,14 @@ struct ImageCarousel: View {
|
||||
@State private var open_sheet: Bool = false
|
||||
@State private var current_url: URL? = nil
|
||||
@State private var image_fill: ImageFill? = nil
|
||||
|
||||
@State private var fillHeight: CGFloat = 350
|
||||
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
@State private var firstImageHeight: CGFloat? = nil
|
||||
@State private var currentImageHeight: CGFloat?
|
||||
@State private var selectedIndex = 0
|
||||
@State private var video_size: CGSize? = nil
|
||||
|
||||
init(state: DamusState, evid: String, urls: [MediaUrl]) {
|
||||
let fillHeight: CGFloat = 350
|
||||
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2
|
||||
|
||||
init(state: DamusState, evid: String, urls: [URL]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
_image_fill = State(initialValue: media_model.fill)
|
||||
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
@@ -87,133 +80,75 @@ struct ImageCarousel: View {
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
firstImageHeight ?? image_fill?.height ?? fillHeight
|
||||
image_fill?.height ?? fillHeight
|
||||
}
|
||||
|
||||
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
||||
func Placeholder(url: URL, geo_size: CGSize) -> some View {
|
||||
Group {
|
||||
if num_urls > 1 {
|
||||
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
|
||||
Color.clear
|
||||
} else if let meta = state.events.lookup_img_metadata(url: url),
|
||||
if let meta = state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
|
||||
} else {
|
||||
Color.clear
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
||||
if self.image_fill == nil,
|
||||
let meta = state.events.lookup_img_metadata(url: url),
|
||||
let size = meta.meta.dim?.size
|
||||
{
|
||||
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 {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
||||
.onChange(of: video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
if index == 0 {
|
||||
firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.padding(0)
|
||||
|
||||
}
|
||||
|
||||
var Medias: some View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(urls, id: \.absoluteString) { url in
|
||||
GeometryReader { geo in
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
|
||||
}
|
||||
.frame(height: height)
|
||||
.onChange(of: selectedIndex) { value in
|
||||
selectedIndex = value
|
||||
.frame(height: self.height)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Medias
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Modifier
|
||||
@@ -264,11 +199,9 @@ public struct ImageFill {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Provider
|
||||
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: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ struct InvoiceView: View {
|
||||
UIPasteboard.general.string = invoice.string
|
||||
} label: {
|
||||
if !copied {
|
||||
Image("copy2")
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Image("check-circle")
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(DamusColors.green)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ struct InvoiceView: View {
|
||||
var PayButton: some View {
|
||||
Button {
|
||||
if settings.show_wallet_selector {
|
||||
present_sheet(.select_wallet(invoice: invoice.string))
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ struct InvoiceView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("", image: "zap.fill")
|
||||
Label("", systemImage: "bolt.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||
Spacer()
|
||||
@@ -79,6 +79,9 @@ struct InvoiceView: View {
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +116,3 @@ struct InvoiceView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet, sheet)
|
||||
}
|
||||
|
||||
@@ -32,13 +32,12 @@ struct NIP05Badge: View {
|
||||
Group {
|
||||
if nip05_color {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("verified.fill")
|
||||
.mask(Image(systemName: "checkmark.seal.fill")
|
||||
.resizable()
|
||||
).frame(width: 18, height: 18)
|
||||
).frame(width: 14, height: 14)
|
||||
} else if show_domain {
|
||||
Image("verified")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.footnote)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
}
|
||||
}
|
||||
@@ -85,11 +84,7 @@ func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
|
||||
struct NIP05Badge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
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: Contacts(our_pubkey: "sdkfjsdf"), show_domain: true, clickable: false)
|
||||
}
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, clickable: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ struct Reposted: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image("repost")
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// SearchIconView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-07-12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchHeaderView: View {
|
||||
let state: DamusState
|
||||
let described: DescribedSearch
|
||||
@State var is_following: Bool
|
||||
|
||||
init(state: DamusState, described: DescribedSearch) {
|
||||
self.state = state
|
||||
self.described = described
|
||||
|
||||
let is_following = (described.is_hashtag.map {
|
||||
ht in is_following_hashtag(contacts: state.contacts.event, hashtag: ht)
|
||||
}) ?? false
|
||||
|
||||
self._is_following = State(wrappedValue: is_following)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var SearchText: Text {
|
||||
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
|
||||
|
||||
VStack(alignment: .leading, spacing: 10.0) {
|
||||
SearchText
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
.font(.title.bold())
|
||||
|
||||
if state.is_privkey_user, case .hashtag(let ht) = described {
|
||||
if is_following {
|
||||
UnfollowButton(ht)
|
||||
} else {
|
||||
FollowButton(ht)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.followed)) { notif in
|
||||
let ref = notif.object as! ReferencedId
|
||||
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
|
||||
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 }
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
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: .unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
//
|
||||
// SupporterBadge.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SupporterBadge: View {
|
||||
let percent: Int
|
||||
|
||||
let size: CGFloat = 17
|
||||
|
||||
var body: some View {
|
||||
if percent < 100 {
|
||||
Image("star.fill")
|
||||
.resizable()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundColor(support_level_color(percent))
|
||||
} else {
|
||||
Image("star.fill")
|
||||
.resizable()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func support_level_color(_ percent: Int) -> Color {
|
||||
if percent == 0 {
|
||||
return .gray
|
||||
}
|
||||
|
||||
let percent_f = Double(percent) / 100.0
|
||||
let cutoff = 0.5
|
||||
let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
|
||||
let s = 0.9; // Saturation
|
||||
let b = 0.9; // Brightness
|
||||
|
||||
return Color(hue: h, saturation: s, brightness: b)
|
||||
}
|
||||
|
||||
struct SupporterBadge_Previews: PreviewProvider {
|
||||
static func Level(_ p: Int) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
SupporterBadge(percent: p)
|
||||
.frame(width: 50)
|
||||
Text(verbatim: p.formatted())
|
||||
.frame(width: 50)
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
Level(1)
|
||||
Level(10)
|
||||
Level(20)
|
||||
Level(30)
|
||||
Level(40)
|
||||
Level(50)
|
||||
}
|
||||
Level(60)
|
||||
Level(70)
|
||||
Level(80)
|
||||
Level(90)
|
||||
Level(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import NaturalLanguage
|
||||
|
||||
|
||||
struct Translated: Equatable {
|
||||
let artifacts: NoteArtifactsSeparated
|
||||
let artifacts: NoteArtifacts
|
||||
let language: String
|
||||
}
|
||||
|
||||
@@ -42,10 +42,9 @@ struct TranslateView: View {
|
||||
.translate_button_style()
|
||||
}
|
||||
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated) -> some View {
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> 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)
|
||||
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
|
||||
@@ -12,7 +12,7 @@ struct TruncatedText: View {
|
||||
let maxChars: Int = 280
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
@@ -28,6 +28,16 @@ struct TruncatedText: View {
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
func getTruncatedString() -> AttributedString? {
|
||||
let nsAttributedString = NSAttributedString(text.attributed)
|
||||
if nsAttributedString.length < maxChars { return nil }
|
||||
|
||||
let range = NSRange(location: 0, length: maxChars)
|
||||
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
|
||||
|
||||
return AttributedString(truncatedAttributedString) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
struct TruncatedText_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,27 +11,30 @@ struct UserViewRow: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.background(.clear)
|
||||
.background(
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let spacer: Bool
|
||||
|
||||
@State var about_text: Text? = nil
|
||||
|
||||
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
|
||||
self.damus_state = damus_state
|
||||
self.pubkey = pubkey
|
||||
self.spacer = spacer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
@@ -39,16 +42,14 @@ struct UserView: View {
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
|
||||
if let about_text {
|
||||
about_text
|
||||
if let about = profile?.about {
|
||||
Text(about)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
if spacer {
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ struct WebsiteLink: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image("link")
|
||||
Image(systemName: "link")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
|
||||
@@ -23,8 +23,6 @@ struct WebsiteLink: View {
|
||||
Text(link_text)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.accentColor)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,41 +10,46 @@ import SwiftUI
|
||||
enum ZappingEventType {
|
||||
case failed(ZappingError)
|
||||
case got_zap_invoice(String)
|
||||
case sent_from_nwc
|
||||
}
|
||||
|
||||
enum ZappingError {
|
||||
case fetching_invoice
|
||||
case bad_lnurl
|
||||
case canceled
|
||||
case send_failed
|
||||
}
|
||||
|
||||
struct ZappingEvent {
|
||||
let is_custom: Bool
|
||||
let type: ZappingEventType
|
||||
let target: ZapTarget
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
class ZapButtonModel: ObservableObject {
|
||||
var invoice: String? = nil
|
||||
@Published var zapping: String = ""
|
||||
@Published var showing_select_wallet: Bool = false
|
||||
@Published var showing_zap_customizer: Bool = false
|
||||
}
|
||||
|
||||
struct ZapButton: View {
|
||||
let damus_state: DamusState
|
||||
let target: ZapTarget
|
||||
let event: NostrEvent
|
||||
let lnurl: String
|
||||
|
||||
@ObservedObject var zaps: ZapsDataModel
|
||||
@StateObject var button: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
var our_zap: Zapping? {
|
||||
zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey })
|
||||
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
|
||||
}
|
||||
|
||||
var zap_img: String {
|
||||
switch our_zap {
|
||||
case .none:
|
||||
return "zap"
|
||||
return "bolt"
|
||||
case .zap:
|
||||
return "zap.fill"
|
||||
return "bolt.fill"
|
||||
case .pending:
|
||||
return "zap.fill"
|
||||
return "bolt.fill"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +60,18 @@ struct ZapButton: View {
|
||||
|
||||
// always orange !
|
||||
return Color.orange
|
||||
/*
|
||||
if our_zap.is_paid {
|
||||
return Color.orange
|
||||
} else {
|
||||
return Color.yellow
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func tap() {
|
||||
guard let our_zap else {
|
||||
send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,17 +118,12 @@ struct ZapButton: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
if !damus_state.settings.nozaps || zaps.zap_total > 0 {
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(zap_img)
|
||||
.resizable()
|
||||
.foregroundColor(zap_color)
|
||||
.font(.footnote.weight(.medium))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width:20, height: 20)
|
||||
})
|
||||
}
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(systemName: zap_img)
|
||||
.foregroundColor(zap_color)
|
||||
.font(.footnote.weight(.medium))
|
||||
})
|
||||
|
||||
if zaps.zap_total > 0 {
|
||||
Text(verbatim: format_msats_abbrev(zaps.zap_total))
|
||||
@@ -126,15 +133,41 @@ struct ZapButton: View {
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||
guard !damus_state.settings.nozaps else { return }
|
||||
|
||||
present_sheet(.zap(target: target, lnurl: lnurl))
|
||||
button.showing_zap_customizer = true
|
||||
})
|
||||
.highPriorityGesture(TapGesture().onEnded {
|
||||
guard !damus_state.settings.nozaps else { return }
|
||||
|
||||
tap()
|
||||
})
|
||||
.sheet(isPresented: $button.showing_zap_customizer) {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard zap_ev.event.id == self.event.id else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
self.button.invoice = inv
|
||||
self.button.showing_select_wallet = true
|
||||
} else {
|
||||
let wallet = damus_state.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +177,7 @@ struct ZapButton_Previews: PreviewProvider {
|
||||
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 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(), event: test_event, lnurl: "lnurl", zaps: zaps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +193,14 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
|
||||
return .external(ExtPendingZapState(state: .fetching_invoice))
|
||||
}
|
||||
|
||||
func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
guard let keypair = damus_state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
let content = comment ?? ""
|
||||
|
||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||
@@ -174,7 +208,8 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
return
|
||||
}
|
||||
|
||||
let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
|
||||
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
|
||||
let amount_msat = Int64(zap_amount) * 1000
|
||||
let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings)
|
||||
let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state)
|
||||
let zapreq = mzapreq.potentially_anon_outer_request.ev
|
||||
@@ -194,7 +229,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
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)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -204,11 +239,11 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
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 {
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, 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)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
@@ -221,50 +256,21 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
// 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)
|
||||
guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv),
|
||||
case .nwc(let pzap_state) = pending_zap_state
|
||||
else {
|
||||
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)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
@@ -14,38 +13,17 @@ struct TimestampedProfile {
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
struct ZapSheet {
|
||||
let target: ZapTarget
|
||||
let lnurl: String
|
||||
}
|
||||
|
||||
struct SelectWallet {
|
||||
let invoice: String
|
||||
}
|
||||
|
||||
enum Sheets: Identifiable {
|
||||
case post(PostAction)
|
||||
case report(ReportTarget)
|
||||
case event(NostrEvent)
|
||||
case zap(ZapSheet)
|
||||
case select_wallet(SelectWallet)
|
||||
case filter
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||
}
|
||||
|
||||
static func select_wallet(invoice: String) -> Sheets {
|
||||
return .select_wallet(SelectWallet(invoice: invoice))
|
||||
}
|
||||
|
||||
|
||||
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 .select_wallet: return "select-wallet"
|
||||
case .filter: return "filter"
|
||||
}
|
||||
}
|
||||
@@ -82,14 +60,21 @@ struct ContentView: View {
|
||||
@State var damus_state: DamusState? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var active_profile: String? = nil
|
||||
@State var active_search: NostrFilter? = nil
|
||||
@State var active_event: NostrEvent? = nil
|
||||
@State var profile_open: Bool = false
|
||||
@State var thread_open: Bool = false
|
||||
@State var search_open: Bool = false
|
||||
@State var wallet_open: Bool = false
|
||||
@State var active_nwc: WalletConnectURL? = nil
|
||||
@State var muting: String? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
|
||||
let sub_id = UUID().description
|
||||
|
||||
@@ -121,7 +106,7 @@ struct ContentView: View {
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post(.posting(.none))
|
||||
self.active_sheet = .post(.posting)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,8 +114,8 @@ struct ContentView: View {
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
|
||||
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
|
||||
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
||||
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
@@ -142,13 +127,16 @@ struct ContentView: View {
|
||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||
ZStack {
|
||||
if let damus = self.damus_state {
|
||||
TimelineView<AnyView>(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
|
||||
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
navigationCoordinator.popToRoot()
|
||||
profile_open = false
|
||||
thread_open = false
|
||||
search_open = false
|
||||
wallet_open = false
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
@@ -159,6 +147,21 @@ struct ContentView: View {
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
VStack {
|
||||
NavigationLink(destination: WalletView(model: damus_state!.wallet), isActive: $wallet_open) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||
EmptyView()
|
||||
}
|
||||
if let active_event {
|
||||
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
|
||||
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
||||
EmptyView()
|
||||
}
|
||||
switch selected_timeline {
|
||||
case .search:
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -200,6 +203,28 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeSearchView: some View {
|
||||
Group {
|
||||
if let search = self.active_search {
|
||||
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeProfileView: some View {
|
||||
Group {
|
||||
if let pk = self.active_profile {
|
||||
let profile_model = ProfileModel(pubkey: pk, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pk)
|
||||
ProfileView(damus_state: damus_state!, profile: profile_model, followers: followers)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let damus_state {
|
||||
@@ -215,36 +240,32 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
let thread = ThreadModel(event: ev, damus_state: damus_state!)
|
||||
navigationCoordinator.push(route: Route.Thread(thread: thread))
|
||||
popToRoot()
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
|
||||
func open_wallet(nwc: WalletConnectURL) {
|
||||
self.damus_state!.wallet.new(nwc)
|
||||
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet))
|
||||
}
|
||||
|
||||
func open_script(_ script: [UInt8]) {
|
||||
print("pushing script nav")
|
||||
let model = ScriptModel(data: script, state: .not_loaded)
|
||||
navigationCoordinator.push(route: Route.Script(script: model))
|
||||
self.wallet_open = true
|
||||
}
|
||||
|
||||
func open_profile(id: String) {
|
||||
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: id)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
popToRoot()
|
||||
self.active_profile = id
|
||||
self.profile_open = true
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
let search = SearchModel(state: damus_state!, search: filt)
|
||||
navigationCoordinator.push(route: Route.Search(search: search))
|
||||
popToRoot()
|
||||
self.active_search = filt
|
||||
self.search_open = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
NavigationStack(path: $navigationCoordinator.path) {
|
||||
NavigationView {
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
@@ -267,14 +288,13 @@ struct ContentView: View {
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
present_sheet(.filter)
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,16 +304,10 @@ struct ContentView: View {
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
navigationCoordinator.popToRoot()
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
}
|
||||
@@ -301,7 +315,6 @@ struct ContentView: View {
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
setup_notifications()
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
@@ -312,10 +325,6 @@ struct ContentView: View {
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .zap(let zapsheet):
|
||||
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
||||
case .select_wallet(let select):
|
||||
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
||||
case .filter:
|
||||
let timeline = selected_timeline
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -337,9 +346,7 @@ struct ContentView: View {
|
||||
case .filter(let filt): self.open_search(filt: filt)
|
||||
case .profile(let id): self.open_profile(id: id)
|
||||
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)
|
||||
}
|
||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.compose)) { notif in
|
||||
@@ -390,24 +397,21 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
ds.postbox.send(ev)
|
||||
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
if let profile = ds.profiles.profiles[ev.pubkey] {
|
||||
ds.postbox.send(profile.event)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.unfollow)) { notif in
|
||||
guard let state = self.damus_state else { return }
|
||||
_ = handle_unfollow_notif(state: state, notif: notif)
|
||||
}
|
||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||
let unfollow = notif.object as! ReferencedId
|
||||
home.resubscribe(.unfollowing(unfollow))
|
||||
guard let state = self.damus_state else {
|
||||
return
|
||||
}
|
||||
handle_unfollow(state: state, notif: notif)
|
||||
}
|
||||
.onReceive(handle_notify(.follow)) { notif in
|
||||
guard let state = self.damus_state else { return }
|
||||
guard handle_follow_notif(state: state, notif: notif) else { return }
|
||||
}
|
||||
.onReceive(handle_notify(.followed)) { notif in
|
||||
home.resubscribe(.following)
|
||||
guard let state = self.damus_state else {
|
||||
return
|
||||
}
|
||||
handle_follow(state: state, notif: notif)
|
||||
}
|
||||
.onReceive(handle_notify(.post)) { notif in
|
||||
guard let state = self.damus_state,
|
||||
@@ -428,31 +432,6 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.present_sheet)) { notif in
|
||||
let sheet = notif.object as! Sheets
|
||||
self.active_sheet = sheet
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if damus_state!.settings.show_wallet_selector {
|
||||
present_sheet(.select_wallet(invoice: inv))
|
||||
} else {
|
||||
let wallet = damus_state!.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
}
|
||||
case .sent_from_nwc:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||
switch phase {
|
||||
case .background:
|
||||
@@ -475,11 +454,6 @@ struct ContentView: View {
|
||||
let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
if local.type == .profile_zap {
|
||||
open_profile(id: local.event_id)
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
@@ -488,16 +462,13 @@ struct ContentView: View {
|
||||
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))
|
||||
damus_state.dms.open_dm_by_pk(target.pubkey)
|
||||
|
||||
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
|
||||
@@ -528,7 +499,7 @@ 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 = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
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.")
|
||||
@@ -588,7 +559,7 @@ struct ContentView: View {
|
||||
}, 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 = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
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.")
|
||||
@@ -612,44 +583,43 @@ struct ContentView: View {
|
||||
|
||||
func connect() {
|
||||
let pool = RelayPool()
|
||||
let model_cache = RelayModelCache()
|
||||
let metadatas = RelayMetadatas()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
if let url = RelayURL(relay) {
|
||||
let descriptor = RelayDescriptor(url: url, info: .rw)
|
||||
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
// dumb stuff needed for property wrappers
|
||||
UserSettingsStore.pubkey = pubkey
|
||||
let settings = UserSettingsStore()
|
||||
UserSettingsStore.shared = settings
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
if let url = RelayURL(relay) {
|
||||
let descriptor = RelayDescriptor(url: url, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
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(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: settings,
|
||||
relay_filters: relay_filters,
|
||||
relay_model_cache: model_cache,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts(),
|
||||
events: EventCache(),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
@@ -657,9 +627,7 @@ struct ContentView: View {
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
user_search_cache: user_search_cache
|
||||
wallet: WalletModel(settings: settings)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -724,7 +692,7 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
|
||||
let kinds = filter.kinds ?? []
|
||||
let initial: Int64? = nil
|
||||
let earliest = kinds.reduce(initial) { earliest, kind in
|
||||
let last = last_of_kind[kind.rawValue]
|
||||
let last = last_of_kind[kind]
|
||||
let since: Int64? = get_since_time(last_event: last)
|
||||
|
||||
if earliest == nil {
|
||||
@@ -768,57 +736,24 @@ func setup_notifications() {
|
||||
}
|
||||
}
|
||||
|
||||
struct FindEvent {
|
||||
let type: FindEventType
|
||||
let find_from: [String]?
|
||||
|
||||
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
||||
}
|
||||
|
||||
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
|
||||
return FindEvent(type: .event(evid), find_from: find_from)
|
||||
}
|
||||
}
|
||||
|
||||
enum FindEventType {
|
||||
case profile(String)
|
||||
case event(String)
|
||||
}
|
||||
|
||||
enum FoundEvent {
|
||||
case profile(Profile, NostrEvent)
|
||||
case invalid_profile(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
func find_event(state: DamusState, query query_: FindEvent, 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))
|
||||
return
|
||||
}
|
||||
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
||||
|
||||
case .event(let evid):
|
||||
if let ev = state.events.lookup(evid) {
|
||||
callback(.event(ev))
|
||||
return
|
||||
}
|
||||
|
||||
filter = NostrFilter(ids: [evid], limit: 1)
|
||||
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
|
||||
if let ev = state.events.lookup(evid) {
|
||||
callback(ev)
|
||||
return
|
||||
}
|
||||
|
||||
let subid = UUID().description
|
||||
var attempts: Int = 0
|
||||
|
||||
var has_event = false
|
||||
guard let filter else { return }
|
||||
|
||||
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [NostrKind.metadata.rawValue]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
var attempts = 0
|
||||
|
||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
@@ -834,22 +769,15 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
||||
break
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
|
||||
state.pool.unsubscribe(sub_id: subid)
|
||||
|
||||
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))
|
||||
return
|
||||
}
|
||||
if search_type == .profile && ev.known_kind == .metadata {
|
||||
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) {
|
||||
callback(ev)
|
||||
}
|
||||
case .event:
|
||||
callback(.event(ev))
|
||||
} else {
|
||||
callback(ev)
|
||||
}
|
||||
case .eose:
|
||||
if !has_event {
|
||||
@@ -872,85 +800,60 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
||||
}
|
||||
switch timeline {
|
||||
case .home:
|
||||
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where notes and replies appear from those who the user is following.")
|
||||
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
|
||||
case .notifications:
|
||||
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
case .search:
|
||||
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where notes from all connected relay servers appear.")
|
||||
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
case .dms:
|
||||
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
||||
guard let keypair = state.keypair.to_full() else {
|
||||
return false
|
||||
func handle_unfollow(state: DamusState, notif: Notification) {
|
||||
guard let privkey = state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let old_contacts = state.contacts.event
|
||||
|
||||
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if let ev = unfollow_user(postbox: state.postbox,
|
||||
our_contacts: state.contacts.event,
|
||||
pubkey: state.pubkey,
|
||||
privkey: privkey,
|
||||
unfollow: pk) {
|
||||
notify(.unfollowed, pk)
|
||||
|
||||
state.contacts.event = ev
|
||||
state.contacts.remove_friend(pk)
|
||||
//friend_events = friend_events.filter { $0.pubkey != pk }
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
||||
guard let keypair = state.keypair.to_full() else {
|
||||
return false
|
||||
func handle_follow(state: DamusState, notif: Notification) {
|
||||
guard let privkey = state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
notify(.followed, follow)
|
||||
|
||||
state.contacts.event = ev
|
||||
if follow.key == "p" {
|
||||
state.contacts.add_friend_pubkey(follow.ref_id)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func handle_follow_notif(state: DamusState, notif: Notification) -> Bool {
|
||||
let fnotify = notif.object as! FollowTarget
|
||||
switch fnotify {
|
||||
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))
|
||||
if let ev = follow_user(pool: state.pool,
|
||||
our_contacts: state.contacts.event,
|
||||
pubkey: state.pubkey,
|
||||
privkey: privkey,
|
||||
follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
|
||||
notify(.followed, fnotify.pubkey)
|
||||
|
||||
state.contacts.event = ev
|
||||
|
||||
switch fnotify {
|
||||
case .pubkey(let pk):
|
||||
state.contacts.add_friend_pubkey(pk)
|
||||
case .contact(let ev):
|
||||
state.contacts.add_friend_contact(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
|
||||
@@ -981,7 +884,6 @@ enum OpenResult {
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
}
|
||||
|
||||
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
|
||||
@@ -1000,18 +902,15 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
||||
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
|
||||
guard let res, case .event(let ev) = res else { return }
|
||||
result(.event(ev))
|
||||
find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
result(.event(ev))
|
||||
}
|
||||
}
|
||||
}
|
||||
case .filter(let filt):
|
||||
result(.filter(filt))
|
||||
break
|
||||
// TODO: handle filter searches?
|
||||
case .script(let script):
|
||||
result(.script(script))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject {
|
||||
@Published var our_zap: Zapping?
|
||||
@Published var likes: Int
|
||||
@Published var boosts: Int
|
||||
@Published private(set) var zaps: Int
|
||||
@Published var zaps: Int
|
||||
@Published var zap_total: Int64
|
||||
@Published var replies: Int
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import Foundation
|
||||
class Contacts {
|
||||
private var friends: Set<String> = Set()
|
||||
private var friend_of_friends: Set<String> = 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
|
||||
@@ -60,25 +58,12 @@ class Contacts {
|
||||
|
||||
func remove_friend(_ pubkey: String) {
|
||||
friends.remove(pubkey)
|
||||
|
||||
pubkey_to_our_friends.forEach {
|
||||
pubkey_to_our_friends[$0.key]?.remove(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
func get_friend_list() -> Set<String> {
|
||||
return friends
|
||||
func get_friend_list() -> [String] {
|
||||
return Array(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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func add_friend_pubkey(_ pubkey: String) {
|
||||
friends.insert(pubkey)
|
||||
}
|
||||
@@ -88,15 +73,6 @@ class Contacts {
|
||||
for tag in contact.tags {
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
friend_of_friends.insert(tag[1])
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,43 +96,39 @@ class Contacts {
|
||||
func follow_state(_ pubkey: String) -> 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] {
|
||||
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_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {
|
||||
guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: pubkey, follow: follow) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: keypair.privkey)
|
||||
|
||||
box.send(ev)
|
||||
|
||||
ev.sign(privkey: privkey)
|
||||
|
||||
|
||||
pool.send(.event(ev))
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? {
|
||||
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
|
||||
guard let cs = our_contacts else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let ev = unfollow_reference_event(our_contacts: cs, our_pubkey: keypair.pubkey, unfollow: unfollow)
|
||||
let ev = unfollow_user_event(our_contacts: cs, our_pubkey: pubkey, unfollow: unfollow)
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: keypair.privkey)
|
||||
|
||||
ev.sign(privkey: privkey)
|
||||
|
||||
postbox.send(ev)
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
func unfollow_reference_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: ReferencedId) -> NostrEvent {
|
||||
func unfollow_user_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: String) -> NostrEvent {
|
||||
let tags = our_contacts.tags.filter { tag in
|
||||
if tag.count >= 2 && tag[0] == unfollow.key && tag[1] == unfollow.ref_id {
|
||||
if tag.count >= 2 && tag[0] == "p" && tag[1] == unfollow {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -228,16 +200,12 @@ 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 follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
|
||||
// don't update if we're already following
|
||||
if is_already_following(contacts: our_contacts, follow: follow) {
|
||||
if our_contacts.references(id: follow.ref_id, key: "p") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let kind = NostrKind.contacts.rawValue
|
||||
var tags = our_contacts.tags
|
||||
tags.append(refid_to_tag(follow))
|
||||
|
||||
@@ -14,8 +14,8 @@ class CreateAccountModel: ObservableObject {
|
||||
@Published var about: String = ""
|
||||
@Published var pubkey: String = ""
|
||||
@Published var privkey: String = ""
|
||||
@Published var profile_image: URL? = nil
|
||||
|
||||
@Published var profile_image: String? = nil
|
||||
|
||||
var pubkey_bech32: String {
|
||||
return bech32_pubkey(self.pubkey) ?? ""
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct DamusState {
|
||||
let lnurls: LNUrls
|
||||
let settings: UserSettingsStore
|
||||
let relay_filters: RelayFilters
|
||||
let relay_model_cache: RelayModelCache
|
||||
let relay_metadata: RelayMetadatas
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
@@ -30,24 +30,13 @@ struct DamusState {
|
||||
let replies: ReplyCounter
|
||||
let muted_threads: MutedThreadsManager
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let user_search_cache: UserSearchCache
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
// store generic zap mapping
|
||||
self.zaps.add_zap(zap: zap)
|
||||
let stored = self.events.store_zap(zap: zap)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// associate with events as well
|
||||
return stored
|
||||
return self.events.store_zap(zap: zap)
|
||||
}
|
||||
|
||||
var pubkey: String {
|
||||
@@ -59,6 +48,5 @@ 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) }
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel()) }
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@ class DirectMessagesModel: ObservableObject {
|
||||
self.active_model = model
|
||||
}
|
||||
|
||||
func open_dm_by_pk(_ pubkey: String) {
|
||||
self.set_active_dm(pubkey)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func open_dm_by_model(_ model: DirectMessageModel) {
|
||||
self.set_active_dm_model(model)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func set_active_dm(_ pubkey: String) {
|
||||
for model in self.dms where model.pubkey == pubkey {
|
||||
self.set_active_dm_model(model)
|
||||
|
||||
@@ -24,7 +24,7 @@ class EventsModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func get_filter() -> NostrFilter {
|
||||
var filter = NostrFilter(kinds: [kind])
|
||||
var filter = NostrFilter.filter_kinds([kind.rawValue])
|
||||
filter.referenced_ids = [target]
|
||||
filter.limit = 500
|
||||
return filter
|
||||
|
||||
@@ -30,8 +30,9 @@ class FollowersModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
NostrFilter(kinds: [.contacts],
|
||||
pubkeys: [target])
|
||||
var filter = NostrFilter.filter_contacts
|
||||
filter.pubkeys = [target]
|
||||
return filter
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
@@ -55,13 +56,14 @@ class FollowersModel: ObservableObject {
|
||||
}
|
||||
|
||||
func load_profiles(relay_id: String) {
|
||||
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
|
||||
if authors.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata],
|
||||
authors: authors)
|
||||
filter.authors = authors
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ class FollowingModel {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var f = NostrFilter(kinds: [.metadata])
|
||||
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
|
||||
// don't fetch profiles we already have
|
||||
if damus_state.profiles.has_fresh_profile(id: pk) {
|
||||
if damus_state.profiles.lookup(id: pk) != nil {
|
||||
return
|
||||
}
|
||||
acc.append(pk)
|
||||
|
||||
@@ -23,41 +23,7 @@ struct NewEventsBits: OptionSet {
|
||||
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||
}
|
||||
|
||||
enum Resubscribe {
|
||||
case following
|
||||
case unfollowing(ReferencedId)
|
||||
}
|
||||
|
||||
enum HomeResubFilter {
|
||||
case pubkey(String)
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filter(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
case .pubkey(let pk):
|
||||
return ev.pubkey == pk
|
||||
case .hashtag(let ht):
|
||||
if contacts.is_friend(ev.pubkey) {
|
||||
return false
|
||||
}
|
||||
return ev.references(id: ht, key: "t")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HomeModel {
|
||||
class HomeModel: ObservableObject {
|
||||
// Don't trigger a user notification for events older than a certain age
|
||||
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
|
||||
|
||||
@@ -70,7 +36,6 @@ class HomeModel {
|
||||
var done_init: Bool = false
|
||||
var incoming_dms: [NostrEvent] = []
|
||||
let dm_debouncer = Debouncer(interval: 0.5)
|
||||
let resub_debouncer = Debouncer(interval: 3.0)
|
||||
var should_debounce_dms = true
|
||||
|
||||
let home_subid = UUID().description
|
||||
@@ -84,10 +49,9 @@ class HomeModel {
|
||||
|
||||
var signal = SignalModel()
|
||||
|
||||
var notifications = NotificationsModel()
|
||||
var notification_status = NotificationStatusModel()
|
||||
var events: EventHolder = EventHolder()
|
||||
var zap_button: ZapButtonModel = ZapButtonModel()
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var events: EventHolder = EventHolder()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
@@ -125,31 +89,6 @@ class HomeModel {
|
||||
}
|
||||
}
|
||||
|
||||
func resubscribe(_ resubbing: Resubscribe) {
|
||||
if self.should_debounce_dms {
|
||||
// don't resub on initial load
|
||||
return
|
||||
}
|
||||
|
||||
print("hit resub debouncer")
|
||||
|
||||
resub_debouncer.debounce {
|
||||
print("resub")
|
||||
self.unsubscribe_to_home_filters()
|
||||
|
||||
switch resubbing {
|
||||
case .following:
|
||||
break
|
||||
case .unfollowing(let r):
|
||||
if let filter = HomeResubFilter(from: r) {
|
||||
self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) }
|
||||
}
|
||||
}
|
||||
|
||||
self.subscribe_to_home_filters()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -166,7 +105,6 @@ class HomeModel {
|
||||
|
||||
switch kind {
|
||||
case .chat: fallthrough
|
||||
case .longform: fallthrough
|
||||
case .text:
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
case .contacts:
|
||||
@@ -194,11 +132,11 @@ class HomeModel {
|
||||
case .nwc_request:
|
||||
break
|
||||
case .nwc_response:
|
||||
handle_nwc_response(ev, relay: relay_id)
|
||||
handle_nwc_response(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
|
||||
func handle_nwc_response(_ ev: NostrEvent) {
|
||||
Task { @MainActor in
|
||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||
@@ -210,54 +148,78 @@ class HomeModel {
|
||||
// since command results are not returned for ephemeral events,
|
||||
// remove the request from the postbox which is likely failing over and over
|
||||
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
|
||||
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
|
||||
print("nwc: got response, removed \(resp.req_id) from the postbox")
|
||||
} else {
|
||||
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
|
||||
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove")
|
||||
}
|
||||
|
||||
guard resp.response.error == nil else {
|
||||
print("nwc error: \(resp.response)")
|
||||
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
if resp.response.error == nil {
|
||||
nwc_success(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
return
|
||||
}
|
||||
|
||||
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
|
||||
nwc_success(state: self.damus_state, resp: resp)
|
||||
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.notifications.insert_zap(.zap(zap)) {
|
||||
return
|
||||
}
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert_zap(.zap(zap)) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.damus_state.settings.zap_vibration {
|
||||
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||
if damus_state.settings.zap_vibration {
|
||||
// Generate zap vibration
|
||||
zap_vibrate(zap_amount: zap.invoice.amount)
|
||||
}
|
||||
|
||||
if self.damus_state.settings.zap_notification {
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
switch zap.target {
|
||||
case .profile(let profile_id):
|
||||
create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id)
|
||||
case .note(let note_target):
|
||||
create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id)
|
||||
}
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handle_zap_event(_ ev: NostrEvent) {
|
||||
// These are zap notifications
|
||||
guard let ptag = event_tag(ev, name: "p") else {
|
||||
return
|
||||
}
|
||||
|
||||
let our_keypair = damus_state.keypair
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.notification_status.new_events = new_bits
|
||||
DispatchQueue.main.async {
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -432,31 +394,37 @@ class HomeModel {
|
||||
|
||||
/// Send the initial filters, just our contact list mostly
|
||||
func send_initial_filters(relay_id: String) {
|
||||
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
|
||||
let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
|
||||
pool.send(.subscribe(subscription), to: [relay_id])
|
||||
var filter = NostrFilter.filter_contacts
|
||||
filter.authors = [self.damus_state.pubkey]
|
||||
filter.limit = 1
|
||||
|
||||
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
|
||||
}
|
||||
|
||||
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
|
||||
func send_home_filters(relay_id: String?) {
|
||||
// TODO: since times should be based on events from a specific relay
|
||||
// perhaps we could mark this in the relay pool somehow
|
||||
|
||||
let friends = get_friends()
|
||||
var friends = damus_state.contacts.get_friend_list()
|
||||
friends.append(damus_state.pubkey)
|
||||
|
||||
var contacts_filter = NostrFilter(kinds: [.metadata])
|
||||
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
contacts_filter.authors = friends
|
||||
|
||||
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
||||
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter(kinds: [.list])
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
|
||||
our_blocklist_filter.parameter = ["mute"]
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var dms_filter = NostrFilter(kinds: [.dm])
|
||||
var dms_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.dm.rawValue,
|
||||
])
|
||||
|
||||
var our_dms_filter = NostrFilter(kinds: [.dm])
|
||||
var our_dms_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.dm.rawValue,
|
||||
])
|
||||
|
||||
// friends only?...
|
||||
//dms_filter.authors = friends
|
||||
@@ -464,83 +432,58 @@ class HomeModel {
|
||||
dms_filter.pubkeys = [ damus_state.pubkey ]
|
||||
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||
|
||||
var notifications_filter_kinds: [NostrKind] = [
|
||||
.text,
|
||||
.boost,
|
||||
.zap,
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
notifications_filter_kinds.append(.like)
|
||||
}
|
||||
var notifications_filter = NostrFilter(kinds: notifications_filter_kinds)
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 500
|
||||
|
||||
var notifications_filters = [notifications_filter]
|
||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
||||
var dms_filters = [dms_filter, our_dms_filter]
|
||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||
|
||||
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
||||
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
||||
|
||||
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||
|
||||
subscribe_to_home_filters(relay_id: relay_id)
|
||||
|
||||
let relay_ids = relay_id.map { [$0] }
|
||||
|
||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids)
|
||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids)
|
||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
|
||||
}
|
||||
|
||||
func get_last_of_kind(relay_id: String?) -> [Int: NostrEvent] {
|
||||
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||
}
|
||||
|
||||
func unsubscribe_to_home_filters() {
|
||||
pool.send(.unsubscribe(home_subid))
|
||||
}
|
||||
|
||||
func get_friends() -> [String] {
|
||||
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) {
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text, .longform, .boost
|
||||
var home_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.boost.rawValue
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(.like)
|
||||
home_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
|
||||
let friends = fs ?? get_friends()
|
||||
var home_filter = NostrFilter(kinds: home_filter_kinds)
|
||||
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
|
||||
// include our pubkey as well even if we're not technically a friend
|
||||
home_filter.authors = friends
|
||||
home_filter.limit = 500
|
||||
|
||||
var home_filters = [home_filter]
|
||||
|
||||
let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
|
||||
if followed_hashtags.count != 0 {
|
||||
var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags)
|
||||
hashtag_filter.limit = 100
|
||||
home_filters.append(hashtag_filter)
|
||||
var notifications_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
notifications_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 500
|
||||
|
||||
let relay_ids = relay_id.map { [$0] }
|
||||
home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
|
||||
let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
|
||||
var home_filters = [home_filter]
|
||||
var notifications_filters = [notifications_filter]
|
||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
||||
var dms_filters = [dms_filter, our_dms_filter]
|
||||
|
||||
pool.send(.subscribe(sub), to: relay_ids)
|
||||
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||
|
||||
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
|
||||
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
||||
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
||||
|
||||
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||
|
||||
if let relay_id {
|
||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
|
||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
|
||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
|
||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id])
|
||||
} else {
|
||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
|
||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
|
||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
|
||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handle_list_event(_ ev: NostrEvent) {
|
||||
// we only care about our lists
|
||||
guard ev.pubkey == damus_state.pubkey else {
|
||||
@@ -609,8 +552,8 @@ class HomeModel {
|
||||
|
||||
@discardableResult
|
||||
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
|
||||
if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||
self.notification_status.new_events = new_bits
|
||||
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||
new_events = new_bits
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -642,7 +585,7 @@ class HomeModel {
|
||||
}
|
||||
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
notification_status.new_events = notifs
|
||||
self.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")
|
||||
@@ -660,7 +603,7 @@ class HomeModel {
|
||||
|
||||
if !should_debounce_dms {
|
||||
self.incoming_dms.append(ev)
|
||||
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
@@ -670,7 +613,7 @@ class HomeModel {
|
||||
incoming_dms.append(ev)
|
||||
|
||||
dm_debouncer.debounce { [self] in
|
||||
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
@@ -697,40 +640,35 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
|
||||
contacts.add_friend_contact(ev)
|
||||
}
|
||||
|
||||
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let contacts = state.contacts
|
||||
var new_refs = Set<ReferencedId>()
|
||||
func load_our_contacts(contacts: Contacts, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
var new_pks = Set<String>()
|
||||
// our contacts
|
||||
for tag in ev.tags {
|
||||
guard let ref = tag_to_refid(tag) else { continue }
|
||||
new_refs.insert(ref)
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
new_pks.insert(tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
var old_refs = Set<ReferencedId>()
|
||||
var old_pks = Set<String>()
|
||||
// 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)
|
||||
if tag.count >= 2 && tag[0] == "p" {
|
||||
old_pks.insert(tag[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
let diff = new_pks.symmetricDifference(old_pks)
|
||||
for pk in diff {
|
||||
if new_pks.contains(pk) {
|
||||
notify(.followed, pk)
|
||||
contacts.add_friend_pubkey(pk)
|
||||
} else {
|
||||
notify(.unfollowed, ref)
|
||||
if ref.key == "p" {
|
||||
contacts.remove_friend(ref.ref_id)
|
||||
}
|
||||
notify(.unfollowed, pk)
|
||||
contacts.remove_friend(pk)
|
||||
}
|
||||
}
|
||||
|
||||
state.user_search_cache.updateOwnContactsPetnames(id: contacts.our_pubkey, oldEvent: m_old_ev, newEvent: ev)
|
||||
}
|
||||
|
||||
|
||||
@@ -791,11 +729,9 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
}
|
||||
|
||||
var old_nip05: String? = nil
|
||||
let mprof = profiles.lookup_with_timestamp(id: ev.pubkey)
|
||||
|
||||
if let mprof {
|
||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
old_nip05 = mprof.profile.nip05
|
||||
if mprof.event.created_at > ev.created_at {
|
||||
if mprof.timestamp > ev.created_at {
|
||||
// skip if we already have an newer profile
|
||||
return
|
||||
}
|
||||
@@ -812,8 +748,8 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
print("validated nip05 for '\(nip05)'")
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
profiles.set_validated(ev.pubkey, nip05: validated)
|
||||
DispatchQueue.main.async {
|
||||
profiles.validated[ev.pubkey] = validated
|
||||
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
@@ -840,14 +776,11 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
}
|
||||
|
||||
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 {
|
||||
case .unknown:
|
||||
Task.detached(priority: .medium) {
|
||||
Task {
|
||||
let result = validate_event(ev: ev)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -868,11 +801,11 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: (() -> 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)
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -880,7 +813,7 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
completion?(profile)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -905,7 +838,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
let m_old_ev = state.contacts.event
|
||||
state.contacts.event = ev
|
||||
|
||||
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_contacts(contacts: state.contacts, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
}
|
||||
|
||||
@@ -944,7 +877,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
if new.contains(d) {
|
||||
if let url = RelayURL(d) {
|
||||
let descriptor = RelayDescriptor(url: url, info: decoded[d] ?? .rw)
|
||||
add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
|
||||
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
} else {
|
||||
state.pool.remove_relay(d)
|
||||
@@ -953,17 +886,16 @@ 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, ())
|
||||
}
|
||||
}
|
||||
|
||||
func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
||||
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) {
|
||||
try? pool.add_relay(descriptor)
|
||||
let url = descriptor.url
|
||||
|
||||
let relay_id = url.id
|
||||
guard model_cache.model(withURL: url) == nil else {
|
||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -972,13 +904,8 @@ func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, po
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
let model = RelayModel(url, metadata: meta)
|
||||
model_cache.insert(model: model)
|
||||
|
||||
if logging_enabled {
|
||||
pool.setLog(model.log, for: relay_id)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
metadatas.insert(relay_id: relay_id, metadata: meta)
|
||||
|
||||
// if this is the first time adding filters, we should filter non-paid relays
|
||||
if new_relay_filters && !meta.is_paid {
|
||||
@@ -1176,12 +1103,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 src = zap.private_request ?? zap.request.ev
|
||||
let anon = event_is_anonymous(ev: src)
|
||||
let pk = anon ? "anon" : src.pubkey
|
||||
let profile = profiles.lookup(id: pk)
|
||||
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)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
|
||||
|
||||
if src.content.isEmpty {
|
||||
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||||
@@ -1192,28 +1120,7 @@ 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) {
|
||||
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()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else {
|
||||
print("Local notification scheduled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
@@ -1234,27 +1141,6 @@ 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)
|
||||
|
||||
// special case for longform events
|
||||
if ev.known_kind == .longform {
|
||||
let longform = LongformEvent(event: ev)
|
||||
return longform.title ?? longform.summary ?? "Longform Event"
|
||||
}
|
||||
|
||||
switch artifacts {
|
||||
case .parts:
|
||||
// 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))
|
||||
|
||||
case .separated(let artifacts):
|
||||
return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len))
|
||||
}
|
||||
}
|
||||
|
||||
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
guard let type = ev.known_kind else {
|
||||
@@ -1278,22 +1164,21 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
}
|
||||
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
let blocks = ev.blocks(damus_state.keypair.privkey).blocks
|
||||
let blocks = ev.blocks(damus_state.keypair.privkey)
|
||||
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)
|
||||
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
|
||||
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
|
||||
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
|
||||
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)
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
|
||||
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,
|
||||
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 notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
|
||||
@@ -1314,12 +1199,12 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||
identifier = "myBoostNotification"
|
||||
case .like:
|
||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
|
||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content)
|
||||
identifier = "myLikeNotification"
|
||||
case .dm:
|
||||
title = displayName
|
||||
identifier = "myDMNotification"
|
||||
case .zap, .profile_zap:
|
||||
case .zap:
|
||||
// not handled here
|
||||
break
|
||||
}
|
||||
@@ -1341,104 +1226,3 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ProcessZapResult {
|
||||
case already_processed(Zap)
|
||||
case done(Zap)
|
||||
case failed
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ptags.first?.id
|
||||
}
|
||||
|
||||
// we have an e-tag
|
||||
|
||||
// ensure that there is only 1 etag to stop fake note zap attacks
|
||||
guard etags.count == 1 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we can't trust the p tag on note zaps because they can be faked
|
||||
return events.lookup(etag.id)?.pubkey
|
||||
}
|
||||
|
||||
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 {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
// just return the zap if we already have it
|
||||
if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap {
|
||||
completion(.already_processed(z))
|
||||
return
|
||||
}
|
||||
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
completion(.done(zap))
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.profiles.zappers[ptag] = zapper
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
}
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
completion(.done(zap))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> 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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
damus_state.add_zap(zap: .zap(zap))
|
||||
|
||||
return zap
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
enum MentionType {
|
||||
case pubkey
|
||||
case event
|
||||
|
||||
|
||||
var ref: String {
|
||||
switch self {
|
||||
case .pubkey:
|
||||
@@ -25,14 +25,6 @@ struct Mention: Equatable {
|
||||
let index: Int?
|
||||
let type: MentionType
|
||||
let ref: ReferencedId
|
||||
|
||||
static func note(_ id: String) -> Mention {
|
||||
return Mention(index: nil, type: .event, ref: .e(id))
|
||||
}
|
||||
|
||||
static func pubkey(_ pubkey: String) -> Mention {
|
||||
return Mention(index: nil, type: .pubkey, ref: .p(pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
typealias Invoice = LightningInvoice<Amount>
|
||||
@@ -122,12 +114,12 @@ enum Block: Equatable {
|
||||
|
||||
return mention.type == .event
|
||||
}
|
||||
|
||||
var is_mention: Mention? {
|
||||
if case .mention(let m) = self {
|
||||
return m
|
||||
|
||||
var is_mention: Bool {
|
||||
if case .mention = self {
|
||||
return true
|
||||
}
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,15 +150,10 @@ func render_blocks(blocks: [Block]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
struct Blocks {
|
||||
let words: Int
|
||||
let blocks: [Block]
|
||||
}
|
||||
|
||||
func parse_mentions(content: String, tags: [[String]]) -> Blocks {
|
||||
func parse_mentions(content: String, tags: [[String]]) -> [Block] {
|
||||
var out: [Block] = []
|
||||
|
||||
var bs = note_blocks()
|
||||
var bs = blocks()
|
||||
bs.num_blocks = 0;
|
||||
|
||||
blocks_init(&bs)
|
||||
@@ -187,10 +174,9 @@ func parse_mentions(content: String, tags: [[String]]) -> Blocks {
|
||||
i += 1
|
||||
}
|
||||
|
||||
let words = Int(bs.words)
|
||||
blocks_free(&bs)
|
||||
|
||||
return Blocks(words: words, blocks: out)
|
||||
return out
|
||||
}
|
||||
|
||||
func strblock_to_string(_ s: str_block_t) -> String? {
|
||||
@@ -340,13 +326,7 @@ func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
|
||||
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))
|
||||
@@ -408,6 +388,65 @@ func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
return .mention(Mention(index: ind, type: mention_type, ref: ref))
|
||||
}
|
||||
|
||||
func parse_while(_ p: Parser, match: (Character) -> Bool) -> String? {
|
||||
var i: Int = 0
|
||||
let sub = substring(p.str, start: p.pos, end: p.str.count)
|
||||
let start = p.pos
|
||||
for c in sub {
|
||||
if match(c) {
|
||||
p.pos += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
let end = start + i
|
||||
if start == end {
|
||||
return nil
|
||||
}
|
||||
return String(substring(p.str, start: start, end: end))
|
||||
}
|
||||
|
||||
func is_hashtag_char(_ c: Character) -> Bool {
|
||||
return c.isLetter || c.isNumber
|
||||
}
|
||||
|
||||
func prev_char(_ p: Parser, n: Int) -> Character? {
|
||||
if p.pos - n < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let ind = p.str.index(p.str.startIndex, offsetBy: p.pos - n)
|
||||
return p.str[ind]
|
||||
}
|
||||
|
||||
func is_punctuation(_ c: Character) -> Bool {
|
||||
return c.isWhitespace || c.isPunctuation
|
||||
}
|
||||
|
||||
func parse_hashtag(_ p: Parser) -> String? {
|
||||
let start = p.pos
|
||||
|
||||
if !parse_char(p, "#") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let prev = prev_char(p, n: 2) {
|
||||
// we don't allow adjacent hashtags
|
||||
if !is_punctuation(prev) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard let str = parse_while(p, match: is_hashtag_char) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
||||
var i: Int = 0
|
||||
for tag in tags {
|
||||
@@ -438,31 +477,44 @@ func parse_mention_type(_ c: String) -> MentionType? {
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]], silent_mentions: Bool) -> PostTags {
|
||||
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
|
||||
var new_tags = tags
|
||||
|
||||
var blocks: [Block] = []
|
||||
|
||||
for post_block in post_blocks {
|
||||
switch post_block {
|
||||
case .mention(let mention):
|
||||
let mention_type = mention.type
|
||||
|
||||
if silent_mentions || mention_type == .event {
|
||||
case .ref(let ref):
|
||||
guard let mention_type = parse_mention_type(ref.key) else {
|
||||
continue
|
||||
}
|
||||
|
||||
new_tags.append(refid_to_tag(mention.ref))
|
||||
|
||||
if silent_mentions || mention_type == .event {
|
||||
let mention = Mention(index: nil, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
|
||||
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
} else {
|
||||
let ind = new_tags.count
|
||||
new_tags.append(refid_to_tag(ref))
|
||||
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
}
|
||||
case .hashtag(let hashtag):
|
||||
new_tags.append(["t", hashtag.lowercased()])
|
||||
case .text: break
|
||||
case .invoice: break
|
||||
case .relay: break
|
||||
case .url(let url):
|
||||
new_tags.append(["r", url.absoluteString])
|
||||
break
|
||||
blocks.append(.hashtag(hashtag))
|
||||
case .text(let txt):
|
||||
blocks.append(Block.text(txt))
|
||||
}
|
||||
}
|
||||
|
||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||
return PostTags(blocks: blocks, tags: new_tags)
|
||||
}
|
||||
|
||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// NotificationStatusModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class NotificationStatusModel: ObservableObject {
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
}
|
||||
@@ -21,12 +21,12 @@ class ZapGroup {
|
||||
}
|
||||
|
||||
func zap_requests() -> [NostrEvent] {
|
||||
zaps.map { z in z.request.ev }
|
||||
zaps.map { z in z.request }
|
||||
}
|
||||
|
||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||
for zap in zaps {
|
||||
if !isIncluded(zap.request.ev) {
|
||||
if !isIncluded(zap.request) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class ZapGroup {
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
|
||||
let new_zaps = zaps.filter { isIncluded($0.request.ev) }
|
||||
let new_zaps = zaps.filter { isIncluded($0.request) }
|
||||
guard new_zaps.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class ZapGroup {
|
||||
|
||||
msat_total += zap.amount
|
||||
|
||||
if !zappers.contains(zap.request.ev.pubkey) {
|
||||
zappers.insert(zap.request.ev.pubkey)
|
||||
if !zappers.contains(zap.request.pubkey) {
|
||||
zappers.insert(zap.request.pubkey)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
for zap in incoming_zaps {
|
||||
pks.insert(zap.request.ev.pubkey)
|
||||
pks.insert(zap.request.pubkey)
|
||||
}
|
||||
|
||||
return Array(pks)
|
||||
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
changed = changed || incoming_events.count != count
|
||||
|
||||
count = profile_zaps.zaps.count
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
|
||||
changed = changed || profile_zaps.zaps.count != count
|
||||
|
||||
for el in reactions {
|
||||
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
for el in zaps {
|
||||
count = el.value.zaps.count
|
||||
el.value.zaps = el.value.zaps.filter {
|
||||
isIncluded($0.request.ev)
|
||||
isIncluded($0.request)
|
||||
}
|
||||
changed = changed || el.value.zaps.count != count
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ struct NostrPost {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: parse nostr:{e,p}:pubkey uris as well
|
||||
func parse_post_mention_type(_ p: Parser) -> MentionType? {
|
||||
if parse_char(p, "@") {
|
||||
return .pubkey
|
||||
@@ -64,7 +65,6 @@ func parse_post_mention(_ p: Parser, mention_type: MentionType) -> ReferencedId?
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with our C parser
|
||||
func parse_post_bech32_mention(_ p: Parser) -> ReferencedId? {
|
||||
let start = p.pos
|
||||
if parse_str(p, "note") {
|
||||
@@ -110,7 +110,32 @@ func parse_post_bech32_mention(_ p: Parser) -> ReferencedId? {
|
||||
}
|
||||
|
||||
/// Return a list of tags
|
||||
func parse_post_blocks(content: String) -> [Block] {
|
||||
return parse_mentions(content: content, tags: []).blocks
|
||||
func parse_post_blocks(content: String) -> [PostBlock] {
|
||||
let p = Parser(pos: 0, str: content)
|
||||
var blocks: [PostBlock] = []
|
||||
var starting_from: Int = 0
|
||||
|
||||
if content.count == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
while p.pos < content.count {
|
||||
let pre_mention = p.pos
|
||||
if let reference = parse_post_reference(p) {
|
||||
blocks.append(parse_post_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||
blocks.append(.ref(reference))
|
||||
starting_from = p.pos
|
||||
} else if let hashtag = parse_hashtag(p) {
|
||||
blocks.append(parse_post_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||
blocks.append(.hashtag(hashtag))
|
||||
starting_from = p.pos
|
||||
} else {
|
||||
p.pos += 1
|
||||
}
|
||||
}
|
||||
|
||||
blocks.append(parse_post_textblock(str: content, from: starting_from, to: content.count))
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,16 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform])
|
||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||
var text_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
])
|
||||
|
||||
var profile_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.contacts.rawValue,
|
||||
NostrKind.metadata.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
|
||||
@@ -7,27 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ReportType: String, CustomStringConvertible, CaseIterable {
|
||||
case spam
|
||||
case nudity
|
||||
case profanity
|
||||
enum ReportType: String {
|
||||
case explicit
|
||||
case illegal
|
||||
case spam
|
||||
case impersonation
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .spam:
|
||||
return NSLocalizedString("Spam", comment: "Description of report type for spam.")
|
||||
case .nudity:
|
||||
return NSLocalizedString("Nudity", comment: "Description of report type for nudity.")
|
||||
case .profanity:
|
||||
return NSLocalizedString("Profanity", comment: "Description of report type for profanity.")
|
||||
case .illegal:
|
||||
return NSLocalizedString("Illegal Content", comment: "Description of report type for illegal content.")
|
||||
case .impersonation:
|
||||
return NSLocalizedString("Impersonation", comment: "Description of report type for impersonation.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReportNoteTarget {
|
||||
@@ -47,12 +31,16 @@ struct Report {
|
||||
}
|
||||
|
||||
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
|
||||
var tags: [[String]]
|
||||
switch target {
|
||||
case .user(let pubkey):
|
||||
return [["p", pubkey, type.rawValue]]
|
||||
tags = [["p", pubkey]]
|
||||
case .note(let notet):
|
||||
return [["e", notet.note_id, type.rawValue], ["p", notet.pubkey]]
|
||||
tags = [["e", notet.note_id], ["p", notet.pubkey]]
|
||||
}
|
||||
|
||||
tags.append(["report", type.rawValue])
|
||||
return tags
|
||||
}
|
||||
|
||||
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
|
||||
|
||||
@@ -18,7 +18,6 @@ class SearchHomeModel: ObservableObject {
|
||||
let base_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 250
|
||||
//let multiple_events_per_pubkey: Bool = false
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
@@ -28,7 +27,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_base_filter() -> NostrFilter {
|
||||
var filter = NostrFilter(kinds: [.text, .chat])
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
|
||||
filter.limit = self.limit
|
||||
filter.until = Int64(Date.now.timeIntervalSince1970)
|
||||
return filter
|
||||
@@ -61,7 +60,7 @@ class SearchHomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
|
||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||
if seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
seen_pubkey.insert(ev.pubkey)
|
||||
@@ -91,6 +90,20 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for pk in event_pubkeys {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
@@ -101,7 +114,17 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
|
||||
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for pk in pks {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
|
||||
@@ -109,11 +132,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
|
||||
|
||||
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) {
|
||||
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
|
||||
pubkeys.insert(bev.pubkey)
|
||||
}
|
||||
|
||||
if !profiles.has_fresh_profile(id: ev.pubkey) {
|
||||
if profiles.lookup(id: ev.pubkey) == nil {
|
||||
pubkeys.insert(ev.pubkey)
|
||||
}
|
||||
}
|
||||
@@ -127,16 +150,16 @@ enum PubkeysToLoad {
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
|
||||
filter.authors = authors
|
||||
|
||||
guard !authors.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
print("loading \(authors.count) profiles from \(relay_id)")
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata],
|
||||
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 {
|
||||
|
||||
@@ -34,7 +34,7 @@ class SearchModel: ObservableObject {
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [.text, .like, .longform]
|
||||
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
|
||||
@@ -10,21 +10,15 @@ import Foundation
|
||||
/// manages the lifetime of a thread
|
||||
class ThreadModel: ObservableObject {
|
||||
@Published var event: NostrEvent
|
||||
let original_event: NostrEvent
|
||||
var event_map: Set<NostrEvent>
|
||||
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
self.original_event = event
|
||||
add_event(event)
|
||||
}
|
||||
|
||||
var is_original: Bool {
|
||||
return original_event.id == event.id
|
||||
}
|
||||
|
||||
let damus_state: DamusState
|
||||
|
||||
let profiles_subid = UUID().description
|
||||
@@ -62,16 +56,16 @@ class ThreadModel: ObservableObject {
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [.text]
|
||||
ref_events.kinds = [NostrKind.text.rawValue]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
|
||||
var kinds: [NostrKind] = [.zap, .text, .boost]
|
||||
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
kinds.append(.like)
|
||||
kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
meta_events.kinds = kinds
|
||||
|
||||
@@ -107,10 +101,6 @@ class ThreadModel: ObservableObject {
|
||||
|
||||
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 {
|
||||
process_zap_event(damus_state: damus_state, ev: ev) { zap in
|
||||
|
||||
}
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev)
|
||||
}
|
||||
@@ -126,10 +116,3 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func get_top_zap(events: EventCache, evid: String) -> Zapping? {
|
||||
return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in
|
||||
!zap.request.marked_hidden
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// Trie.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/26/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Tree data structure of all the substring permutations of a collection of strings optimized for searching for values of type V.
|
||||
///
|
||||
/// Each node in the tree can have child nodes.
|
||||
/// Each node represents a single character in substrings, and each of its child nodes represent the subsequent character in those substrings.
|
||||
///
|
||||
/// A node that has no children mean that there are no substrings with any additional characters beyond the branch of letters leading up to that node.
|
||||
///
|
||||
/// A node that has values mean that there are strings that end in the character represented by the node and contain the substring represented by the branch of letters leading up to that node.
|
||||
///
|
||||
/// https://en.wikipedia.org/wiki/Trie
|
||||
class Trie<V: Hashable> {
|
||||
private var children: [Character : Trie] = [:]
|
||||
|
||||
/// Separate exact matches from strict substrings so that exact matches appear first in returned results.
|
||||
private var exactMatchValues = Set<V>()
|
||||
private var substringMatchValues = Set<V>()
|
||||
|
||||
private var parent: Trie? = nil
|
||||
}
|
||||
|
||||
extension Trie {
|
||||
var hasChildren: Bool {
|
||||
return !self.children.isEmpty
|
||||
}
|
||||
|
||||
var hasValues: Bool {
|
||||
return !self.exactMatchValues.isEmpty || !self.substringMatchValues.isEmpty
|
||||
}
|
||||
|
||||
/// Finds the branch that matches the specified key and returns the values from all of its descendant nodes.
|
||||
func find(key: String) -> [V] {
|
||||
var currentNode = self
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for char in key {
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Perform breadth-first search from matching branch and collect values from all descendants.
|
||||
var substringMatches = Set<V>(currentNode.substringMatchValues)
|
||||
var queue = Array(currentNode.children.values)
|
||||
|
||||
while !queue.isEmpty {
|
||||
let node = queue.removeFirst()
|
||||
substringMatches.formUnion(node.exactMatchValues)
|
||||
substringMatches.formUnion(node.substringMatchValues)
|
||||
queue.append(contentsOf: node.children.values)
|
||||
}
|
||||
|
||||
// Prioritize exact matches to be returned first, and then remove exact matches from the set of partial substring matches that are appended afterward.
|
||||
return Array(currentNode.exactMatchValues) + (substringMatches.subtracting(currentNode.exactMatchValues))
|
||||
}
|
||||
|
||||
/// Inserts value of type V into this trie for the specified key. This function stores all substring endings of the key, not only the key itself.
|
||||
/// Runtime performance is O(n^2) and storage cost is O(n), where n is the number of characters in the key.
|
||||
func insert(key: String, value: V) {
|
||||
// Create root branches for each character of the key to enable substring searches instead of only just prefix searches.
|
||||
// Hence the nested loop.
|
||||
for i in 0..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for char in key[key.index(key.startIndex, offsetBy: i)...] {
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
let child = Trie()
|
||||
child.parent = currentNode
|
||||
currentNode.children[char] = child
|
||||
currentNode = child
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
currentNode.exactMatchValues.insert(value)
|
||||
} else {
|
||||
currentNode.substringMatchValues.insert(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes value of type V from this trie for the specified key.
|
||||
func remove(key: String, value: V) {
|
||||
for i in 0..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
var foundLeafNode = true
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for j in i..<key.count {
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
foundLeafNode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundLeafNode {
|
||||
currentNode.exactMatchValues.remove(value)
|
||||
currentNode.substringMatchValues.remove(value)
|
||||
|
||||
// Clean up the tree if this leaf node no longer holds values or children.
|
||||
for j in (i..<key.count).reversed() {
|
||||
if let parent = currentNode.parent, !currentNode.hasValues && !currentNode.hasChildren {
|
||||
currentNode = parent
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
currentNode.children.removeValue(forKey: char)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//
|
||||
// UserSearchCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/27/23.
|
||||
//
|
||||
|
||||
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] {
|
||||
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) {
|
||||
// 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 {
|
||||
if let oldName = oldProfile.name, newProfile.name?.caseInsensitiveCompare(oldName) != .orderedSame {
|
||||
trie.remove(key: oldName.lowercased(), value: id)
|
||||
}
|
||||
if let oldDisplayName = oldProfile.display_name, newProfile.display_name?.caseInsensitiveCompare(oldDisplayName) != .orderedSame {
|
||||
trie.remove(key: oldDisplayName.lowercased(), value: id)
|
||||
}
|
||||
if let oldNip05 = oldProfile.nip05, newProfile.nip05?.caseInsensitiveCompare(oldNip05) != .orderedSame {
|
||||
trie.remove(key: oldNip05.lowercased(), value: id)
|
||||
}
|
||||
}
|
||||
|
||||
addProfile(id: id, profiles: profiles, profile: newProfile)
|
||||
}
|
||||
|
||||
/// Adds a profile to the user search cache.
|
||||
private func addProfile(id: String, profiles: Profiles, profile: Profile) {
|
||||
// Searchable by name.
|
||||
if let name = profile.name {
|
||||
trie.insert(key: name.lowercased(), value: id)
|
||||
}
|
||||
|
||||
// Searchable by display name.
|
||||
if let displayName = profile.display_name {
|
||||
trie.insert(key: displayName.lowercased(), value: id)
|
||||
}
|
||||
|
||||
// Searchable by NIP-05 identifier.
|
||||
if let nip05 = profiles.is_validated(id) {
|
||||
trie.insert(key: "\(nip05.username.lowercased())@\(nip05.host.lowercased())", value: id)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let petname = tag[3]
|
||||
|
||||
petnames[pubkey] = petname
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let oldPetname = tag[3]
|
||||
|
||||
if let newPetname = petnames[pubkey] {
|
||||
if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame {
|
||||
petnames.removeValue(forKey: pubkey)
|
||||
} else {
|
||||
trie.remove(key: oldPetname, value: pubkey)
|
||||
}
|
||||
} else {
|
||||
trie.remove(key: oldPetname, value: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new petnames to the user search cache.
|
||||
for (pubkey, petname) in petnames {
|
||||
trie.insert(key: petname, value: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,6 @@ let fallback_zap_amount = 1000
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
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
|
||||
@@ -80,7 +77,6 @@ let fallback_zap_amount = 1000
|
||||
class UserSettingsStore: ObservableObject {
|
||||
static var pubkey: String? = nil
|
||||
static var shared: UserSettingsStore? = nil
|
||||
static var bool_options = Set<String>()
|
||||
|
||||
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
|
||||
var default_wallet: Wallet
|
||||
@@ -88,7 +84,7 @@ class UserSettingsStore: ObservableObject {
|
||||
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
|
||||
var default_media_uploader: MediaUploader
|
||||
|
||||
@Setting(key: "show_wallet_selector", default_value: false)
|
||||
@Setting(key: "show_wallet_selector", default_value: true)
|
||||
var show_wallet_selector: Bool
|
||||
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
@@ -130,10 +126,6 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "truncate_timeline_text", default_value: false)
|
||||
var truncate_timeline_text: Bool
|
||||
|
||||
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
|
||||
@Setting(key: "nozaps", default_value: true)
|
||||
var nozaps: Bool
|
||||
|
||||
@Setting(key: "truncate_mention_text", default_value: true)
|
||||
var truncate_mention_text: Bool
|
||||
|
||||
@@ -145,21 +137,12 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
|
||||
@Setting(key: "onlyzaps_mode", default_value: false)
|
||||
var onlyzaps_mode: Bool
|
||||
|
||||
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
||||
var disable_animation: Bool
|
||||
|
||||
@Setting(key: "donation_percent", default_value: 0)
|
||||
var donation_percent: Int
|
||||
|
||||
@Setting(key: "developer_mode", default_value: false)
|
||||
var developer_mode: Bool
|
||||
|
||||
// Helper for inverse of disable_animation.
|
||||
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
||||
|
||||
@@ -14,17 +14,14 @@ enum WalletConnectState {
|
||||
}
|
||||
|
||||
class WalletModel: ObservableObject {
|
||||
var settings: UserSettingsStore
|
||||
let settings: UserSettingsStore?
|
||||
private(set) var previous_state: WalletConnectState
|
||||
var inital_percent: Int
|
||||
|
||||
@Published private(set) var connect_state: WalletConnectState
|
||||
|
||||
init(state: WalletConnectState, settings: UserSettingsStore) {
|
||||
self.connect_state = state
|
||||
init() {
|
||||
self.connect_state = .none
|
||||
self.previous_state = .none
|
||||
self.settings = settings
|
||||
self.inital_percent = settings.donation_percent
|
||||
self.settings = nil
|
||||
}
|
||||
|
||||
init(settings: UserSettingsStore) {
|
||||
@@ -37,7 +34,6 @@ class WalletModel: ObservableObject {
|
||||
self.previous_state = .none
|
||||
self.connect_state = .none
|
||||
}
|
||||
self.inital_percent = settings.donation_percent
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
@@ -46,7 +42,7 @@ class WalletModel: ObservableObject {
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
self.settings.nostr_wallet_connect = nil
|
||||
self.settings?.nostr_wallet_connect = nil
|
||||
self.connect_state = .none
|
||||
self.previous_state = .none
|
||||
}
|
||||
@@ -56,7 +52,7 @@ class WalletModel: ObservableObject {
|
||||
}
|
||||
|
||||
func connect(_ nwc: WalletConnectURL) {
|
||||
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||
self.settings?.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||
notify(.attached_wallet, nwc)
|
||||
self.connect_state = .existing(nwc)
|
||||
self.previous_state = .existing(nwc)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ZapButtonModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapButtonModel: ObservableObject {
|
||||
var invoice: String? = nil
|
||||
@Published var zapping: String = ""
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// CustomizeZapModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class CustomizeZapModel: ObservableObject {
|
||||
@Published var comment: String = ""
|
||||
@Published var custom_amount: String = ""
|
||||
@Published var custom_amount_sats: Int? = nil
|
||||
@Published var zap_type: ZapType = .pub
|
||||
@Published var invoice: String = ""
|
||||
@Published var error: String? = nil
|
||||
@Published var zapping: Bool = false
|
||||
@Published var show_zap_types: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func set_defaults(settings: UserSettingsStore) {
|
||||
self.zap_type = settings.default_zap_type
|
||||
self.custom_amount = String(settings.default_zap_amount)
|
||||
self.custom_amount_sats = settings.default_zap_amount
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter(kinds: [.zap])
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
@@ -53,13 +53,18 @@ class ZapsModel: ObservableObject {
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request }
|
||||
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
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
state.events.store_zap(zap: zap)
|
||||
return
|
||||
}
|
||||
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
|
||||
<attribute name="about" optional="YES" attributeType="String"/>
|
||||
<attribute name="banner" optional="YES" attributeType="String"/>
|
||||
<attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="display_name" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lud06" optional="YES" attributeType="String"/>
|
||||
<attribute name="lud16" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="nip05" optional="YES" attributeType="String"/>
|
||||
<attribute name="picture" optional="YES" attributeType="String"/>
|
||||
<attribute name="website" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
class Profile: Codable {
|
||||
var value: [String: AnyCodable]
|
||||
|
||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?, damus_donation: Int?) {
|
||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
|
||||
self.value = [:]
|
||||
self.name = name
|
||||
self.display_name = display_name
|
||||
@@ -21,30 +21,12 @@ class Profile: Codable {
|
||||
self.lud06 = lud06
|
||||
self.lud16 = lud16
|
||||
self.nip05 = nip05
|
||||
self.damus_donation = damus_donation
|
||||
}
|
||||
|
||||
convenience init(persisted_profile: PersistedProfile) {
|
||||
self.init(name: persisted_profile.name,
|
||||
display_name: persisted_profile.display_name,
|
||||
about: persisted_profile.about,
|
||||
picture: persisted_profile.picture,
|
||||
banner: persisted_profile.banner,
|
||||
website: persisted_profile.website,
|
||||
lud06: persisted_profile.lud06,
|
||||
lud16: persisted_profile.lud16,
|
||||
nip05: persisted_profile.nip05,
|
||||
damus_donation: Int(persisted_profile.damus_donation))
|
||||
}
|
||||
|
||||
private func str(_ str: String) -> String? {
|
||||
return get_val(str)
|
||||
}
|
||||
|
||||
private func int(_ key: String) -> Int? {
|
||||
return get_val(key)
|
||||
}
|
||||
|
||||
private func get_val<T>(_ v: String) -> T? {
|
||||
guard let val = self.value[v] else{
|
||||
return nil
|
||||
@@ -70,10 +52,6 @@ class Profile: Codable {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
private func set_int(_ key: String, _ val: Int?) {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
var reactions: Bool? {
|
||||
get { return get_val("reactions"); }
|
||||
set(s) { set_val("reactions", s) }
|
||||
@@ -99,11 +77,6 @@ class Profile: Codable {
|
||||
set(s) { set_str("about", s) }
|
||||
}
|
||||
|
||||
var damus_donation: Int? {
|
||||
get { return int("damus_donation_v2"); }
|
||||
set(s) { set_int("damus_donation_v2", s) }
|
||||
}
|
||||
|
||||
var picture: String? {
|
||||
get { return str("picture"); }
|
||||
set(s) { set_str("picture", s) }
|
||||
@@ -207,7 +180,7 @@ class Profile: Codable {
|
||||
}
|
||||
|
||||
func make_test_profile() -> Profile {
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com")
|
||||
}
|
||||
|
||||
func make_ln_url(_ str: String?) -> URL? {
|
||||
|
||||
@@ -36,14 +36,6 @@ struct ReferencedId: Identifiable, Hashable, Equatable {
|
||||
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
|
||||
}
|
||||
|
||||
static func p(_ pk: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: pk, relay_id: relay_id, key: "p")
|
||||
}
|
||||
|
||||
static func t(_ hashtag: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: hashtag, relay_id: relay_id, key: "t")
|
||||
}
|
||||
}
|
||||
|
||||
class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable {
|
||||
@@ -76,11 +68,11 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
let content: String
|
||||
|
||||
var is_textlike: Bool {
|
||||
return kind == 1 || kind == 42 || kind == 30023
|
||||
return kind == 1 || kind == 42
|
||||
}
|
||||
|
||||
var too_big: Bool {
|
||||
return known_kind != .longform && self.content.utf8.count > 16000
|
||||
return self.content.utf8.count > 16000
|
||||
}
|
||||
|
||||
var should_show_event: Bool {
|
||||
@@ -91,8 +83,8 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return calculate_event_id(ev: self) == self.id
|
||||
}
|
||||
|
||||
private var _blocks: Blocks? = nil
|
||||
func blocks(_ privkey: String?) -> Blocks {
|
||||
private var _blocks: [Block]? = nil
|
||||
func blocks(_ privkey: String?) -> [Block] {
|
||||
if let bs = _blocks {
|
||||
return bs
|
||||
}
|
||||
@@ -101,7 +93,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return blocks
|
||||
}
|
||||
|
||||
func get_blocks(content: String) -> Blocks {
|
||||
func get_blocks(content: String) -> [Block] {
|
||||
return parse_mentions(content: content, tags: self.tags)
|
||||
}
|
||||
|
||||
@@ -126,7 +118,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
if let rs = _event_refs {
|
||||
return rs
|
||||
}
|
||||
let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags)
|
||||
let refs = interpret_event_refs(blocks: self.blocks(privkey), tags: self.tags)
|
||||
self._event_refs = refs
|
||||
return refs
|
||||
}
|
||||
@@ -240,7 +232,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
func note_language(_ privkey: String?) -> String? {
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
|
||||
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
|
||||
let originalBlocks = blocks(privkey).blocks
|
||||
let originalBlocks = blocks(privkey)
|
||||
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
@@ -461,8 +453,10 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
|
||||
let relay_json = encode_json(relays)!
|
||||
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
|
||||
let tags = [
|
||||
["p", damus_pubkey],
|
||||
["p", jb55_pubkey],
|
||||
["p", keypair.pubkey] // you're a friend of yourself!
|
||||
]
|
||||
let ev = NostrEvent(content: relay_json,
|
||||
@@ -498,11 +492,11 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
|
||||
return ev
|
||||
}
|
||||
|
||||
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
|
||||
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent {
|
||||
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
||||
tags.append(["e", liked.id])
|
||||
tags.append(["p", liked.pubkey])
|
||||
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
|
||||
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags)
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: privkey)
|
||||
|
||||
@@ -607,7 +601,7 @@ enum MakeZapRequest {
|
||||
|
||||
var private_inner_request: ZapRequest {
|
||||
switch self {
|
||||
case .priv(_, let pzr):
|
||||
case .priv(let _, let pzr):
|
||||
return pzr.req
|
||||
case .normal(let zr):
|
||||
return zr
|
||||
@@ -948,7 +942,7 @@ func last_etag(tags: [[String]]) -> String? {
|
||||
}
|
||||
|
||||
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
||||
let blocks = ev.blocks(privkey).blocks.filter { block in
|
||||
let blocks = ev.blocks(privkey).filter { block in
|
||||
guard case .mention(let mention) = block else {
|
||||
return false
|
||||
}
|
||||
@@ -972,28 +966,6 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
|
||||
If the known kind is not a `NostrKind.like`, it will return `nil`.
|
||||
If the event content is an empty string or `+`, it will map that to a heart ❤️ emoji.
|
||||
If the event content is a "-", it will map that to a dislike 👎 emoji.
|
||||
Otherwise, it will return the event content at face value without transforming it.
|
||||
*/
|
||||
func to_reaction_emoji(ev: NostrEvent) -> String? {
|
||||
guard ev.known_kind == NostrKind.like else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ev.content {
|
||||
case "", "+":
|
||||
return "❤️"
|
||||
case "-":
|
||||
return "👎"
|
||||
default:
|
||||
return ev.content
|
||||
}
|
||||
}
|
||||
|
||||
extension [ReferencedId] {
|
||||
var pRefs: [ReferencedId] {
|
||||
get {
|
||||
|
||||
@@ -9,15 +9,15 @@ import Foundation
|
||||
|
||||
struct NostrFilter: Codable, Equatable {
|
||||
var ids: [String]?
|
||||
var kinds: [NostrKind]?
|
||||
var kinds: [Int]?
|
||||
var referenced_ids: [String]?
|
||||
var pubkeys: [String]?
|
||||
var since: Int64?
|
||||
var until: Int64?
|
||||
var limit: UInt32?
|
||||
var authors: [String]?
|
||||
var hashtag: [String]?
|
||||
var parameter: [String]?
|
||||
var hashtag: [String]? = nil
|
||||
var parameter: [String]? = nil
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case ids
|
||||
@@ -32,23 +32,31 @@ struct NostrFilter: Codable, Equatable {
|
||||
case limit
|
||||
}
|
||||
|
||||
init(ids: [String]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [String]? = nil, pubkeys: [String]? = nil, since: Int64? = nil, until: Int64? = nil, limit: UInt32? = nil, authors: [String]? = nil, hashtag: [String]? = nil) {
|
||||
self.ids = ids
|
||||
self.kinds = kinds
|
||||
self.referenced_ids = referenced_ids
|
||||
self.pubkeys = pubkeys
|
||||
self.since = since
|
||||
self.until = until
|
||||
self.limit = limit
|
||||
self.authors = authors
|
||||
self.hashtag = hashtag
|
||||
}
|
||||
|
||||
public static func copy(from: NostrFilter) -> NostrFilter {
|
||||
NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
|
||||
return NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
|
||||
}
|
||||
|
||||
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
|
||||
NostrFilter(hashtag: htags.map { $0.lowercased() })
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() })
|
||||
}
|
||||
|
||||
public static func filter_ids(_ ids: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
|
||||
}
|
||||
|
||||
public static var filter_profiles: NostrFilter {
|
||||
return filter_kinds([NostrKind.metadata.rawValue])
|
||||
}
|
||||
|
||||
public static var filter_contacts: NostrFilter {
|
||||
return filter_kinds([NostrKind.contacts.rawValue])
|
||||
}
|
||||
|
||||
public static func filter_authors(_ authors: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: authors)
|
||||
}
|
||||
|
||||
public static func filter_kinds(_ kinds: [Int]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: kinds, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
enum NostrKind: Int, Codable {
|
||||
enum NostrKind: Int {
|
||||
case metadata = 0
|
||||
case text = 1
|
||||
case contacts = 3
|
||||
@@ -20,7 +20,6 @@ enum NostrKind: Int, Codable {
|
||||
case channel_meta = 41
|
||||
case chat = 42
|
||||
case list = 30000
|
||||
case longform = 30023
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
case nwc_request = 23194
|
||||
|
||||
@@ -11,7 +11,6 @@ import Foundation
|
||||
enum NostrLink: Equatable {
|
||||
case ref(ReferencedId)
|
||||
case filter(NostrFilter)
|
||||
case script([UInt8])
|
||||
}
|
||||
|
||||
func encode_pubkey_uri(_ ref: ReferencedId) -> String {
|
||||
@@ -61,13 +60,24 @@ func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
|
||||
if !parse_str(p, "nostr:") {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ref = parse_post_bech32_mention(p) else {
|
||||
|
||||
guard let typ = parse_nostr_ref_uri_type(p) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
return ref
|
||||
|
||||
if !parse_char(p, ":") {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let pk = parse_hexstr(p, len: 64) else {
|
||||
p.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: parse relays from nostr uris
|
||||
return ReferencedId(ref_id: pk, relay_id: nil, key: typ)
|
||||
}
|
||||
|
||||
func decode_universal_link(_ s: String) -> NostrLink? {
|
||||
@@ -106,8 +116,6 @@ func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
|
||||
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
|
||||
case .note(let id):
|
||||
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
|
||||
case .nscript(let data):
|
||||
return .script(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,12 +124,8 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
return decode_universal_link(s)
|
||||
}
|
||||
|
||||
var uri = s
|
||||
uri = uri.replacingOccurrences(of: "nostr://", with: "")
|
||||
var uri = s.replacingOccurrences(of: "nostr://", with: "")
|
||||
uri = uri.replacingOccurrences(of: "nostr:", with: "")
|
||||
|
||||
// Fix for non-latin characters resulting in second colon being encoded
|
||||
uri = uri.replacingOccurrences(of: "damus:t%3A", with: "t:")
|
||||
|
||||
uri = uri.replacingOccurrences(of: "damus://", with: "")
|
||||
uri = uri.replacingOccurrences(of: "damus:", with: "")
|
||||
@@ -136,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
}
|
||||
|
||||
if tag_is_hashtag(parts) {
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
return .filter(NostrFilter.filter_hashtag([parts[1]]))
|
||||
}
|
||||
|
||||
if let rid = tag_to_refid(parts) {
|
||||
|
||||
@@ -12,28 +12,6 @@ struct NostrSubscribe {
|
||||
let sub_id: String
|
||||
}
|
||||
|
||||
|
||||
enum NostrRequestType {
|
||||
case typical(NostrRequest)
|
||||
case custom(String)
|
||||
|
||||
var is_write: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
}
|
||||
|
||||
return req.is_write
|
||||
}
|
||||
|
||||
var is_read: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
}
|
||||
|
||||
return req.is_read
|
||||
}
|
||||
}
|
||||
|
||||
enum NostrRequest {
|
||||
case subscribe(NostrSubscribe)
|
||||
case unsubscribe(String)
|
||||
@@ -53,5 +31,4 @@ enum NostrRequest {
|
||||
var is_read: Bool {
|
||||
return !is_write
|
||||
}
|
||||
|
||||
}
|
||||
|
||||