Compare commits
233 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
fe9d6da027
|
|||
| 02fc065005 | |||
| 668b0a94df | |||
| ebba9d3004 | |||
| 8733a34933 | |||
| 0de6cfe344 | |||
| a4d40dbfa6 | |||
| 05d332eac3 | |||
| b5a3697d78 | |||
| 247270f3d3 | |||
| 9327068264 | |||
| c277c14bcd | |||
| e688a691fc | |||
| b470af8f1d | |||
| efd1168217 | |||
| f7a3f9ab76 | |||
| 95041600dc | |||
| 8a8d2ebbc3 | |||
| fad0a6b783 | |||
| f5d7465368 | |||
| 8a785559c6 | |||
| d4c8c15cc3 | |||
| 41a462871c | |||
| 76a669acc2 | |||
| 39236dc094 | |||
| 5860125802 | |||
| ae96c3b707 | |||
| 136f6f37e8 | |||
| 21f84f722b | |||
| 47747379ee | |||
| a1b95d40e6 | |||
| 0fc69d862a | |||
|
fb0330476d
|
|||
|
4b978594fa
|
|||
| f0bbba7a33 | |||
| b5faae9d1c | |||
| a4d4954abd | |||
| 735376b00f | |||
| 042e02d2e4 | |||
| 40468b1603 | |||
| 8c19ec1532 | |||
| 1ac9620242 | |||
| d5ecc9bce4 | |||
| d82b69aac5 | |||
| bad6ba3643 | |||
| 5c131e62d7 | |||
| 29ab48287f | |||
| 5c854519db | |||
| 2cc04e24a3 | |||
| d24bea366d | |||
| 85ce8cb93c | |||
|
32bb8c365d
|
|||
|
d9285ab3ca
|
|||
|
3cba771655
|
|||
|
f6f2517fda
|
|||
|
047325e6b2
|
|||
|
ba2108d659
|
|||
|
863c7baa8b
|
|||
|
8a5e95e47a
|
|||
|
de0997216d
|
|||
|
cc64c82ec4
|
|||
|
e2ca02399b
|
|||
|
5418f55cee
|
|||
|
eb65d473cd
|
|||
|
dd337c4805
|
|||
|
2f6ed72f6d
|
|||
|
d71bb33408
|
|||
|
72cfb2b071
|
|||
|
a67cb2df90
|
|||
| 23e9ce1455 | |||
| 5f1132cbc8 | |||
| 806c6257df | |||
| 18aafb086e | |||
| fc534ea42d | |||
| 54c8958250 | |||
| e9f71ed07c | |||
| c719058487 | |||
| ab853c406c | |||
| 8a88824677 | |||
| 0b3918710a | |||
| 1320ff6bec | |||
| 10cab37270 | |||
| 179da97090 | |||
| 2b2d124495 | |||
| 2a2af056eb | |||
| f56edd5547 | |||
| b30d0c01db | |||
| e8d63768c1 | |||
| 1f648057d5 | |||
| ea14099b62 | |||
| f5942f5123 | |||
| cb8585e4f8 | |||
| 526689c742 | |||
| 32a9856e2a | |||
| 6082a2829f | |||
| b3f6b451bf | |||
| 91d51c5e76 | |||
| 3262fe806a | |||
| d04e9c9b5f | |||
| 015eb5f9fe | |||
| 4a525a7581 | |||
| 72e14fc3a8 | |||
| af275965ee | |||
| 9f5913828a | |||
| c7c21cdee7 | |||
| 44a2c4ba7b | |||
| a74aea9d12 | |||
| 0866c70346 | |||
| 2b34e88a47 | |||
| 2aa8d527b9 | |||
| 88cbb55953 | |||
| e738c6c1ca | |||
| f4e0c8df5c | |||
| 269269d056 | |||
| fd49539615 | |||
| dbb5c19002 | |||
| 6961113734 | |||
| 4642656ce2 | |||
| 0c2132b122 | |||
| a1311a940a | |||
| af18975240 | |||
| f500da03e8 | |||
| 8a230861bf | |||
| 13354b0eb5 | |||
| c6f4643b5a | |||
| ee34b1c0a3 | |||
| a2cd51b6e7 | |||
| 831a409fe6 | |||
| 11e22628bb | |||
|
6598b5b4bb
|
|||
|
ca293ef29b
|
|||
|
8051324d3e
|
|||
|
a11cf66088
|
|||
|
a10142d3b9
|
|||
|
83fcd8600a
|
|||
|
6283157ef4
|
|||
|
8b2f45da41
|
|||
|
ed467a2f79
|
|||
|
feace9b70d
|
|||
|
62bccd6b60
|
|||
|
6f4d12cab4
|
|||
|
436fe830c7
|
|||
|
69b6d54b0e
|
|||
|
13522028ba
|
|||
|
1459581ec9
|
|||
|
31aedd2a6e
|
|||
|
bb1dff13ce
|
|||
|
3b8c884b30
|
|||
|
30299fafed
|
|||
| 46c208c9a5 | |||
| 8e78bf9e1a | |||
| 82fff4591c | |||
| b8226d674d | |||
| 3f3892ba1d | |||
| 1b6224e665 | |||
| 782f8d6a69 | |||
| 9ad6af0e4f | |||
| e16ea0f4dc | |||
| 5b1808d8e7 | |||
| d50b2deb26 | |||
| 9de4730e17 | |||
| 382265dd39 | |||
|
6c7f8cdbe5
|
|||
| 36a92b3795 | |||
| 6d4d218c28 | |||
|
8679da1275
|
|||
|
59dcbc130c
|
|||
|
badd3210e5
|
|||
|
dd6cc3cf4f
|
|||
|
36a0ca9c80
|
|||
|
a7d3b19665
|
|||
|
226e567987
|
|||
|
898ffc0186
|
|||
|
4d0f7b576b
|
|||
|
230d049384
|
|||
|
774a4173b0
|
|||
|
f8b5d91720
|
|||
|
0832c82ee8
|
|||
| 4786c6f0cb | |||
| 8d0aea22fd | |||
| 3d27e49e70 | |||
| e9be227009 | |||
| 2e640db012 | |||
| eed16449fe | |||
| 3661d64450 | |||
| 24c82293b3 | |||
| 7fb1bc48c4 | |||
| 2ae4a156da | |||
| f700dd799f | |||
| 00548adc1f | |||
| 7c08d4af45 | |||
| c7bf1da797 | |||
| c5341ba337 | |||
| 95fb7bccf8 | |||
| 6499738994 | |||
| 532eaa35bf | |||
|
9c1bb30f5f
|
|||
|
4683b417b9
|
|||
|
2176b58215
|
|||
|
3c7627bb9a
|
|||
|
463ca5ce7b
|
|||
|
b6a0626890
|
|||
|
dbfad189cb
|
|||
|
8159213ef0
|
|||
|
684841c03a
|
|||
|
e429bd7df9
|
|||
|
45ebcf565a
|
|||
|
c031b08c9a
|
|||
|
d98503732e
|
|||
|
bfe0a39370
|
|||
|
36de599326
|
|||
|
3cbdfb7ddf
|
|||
|
ddd309634f
|
|||
|
6bbf56f80c
|
|||
|
de1332d6ea
|
|||
|
648f25ea7a
|
|||
|
b46dc04ae8
|
|||
|
8cabfaf90e
|
|||
|
0b02956bb7
|
|||
|
4abea02d6e
|
|||
|
5d20c49f7d
|
|||
|
c11e0fa919
|
|||
|
54bfeb04da
|
|||
|
214eec9cb6
|
|||
|
b48de9525e
|
|||
|
690f3ec740
|
|||
|
6df4ffc1f0
|
|||
|
83cead1fbb
|
|||
|
478600cf0e
|
|||
|
75002802fa
|
|||
|
faf072d432
|
|||
|
e888da26a7
|
|||
| b532dc48e1 |
+111
@@ -1,3 +1,114 @@
|
||||
## [1.4.3-1] - 2023-04-15
|
||||
|
||||
### Added
|
||||
|
||||
- Add deep links for local notifications (Swift + Will)
|
||||
- Add thread muting (Terry Yiu)
|
||||
- Preview media uploads when posting (Swift)
|
||||
- Add QR Code in profiles (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Ask permission before uploading media (Swift)
|
||||
- Show DM message in local notification (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix tap area when mentioning users (OlegAba)
|
||||
- Fix invalid DM author notifications (William Casarin)
|
||||
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
|
||||
|
||||
|
||||
[1.4.3-1]: https://github.com/damus-io/damus/releases/tag/v1.4.3-1
|
||||
|
||||
## [1.4.2-2] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Include #btc in custom #bitcoin hashtag (William Casarin)
|
||||
- Make notification dots configurable (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Display follows in most recent to oldest (Luis Cabrera)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hitches caused by syncronous loading of cached images (William Casarin)
|
||||
- Fix tabs sometimes not switching (William Casarin)
|
||||
|
||||
|
||||
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
|
||||
|
||||
## [1.4.1-8] - 2023-04-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for nostr: bech32 urls in posts and DMs (NIP19) (Bartholomew Joyce)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't leak mentions in DMs (William Casarin)
|
||||
- Fix tap area when mentioning users (OlegAba)
|
||||
|
||||
[1.4.1-8]: https://github.com/damus-io/damus/releases/tag/v1.4.1-8
|
||||
## [1.4.1-7] - 2023-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add #zap and #zapathon custom hashtags (William Casarin)
|
||||
- Add custom #plebchain icon (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Add validation to prevent whitespaces be inputted on NIP-05 input field (Terry Yiu)
|
||||
- Change reply color from blue to purple. Blue is banned from Damus. (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix padding in post view (OlegAba)
|
||||
- Show most recently bookmarked notes at the top (Bryan Montz)
|
||||
|
||||
|
||||
[1.4.1-7]: https://github.com/damus-io/damus/releases/tag/v1.4.1-7
|
||||
|
||||
## [1.4.1-6] - 2023-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Custom hashtags for #bitcoin, #nostr and #coffeechain (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable translations in DMs by default (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't show Translating... if we're not actually translating (William Casarin)
|
||||
|
||||
|
||||
[1.4.1-6]: https://github.com/damus-io/damus/releases/tag/v1.4.1-6
|
||||
|
||||
## [1.4.1-4] - 2023-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Cache translations (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix translation text popping (William Casarin)
|
||||
- Fix broken auto-translations (William Casarin)
|
||||
- Fix extraneous padding on some image posts (William Casarin)
|
||||
- Fix crash in relay list view (William Casarin)
|
||||
|
||||
[1.4.1-4]: https://github.com/damus-io/damus/releases/tag/v1.4.1-4
|
||||
|
||||
## [1.4.1-3] - 2023-04-05
|
||||
|
||||
### Added
|
||||
|
||||
+10
-3
@@ -91,13 +91,12 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat
|
||||
return 1;
|
||||
}
|
||||
|
||||
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
|
||||
bech32_encoding bech32_decode_len(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t input_len) {
|
||||
uint32_t chk = 1;
|
||||
size_t i;
|
||||
size_t input_len = strlen(input);
|
||||
size_t hrp_len;
|
||||
int have_lower = 0, have_upper = 0;
|
||||
if (input_len < 8 || input_len > max_input_len) {
|
||||
if (input_len < 8) {
|
||||
return BECH32_ENCODING_NONE;
|
||||
}
|
||||
*data_len = 0;
|
||||
@@ -154,6 +153,14 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const
|
||||
}
|
||||
}
|
||||
|
||||
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
|
||||
size_t len = strlen(input);
|
||||
if (len > max_input_len) {
|
||||
return BECH32_ENCODING_NONE;
|
||||
}
|
||||
return bech32_decode_len(hrp, data, data_len, input, len);
|
||||
}
|
||||
|
||||
int bech32_convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) {
|
||||
uint32_t val = 0;
|
||||
int bits = 0;
|
||||
|
||||
@@ -118,6 +118,14 @@ bech32_encoding bech32_decode(
|
||||
size_t max_input_len
|
||||
);
|
||||
|
||||
bech32_encoding bech32_decode_len(
|
||||
char *hrp,
|
||||
uint8_t *data,
|
||||
size_t *data_len,
|
||||
const char *input,
|
||||
size_t input_len
|
||||
);
|
||||
|
||||
/* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes.
|
||||
* @outlen is incremented as bytes are added.
|
||||
* @pad is true if we're to pad, otherwise truncate last byte if necessary
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// block.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef block_h
|
||||
#define block_h
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include "str_block.h"
|
||||
|
||||
#define MAX_BLOCKS 1024
|
||||
|
||||
enum block_type {
|
||||
BLOCK_HASHTAG = 1,
|
||||
BLOCK_TEXT = 2,
|
||||
BLOCK_MENTION_INDEX = 3,
|
||||
BLOCK_MENTION_BECH32 = 4,
|
||||
BLOCK_URL = 5,
|
||||
BLOCK_INVOICE = 6,
|
||||
};
|
||||
|
||||
|
||||
typedef struct invoice_block {
|
||||
struct str_block invstr;
|
||||
union {
|
||||
struct bolt11 *bolt11;
|
||||
};
|
||||
} invoice_block_t;
|
||||
|
||||
typedef struct mention_bech32_block {
|
||||
struct str_block str;
|
||||
struct nostr_bech32 bech32;
|
||||
} mention_bech32_block_t;
|
||||
|
||||
typedef struct block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
struct invoice_block invoice;
|
||||
struct mention_bech32_block mention_bech32;
|
||||
int mention_index;
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct blocks {
|
||||
int num_blocks;
|
||||
struct block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct blocks *blocks);
|
||||
void blocks_free(struct blocks *blocks);
|
||||
|
||||
#endif /* block_h */
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// cursor.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef cursor_h
|
||||
#define cursor_h
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct cursor {
|
||||
const u8 *p;
|
||||
const u8 *start;
|
||||
const u8 *end;
|
||||
};
|
||||
|
||||
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 !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_boundary(c))
|
||||
return 1;
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = 1;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (!is_alphanumeric(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = 1;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
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 */
|
||||
+43
-123
@@ -6,127 +6,13 @@
|
||||
//
|
||||
|
||||
#include "damus.h"
|
||||
#include "cursor.h"
|
||||
#include "bolt11.h"
|
||||
#include "bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct cursor {
|
||||
const u8 *p;
|
||||
const u8 *start;
|
||||
const u8 *end;
|
||||
};
|
||||
|
||||
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 !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
c->end = content + len;
|
||||
c->p = content;
|
||||
}
|
||||
|
||||
static int consume_until_boundary(struct cursor *cur) {
|
||||
char c;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_boundary(c))
|
||||
return 1;
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
bool consumedAtLeastOne = false;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = true;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
static 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_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 int parse_mention(struct cursor *cur, struct block *block) {
|
||||
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
||||
int d1, d2, d3, ind;
|
||||
const u8 *start = cur->p;
|
||||
|
||||
@@ -151,8 +37,8 @@ static int parse_mention(struct cursor *cur, struct block *block) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->type = BLOCK_MENTION;
|
||||
block->block.mention = ind;
|
||||
block->type = BLOCK_MENTION_INDEX;
|
||||
block->block.mention_index = ind;
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -274,6 +160,27 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "nostr:"))
|
||||
return 0;
|
||||
|
||||
block->block.str.start = (const char *)cur->p;
|
||||
|
||||
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
block->type = BLOCK_MENTION_BECH32;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -303,7 +210,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
|
||||
|
||||
pre_mention = cur.p;
|
||||
if (cp == -1 || is_whitespace(cp)) {
|
||||
if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
@@ -315,6 +222,10 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,8 +246,17 @@ void blocks_init(struct blocks *blocks) {
|
||||
}
|
||||
|
||||
void blocks_free(struct blocks *blocks) {
|
||||
if (blocks->blocks) {
|
||||
free(blocks->blocks);
|
||||
blocks->num_blocks = 0;
|
||||
if (!blocks->blocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < blocks->num_blocks; ++i) {
|
||||
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
|
||||
free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
|
||||
blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
free(blocks->blocks);
|
||||
blocks->num_blocks = 0;
|
||||
}
|
||||
|
||||
+3
-38
@@ -9,45 +9,10 @@
|
||||
#define damus_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "nostr_bech32.h"
|
||||
#include "block.h"
|
||||
typedef unsigned char u8;
|
||||
|
||||
#define MAX_BLOCKS 1024
|
||||
|
||||
enum block_type {
|
||||
BLOCK_HASHTAG = 1,
|
||||
BLOCK_TEXT = 2,
|
||||
BLOCK_MENTION = 3,
|
||||
BLOCK_URL = 4,
|
||||
BLOCK_INVOICE = 5,
|
||||
};
|
||||
|
||||
typedef struct str_block {
|
||||
const char *start;
|
||||
const char *end;
|
||||
} str_block_t;
|
||||
|
||||
typedef struct invoice_block {
|
||||
struct str_block invstr;
|
||||
union {
|
||||
struct bolt11 *bolt11;
|
||||
};
|
||||
} invoice_block_t;
|
||||
|
||||
typedef struct block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
struct invoice_block invoice;
|
||||
int mention;
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct blocks {
|
||||
int num_blocks;
|
||||
struct block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct blocks *blocks);
|
||||
void blocks_free(struct blocks *blocks);
|
||||
int damus_parse_content(struct blocks *blocks, const char *content);
|
||||
|
||||
#endif /* damus_h */
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// nostr_bech32.c
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include "cursor.h"
|
||||
#include "bech32.h"
|
||||
|
||||
#define MAX_TLVS 16
|
||||
|
||||
#define TLV_SPECIAL 0
|
||||
#define TLV_RELAY 1
|
||||
#define TLV_AUTHOR 2
|
||||
#define TLV_KIND 3
|
||||
#define TLV_KNOWN_TLVS 4
|
||||
|
||||
struct nostr_tlv {
|
||||
u8 type;
|
||||
u8 len;
|
||||
const u8 *value;
|
||||
};
|
||||
|
||||
struct nostr_tlvs {
|
||||
struct nostr_tlv tlvs[MAX_TLVS];
|
||||
int num_tlvs;
|
||||
};
|
||||
|
||||
static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
|
||||
// get the tlv tag
|
||||
if (!pull_byte(cur, &tlv->type))
|
||||
return 0;
|
||||
|
||||
// unknown, fail!
|
||||
if (tlv->type >= TLV_KNOWN_TLVS)
|
||||
return 0;
|
||||
|
||||
// get the length
|
||||
if (!pull_byte(cur, &tlv->len))
|
||||
return 0;
|
||||
|
||||
// is the reported length greater then our buffer? if so fail
|
||||
if (cur->p + tlv->len > cur->end)
|
||||
return 0;
|
||||
|
||||
tlv->value = cur->p;
|
||||
cur->p += tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
|
||||
int i;
|
||||
tlvs->num_tlvs = 0;
|
||||
|
||||
for (i = 0; i < MAX_TLVS; i++) {
|
||||
if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
|
||||
tlvs->num_tlvs++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tlvs->num_tlvs == 0)
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
|
||||
*tlv = NULL;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
if (tlvs->tlvs[i].type == type) {
|
||||
*tlv = &tlvs->tlvs[i];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
|
||||
// Parse type
|
||||
if (strcmp(prefix, "note") == 0) {
|
||||
*type = NOSTR_BECH32_NOTE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "npub") == 0) {
|
||||
*type = NOSTR_BECH32_NPUB;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nprofile") == 0) {
|
||||
*type = NOSTR_BECH32_NPROFILE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nevent") == 0) {
|
||||
*type = NOSTR_BECH32_NEVENT;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nrelay") == 0) {
|
||||
*type = NOSTR_BECH32_NRELAY;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "naddr") == 0) {
|
||||
*type = NOSTR_BECH32_NADDR;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
|
||||
return pull_bytes(cur, 32, ¬e->event_id);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
|
||||
return pull_bytes(cur, 32, &npub->pubkey);
|
||||
}
|
||||
|
||||
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
|
||||
struct nostr_tlv *tlv;
|
||||
struct str_block *str;
|
||||
|
||||
relays->num_relays = 0;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
tlv = &tlvs->tlvs[i];
|
||||
if (tlv->type != TLV_RELAY)
|
||||
continue;
|
||||
|
||||
if (relays->num_relays + 1 > MAX_RELAYS)
|
||||
break;
|
||||
|
||||
str = &relays->relays[relays->num_relays++];
|
||||
str->start = (const char*)tlv->value;
|
||||
str->end = (const char*)(tlv->value + tlv->len);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nevent->event_id = tlv->value;
|
||||
|
||||
if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
|
||||
nevent->pubkey = tlv->value;
|
||||
} else {
|
||||
nevent->pubkey = NULL;
|
||||
}
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nevent->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->identifier.start = (const char*)tlv->value;
|
||||
naddr->identifier.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->pubkey = tlv->value;
|
||||
|
||||
return tlvs_to_relays(&tlvs, &naddr->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nprofile->pubkey = tlv->value;
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nprofile->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
nrelay->relay.start = (const char*)tlv->value;
|
||||
nrelay->relay.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
const u8 *start, *end;
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!consume_until_non_alphanumeric(cur, 1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
end = cur->p;
|
||||
|
||||
size_t data_len;
|
||||
size_t input_len = end - start;
|
||||
if (input_len < 10 || input_len > 10000) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buffer = malloc(input_len * 2);
|
||||
if (!obj->buffer)
|
||||
return 0;
|
||||
|
||||
u8 data[input_len];
|
||||
char prefix[input_len];
|
||||
|
||||
if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buflen = 0;
|
||||
if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!parse_nostr_bech32_type(prefix, &obj->type)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
struct cursor bcur;
|
||||
make_cursor(&bcur, obj->buffer, obj->buflen);
|
||||
|
||||
switch (obj->type) {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPUB:
|
||||
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NADDR:
|
||||
if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
|
||||
goto fail;
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
free(obj->buffer);
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// nostr_bech32.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef nostr_bech32_h
|
||||
#define nostr_bech32_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "str_block.h"
|
||||
#include "cursor.h"
|
||||
typedef unsigned char u8;
|
||||
#define MAX_RELAYS 10
|
||||
|
||||
struct relays {
|
||||
struct str_block relays[MAX_RELAYS];
|
||||
int num_relays;
|
||||
};
|
||||
|
||||
enum nostr_bech32_type {
|
||||
NOSTR_BECH32_NOTE = 1,
|
||||
NOSTR_BECH32_NPUB = 2,
|
||||
NOSTR_BECH32_NPROFILE = 3,
|
||||
NOSTR_BECH32_NEVENT = 4,
|
||||
NOSTR_BECH32_NRELAY = 5,
|
||||
NOSTR_BECH32_NADDR = 6,
|
||||
};
|
||||
|
||||
struct bech32_note {
|
||||
const u8 *event_id;
|
||||
};
|
||||
|
||||
struct bech32_npub {
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nevent {
|
||||
struct relays relays;
|
||||
const u8 *event_id;
|
||||
const u8 *pubkey; // optional
|
||||
};
|
||||
|
||||
struct bech32_nprofile {
|
||||
struct relays relays;
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_naddr {
|
||||
struct relays relays;
|
||||
struct str_block identifier;
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nrelay {
|
||||
struct str_block relay;
|
||||
};
|
||||
|
||||
typedef struct nostr_bech32 {
|
||||
enum nostr_bech32_type type;
|
||||
u8 *buffer; // holds strings and tlv stuff
|
||||
size_t buflen;
|
||||
|
||||
union {
|
||||
struct bech32_note note;
|
||||
struct bech32_npub npub;
|
||||
struct bech32_nevent nevent;
|
||||
struct bech32_nprofile nprofile;
|
||||
struct bech32_naddr naddr;
|
||||
struct bech32_nrelay nrelay;
|
||||
} data;
|
||||
} nostr_bech32_t;
|
||||
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
|
||||
|
||||
#endif /* nostr_bech32_h */
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// str_block.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef str_block_h
|
||||
#define str_block_h
|
||||
|
||||
typedef struct str_block {
|
||||
const char *start;
|
||||
const char *end;
|
||||
} str_block_t;
|
||||
|
||||
#endif /* str_block_h */
|
||||
@@ -17,7 +17,7 @@
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
@@ -137,6 +137,11 @@
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
|
||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C929DF80350036AF10 /* TruncatedText.swift */; };
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
|
||||
@@ -187,6 +192,8 @@
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
||||
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
|
||||
@@ -324,10 +331,7 @@
|
||||
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -335,6 +339,9 @@
|
||||
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -381,9 +388,6 @@
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -537,6 +541,15 @@
|
||||
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
|
||||
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
|
||||
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
|
||||
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostr_bech32.h; sourceTree = "<group>"; };
|
||||
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostr_bech32.c; sourceTree = "<group>"; };
|
||||
4C8D00D029E38E4C0036AF10 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
|
||||
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
|
||||
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
|
||||
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
|
||||
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
@@ -587,6 +600,8 @@
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
||||
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
|
||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
|
||||
@@ -773,6 +788,11 @@
|
||||
4C3EA67428FF7A5A00C48A62 /* take.c */,
|
||||
4C3EA67628FF7A9800C48A62 /* talstr.c */,
|
||||
4C3EA67828FF7ABF00C48A62 /* list.c */,
|
||||
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */,
|
||||
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */,
|
||||
4C8D00D029E38E4C0036AF10 /* cursor.h */,
|
||||
4C8D00D129E397AD0036AF10 /* block.h */,
|
||||
4C8D00D229E3C19F0036AF10 /* str_block.h */,
|
||||
);
|
||||
path = "damus-c";
|
||||
sourceTree = "<group>";
|
||||
@@ -823,7 +843,7 @@
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
3A48E23A29D518F000BA313D /* Translations.swift */,
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -986,6 +1006,9 @@
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
||||
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
|
||||
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -1001,6 +1024,7 @@
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
@@ -1105,6 +1129,7 @@
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
|
||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -1180,6 +1205,7 @@
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -1383,8 +1409,7 @@
|
||||
"es-419",
|
||||
"es-ES",
|
||||
fa,
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
fr,
|
||||
"hu-HU",
|
||||
id,
|
||||
"it-IT",
|
||||
@@ -1472,6 +1497,7 @@
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
@@ -1490,6 +1516,7 @@
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
@@ -1514,8 +1541,10 @@
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */,
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||
@@ -1624,12 +1653,15 @@
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
|
||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
||||
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
|
||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
||||
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
|
||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||
@@ -1666,7 +1698,6 @@
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||
@@ -1702,6 +1733,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||
@@ -1751,7 +1783,6 @@
|
||||
3A5C4575296A879E0032D398 /* es-419 */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
@@ -1773,10 +1804,10 @@
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C4029E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1786,7 +1817,6 @@
|
||||
children = (
|
||||
3ACB685B297633BC00C46468 /* es-419 */,
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
@@ -1808,10 +1838,10 @@
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3F29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1821,7 +1851,6 @@
|
||||
children = (
|
||||
3ACB685E297633BC00C46468 /* es-419 */,
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
@@ -1844,10 +1873,10 @@
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3E29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1983,7 +2012,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2008,9 +2037,12 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -2027,7 +2059,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2052,9 +2084,12 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"red" : "0xBE",
|
||||
"green" : "0x5F",
|
||||
"blue" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xED",
|
||||
"green" : "0x26",
|
||||
"red" : "0xBF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x05",
|
||||
"green" : "0xDF",
|
||||
"red" : "0xFA"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="12.843903"
|
||||
height="17"
|
||||
viewBox="0 0 12.843902 16.999999"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="bitcoin-hashtag.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.12"
|
||||
inkscape:cx="4.2577121"
|
||||
inkscape:cy="7.535347"
|
||||
inkscape:window-width="1526"
|
||||
inkscape:window-height="957"
|
||||
inkscape:window-x="1637"
|
||||
inkscape:window-y="10"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="surface1"
|
||||
transform="matrix(0.94507527,0,0,0.94507527,-4.5943665,-3.2875042)">
|
||||
<path
|
||||
style="fill:#f59119;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40637"
|
||||
d="M 18.388175,10.742602 C 18.668352,8.874761 17.240002,7.8694225 15.295251,7.193703 L 15.927019,4.6611305 14.383304,4.2765743 13.768015,6.7432244 C 13.361486,6.644338 12.943967,6.545453 12.531944,6.4520613 L 13.152726,3.9689298 11.609011,3.584375 10.977241,6.1169476 C 10.642129,6.0400371 10.312509,5.9686201 9.988384,5.886215 L 9.993834,5.880715 7.8623408,5.3478364 7.4558114,6.9959316 c 0,0 1.1426793,0.2636953 1.1207046,0.2801765 0.6207823,0.1538223 0.7361485,0.5658464 0.714174,0.8954659 l -0.7196673,2.889659 c 0.043949,0.01099 0.098885,0.02747 0.1648089,0.04944 L 8.5710227,11.072223 7.5601911,15.11555 c -0.076912,0.192277 -0.26919,0.477948 -0.7031873,0.368073 0.010984,0.02198 -1.1261994,-0.280174 -1.1261994,-0.280174 l -0.7636164,1.76346 2.0106754,0.505417 c 0.3735682,0.0934 0.7361485,0.186784 1.0987301,0.280176 l -0.6427568,2.565534 1.5437155,0.384556 0.6317701,-2.538067 c 0.4230107,0.115364 0.8295407,0.219746 1.2305777,0.318632 l -0.63177,2.527079 1.543716,0.384557 0.637263,-2.560042 c 2.631459,0.499922 4.614667,0.296656 5.444208,-2.082094 0.670226,-1.917284 -0.03296,-3.021507 -1.417363,-3.741176 1.010832,-0.236227 1.768957,-0.895465 1.972221,-2.268877 z m -3.526922,4.944286 c -0.477949,1.917283 -3.702721,0.884477 -4.752008,0.620782 l 0.851514,-3.395075 c 1.043795,0.2582 4.394922,0.774604 3.900494,2.774293 z M 15.3392,10.715134 c -0.433999,1.74698 -3.120394,0.857008 -3.993884,0.642757 l 0.769111,-3.081939 c 0.87349,0.219746 3.675252,0.620784 3.224773,2.439182 z m 0,0"
|
||||
id="path2" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 13.999999 18"
|
||||
enable-background="new 0 0 1000 1000"
|
||||
xml:space="preserve"
|
||||
id="svg4"
|
||||
sodipodi:docname="coffee.svg"
|
||||
width="14"
|
||||
height="18"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs4" /><sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="10.680141"
|
||||
inkscape:cx="-17.181421"
|
||||
inkscape:cy="4.07298"
|
||||
inkscape:window-width="1368"
|
||||
inkscape:window-height="947"
|
||||
inkscape:window-x="1764"
|
||||
inkscape:window-y="58"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<metadata
|
||||
id="metadata1"> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g
|
||||
id="g4"
|
||||
transform="matrix(0.01779387,0,0,0.01779387,-1.8340539,0.04465199)"><g
|
||||
transform="matrix(0.1,0,0,-0.1,0,511)"
|
||||
id="g3"><path
|
||||
d="m 4302.6,4870.6 c 149.5,-177.8 240.5,-319.3 347.6,-545.6 119.2,-254.6 169.7,-448.6 183.9,-699.2 22.2,-462.7 -137.4,-778 -539.5,-1060.9 -474.9,-335.4 -685,-739.6 -687,-1315.5 0,-260.7 38.4,-501.1 115.2,-739.6 50.5,-149.5 56.6,-159.6 60.6,-97 18.2,234.4 56.6,476.9 101,626.4 121.2,422.3 305.1,622.4 885.1,959.8 424.3,248.5 575.9,487 575.9,905.3 -2,501.1 -359.7,1295.3 -798.2,1768.1 -82.8,88.9 -198,202.1 -256.6,250.6 l -105.1,86.9 z"
|
||||
id="path1"
|
||||
style="fill:#be5f00;fill-opacity:1" /><path
|
||||
d="m 5981.8,3577.3 c 272.8,-369.8 309.2,-846.7 90.9,-1192.2 -147.5,-232.4 -373.8,-406.2 -822.4,-638.5 -592,-303.1 -854.7,-683 -854.7,-1232.6 0,-276.8 14.2,-343.5 72.7,-343.5 38.4,0 48.5,16.2 68.7,111.1 34.4,167.7 135.4,349.6 262.7,476.9 147.5,145.5 349.6,244.5 838.6,412.2 503.2,171.8 725.4,280.9 846.7,416.3 210.1,232.4 276.8,535.5 202.1,903.2 -76.8,373.8 -216.2,618.3 -537.5,943.7 -155.7,155.6 -214.3,206.1 -167.8,143.4 z"
|
||||
id="path2"
|
||||
style="fill:#be5f00;fill-opacity:1" /><path
|
||||
d="M 2748.7,592.8 C 2158.7,507.9 1732.3,352.3 1542.4,156.3 1415.1,25 1409,-11.4 1427.2,-445.8 c 34.3,-832.5 181.9,-1729.7 462.7,-2829 153.6,-600.1 309.2,-1113.4 351.6,-1159.9 56.6,-62.6 272.8,-157.6 476.9,-210.1 668.8,-173.8 2172.2,-196 2960.3,-42.4 357.7,68.7 604.2,163.7 731.5,278.9 68.7,60.6 84.9,92.9 107.1,198 64.7,335.4 56.6,319.3 131.3,319.3 107.1,0 438.5,92.9 602.2,167.7 220.3,103.1 363.7,202.1 567.8,398.1 470.8,452.6 759.8,1149.8 761.8,1840.8 0,398.1 -72.7,614.3 -274.8,818.4 -220.3,222.3 -466.8,309.2 -937.6,325.4 l -309.2,12.1 14.2,218.2 14.2,218.2 -56.6,52.5 C 6866.9,314 6274.9,499.9 5696.9,582.7 L 5614,594.8 5533.2,457.4 5450.4,322 5565.6,305.8 c 588,-84.9 868.9,-159.6 978,-256.6 56.6,-50.5 60.6,-62.6 44.5,-113.2 -80.8,-226.3 -1000.2,-371.8 -2329.8,-371.8 -1220.5,0 -2097.5,123.3 -2295.5,321.3 -52.5,54.5 -56.6,66.7 -36.4,111.1 48.5,107.1 341.5,206.1 822.4,276.8 143.5,20.2 301.1,38.4 349.6,38.4 46.5,0 84.9,4 84.9,8.1 0,8.1 -175.8,295 -185.9,305.1 -4.2,2.1 -115.3,-12 -248.7,-32.2 z m 4797.1,-1614.5 c 153.6,-26.3 272.8,-82.8 317.2,-153.6 56.6,-84.9 60.6,-400.1 8.1,-652.7 -111.1,-545.6 -404.1,-996.2 -788.1,-1220.5 -139.4,-80.8 -333.4,-151.5 -355.6,-129.3 -8.1,8.1 18.2,216.2 58.6,462.7 78.8,482.9 159.6,1073 202.1,1450.8 l 24.2,232.4 70.7,12.1 c 115.3,20.3 325.4,20.3 462.8,-1.9 z"
|
||||
id="path3"
|
||||
style="fill:#be5f00;fill-opacity:1" /></g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.0"
|
||||
width="18pt"
|
||||
height="18.199053pt"
|
||||
viewBox="0 0 18 18.199053"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="svg1"
|
||||
sodipodi:docname="nostr-hashtag.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.4017383"
|
||||
inkscape:cx="50.354161"
|
||||
inkscape:cy="-13.514168"
|
||||
inkscape:window-width="1497"
|
||||
inkscape:window-height="866"
|
||||
inkscape:window-x="1747"
|
||||
inkscape:window-y="96"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1" />
|
||||
<metadata
|
||||
id="metadata1">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g
|
||||
transform="matrix(0.00138509,0,0,-0.00138509,0.3,17.927982)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
id="g1">
|
||||
<path
|
||||
d="m 11315,12756 c -49,-23 -135,-71 -190,-106 -128,-81 -170,-100 -222,-100 -37,0 -43,-3 -43,-22 0,-11 12,-36 26,-55 l 26,-34 -20,-37 c -128,-248 -171,-359 -212,-547 -56,-262 -43,-645 36,-1045 48,-244 104,-451 234,-865 158,-503 280,-942 320,-1145 27,-140 60,-448 60,-559 0,-164 -39,-366 -92,-478 -57,-123 -176,-222 -286,-239 -49,-8 -50,-23 -5,-38 18,-6 34,-14 38,-18 13,-13 -25,-66 -97,-134 -121,-115 -267,-174 -432,-174 -200,0 -327,87 -732,507 -233,240 -541,529 -604,567 -31,18 -32,18 -27,0 3,-11 9,-36 13,-56 8,-49 -5,-49 -65,1 -75,62 -168,118 -250,150 -134,51 -214,63 -481,70 l -245,6 -65,41 c -100,63 -125,68 -295,66 -186,-3 -262,7 -484,64 -279,71 -341,67 -780,-61 -334,-97 -442,-118 -761,-150 -200,-19 -365,-19 -496,1 -58,9 -107,14 -111,11 -3,-4 6,-22 21,-41 l 27,-35 -23,-5 c -13,-3 -61,-8 -106,-11 -113,-9 -157,-26 -219,-87 -50,-49 -54,-51 -140,-64 -60,-10 -108,-25 -153,-48 -304,-157 -331,-166 -471,-166 -48,0 -120,7 -160,15 -40,9 -74,13 -78,10 -3,-3 0,-20 7,-37 6,-18 10,-34 7,-36 -2,-3 -24,0 -47,7 -24,6 -103,16 -176,23 -163,15 -350,1 -521,-40 l -114,-26 51,-23 c 53,-23 76,-50 66,-77 -3,-8 -51,-38 -105,-66 -251,-129 -515,-384 -727,-703 -110,-166 -208,-325 -217,-355 -6,-21 -5,-22 34,-16 36,6 41,4 41,-12 0,-11 -15,-45 -34,-76 -46,-75 -76,-161 -127,-359 -78,-307 -137,-444 -243,-569 -38,-44 -51,-67 -43,-72 7,-4 40,-8 75,-8 37,0 62,-4 62,-11 0,-6 -12,-26 -27,-45 -32,-43 -24,-53 48,-60 108,-11 103,-6 95,-82 -4,-37 -11,-128 -15,-202 -5,-74 -16,-164 -26,-200 -22,-85 -75,-190 -136,-269 -27,-35 -49,-67 -49,-72 0,-4 17,-11 38,-14 20,-3 49,-8 64,-10 64,-10 285,31 408,76 101,37 249,117 348,188 54,38 110,75 126,80 15,6 70,11 123,11 267,0 582,98 766,239 34,26 72,53 83,59 25,13 31,8 58,-46 21,-40 38,-32 57,26 30,93 97,200 141,223 25,14 38,3 98,-86 27,-38 73,-95 104,-125 31,-30 56,-63 56,-75 0,-11 -20,-60 -45,-110 -42,-84 -81,-202 -70,-213 3,-3 27,16 54,41 50,47 81,59 81,34 0,-8 -10,-49 -21,-93 -26,-97 -32,-275 -10,-313 l 14,-25 36,41 c 42,48 118,88 251,131 102,33 185,47 345,57 55,4 159,13 230,21 292,34 285,35 368,-56 66,-72 122,-115 170,-130 20,-6 37,-15 37,-19 0,-12 -22,-35 -75,-77 -27,-21 -151,-133 -275,-248 -424,-394 -462,-415 -864,-486 -65,-12 -145,-29 -177,-39 -191,-60 -348,-213 -554,-541 -103,-165 -162,-241 -292,-383 -176,-190 -332,-333 -778,-711 -497,-422 -750,-737 -895,-1113 -64,-165 -104,-203 -216,-203 -38,0 -94,7 -124,15 -74,20 -244,20 -301,0 -27,-10 -61,-33 -87,-62 -41,-45 -43,-46 -89,-39 -25,3 -67,18 -92,33 C 457,781 369,812 175,813 49,814 0,786 0,713 0,669 31,628 122,557 204,491 254,434 469,153 527,77 549,57 597,33 652,7 662,5 780,6 c 69,0 150,6 180,14 30,7 132,32 225,56 281,72 308,76 511,84 181,6 195,8 247,34 76,37 137,107 231,267 230,392 287,481 394,626 219,293 469,581 756,869 284,284 368,342 641,439 164,59 295,123 410,199 101,68 257,219 295,286 14,24 56,76 93,116 100,106 177,142 437,203 165,40 229,62 390,133 191,85 220,94 460,143 118,24 239,49 267,56 29,7 56,10 59,6 4,-3 15,-50 26,-104 14,-73 21,-159 25,-331 6,-265 -3,-385 -47,-605 -50,-247 -105,-393 -222,-592 -59,-98 -74,-167 -58,-256 23,-126 61,-177 174,-232 95,-47 169,-52 357,-28 125,16 244,22 505,26 667,12 1011,-35 1799,-241 277,-73 308,-86 376,-161 72,-79 113,-170 175,-388 19,-66 42,-130 50,-143 42,-63 155,-112 261,-112 109,0 124,57 54,206 -48,102 -53,149 -17,165 31,15 73,-5 102,-48 12,-18 45,-87 75,-153 57,-132 106,-197 173,-233 35,-18 56,-21 136,-20 73,2 110,-3 159,-19 89,-29 333,-32 403,-5 114,44 140,145 60,231 -23,24 -112,72 -272,143 -71,32 -199,124 -250,179 -71,78 -111,140 -224,343 -132,236 -172,297 -262,391 -58,61 -94,89 -148,115 -113,56 -85,53 -866,85 -223,9 -616,36 -656,45 -11,2 -86,12 -165,20 -179,20 -595,95 -712,130 -145,42 -319,139 -371,208 -32,42 -53,102 -67,187 -22,144 48,316 261,640 125,190 193,317 230,428 31,95 60,282 60,392 0,76 4,102 16,114 20,20 25,18 192,-102 73,-52 168,-114 210,-137 83,-47 132,-82 132,-95 0,-4 -14,-17 -32,-28 l -32,-21 29,-11 c 108,-41 305,-52 707,-39 186,6 345,9 353,5 8,-3 15,-15 15,-27 0,-20 3,-21 54,-15 110,13 266,81 426,187 89,58 99,63 170,69 161,14 229,78 523,495 88,124 143,185 169,185 2,0 2,-27 0,-60 -2,-33 0,-60 5,-60 23,0 140,145 212,263 117,192 165,317 271,712 106,394 111,410 135,410 15,0 22,-10 29,-40 13,-53 12,-52 42,-24 40,36 103,133 154,234 25,50 50,90 55,90 6,0 11,-17 13,-37 2,-21 7,-38 12,-38 19,0 93,101 139,190 29,56 87,206 140,363 50,147 114,318 142,380 132,288 199,526 246,867 25,190 25,756 0,975 -23,195 -53,379 -78,485 -46,194 -152,515 -233,706 -130,306 -218,564 -260,764 -48,228 -48,572 1,770 31,128 82,204 166,246 64,33 118,83 187,174 120,158 184,199 503,321 250,96 347,153 347,204 0,41 -42,57 -174,68 -112,9 -385,70 -439,99 -10,5 -27,29 -37,54 -23,53 -99,137 -159,176 -56,35 -72,36 -40,1 24,-26 23,-26 -13,3 -21,16 -42,41 -47,56 -9,26 -14,28 -60,28 -50,0 -101,16 -101,32 0,4 -42,8 -92,7 -88,0 -98,-3 -183,-43 z m 550,-126 c 10,-11 16,-20 13,-20 -3,0 -13,9 -23,20 -10,11 -16,20 -13,20 3,0 13,-9 23,-20 z"
|
||||
id="path1"
|
||||
style="fill:#cc43c5;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 21.315096 18"
|
||||
width="21.315096"
|
||||
height="18"
|
||||
version="1.1"
|
||||
id="svg21"
|
||||
sodipodi:docname="plebchain.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs21" />
|
||||
<sodipodi:namedview
|
||||
id="namedview21"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="19.666667"
|
||||
inkscape:cx="12.762712"
|
||||
inkscape:cy="12.991525"
|
||||
inkscape:window-width="1418"
|
||||
inkscape:window-height="883"
|
||||
inkscape:window-x="1745"
|
||||
inkscape:window-y="10"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg21" />
|
||||
<path
|
||||
d="M 18.625339,11.886754 C 17.668676,9.87076 15.553749,8.748431 12.298294,7.531368 12.258627,7.5164347 11.514296,7.2747022 10.649566,6.9942364 9.6532356,6.6713042 8.5682391,6.7520372 7.6316422,7.220569 L 5.7999816,8.136166 13.616623,17.9338 h 4.316652 l 0.703264,-1.662727 c 0.593598,-1.401862 0.641198,-3.009057 -0.0112,-4.384319 z"
|
||||
id="path5"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="M 16.627546,5.1089093 C 16.710146,4.5484447 16.797412,3.95578 16.838479,3.6790475 17.083011,2.0219197 15.937348,0.48005806 14.28022,0.23552546 12.623092,-0.00900704 11.081231,1.1366559 10.836698,2.7937838 c -0.0406,0.2762657 -0.128333,0.8689305 -0.210932,1.4298619 -0.282799,0.058333 -0.513332,0.2851324 -0.558132,0.5870648 -0.0658,0.441465 0.084,1.0145298 0.370999,1.2072625 0.09707,1.4303286 1.118596,2.778524 2.547992,2.989457 1.429395,0.210933 2.796257,-0.785398 3.302589,-2.1265932 0.330399,-0.1021996 0.639331,-0.6071313 0.703731,-1.0490632 0.0434,-0.302399 -0.111533,-0.5856647 -0.365399,-0.7228643 z"
|
||||
id="path10"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="m 21.193864,14.872477 c -0.0392,-0.501665 -0.310799,-0.956663 -0.684131,-1.294529 l -1.900727,-1.723394 c 0,0 0.172666,0.346732 0.332732,1.276796 L 15.53415,11.550288 c -0.878731,-0.405999 -1.835861,-0.616465 -2.804191,-0.616465 h -0.807331 c -0.87733,0 -1.681394,-0.553464 -1.9338602,-1.393928 C 9.8926348,9.220696 9.819835,8.832897 9.7904351,8.363432 V 8.362965 C 10.503966,7.7651672 10.944031,6.8285703 10.968765,5.8700401 11.247364,5.6651742 11.372897,5.0865094 11.28843,4.648311 11.230564,4.3482453 10.990698,4.1317127 10.706032,4.0850462 10.599632,3.5283147 10.487633,2.93985 10.435366,2.6654509 10.121301,1.0199896 8.5327726,-0.05940684 6.8873113,0.25419216 5.2418501,0.56779106 4.1624536,2.1567859 4.4760526,3.8022471 4.5283191,4.0766462 4.6407854,4.665111 4.7467184,5.2218423 4.4989192,5.3697751 4.355653,5.6595742 4.4125861,5.9596399 4.4956525,6.3983051 4.8255848,6.8901701 5.159717,6.9779032 5.2950499,7.2952355 5.4798493,7.5892345 5.7015152,7.8510336 L 5.7999816,8.136166 5.2171168,11.699621 3.2925898,12.11822 C 3.041524,12.177953 2.8109914,12.278752 2.6061254,12.411752 1.8342613,12.912484 1.5458622,13.865414 1.7731282,14.725478 1.8309946,14.943877 2.8833245,17.9338 2.8833245,17.9338 H 15.133284 l 0.408332,-1.691661 3.480855,0.605265 c 1.184397,0.211399 2.267993,-0.748064 2.171393,-1.974927 z"
|
||||
id="path16"
|
||||
style="fill:#f59119;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="m 12.589026,15.06801 -2.251659,-0.776531 c 0,0 0.96693,-0.1106 0.95853,-0.625798 -4.67e-4,-0.03687 -0.01213,-0.240332 -0.307999,-0.210932 -0.0812,0.0079 -0.734064,0.07327 -1.1927962,0.118999 -0.3028657,0.03033 -0.6015314,0.09007 -0.8927304,0.178733 l -0.1591328,0.04853 -3.5256551,-2.101393 c 0,0 0,0 0.3495322,-0.763464 C 6.1779803,9.600561 5.8004482,8.136166 5.8004482,8.136166 L 1.324663,10.373825 C 0.63539857,10.718224 0.2,11.422889 0.2,12.193819 c 0,0.939397 0.64306455,1.756528 1.5558615,1.977727 l 6.4357789,1.371062 2.5834586,0.955731 c 0.167999,0.0532 0.329932,0.0602 0.517065,0.0033 0.187599,-0.05693 0.246399,-0.09847 0.337398,-0.165199 0.2464,-0.180133 0.706065,-0.536665 1.037864,-0.795664 0.170799,-0.133933 0.125533,-0.402266 -0.0784,-0.472732 z"
|
||||
id="path21"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="11.106925"
|
||||
height="18"
|
||||
viewBox="0 0 2.9387073 4.7624999"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
sodipodi:docname="zapathon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="16.96875"
|
||||
inkscape:cy="9.96875"
|
||||
inkscape:window-width="1406"
|
||||
inkscape:window-height="767"
|
||||
inkscape:window-x="1741"
|
||||
inkscape:window-y="214"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-10.993855,-8.058313)">
|
||||
<g
|
||||
transform="matrix(0.01604881,0,0,-0.01604881,10.573102,13.422443)"
|
||||
id="g10"
|
||||
inkscape:label="Bolt"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path14"
|
||||
style="fill:#c98f19;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
|
||||
d="m 94.833712,155.13322 -8.129016,18.00903 -55.167069,-3.59001 18.868958,-24.19503 44.825648,2.77303 z m 83.691438,80.98476 -8.89814,-11.93085 -11.755,-16.519 -1.505,-2.114 -43.191,-60.691 L 63.081111,74.14633 79.705774,38.853232 C 121.34411,95.870625 162.2814,153.39008 203.27601,210.87013 Z m -22.57414,11.14115 12.32197,61.15287 -21.22997,22.33399 -17.432,-69.53486 -7.0256,-30.70796 27.52164,-9.9823 5.84396,26.73826"
|
||||
sodipodi:nodetypes="cccccccccccccccccccccc" />
|
||||
<path
|
||||
id="path16"
|
||||
style="fill:#fadf05;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
|
||||
d="m 178.78514,236.43412 -5.679,-0.377 -8.065,-0.533 -5.9,-0.389 -33.394,-2.212 6.162,29.388 6.223,29.667 8.95029,38.92409 -54.426393,-75.90909 -61.5969,-86.55 44.1332,2.921 8.9137,0.589 -6.1723,-29.396 -0.9867,-4.71 -1.5106,-7.209 -11.7273,-55.917704 16.3074,22.9175 37.169603,52.234204 43.189,60.693 1.505,2.113 16.907,23.756 h -0.002"
|
||||
sodipodi:nodetypes="cccccccccccccccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -12,11 +12,14 @@ class DamusColors {
|
||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||
static let white = Color("DamusWhite")
|
||||
static let black = Color("DamusBlack")
|
||||
static let brown = Color("DamusBrown")
|
||||
static let yellow = Color("DamusYellow")
|
||||
static let lightGrey = Color("DamusLightGrey")
|
||||
static let mediumGrey = Color("DamusMediumGrey")
|
||||
static let darkGrey = Color("DamusDarkGrey")
|
||||
static let green = Color("DamusGreen")
|
||||
static let purple = Color("DamusPurple")
|
||||
static let deepPurple = Color("DamusDeepPurple")
|
||||
static let blue = Color("DamusBlue")
|
||||
}
|
||||
|
||||
|
||||
@@ -8,119 +8,169 @@
|
||||
import SwiftUI
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
struct Translated: Equatable {
|
||||
let artifacts: NoteArtifacts
|
||||
let language: String
|
||||
}
|
||||
|
||||
enum TranslateStatus: Equatable {
|
||||
case havent_tried
|
||||
case trying
|
||||
case translating
|
||||
case translated(Translated)
|
||||
case not_needed
|
||||
}
|
||||
|
||||
struct TranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
let currentLanguage: String
|
||||
|
||||
@State var translated: TranslateStatus
|
||||
|
||||
@State var checkingTranslationStatus: Bool = false
|
||||
@State var translatable: Bool = true
|
||||
|
||||
@State var noteLanguage: String?
|
||||
@State var show_translated_note: Bool
|
||||
@State var translated_artifacts: NoteArtifacts?
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
|
||||
|
||||
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
|
||||
self._noteLanguage = State(initialValue: translationWithLanguage.language)
|
||||
|
||||
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
|
||||
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
self._translated_artifacts = State(initialValue: nil)
|
||||
self.currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
|
||||
if damus_state.pubkey == event.pubkey && damus_state.is_privkey_user {
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
|
||||
self._translated = State(initialValue: .not_needed)
|
||||
} else if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
|
||||
self._translated = State(initialValue: cached)
|
||||
} else {
|
||||
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
|
||||
self._translated = State(initialValue: initval)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
show_translated_note = true
|
||||
processTranslation()
|
||||
self.translated = .trying
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
|
||||
func processTranslation() {
|
||||
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
|
||||
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
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)
|
||||
|
||||
if self.size == .selected {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
} else {
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(self.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func failed_attempt() {
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .not_needed
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
|
||||
}
|
||||
}
|
||||
|
||||
func attempt_translation() async {
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
guard damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translating
|
||||
}
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
|
||||
guard let translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
guard originalContent != translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
checkingTranslationStatus = true
|
||||
show_translated_note = true
|
||||
|
||||
Task {
|
||||
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
|
||||
DispatchQueue.main.async {
|
||||
guard translationWithLanguage != nil else {
|
||||
noteLanguage = damus_state.translations.targetLanguage
|
||||
checkingTranslationStatus = false
|
||||
show_translated_note = false
|
||||
translatable = false
|
||||
return
|
||||
}
|
||||
|
||||
noteLanguage = translationWithLanguage!.language
|
||||
|
||||
// Render translated note.
|
||||
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
|
||||
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
|
||||
translatable = true
|
||||
|
||||
checkingTranslationStatus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
|
||||
return Group {
|
||||
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
|
||||
show_translated_note = false
|
||||
}
|
||||
.translate_button_style()
|
||||
|
||||
SelectableText(attributedString: artifacts.content, size: self.size)
|
||||
}
|
||||
}
|
||||
|
||||
func MainContent(note_lang: String) -> some View {
|
||||
return Group {
|
||||
if translatable {
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
|
||||
if let languageName, let translated_artifacts, show_translated_note {
|
||||
Translated(lang: languageName, artifacts: translated_artifacts)
|
||||
} else if !damus_state.settings.auto_translate {
|
||||
TranslateButton
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
// Render translated note
|
||||
let translated_blocks = event.get_blocks(content: translated_note)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
|
||||
// and cache it
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
|
||||
MainContent(note_lang: note_lang)
|
||||
.task {
|
||||
if show_translated_note {
|
||||
processTranslation()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch translated {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate {
|
||||
Text("")
|
||||
} else {
|
||||
TranslateButton
|
||||
}
|
||||
case .trying:
|
||||
Text("")
|
||||
case .translating:
|
||||
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
case .translated(let translated):
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts)
|
||||
case .not_needed:
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
.onChange(of: translated) { val in
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// TruncatedText.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TruncatedText: View {
|
||||
let text: CompatibleText
|
||||
let maxChars: Int = 280
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
text.text
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.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 {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 100) {
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
|
||||
.frame(width: 200, height: 200)
|
||||
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
|
||||
.frame(width: 200, height: 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,51 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
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(
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(about)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
Spacer()
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(about)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+52
-24
@@ -66,7 +66,6 @@ struct ContentView: View {
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var damus_state: DamusState? = nil
|
||||
@State var selected_timeline: Timeline? = .home
|
||||
@State var is_thread_open: Bool = false
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var is_profile_open: Bool = false
|
||||
@State var event: NostrEvent? = nil
|
||||
@@ -91,11 +90,19 @@ struct ContentView: View {
|
||||
let sub_id = UUID().description
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
var PostingTimelineView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: FilterState.posts.filter)
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
@@ -176,8 +183,7 @@ struct ContentView: View {
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms)
|
||||
|
||||
case .none:
|
||||
EmptyView()
|
||||
@@ -240,6 +246,11 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
@@ -260,13 +271,7 @@ struct ContentView: View {
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(alignment: .center) {
|
||||
if home.signal.signal != home.signal.max_signal {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
SignalView(state: damus_state!, signal: home.signal)
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
@@ -291,7 +296,7 @@ struct ContentView: View {
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, 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())
|
||||
}
|
||||
@@ -335,10 +340,9 @@ struct ContentView: View {
|
||||
} else if ref.key == "e" {
|
||||
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
active_event = ev
|
||||
open_event(ev: ev)
|
||||
}
|
||||
}
|
||||
thread_open = true
|
||||
}
|
||||
case .filter(let filt):
|
||||
active_search = filt
|
||||
@@ -351,11 +355,6 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.boost)) { notif in
|
||||
current_boost = (notif.object as? NostrEvent)
|
||||
}
|
||||
.onReceive(handle_notify(.open_thread)) { obj in
|
||||
//let ev = obj.object as! NostrEvent
|
||||
//thread.set_active_event(ev)
|
||||
//is_thread_open = true
|
||||
}
|
||||
.onReceive(handle_notify(.reply)) { notif in
|
||||
let ev = notif.object as! NostrEvent
|
||||
self.active_sheet = .reply(ev)
|
||||
@@ -468,6 +467,35 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
home.filter_muted()
|
||||
}
|
||||
.onReceive(handle_notify(.mute_thread)) { notif in
|
||||
home.filter_muted()
|
||||
}
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
home.filter_muted()
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { notif in
|
||||
let local = notif.object as! LossyLocalNotification
|
||||
|
||||
guard let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.open_dm_by_pk(target.pubkey)
|
||||
|
||||
case .like: fallthrough
|
||||
case .zap: fallthrough
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||
is_deleted_account = false
|
||||
@@ -561,7 +589,7 @@ struct ContentView: View {
|
||||
}
|
||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||
if let current_boost {
|
||||
self.damus_state?.pool.send(.event(current_boost))
|
||||
self.damus_state?.postbox.send(current_boost)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
@@ -570,6 +598,8 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
self.isSideBarOpened = false
|
||||
|
||||
self.popToRoot()
|
||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||
|
||||
@@ -609,8 +639,6 @@ struct ContentView: View {
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
let settings = UserSettingsStore()
|
||||
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
@@ -622,7 +650,7 @@ struct ContentView: View {
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: settings,
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts(),
|
||||
@@ -631,7 +659,7 @@ struct ContentView: View {
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
translations: Translations(settings)
|
||||
muted_threads: MutedThreadsManager(keypair: keypair)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class BookmarksManager: ObservableObject {
|
||||
if isBookmarked(ev) {
|
||||
bookmarks = bookmarks.filter { $0 != ev }
|
||||
} else {
|
||||
bookmarks.append(ev)
|
||||
bookmarks.insert(ev, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ struct DamusState {
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [String]
|
||||
let replies: ReplyCounter
|
||||
let translations: Translations
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
}
|
||||
@@ -39,7 +40,5 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
let settings = UserSettingsStore()
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(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: ""), translations: Translations(settings))
|
||||
}
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(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))) }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class DirectMessageModel: ObservableObject {
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
let pubkey: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
|
||||
@@ -29,17 +31,19 @@ class DirectMessageModel: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
init(events: [NostrEvent], our_pubkey: String) {
|
||||
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
init(our_pubkey: String) {
|
||||
init(our_pubkey: String, pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,43 @@
|
||||
import Foundation
|
||||
|
||||
class DirectMessagesModel: ObservableObject {
|
||||
@Published var dms: [(String, DirectMessageModel)] = []
|
||||
@Published var dms: [DirectMessageModel] = []
|
||||
@Published var loading: Bool = false
|
||||
@Published var open_dm: Bool = false
|
||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
|
||||
let our_pubkey: String
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
var message_requests: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in dm.1.is_request }
|
||||
var message_requests: [DirectMessageModel] {
|
||||
return dms.filter { dm in dm.is_request }
|
||||
}
|
||||
|
||||
var friend_dms: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in !dm.1.is_request }
|
||||
var friend_dms: [DirectMessageModel] {
|
||||
return dms.filter { dm in !dm.is_request }
|
||||
}
|
||||
|
||||
func set_active_dm_model(_ model: DirectMessageModel) {
|
||||
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)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||
@@ -29,15 +52,15 @@ class DirectMessagesModel: ObservableObject {
|
||||
return dm
|
||||
}
|
||||
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey)
|
||||
dms.append((pubkey, new))
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
|
||||
dms.append(new)
|
||||
return new
|
||||
}
|
||||
|
||||
func lookup(_ pubkey: String) -> DirectMessageModel? {
|
||||
for dm in dms {
|
||||
if pubkey == dm.0 {
|
||||
return dm.1
|
||||
if pubkey == dm.pubkey {
|
||||
return dm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// DraftModel.swift
|
||||
// DraftsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/12/23.
|
||||
@@ -10,4 +10,5 @@ import Foundation
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||
@Published var medias: [UploadedMedia] = []
|
||||
}
|
||||
|
||||
@@ -74,8 +74,12 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if m.type == type {
|
||||
acc.insert(m.index)
|
||||
if let idx = m.index {
|
||||
acc.insert(idx)
|
||||
}
|
||||
}
|
||||
case .relay:
|
||||
return
|
||||
case .text:
|
||||
return
|
||||
case .hashtag:
|
||||
|
||||
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
|
||||
if ev.known_kind == .contacts {
|
||||
handle_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
case .notice(let msg):
|
||||
|
||||
@@ -62,7 +62,7 @@ class FollowingModel {
|
||||
break
|
||||
case .event(_, let ev):
|
||||
if ev.kind == 0 {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
case .notice(let msg):
|
||||
print("followingmodel notice: \(msg)")
|
||||
|
||||
+186
-103
@@ -8,26 +8,19 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct NewEventsBits {
|
||||
let bits: Int
|
||||
|
||||
init() {
|
||||
bits = 0
|
||||
}
|
||||
|
||||
init (prev: NewEventsBits, setting: Timeline) {
|
||||
self.bits = prev.bits | timeline_bit(setting)
|
||||
}
|
||||
|
||||
init (prev: NewEventsBits, unsetting: Timeline) {
|
||||
self.bits = prev.bits & ~timeline_bit(unsetting)
|
||||
}
|
||||
|
||||
func is_set(_ timeline: Timeline) -> Bool {
|
||||
let notification_bit = timeline_bit(timeline)
|
||||
return (bits & notification_bit) == notification_bit
|
||||
}
|
||||
|
||||
struct NewEventsBits: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let home = NewEventsBits(rawValue: 1 << 0)
|
||||
static let zaps = NewEventsBits(rawValue: 1 << 1)
|
||||
static let mentions = NewEventsBits(rawValue: 1 << 2)
|
||||
static let reposts = NewEventsBits(rawValue: 1 << 3)
|
||||
static let likes = NewEventsBits(rawValue: 1 << 4)
|
||||
static let search = NewEventsBits(rawValue: 1 << 5)
|
||||
static let dms = NewEventsBits(rawValue: 1 << 6)
|
||||
|
||||
static let all = NewEventsBits(rawValue: 0xFFFFFFFF)
|
||||
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||
}
|
||||
|
||||
class HomeModel: ObservableObject {
|
||||
@@ -48,34 +41,27 @@ class HomeModel: ObservableObject {
|
||||
let dms_subid = UUID().description
|
||||
let init_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
var loading: Bool = false
|
||||
|
||||
var signal = SignalModel()
|
||||
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var dms: DirectMessagesModel
|
||||
@Published var events = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var signal: SignalModel = SignalModel()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||
}
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||
filter_muted()
|
||||
self.setup_debouncer()
|
||||
}
|
||||
|
||||
|
||||
var pool: RelayPool {
|
||||
return damus_state.pool
|
||||
}
|
||||
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
var dms: DirectMessagesModel {
|
||||
return damus_state.dms
|
||||
}
|
||||
|
||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||
@@ -86,6 +72,13 @@ class HomeModel: ObservableObject {
|
||||
|
||||
return has_event[sub_id]!.contains(ev_id)
|
||||
}
|
||||
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
}
|
||||
|
||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||
@@ -141,7 +134,7 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert_zap(zap) {
|
||||
if !notifications.insert_zap(zap, damus_state: damus_state) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,7 +145,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap)
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +186,6 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_channel_create(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.channels[ev.id] = ev
|
||||
}
|
||||
|
||||
@@ -204,16 +193,21 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
|
||||
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
|
||||
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
|
||||
events.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
self.dms.dms = dms.dms.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
notifications.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey) &&
|
||||
!damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_delete_event(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.deleted_events.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -235,7 +229,7 @@ class HomeModel: ObservableObject {
|
||||
if let inner_ev = ev.inner_event {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
guard inner_ev.is_valid else {
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -304,10 +298,7 @@ class HomeModel: ObservableObject {
|
||||
break
|
||||
}
|
||||
|
||||
update_signal_from_pool(signal: signal, pool: damus_state.pool)
|
||||
|
||||
print("ws_event \(ev)")
|
||||
|
||||
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
|
||||
case .nostr_event(let ev):
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
@@ -326,7 +317,7 @@ class HomeModel: ObservableObject {
|
||||
case .eose(let sub_id):
|
||||
|
||||
if sub_id == dms_subid {
|
||||
var dms = dms.dms.flatMap { $0.1.events }
|
||||
var dms = dms.dms.flatMap { $0.events }
|
||||
dms.append(contentsOf: incoming_dms)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
|
||||
} else if sub_id == notifications_subid {
|
||||
@@ -454,7 +445,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_metadata_event(_ ev: NostrEvent) {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||
@@ -476,16 +467,17 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if let inner_ev = ev.inner_event {
|
||||
damus_state.events.insert(inner_ev)
|
||||
}
|
||||
|
||||
if !notifications.insert_event(ev) {
|
||||
if !notifications.insert_event(ev, damus_state: damus_state) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -527,15 +519,26 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
self.new_events = notifs
|
||||
if damus_state.settings.dm_notification {
|
||||
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_dm(_ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if !should_debounce_dms {
|
||||
self.incoming_dms.append(ev)
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
self.new_events = notifs
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
return
|
||||
@@ -545,11 +548,7 @@ class HomeModel: ObservableObject {
|
||||
|
||||
dm_debouncer.debounce { [self] in
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
self.new_events = notifs
|
||||
if damus_state.settings.dm_notification,
|
||||
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
|
||||
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
|
||||
}
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
}
|
||||
@@ -562,8 +561,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
|
||||
signal.max_signal = pool.relays.count
|
||||
}
|
||||
|
||||
if signal.signal != pool.num_connecting {
|
||||
signal.signal = signal.max_signal - pool.num_connecting
|
||||
if signal.signal != pool.num_connected {
|
||||
signal.signal = pool.num_connected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,11 +656,7 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
||||
print("-----")
|
||||
}
|
||||
|
||||
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
return
|
||||
}
|
||||
|
||||
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
|
||||
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||
DispatchQueue.main.async {
|
||||
notify(.deleted_account, ())
|
||||
@@ -712,6 +707,47 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
|
||||
}
|
||||
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
|
||||
}
|
||||
|
||||
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
||||
let validated = events.is_event_valid(ev.id)
|
||||
|
||||
switch validated {
|
||||
case .unknown:
|
||||
Task {
|
||||
let result = validate_event(ev: ev)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
events.validation[ev.id] = result
|
||||
guard result == .ok else {
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
case .ok:
|
||||
callback()
|
||||
|
||||
case .bad_id: fallthrough
|
||||
case .bad_sig:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||
guard_valid_event(events: events, ev: ev) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func robohash(_ pk: String) -> String {
|
||||
@@ -852,10 +888,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
}
|
||||
|
||||
for (pk, _) in dms.dms {
|
||||
if pk == the_pk {
|
||||
for model in dms.dms {
|
||||
if model.pubkey == the_pk {
|
||||
found = true
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
|
||||
$0.created_at < $1.created_at
|
||||
}
|
||||
|
||||
@@ -865,8 +901,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
|
||||
if !found {
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
|
||||
dms.dms.append((the_pk, model))
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
|
||||
dms.dms.append(model)
|
||||
inserted = true
|
||||
}
|
||||
|
||||
@@ -893,14 +929,53 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
|
||||
}
|
||||
|
||||
if inserted {
|
||||
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
|
||||
return a.1.events.last!.created_at > b.1.events.last!.created_at
|
||||
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
|
||||
return a.events.last!.created_at > b.events.last!.created_at
|
||||
}
|
||||
}
|
||||
|
||||
return new_events
|
||||
}
|
||||
|
||||
func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
|
||||
guard let kind = ev.known_kind else {
|
||||
return []
|
||||
}
|
||||
|
||||
if kind == .zap {
|
||||
return [.zaps]
|
||||
}
|
||||
|
||||
if kind == .boost {
|
||||
return [.reposts]
|
||||
}
|
||||
|
||||
if kind == .text {
|
||||
return [.mentions]
|
||||
}
|
||||
|
||||
if kind == .like {
|
||||
return [.likes]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
|
||||
switch timeline {
|
||||
case .home:
|
||||
return [.home]
|
||||
case .notifications:
|
||||
if let ev {
|
||||
return determine_event_notifications(ev)
|
||||
}
|
||||
return [.notifications]
|
||||
case .search:
|
||||
return [.search]
|
||||
case .dms:
|
||||
return [.dms]
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to determine if we need to notify the user of new events
|
||||
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
|
||||
@@ -909,7 +984,7 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
|
||||
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
||||
save_last_event(ev, timeline: timeline)
|
||||
if shouldNotify {
|
||||
return NewEventsBits(prev: new_events, setting: timeline)
|
||||
return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,12 +1050,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
|
||||
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)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
@@ -1007,53 +1083,60 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
for block in ev.blocks(damus_state.keypair.privkey) {
|
||||
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
|
||||
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
|
||||
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
|
||||
create_local_notification(displayName: displayName, conversation: justContent, type: type)
|
||||
}
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification,
|
||||
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
|
||||
// Don't show notifications from muted threads.
|
||||
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
|
||||
return
|
||||
}
|
||||
|
||||
if let inner_ev = ev.inner_event {
|
||||
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
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 = 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.inner_event {
|
||||
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 displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
|
||||
let e_ref = ev.referenced_ids.first?.ref_id,
|
||||
let content = damus_state.events.lookup(e_ref)?.content {
|
||||
|
||||
create_local_notification(displayName: displayName, conversation: content, type: type)
|
||||
let evid = ev.referenced_ids.first?.ref_id,
|
||||
let liked_event = damus_state.events.lookup(evid)
|
||||
{
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
|
||||
func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
let content = UNMutableNotificationContent()
|
||||
var title = ""
|
||||
var identifier = ""
|
||||
switch type {
|
||||
case .text:
|
||||
|
||||
let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
|
||||
|
||||
switch notify.type {
|
||||
case .mention:
|
||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
case .boost:
|
||||
case .repost:
|
||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||
identifier = "myBoostNotification"
|
||||
case .like:
|
||||
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
|
||||
identifier = "myLikeNotification"
|
||||
case .dm:
|
||||
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
|
||||
title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
|
||||
identifier = "myDMNotification"
|
||||
default:
|
||||
case .zap:
|
||||
// not handled here
|
||||
break
|
||||
}
|
||||
content.title = title
|
||||
content.body = conversation
|
||||
content.body = notify.content
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = notify.to_lossy().to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
|
||||
@@ -25,6 +25,15 @@ enum MediaUpload {
|
||||
return url.pathExtension
|
||||
}
|
||||
}
|
||||
|
||||
var localURL: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_image: Bool {
|
||||
if case .image = self {
|
||||
|
||||
+102
-10
@@ -21,8 +21,8 @@ enum MentionType {
|
||||
}
|
||||
}
|
||||
|
||||
struct Mention {
|
||||
let index: Int
|
||||
struct Mention: Equatable {
|
||||
let index: Int?
|
||||
let type: MentionType
|
||||
let ref: ReferencedId
|
||||
}
|
||||
@@ -58,12 +58,30 @@ struct LightningInvoice<T> {
|
||||
}
|
||||
}
|
||||
|
||||
enum Block {
|
||||
enum Block: Equatable {
|
||||
static func == (lhs: Block, rhs: Block) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.text(let a), .text(let b)):
|
||||
return a == b
|
||||
case (.mention(let a), .mention(let b)):
|
||||
return a == b
|
||||
case (.hashtag(let a), .hashtag(let b)):
|
||||
return a == b
|
||||
case (.url(let a), .url(let b)):
|
||||
return a == b
|
||||
case (.invoice(let a), .invoice(let b)):
|
||||
return a.string == b.string
|
||||
case (_, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case text(String)
|
||||
case mention(Mention)
|
||||
case hashtag(String)
|
||||
case url(URL)
|
||||
case invoice(Invoice)
|
||||
case relay(String)
|
||||
|
||||
var is_invoice: Invoice? {
|
||||
if case .invoice(let invoice) = self {
|
||||
@@ -114,7 +132,17 @@ func render_blocks(blocks: [Block]) -> String {
|
||||
return blocks.reduce("") { str, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
return str + "#[\(m.index)]"
|
||||
if let idx = m.index {
|
||||
return str + "#[\(idx)]"
|
||||
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
|
||||
return str + "nostr:\(pk)"
|
||||
} else if let note_id = bech32_note_id(m.ref.ref_id) {
|
||||
return str + "nostr:\(note_id)"
|
||||
} else {
|
||||
return str + m.ref.ref_id
|
||||
}
|
||||
case .relay(let relay):
|
||||
return str + relay
|
||||
case .text(let txt):
|
||||
return str + txt
|
||||
case .hashtag(let htag):
|
||||
@@ -177,14 +205,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
|
||||
return nil
|
||||
}
|
||||
return .text(str)
|
||||
} else if b.type == BLOCK_MENTION {
|
||||
return convert_mention_block(ind: b.block.mention, tags: tags)
|
||||
} else if b.type == BLOCK_MENTION_INDEX {
|
||||
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
|
||||
} else if b.type == BLOCK_URL {
|
||||
return convert_url_block(b.block.str)
|
||||
} else if b.type == BLOCK_INVOICE {
|
||||
return convert_invoice_block(b.block.invoice)
|
||||
} else if b.type == BLOCK_MENTION_BECH32 {
|
||||
return convert_mention_bech32_block(b.block.mention_bech32)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -307,6 +337,60 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||
}
|
||||
|
||||
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
|
||||
{
|
||||
switch b.bech32.type {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
let note = b.bech32.data.note;
|
||||
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
let nevent = b.bech32.data.nevent;
|
||||
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nevent.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nevent.relays.relays.0)
|
||||
}
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NPUB:
|
||||
let npub = b.bech32.data.npub
|
||||
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
let nprofile = b.bech32.data.nprofile
|
||||
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nprofile.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nprofile.relays.relays.0)
|
||||
}
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
let nrelay = b.bech32.data.nrelay
|
||||
guard let relay_str = strblock_to_string(nrelay.relay) else {
|
||||
return nil
|
||||
}
|
||||
return .relay(relay_str)
|
||||
|
||||
case NOSTR_BECH32_NADDR:
|
||||
// TODO: wtf do I do with this
|
||||
guard let naddr = strblock_to_string(b.str) else {
|
||||
return nil
|
||||
}
|
||||
return .text("nostr:" + naddr)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
if let desc = b11.description {
|
||||
return .description(String(cString: desc))
|
||||
@@ -319,7 +403,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
{
|
||||
let ind = Int(ind)
|
||||
|
||||
@@ -557,7 +641,7 @@ func parse_mention_type(_ c: String) -> MentionType? {
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
|
||||
var new_tags = tags
|
||||
var blocks: [Block] = []
|
||||
|
||||
@@ -567,6 +651,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
guard let mention_type = parse_mention_type(ref.key) else {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -592,7 +684,7 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||
let tags = post.references.map(refid_to_tag)
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
|
||||
new_ev.calculate_id()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// MutedThreadsManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
|
||||
pk_setting_key(pubkey, key: "muted_threads")
|
||||
}
|
||||
|
||||
func loadMutedThreads(pubkey: String) -> [String] {
|
||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||
return UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
}
|
||||
|
||||
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
|
||||
let uniqueMutedThreads = Array(Set(value))
|
||||
|
||||
if uniqueMutedThreads != currentValue {
|
||||
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class MutedThreadsManager: ObservableObject {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let keypair: Keypair
|
||||
|
||||
private var _mutedThreadsSet: Set<String>
|
||||
private var _mutedThreads: [String]
|
||||
var mutedThreads: [String] {
|
||||
get {
|
||||
return _mutedThreads
|
||||
}
|
||||
set {
|
||||
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
|
||||
self._mutedThreads = newValue
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(keypair: Keypair) {
|
||||
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
|
||||
self._mutedThreadsSet = Set(_mutedThreads)
|
||||
self.keypair = keypair
|
||||
}
|
||||
|
||||
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
|
||||
}
|
||||
|
||||
func updateMutedThread(_ ev: NostrEvent) {
|
||||
let threadId = ev.thread_id(privkey: nil)
|
||||
if isMutedThread(ev, privkey: keypair.privkey) {
|
||||
mutedThreads = mutedThreads.filter { $0 != threadId }
|
||||
_mutedThreadsSet.remove(threadId)
|
||||
notify(.unmute_thread, ev)
|
||||
} else {
|
||||
mutedThreads.append(threadId)
|
||||
_mutedThreadsSet.insert(threadId)
|
||||
notify(.mute_thread, ev)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAll() {
|
||||
mutedThreads = []
|
||||
_mutedThreadsSet.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
for el in zaps {
|
||||
let evid = el.key
|
||||
let zapgrp = el.value
|
||||
|
||||
|
||||
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
||||
notifs.append(notif)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
}
|
||||
|
||||
func insert_event(_ ev: NostrEvent) -> Bool {
|
||||
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
return false
|
||||
}
|
||||
|
||||
func insert_zap(_ zap: Zap) -> Bool {
|
||||
func insert_zap(_ zap: Zap, damus_state: DamusState) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||
}
|
||||
@@ -300,7 +300,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
}
|
||||
|
||||
func flush() -> Bool {
|
||||
func flush(_ damus_state: DamusState) -> Bool {
|
||||
var inserted = false
|
||||
|
||||
for zap in incoming_zaps {
|
||||
|
||||
@@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
} else if ev.known_kind == .contacts {
|
||||
handle_profile_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
//
|
||||
// Translations.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 3/29/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NaturalLanguage
|
||||
|
||||
class Translations: ObservableObject {
|
||||
private static let languageDetectionMinConfidence = 0.5
|
||||
|
||||
@Published var translations: [NostrEvent: String] = [:]
|
||||
@Published var languages: [NostrEvent: String] = [:]
|
||||
|
||||
let settings: UserSettingsStore
|
||||
|
||||
let translator: Translator
|
||||
|
||||
let targetLanguage = currentLanguage()
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
init(_ settings: UserSettingsStore) {
|
||||
self.settings = settings
|
||||
self.translator = Translator(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
|
||||
The detected language will be returned only if it has a 50% or more confidence.
|
||||
This is a best effort guess and could be incorrect.
|
||||
*/
|
||||
func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
|
||||
if let cachedLanguage = languages[event] {
|
||||
return cachedLanguage
|
||||
}
|
||||
|
||||
// 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 = event.blocks(state.keypair.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.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
|
||||
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
// Moreover, speakers of one variant can generally understand other variants.
|
||||
let language = localeToLanguage(locale)
|
||||
languages[event] = language
|
||||
return language
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines.
|
||||
*/
|
||||
private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool {
|
||||
return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
func hasCachedTranslation(_ event: NostrEvent) -> Bool {
|
||||
return languages[event] != nil
|
||||
}
|
||||
|
||||
func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? {
|
||||
if let cachedLanguage = languages[event] {
|
||||
if let cachedTranslation = translations[event] {
|
||||
return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
|
||||
guard shouldTranslate(event, state: state) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let noteLanguage = detectLanguage(event, state: state) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if languages[event] != nil {
|
||||
return cachedTranslation(event)
|
||||
}
|
||||
|
||||
do {
|
||||
guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the translated content is identical to the original content, don't return the translation.
|
||||
if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) {
|
||||
// Nil out the translation as it's the same as the original.
|
||||
translations[event] = nil
|
||||
// Leave an entry so that we don't attempt to translate it again in the future.
|
||||
languages[event] = targetLanguage
|
||||
return nil
|
||||
} else {
|
||||
translations[event] = translationWithLanguage.translation
|
||||
languages[event] = translationWithLanguage.language
|
||||
return translationWithLanguage
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
|
||||
// Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
|
||||
// it's annoying and unexpected for the translation to show up.
|
||||
if event.pubkey == state.pubkey && state.is_privkey_user {
|
||||
return false
|
||||
}
|
||||
|
||||
// Avoid translating if no translation service is configured.
|
||||
switch settings.translation_service {
|
||||
case .none:
|
||||
return false
|
||||
case .libretranslate:
|
||||
if URLComponents(string: settings.libretranslate_url) == nil {
|
||||
return false
|
||||
}
|
||||
case .deepl:
|
||||
if settings.deepl_api_key == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If translation was attempted before, use the results of the cached translation to determine if it should be shown.
|
||||
if languages[event] != nil {
|
||||
return translations[event] != nil
|
||||
}
|
||||
|
||||
// Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
|
||||
guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,12 @@ class UserSettingsStore: ObservableObject {
|
||||
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translate_dms: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_timeline_text: Bool {
|
||||
didSet {
|
||||
@@ -170,6 +176,12 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var notification_indicators: Int {
|
||||
didSet {
|
||||
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_mention_text: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
|
||||
@@ -274,7 +286,9 @@ class UserSettingsStore: ObservableObject {
|
||||
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
|
||||
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
|
||||
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
|
||||
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
|
||||
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
|
||||
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
|
||||
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
|
||||
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
|
||||
disable_animation = should_disable_image_animation()
|
||||
@@ -336,6 +350,17 @@ class UserSettingsStore: ObservableObject {
|
||||
private func clearDeepLApiKey() throws {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
}
|
||||
|
||||
func can_translate(_ pubkey: String) -> Bool {
|
||||
switch translation_service {
|
||||
case .none:
|
||||
return false
|
||||
case .libretranslate:
|
||||
return URLComponents(string: libretranslate_url) != nil
|
||||
case .deepl:
|
||||
return deepl_api_key != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||
|
||||
+13
-5
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Profile: Codable {
|
||||
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?) {
|
||||
@@ -39,7 +39,7 @@ struct Profile: Codable {
|
||||
return s
|
||||
}
|
||||
|
||||
private mutating func set_val<T>(_ key: String, _ val: T?) {
|
||||
private func set_val<T>(_ key: String, _ val: T?) {
|
||||
if val == nil {
|
||||
self.value.removeValue(forKey: key)
|
||||
return
|
||||
@@ -48,7 +48,7 @@ struct Profile: Codable {
|
||||
self.value[key] = AnyCodable.init(val)
|
||||
}
|
||||
|
||||
private mutating func set_str(_ key: String, _ val: String?) {
|
||||
private func set_str(_ key: String, _ val: String?) {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
@@ -110,13 +110,21 @@ struct Profile: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
private var _lnurl: String? = nil
|
||||
var lnurl: String? {
|
||||
if let _lnurl {
|
||||
return _lnurl
|
||||
}
|
||||
|
||||
guard let addr = lud16 ?? lud06 else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if addr.contains("@") {
|
||||
return lnaddress_to_lnurl(addr);
|
||||
// this is a heavy op and is used a lot in views, cache it!
|
||||
let addr = lnaddress_to_lnurl(addr);
|
||||
self._lnurl = addr
|
||||
return addr
|
||||
}
|
||||
|
||||
if !addr.lowercased().hasPrefix("lnurl") {
|
||||
@@ -139,7 +147,7 @@ struct Profile: Codable {
|
||||
self.value = [:]
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.value = try container.decode([String: AnyCodable].self)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case unknown
|
||||
case ok
|
||||
case bad_id
|
||||
case bad_sig
|
||||
|
||||
var is_bad: Bool {
|
||||
return self == .bad_id || self == .bad_sig
|
||||
}
|
||||
}
|
||||
|
||||
struct OtherEvent {
|
||||
@@ -82,7 +86,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
}
|
||||
|
||||
var too_big: Bool {
|
||||
return self.content.count > 16000
|
||||
return self.content.utf8.count > 16000
|
||||
}
|
||||
|
||||
var should_show_event: Bool {
|
||||
@@ -93,14 +97,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return calculate_event_id(ev: self) == self.id
|
||||
}
|
||||
|
||||
var is_valid: Bool {
|
||||
return validity == .ok
|
||||
}
|
||||
|
||||
lazy var validity: ValidationResult = {
|
||||
return .ok //validate_event(ev: self)
|
||||
}()
|
||||
|
||||
private var _blocks: [Block]? = nil
|
||||
func blocks(_ privkey: String?) -> [Block] {
|
||||
if let bs = _blocks {
|
||||
@@ -260,6 +256,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return event_is_reply(self, privkey: privkey)
|
||||
}
|
||||
|
||||
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)
|
||||
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.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
|
||||
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
// Moreover, speakers of one variant can generally understand other variants.
|
||||
return localeToLanguage(locale)
|
||||
}
|
||||
|
||||
public var referenced_ids: [ReferencedId] {
|
||||
return get_referenced_ids(key: "e")
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ final class RelayConnection: WebSocketDelegate {
|
||||
self.isConnected = false
|
||||
|
||||
case .text(let txt):
|
||||
if txt.count > 2000 {
|
||||
if txt.utf8.count > 2000 {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let ev = decode_nostr_event(txt: txt) {
|
||||
DispatchQueue.main.async {
|
||||
@@ -105,8 +105,6 @@ final class RelayConnection: WebSocketDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
print("decode failed for \(txt)")
|
||||
// TODO: trigger event error
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -52,6 +52,10 @@ class RelayPool {
|
||||
var num_connecting: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
|
||||
}
|
||||
|
||||
var num_connected: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
|
||||
}
|
||||
|
||||
func remove_handler(sub_id: String) {
|
||||
self.handlers = handlers.filter { $0.sub_id != sub_id }
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
|
||||
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
|
||||
var profile = Profile()
|
||||
let profile = Profile()
|
||||
profile.deleted = true
|
||||
profile.about = "account deleted"
|
||||
profile.name = "nobody"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// CompatibleAttribute.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class CompatibleText: Equatable {
|
||||
var text: Text
|
||||
var attributed: AttributedString
|
||||
|
||||
init() {
|
||||
self.text = Text("")
|
||||
self.attributed = AttributedString(stringLiteral: "")
|
||||
}
|
||||
|
||||
init(stringLiteral: String) {
|
||||
self.text = Text(stringLiteral)
|
||||
self.attributed = AttributedString(stringLiteral: stringLiteral)
|
||||
}
|
||||
|
||||
init(text: Text, attributed: AttributedString) {
|
||||
self.text = text
|
||||
self.attributed = attributed
|
||||
}
|
||||
|
||||
init(attributed: AttributedString) {
|
||||
self.text = Text(attributed)
|
||||
self.attributed = attributed
|
||||
}
|
||||
|
||||
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
|
||||
return lhs.attributed == rhs.attributed
|
||||
}
|
||||
|
||||
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
|
||||
let combinedText = lhs.text + rhs.text
|
||||
let combinedAttributes = lhs.attributed + rhs.attributed
|
||||
return CompatibleText(text: combinedText, attributed: combinedAttributes)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ class EventCache {
|
||||
private var events: [String: NostrEvent] = [:]
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var translations: [String: TranslateStatus] = [:]
|
||||
private var artifacts: [String: NoteArtifacts] = [:]
|
||||
var validation: [String: ValidationResult] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
|
||||
@@ -24,6 +27,30 @@ class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
func is_event_valid(_ evid: String) -> ValidationResult {
|
||||
guard let result = validation[evid] else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
|
||||
self.translations[evid] = translated
|
||||
}
|
||||
|
||||
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
|
||||
self.artifacts[evid] = artifacts
|
||||
}
|
||||
|
||||
func lookup_artifacts(evid: String) -> NoteArtifacts? {
|
||||
return self.artifacts[evid]
|
||||
}
|
||||
|
||||
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
|
||||
return self.translations[evid]
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
@@ -87,6 +114,8 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
translations = [:]
|
||||
artifacts = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ extension KFOptionSetter {
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
options.loadDiskFileSynchronously = false
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Hashtags.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CustomHashtag {
|
||||
let name: String
|
||||
let offset: CGFloat?
|
||||
let color: Color?
|
||||
|
||||
init(name: String, color: Color? = nil, offset: CGFloat? = nil) {
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
static let coffee = CustomHashtag(name: "coffee", color: DamusColors.brown, offset: -1.0)
|
||||
static let bitcoin = CustomHashtag(name: "bitcoin", color: Color.orange, offset: -3.0)
|
||||
static let nostr = CustomHashtag(name: "nostr", color: DamusColors.purple, offset: -2.0)
|
||||
static let plebchain = CustomHashtag(name: "plebchain", color: DamusColors.deepPurple, offset: -3.0)
|
||||
static let zap = CustomHashtag(name: "zap", color: DamusColors.yellow, offset: -4.0)
|
||||
}
|
||||
|
||||
|
||||
let custom_hashtags: [String: CustomHashtag] = [
|
||||
"bitcoin": CustomHashtag.bitcoin,
|
||||
"btc": CustomHashtag.bitcoin,
|
||||
"nostr": CustomHashtag.nostr,
|
||||
"coffee": CustomHashtag.coffee,
|
||||
"coffeechain": CustomHashtag.coffee,
|
||||
"plebchain": CustomHashtag.plebchain,
|
||||
"zap": CustomHashtag.zap,
|
||||
"zapathon": CustomHashtag.zap,
|
||||
]
|
||||
|
||||
func hashtag_str(_ htag: String) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: "#\(htag)")
|
||||
attributedString.link = URL(string: "damus:t:\(htag)")
|
||||
|
||||
let lowertag = htag.lowercased()
|
||||
|
||||
var text = Text(attributedString)
|
||||
if let custom_hashtag = custom_hashtags[lowertag] {
|
||||
if let col = custom_hashtag.color {
|
||||
attributedString.foregroundColor = col
|
||||
}
|
||||
|
||||
let name = custom_hashtag.name
|
||||
|
||||
if let img = UIImage(named: "\(name)-hashtag") {
|
||||
attributedString = attributedString + " "
|
||||
attributed_string_attach_icon(&attributedString, img: img)
|
||||
}
|
||||
text = Text(attributedString)
|
||||
let img = Image("\(name)-hashtag")
|
||||
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
|
||||
} else {
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
}
|
||||
|
||||
return CompatibleText(text: text, attributed: attributedString)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// LocalNotification.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LossyLocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event_id: String
|
||||
|
||||
func to_user_info() -> [AnyHashable: Any] {
|
||||
return [
|
||||
"type": self.type.rawValue,
|
||||
"evid": self.event_id
|
||||
]
|
||||
}
|
||||
|
||||
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
|
||||
let target_id = user_info["evid"] as! String
|
||||
let typestr = user_info["type"] as! String
|
||||
let type = LocalNotificationType(rawValue: typestr)!
|
||||
|
||||
return LossyLocalNotification(type: type, event_id: target_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event: NostrEvent
|
||||
let target: NostrEvent
|
||||
let content: String
|
||||
|
||||
func to_lossy() -> LossyLocalNotification {
|
||||
return LossyLocalNotification(type: self.type, event_id: self.target.id)
|
||||
}
|
||||
}
|
||||
|
||||
enum LocalNotificationType: String {
|
||||
case dm
|
||||
case like
|
||||
case mention
|
||||
case repost
|
||||
case zap
|
||||
}
|
||||
@@ -22,14 +22,6 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
|
||||
return bundle.localizedString(forKey: key, value: fallback, table: nil)
|
||||
}
|
||||
|
||||
func currentLanguage() -> String {
|
||||
if #available(iOS 16, *) {
|
||||
return Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
return Locale.current.languageCode ?? "en"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the variant part of a locale code so that it contains only the language code.
|
||||
*/
|
||||
|
||||
@@ -104,6 +104,15 @@ extension Notification.Name {
|
||||
static var zapping: Notification.Name {
|
||||
return Notification.Name("zapping")
|
||||
}
|
||||
static var mute_thread: Notification.Name {
|
||||
return Notification.Name("mute_thread")
|
||||
}
|
||||
static var unmute_thread: Notification.Name {
|
||||
return Notification.Name("unmute_thread")
|
||||
}
|
||||
static var local_notification: Notification.Name {
|
||||
return Notification.Name("local_notification")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
@@ -20,28 +20,18 @@ public struct Translator {
|
||||
self.userSettingsStore = userSettingsStore
|
||||
}
|
||||
|
||||
/**
|
||||
Translates a string from source language to target language.
|
||||
If the translation provider supports its own language detection, it may determine the source language by itself that could be
|
||||
different from what is passed in as the sourceLanguage argument.
|
||||
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
|
||||
If the translation was unable to be fetched for whatever reason, nil is returned.
|
||||
*/
|
||||
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
|
||||
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
switch userSettingsStore.translation_service {
|
||||
case .libretranslate:
|
||||
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .deepl:
|
||||
return try await translateWithDeepL(text, to: targetLanguage)
|
||||
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .none:
|
||||
return nil
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
|
||||
*/
|
||||
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
|
||||
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
@@ -61,15 +51,10 @@ public struct Translator {
|
||||
let translatedText: String
|
||||
}
|
||||
let response: Response = try await decodedData(for: request)
|
||||
let translation = response.translatedText
|
||||
|
||||
return TranslationWithLanguage(translation: translation, language: targetLanguage)
|
||||
return response.translatedText
|
||||
}
|
||||
|
||||
/**
|
||||
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
|
||||
*/
|
||||
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
|
||||
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
if userSettingsStore.deepl_api_key == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -83,9 +68,10 @@ public struct Translator {
|
||||
|
||||
struct RequestBody: Encodable {
|
||||
let text: [String]
|
||||
let source_lang: String
|
||||
let target_lang: String
|
||||
}
|
||||
let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
|
||||
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
|
||||
request.httpBody = try encoder.encode(body)
|
||||
|
||||
struct Response: Decodable {
|
||||
@@ -97,13 +83,7 @@ public struct Translator {
|
||||
}
|
||||
|
||||
let response: Response = try await decodedData(for: request)
|
||||
|
||||
if response.translations.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let translation = response.translations.map { $0.text }.joined(separator: " ")
|
||||
return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
|
||||
return response.translations.map { $0.text }.joined(separator: " ")
|
||||
}
|
||||
|
||||
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
|
||||
@@ -124,11 +104,6 @@ public struct Translator {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TranslationWithLanguage {
|
||||
let translation: String
|
||||
let language: String
|
||||
}
|
||||
|
||||
private extension URLSession {
|
||||
func data(for request: URLRequest) async throws -> Data {
|
||||
var task: URLSessionDataTask?
|
||||
|
||||
@@ -48,13 +48,13 @@ struct EventActionBar: View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) {
|
||||
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.reply, event)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.replied ? Color.blue : Color.gray)
|
||||
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -122,9 +122,9 @@ enum MediaUploader: String, CaseIterable, Identifiable {
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return .init(index: -1, tag: "nostrBuild", displayName: NSLocalizedString("NostrBuild", comment: "Dropdown option label for system default for NostrBuild image uploader."))
|
||||
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
|
||||
case .nostrImg:
|
||||
return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader."))
|
||||
return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,20 @@ import SwiftUI
|
||||
|
||||
struct DMChatView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
@EnvironmentObject var dms: DirectMessageModel
|
||||
@ObservedObject var dms: DirectMessageModel
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
|
||||
var pubkey: String {
|
||||
dms.pubkey
|
||||
}
|
||||
|
||||
var Messages: some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks)}
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)}
|
||||
}
|
||||
EndBlock(height: 80)
|
||||
}
|
||||
@@ -120,7 +123,7 @@ struct DMChatView: View {
|
||||
func send_message() {
|
||||
let tags = [["p", pubkey]]
|
||||
let post_blocks = parse_post_blocks(content: dms.draft)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: true)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
|
||||
guard let dm = create_dm(content, to_pk: pubkey, tags: post_tags.tags, keypair: damus_state.keypair) else {
|
||||
@@ -177,10 +180,9 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
|
||||
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey")
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk")
|
||||
|
||||
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
|
||||
.environmentObject(model)
|
||||
DMChatView(damus_state: test_damus_state(), dms: model)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ struct DMView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var dm_options: EventViewOptions {
|
||||
if self.damus_state.settings.translate_dms {
|
||||
return []
|
||||
}
|
||||
|
||||
return [.no_translate]
|
||||
}
|
||||
|
||||
var DM: some View {
|
||||
HStack {
|
||||
if is_ours {
|
||||
@@ -33,7 +41,7 @@ struct DMView: View {
|
||||
|
||||
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: [])
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: dm_options)
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
.padding([.bottom], 25)
|
||||
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
|
||||
|
||||
@@ -16,21 +16,12 @@ struct DirectMessagesView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
@State var dm_type: DMType = .friend
|
||||
@State var open_dm: Bool = false
|
||||
@State var pubkey: String = ""
|
||||
@EnvironmentObject var model: DirectMessagesModel
|
||||
@State var active_model: DirectMessageModel
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
|
||||
}
|
||||
@ObservedObject var model: DirectMessagesModel
|
||||
|
||||
func MainContent(requests: Bool) -> some View {
|
||||
ScrollView {
|
||||
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
|
||||
.environmentObject(active_model)
|
||||
NavigationLink(destination: chat, isActive: $open_dm) {
|
||||
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
|
||||
NavigationLink(destination: chat, isActive: $model.open_dm) {
|
||||
EmptyView()
|
||||
}
|
||||
LazyVStack(spacing: 0) {
|
||||
@@ -38,7 +29,7 @@ struct DirectMessagesView: View {
|
||||
EmptyTimelineView()
|
||||
} else {
|
||||
let dms = requests ? model.message_requests : model.friend_dms
|
||||
ForEach(dms, id: \.0) { tup in
|
||||
ForEach(dms, id: \.pubkey) { tup in
|
||||
MaybeEvent(tup)
|
||||
.padding(.top, 10)
|
||||
|
||||
@@ -51,14 +42,20 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
|
||||
var options: EventViewOptions {
|
||||
if self.damus_state.settings.translate_dms {
|
||||
return [.truncate_content, .no_action_bar]
|
||||
}
|
||||
|
||||
return [.truncate_content, .no_action_bar, .no_translate]
|
||||
}
|
||||
|
||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||
Group {
|
||||
if let ev = tup.1.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: tup.0)
|
||||
if let ev = model.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
.onTapGesture {
|
||||
pubkey = tup.0
|
||||
active_model = tup.1
|
||||
open_dm = true
|
||||
self.model.open_dm_by_model(model)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
@@ -98,8 +95,6 @@ struct DirectMessagesView_Previews: PreviewProvider {
|
||||
kind: 4,
|
||||
tags: [])
|
||||
let ds = test_damus_state()
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
|
||||
DirectMessagesView(damus_state: ds)
|
||||
.environmentObject(model)
|
||||
DirectMessagesView(damus_state: ds, model: ds.dms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
let PPM_SIZE: CGFloat = 80.0
|
||||
let BANNER_HEIGHT: CGFloat = 150.0;
|
||||
@@ -197,6 +198,9 @@ struct EditMetadataView: View {
|
||||
TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onReceive(Just(nip05)) { newValue in
|
||||
self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}, header: {
|
||||
Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.")
|
||||
}, footer: {
|
||||
@@ -227,6 +231,7 @@ struct EditMetadataView: View {
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(edges: .top)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
func uploadedProfilePicture(image_url: URL?) {
|
||||
|
||||
+26
-53
@@ -14,28 +14,6 @@ enum EventViewKind {
|
||||
case selected
|
||||
}
|
||||
|
||||
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
switch size {
|
||||
case .small:
|
||||
return .body
|
||||
case .normal:
|
||||
return .body
|
||||
case .selected:
|
||||
return .custom("selected", size: 21.0)
|
||||
}
|
||||
}
|
||||
|
||||
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
|
||||
switch size {
|
||||
case .small:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .normal:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .selected:
|
||||
return .preferredFont(forTextStyle: .title2)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventView: View {
|
||||
let event: NostrEvent
|
||||
let options: EventViewOptions
|
||||
@@ -44,25 +22,11 @@ struct EventView: View {
|
||||
|
||||
@EnvironmentObject var action_bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String? = nil, options: EventViewOptions = []) {
|
||||
self.event = event
|
||||
self.options = options
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
self.event = event
|
||||
self.options = []
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String) {
|
||||
self.event = event
|
||||
self.options = [.no_action_bar]
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.pubkey = pubkey ?? event.pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -105,19 +69,6 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
||||
return false
|
||||
}
|
||||
|
||||
func event_validity_color(_ validation: ValidationResult) -> some View {
|
||||
Group {
|
||||
switch validation {
|
||||
case .ok:
|
||||
EmptyView()
|
||||
case .bad_id:
|
||||
Color.orange.opacity(0.4)
|
||||
case .bad_sig:
|
||||
Color.red.opacity(0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pubkey_context_menu(bech32_pubkey: String) -> some View {
|
||||
return self.contextMenu {
|
||||
@@ -129,9 +80,9 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) -> some View {
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) -> some View {
|
||||
return self.contextMenu {
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -156,6 +107,28 @@ func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
|
||||
return model
|
||||
}
|
||||
|
||||
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
switch size {
|
||||
case .small:
|
||||
return .body
|
||||
case .normal:
|
||||
return .body
|
||||
case .selected:
|
||||
return .custom("selected", size: 21.0)
|
||||
}
|
||||
}
|
||||
|
||||
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
|
||||
switch size {
|
||||
case .small:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .normal:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .selected:
|
||||
return .preferredFont(forTextStyle: .title2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct EventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
@@ -73,7 +73,7 @@ struct BuilderEventView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let event = event {
|
||||
if let event {
|
||||
let ev = event.inner_event ?? event
|
||||
let thread = ThreadModel(event: ev, damus_state: damus)
|
||||
let dest = ThreadView(state: damus, thread: thread)
|
||||
|
||||
@@ -23,7 +23,7 @@ struct EmbeddedEventView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks)
|
||||
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ struct EventMenuContext: View {
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -19,7 +20,7 @@ struct EventMenuContext: View {
|
||||
HStack {
|
||||
Menu {
|
||||
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
|
||||
|
||||
} label: {
|
||||
Label("", systemImage: "ellipsis")
|
||||
@@ -36,14 +37,20 @@ struct MenuItems: View {
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
@State private var isBookmarked: Bool = false
|
||||
@State private var isMutedThread: Bool = false
|
||||
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
|
||||
self._isMutedThread = State(initialValue: muted_thread)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.muted_threads = muted_threads
|
||||
self.event = event
|
||||
self.keypair = keypair
|
||||
self.target_pubkey = target_pubkey
|
||||
@@ -86,6 +93,19 @@ struct MenuItems: View {
|
||||
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
|
||||
}
|
||||
|
||||
if event.known_kind != .dm {
|
||||
Button {
|
||||
self.muted_threads.updateMutedThread(event)
|
||||
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
|
||||
isMutedThread = muted
|
||||
} label: {
|
||||
let imageName = isMutedThread ? "speaker" : "speaker.slash"
|
||||
let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.")
|
||||
let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.")
|
||||
Label(isMutedThread ? unmuteThreadString : muteThreadString, systemImage: imageName)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
} label: {
|
||||
@@ -104,7 +124,7 @@ struct MenuItems: View {
|
||||
Button(role: .destructive) {
|
||||
notify(.mute, target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Mute", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
|
||||
Label(NSLocalizedString("Mute User", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ struct SelectedEventView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct EventViewOptions: OptionSet {
|
||||
static let wide = EventViewOptions(rawValue: 1 << 3)
|
||||
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
|
||||
static let pad_content = EventViewOptions(rawValue: 1 << 5)
|
||||
static let no_translate = EventViewOptions(rawValue: 1 << 6)
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
@@ -36,7 +37,6 @@ struct TextEvent: View {
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
@@ -108,7 +108,7 @@ struct TextEvent: View {
|
||||
}
|
||||
|
||||
var ContextButton: some View {
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,20 @@ struct FollowUserView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
static let markdown = Markdown()
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
HStack {
|
||||
UserView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
||||
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +40,9 @@ struct FollowersView: View {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(followers.contacts ?? [], id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
|
||||
.onAppear {
|
||||
@@ -57,7 +63,7 @@ struct FollowingView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(following.contacts, id: \.self) { pk in
|
||||
ForEach(following.contacts.reversed(), id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
|
||||
let sourceType: UIImagePickerController.SourceType
|
||||
let pubkey: String
|
||||
@Binding var image_upload_confirm: Bool
|
||||
var imagesOnly: Bool = false
|
||||
let onImagePicked: (URL) -> Void
|
||||
let onVideoPicked: (URL) -> Void
|
||||
@@ -24,15 +25,18 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
private let sourceType: UIImagePickerController.SourceType
|
||||
private let onImagePicked: (URL) -> Void
|
||||
private let onVideoPicked: (URL) -> Void
|
||||
@Binding var image_upload_confirm: Bool
|
||||
|
||||
init(presentationMode: Binding<PresentationMode>,
|
||||
sourceType: UIImagePickerController.SourceType,
|
||||
onImagePicked: @escaping (URL) -> Void,
|
||||
onVideoPicked: @escaping (URL) -> Void) {
|
||||
onVideoPicked: @escaping (URL) -> Void,
|
||||
image_upload_confirm: Binding<Bool>) {
|
||||
_presentationMode = presentationMode
|
||||
self.sourceType = sourceType
|
||||
self.onImagePicked = onImagePicked
|
||||
self.onVideoPicked = onVideoPicked
|
||||
self._image_upload_confirm = image_upload_confirm
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
@@ -51,9 +55,9 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
onImagePicked(editedImageURL)
|
||||
}
|
||||
}
|
||||
presentationMode.dismiss()
|
||||
image_upload_confirm = true
|
||||
}
|
||||
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
@@ -98,7 +102,7 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
onVideoPicked: { videoURL in
|
||||
// Handle the selected video URL
|
||||
onVideoPicked(videoURL)
|
||||
})
|
||||
}, image_upload_confirm: $image_upload_confirm)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,21 +27,27 @@ func timeline_bit(_ timeline: Timeline) -> Int {
|
||||
}
|
||||
}
|
||||
|
||||
func show_indicator(timeline: Timeline, current: NewEventsBits, indicator_setting: Int) -> Bool {
|
||||
if timeline == .notifications {
|
||||
return (current.rawValue & indicator_setting & NewEventsBits.notifications.rawValue) > 0
|
||||
}
|
||||
return (current.rawValue & indicator_setting) == timeline_to_notification_bits(timeline, ev: nil).rawValue
|
||||
}
|
||||
|
||||
struct TabButton: View {
|
||||
let timeline: Timeline
|
||||
let img: String
|
||||
@Binding var selected: Timeline?
|
||||
@Binding var new_events: NewEventsBits
|
||||
@Binding var isSidebarVisible: Bool
|
||||
|
||||
let settings: UserSettingsStore
|
||||
let action: (Timeline) -> ()
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
Tab
|
||||
|
||||
if new_events.is_set(timeline) {
|
||||
if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) {
|
||||
Circle()
|
||||
.size(CGSize(width: 8, height: 8))
|
||||
.frame(width: 10, height: 10, alignment: .topTrailing)
|
||||
@@ -55,8 +61,8 @@ struct TabButton: View {
|
||||
var Tab: some View {
|
||||
Button(action: {
|
||||
action(timeline)
|
||||
new_events = NewEventsBits(prev: new_events, unsetting: timeline)
|
||||
isSidebarVisible = false
|
||||
let bits = timeline_to_notification_bits(timeline, ev: nil)
|
||||
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
|
||||
}) {
|
||||
Label("", systemImage: selected == timeline ? "\(img).fill" : img)
|
||||
.contentShape(Rectangle())
|
||||
@@ -70,18 +76,18 @@ struct TabButton: View {
|
||||
struct TabBar: View {
|
||||
@Binding var new_events: NewEventsBits
|
||||
@Binding var selected: Timeline?
|
||||
@Binding var isSidebarVisible: Bool
|
||||
|
||||
let settings: UserSettingsStore
|
||||
let action: (Timeline) -> ()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Divider()
|
||||
HStack {
|
||||
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("1")
|
||||
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("2")
|
||||
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("3")
|
||||
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("4")
|
||||
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
|
||||
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
|
||||
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
|
||||
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ struct MutelistView: View {
|
||||
|
||||
var body: some View {
|
||||
List(users, id: \.self) { pubkey in
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
||||
.id(pubkey)
|
||||
.swipeActions {
|
||||
RemoveAction(pubkey: pubkey)
|
||||
|
||||
@@ -29,7 +29,6 @@ struct NoteContentView: View {
|
||||
let size: EventViewKind
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
let translatable: Bool
|
||||
|
||||
@State var artifacts: NoteArtifacts
|
||||
@State var preview: LinkViewRepresentable?
|
||||
@@ -40,11 +39,16 @@ struct NoteContentView: View {
|
||||
self.show_images = show_images
|
||||
self.size = size
|
||||
self.options = options
|
||||
self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
|
||||
self._artifacts = State(initialValue: artifacts)
|
||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
||||
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
|
||||
self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
|
||||
if let cache = damus_state.events.lookup_artifacts(evid: event.id) {
|
||||
self._artifacts = State(initialValue: cache)
|
||||
} else {
|
||||
let artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
damus_state.events.store_artifacts(evid: event.id, artifacts: artifacts)
|
||||
self._artifacts = State(initialValue: artifacts)
|
||||
}
|
||||
}
|
||||
|
||||
var truncate: Bool {
|
||||
@@ -56,8 +60,15 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
var truncatedText: some View {
|
||||
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
|
||||
.font(eventviewsize_to_font(size))
|
||||
Group {
|
||||
if truncate {
|
||||
TruncatedText(text: artifacts.content)
|
||||
.font(eventviewsize_to_font(size))
|
||||
} else {
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invoicesView: some View {
|
||||
@@ -88,10 +99,10 @@ struct NoteContentView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if size == .selected {
|
||||
if with_padding {
|
||||
SelectableText(attributedString: artifacts.content, size: self.size)
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
SelectableText(attributedString: artifacts.content, size: self.size)
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
}
|
||||
} else {
|
||||
if with_padding {
|
||||
@@ -102,7 +113,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if translatable {
|
||||
if !options.contains(.no_translate) && (size == .selected || damus_state.settings.auto_translate) {
|
||||
if with_padding {
|
||||
translateView
|
||||
.padding(.horizontal)
|
||||
@@ -151,6 +162,7 @@ struct NoteContentView: View {
|
||||
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
||||
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
case .relay: return
|
||||
case .text: return
|
||||
case .hashtag: return
|
||||
case .url: return
|
||||
@@ -194,21 +206,28 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func hashtag_str(_ htag: String) -> AttributedString {
|
||||
var attributedString = AttributedString(stringLiteral: "#\(htag)")
|
||||
attributedString.link = URL(string: "damus:t:\(htag)")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
return attributedString
|
||||
}
|
||||
enum ImageName {
|
||||
case systemImage(String)
|
||||
case image(String)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> AttributedString {
|
||||
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
|
||||
let attachment = NSTextAttachment()
|
||||
attachment.image = img
|
||||
let attachmentString = NSAttributedString(attachment: attachment)
|
||||
let wrapped = AttributedString(attachmentString)
|
||||
astr.append(wrapped)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
return attributedString
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
|
||||
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
||||
func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
|
||||
switch m.type {
|
||||
case .pubkey:
|
||||
let pk = m.ref.ref_id
|
||||
@@ -217,13 +236,15 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
||||
var attributedString = AttributedString(stringLiteral: "@\(disp)")
|
||||
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
return attributedString
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
case .event:
|
||||
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
|
||||
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
|
||||
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
return attributedString
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,19 +252,25 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state()
|
||||
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
|
||||
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
let artifacts = NoteArtifacts(content: txt, images: [], invoices: [], links: [])
|
||||
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteArtifacts {
|
||||
let content: AttributedString
|
||||
struct NoteArtifacts: Equatable {
|
||||
static func == (lhs: NoteArtifacts, rhs: NoteArtifacts) -> Bool {
|
||||
return lhs.content == rhs.content
|
||||
}
|
||||
|
||||
let content: CompatibleText
|
||||
let images: [URL]
|
||||
let invoices: [Invoice]
|
||||
let links: [URL]
|
||||
|
||||
static func just_content(_ content: String) -> NoteArtifacts {
|
||||
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
return NoteArtifacts(content: txt, images: [], invoices: [], links: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +290,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
|
||||
.count == 1
|
||||
|
||||
var ind: Int = -1
|
||||
let txt: AttributedString = blocks.reduce("") { str, block in
|
||||
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
|
||||
ind = ind + 1
|
||||
|
||||
switch block {
|
||||
@@ -277,10 +304,19 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
|
||||
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
if let next = blocks[safe: ind+1], case .url(let u) = next, is_image_url(u) {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, is_image_url(u) {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
}
|
||||
}
|
||||
return str + AttributedString(stringLiteral: trimmed)
|
||||
|
||||
return str + CompatibleText(stringLiteral: trimmed)
|
||||
case .relay(let relay):
|
||||
return str + CompatibleText(stringLiteral: relay)
|
||||
|
||||
case .hashtag(let htag):
|
||||
return str + hashtag_str(htag)
|
||||
case .invoice(let invoice):
|
||||
@@ -329,36 +365,6 @@ func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepres
|
||||
return LinkViewRepresentable(meta: .linkmeta(meta))
|
||||
}
|
||||
|
||||
struct TruncatedText: View {
|
||||
|
||||
let text: AttributedString
|
||||
let maxChars: Int?
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
Text(truncatedAttributedString ?? text)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
func getTruncatedString() -> AttributedString? {
|
||||
guard let maxChars = maxChars else { return nil }
|
||||
let nsAttributedString = NSAttributedString(text)
|
||||
if nsAttributedString.length < maxChars { return nil }
|
||||
|
||||
let range = NSRange(location: 0, length: maxChars)
|
||||
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
|
||||
|
||||
return AttributedString(truncatedAttributedString) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// trim suffix whitespace and newlines
|
||||
func trim_suffix(_ str: String) -> String {
|
||||
|
||||
@@ -12,6 +12,10 @@ enum NotificationFilterState: String {
|
||||
case zaps
|
||||
case replies
|
||||
|
||||
func is_other( item: NotificationItem) -> Bool {
|
||||
return item.is_zap == nil && item.is_reply == nil
|
||||
}
|
||||
|
||||
func filter(_ item: NotificationItem) -> Bool {
|
||||
switch self {
|
||||
case .all:
|
||||
@@ -27,33 +31,38 @@ enum NotificationFilterState: String {
|
||||
struct NotificationsView: View {
|
||||
let state: DamusState
|
||||
@ObservedObject var notifications: NotificationsModel
|
||||
@State var filter_state: NotificationFilterState
|
||||
@State var filter_state: NotificationFilterState = .all
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(state: DamusState, notifications: NotificationsModel) {
|
||||
self.state = state
|
||||
self._notifications = ObservedObject(initialValue: notifications)
|
||||
self._filter_state = State(initialValue: load_notification_filter_state(pubkey: state.pubkey))
|
||||
var mystery: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name)", comment: "Text telling the user to wake up, where the argument is their display name.")
|
||||
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
|
||||
}
|
||||
.id("what")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $filter_state) {
|
||||
mystery
|
||||
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
NotificationTab(NotificationFilterState.all)
|
||||
.tag(NotificationFilterState.all)
|
||||
.id(NotificationFilterState.all)
|
||||
|
||||
NotificationTab(NotificationFilterState.zaps)
|
||||
.tag(NotificationFilterState.zaps)
|
||||
.id(NotificationFilterState.zaps)
|
||||
|
||||
NotificationTab(NotificationFilterState.replies)
|
||||
.tag(NotificationFilterState.replies)
|
||||
.id(NotificationFilterState.replies)
|
||||
}
|
||||
.onChange(of: filter_state) { val in
|
||||
save_notification_filter_state(pubkey: state.pubkey, state: val)
|
||||
}
|
||||
.onAppear {
|
||||
self.filter_state = load_notification_filter_state(pubkey: state.pubkey)
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
@@ -94,20 +103,20 @@ struct NotificationsView: View {
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
.onReceive(handle_notify(.scroll_to_top)) { notif in
|
||||
let _ = notifications.flush()
|
||||
let _ = notifications.flush(state)
|
||||
self.notifications.should_queue = false
|
||||
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
let _ = notifications.flush()
|
||||
let _ = notifications.flush(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationsView(state: test_damus_state(), notifications: NotificationsModel())
|
||||
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilterState.all)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
-30
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
enum NostrPostResult {
|
||||
case post(NostrPost)
|
||||
@@ -21,9 +22,12 @@ struct PostView: View {
|
||||
@State var attach_media: Bool = false
|
||||
@State var attach_camera: Bool = false
|
||||
@State var error: String? = nil
|
||||
|
||||
@State var uploadedMedias: [UploadedMedia] = []
|
||||
@State var image_upload_confirm: Bool = false
|
||||
@State var originalReferences: [ReferencedId] = []
|
||||
@State var references: [ReferencedId] = []
|
||||
|
||||
@State var mediaToUpload: MediaUpload? = nil
|
||||
|
||||
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
||||
|
||||
@@ -57,7 +61,14 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
|
||||
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
|
||||
|
||||
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
|
||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
|
||||
|
||||
content.append(" " + imagesString + " ")
|
||||
|
||||
let new_post = NostrPost(content: content, references: references, kind: kind)
|
||||
|
||||
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
|
||||
@@ -66,13 +77,15 @@ struct PostView: View {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else {
|
||||
damus_state.drafts.post = NSMutableAttributedString(string: "")
|
||||
uploadedMedias = []
|
||||
damus_state.drafts.medias = []
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
var is_post_empty: Bool {
|
||||
return post.string.allSatisfy { $0.isWhitespace }
|
||||
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
|
||||
}
|
||||
|
||||
var ImageButton: some View {
|
||||
@@ -165,32 +178,23 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding([.bottom], 10)
|
||||
}
|
||||
|
||||
func append_url(_ url: String) {
|
||||
let uploadedImageURL = NSMutableAttributedString(string: url)
|
||||
let combinedAttributedString = NSMutableAttributedString()
|
||||
combinedAttributedString.append(post)
|
||||
if !post.string.hasSuffix(" ") {
|
||||
combinedAttributedString.append(NSAttributedString(string: " "))
|
||||
}
|
||||
combinedAttributedString.append(uploadedImageURL)
|
||||
|
||||
// make sure we have a space at the end
|
||||
combinedAttributedString.append(NSAttributedString(string: " "))
|
||||
post = combinedAttributedString
|
||||
.padding()
|
||||
}
|
||||
|
||||
func handle_upload(media: MediaUpload) {
|
||||
let uploader = get_media_uploader(damus_state.pubkey)
|
||||
|
||||
Task.init {
|
||||
let img = getImage(media: media)
|
||||
let res = await image_upload.start(media: media, uploader: uploader)
|
||||
|
||||
switch res {
|
||||
case .success(let url):
|
||||
append_url(url)
|
||||
guard let url = URL(string: url) else {
|
||||
self.error = "Error uploading image :("
|
||||
return
|
||||
}
|
||||
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img)
|
||||
uploadedMedias.append(uploadedMedia)
|
||||
|
||||
case .failed(let error):
|
||||
if let error {
|
||||
@@ -206,7 +210,7 @@ struct PostView: View {
|
||||
var body: some View {
|
||||
GeometryReader { (deviceSize: GeometryProxy) in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
|
||||
let searching = get_searching_string(post.string)
|
||||
|
||||
TopBar
|
||||
@@ -219,13 +223,19 @@ struct PostView: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top) {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
.padding(.leading, replying_to != nil ? 15 : 0)
|
||||
|
||||
TextEntry
|
||||
}
|
||||
.frame(height: deviceSize.size.height*0.78)
|
||||
.frame(height: uploadedMedias.isEmpty ? deviceSize.size.height*0.78 : deviceSize.size.height*0.2)
|
||||
.id("post")
|
||||
|
||||
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
|
||||
.onChange(of: uploadedMedias) { _ in
|
||||
damus_state.drafts.medias = uploadedMedias
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxHeight: searching == nil ? .infinity : 70)
|
||||
.onAppear {
|
||||
@@ -236,26 +246,35 @@ struct PostView: View {
|
||||
// This if-block observes @ for tagging
|
||||
if let searching {
|
||||
UserSearch(damus_state: damus_state, search: searching, post: $post)
|
||||
.padding(.leading, replying_to != nil ? 15 : 0)
|
||||
.frame(maxHeight: .infinity)
|
||||
} else {
|
||||
Divider()
|
||||
.padding([.bottom], 10)
|
||||
VStack(alignment: .leading) {
|
||||
AttachmentBar
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.sheet(isPresented: $attach_media) {
|
||||
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey) { img in
|
||||
handle_upload(media: .image(img))
|
||||
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
|
||||
self.mediaToUpload = .image(img)
|
||||
} onVideoPicked: { url in
|
||||
handle_upload(media: .video(url))
|
||||
self.mediaToUpload = .video(url)
|
||||
}
|
||||
.alert("Are you sure you want to upload this image?", isPresented: $image_upload_confirm) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
if let mediaToUpload {
|
||||
self.handle_upload(media: mediaToUpload)
|
||||
self.attach_media = false
|
||||
}
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $attach_camera) {
|
||||
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey) { img in
|
||||
// image_upload_confirm isn't handled here, I don't know we need to display it here too tbh
|
||||
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
|
||||
handle_upload(media: .image(img))
|
||||
} onVideoPicked: { url in
|
||||
handle_upload(media: .video(url))
|
||||
@@ -273,6 +292,7 @@ struct PostView: View {
|
||||
}
|
||||
} else {
|
||||
post = damus_state.drafts.post
|
||||
uploadedMedias = damus_state.drafts.medias
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
@@ -284,6 +304,7 @@ struct PostView: View {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.post = NSMutableAttributedString(string : "")
|
||||
damus_state.drafts.medias = uploadedMedias
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
@@ -324,3 +345,76 @@ struct PostView_Previews: PreviewProvider {
|
||||
PostView(replying_to: nil, damus_state: test_damus_state())
|
||||
}
|
||||
}
|
||||
|
||||
struct PVImageCarouselView: View {
|
||||
@Binding var media: [UploadedMedia]
|
||||
|
||||
let deviceWidth: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(media.map({$0.representingImage}), id: \.self) { image in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: media.count == 1 ? deviceWidth*0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.padding(20)
|
||||
.onTapGesture {
|
||||
if let index = media.map({$0.representingImage}).firstIndex(of: image) {
|
||||
media.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate func getImage(media: MediaUpload) -> UIImage {
|
||||
var uiimage: UIImage = UIImage()
|
||||
if media.is_image {
|
||||
// fetch the image data
|
||||
if let data = try? Data(contentsOf: media.localURL) {
|
||||
uiimage = UIImage(data: data) ?? UIImage()
|
||||
}
|
||||
} else {
|
||||
let asset = AVURLAsset(url: media.localURL)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
let time = CMTimeMake(value: 1, timescale: 60) // get the thumbnail image at the 1st second
|
||||
do {
|
||||
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
|
||||
uiimage = UIImage(cgImage: cgImage)
|
||||
} catch {
|
||||
print("No thumbnail: \(error)")
|
||||
}
|
||||
// create a play icon on the top to differentiate if media upload is image or a video, gif is an image
|
||||
let playIcon = UIImage(systemName: "play.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
let size = uiimage.size
|
||||
let scale = UIScreen.main.scale
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
||||
uiimage.draw(at: .zero)
|
||||
let playIconSize = CGSize(width: 60, height: 60)
|
||||
let playIconOrigin = CGPoint(x: (size.width - playIconSize.width) / 2, y: (size.height - playIconSize.height) / 2)
|
||||
playIcon?.draw(in: CGRect(origin: playIconOrigin, size: playIconSize))
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
uiimage = newImage ?? UIImage()
|
||||
}
|
||||
return uiimage
|
||||
}
|
||||
|
||||
struct UploadedMedia: Equatable {
|
||||
let localURL: URL
|
||||
let uploadedURL: URL
|
||||
let representingImage: UIImage
|
||||
}
|
||||
|
||||
@@ -72,19 +72,23 @@ struct UserSearch: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
Divider()
|
||||
if users.count == 0 {
|
||||
EmptyUserSearchView()
|
||||
} else {
|
||||
ForEach(users) { user in
|
||||
UserView(damus_state: damus_state, pubkey: user.pubkey)
|
||||
.onTapGesture {
|
||||
on_user_tapped(user: user)
|
||||
}
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
if users.count == 0 {
|
||||
EmptyUserSearchView()
|
||||
} else {
|
||||
ForEach(users) { user in
|
||||
UserView(damus_state: damus_state, pubkey: user.pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
on_user_tapped(user: user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ struct EditProfilePictureControl: View {
|
||||
|
||||
@State private var show_camera = false
|
||||
@State private var show_library = false
|
||||
|
||||
@State var image_upload_confirm: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button(action: {
|
||||
@@ -44,14 +45,16 @@ struct EditProfilePictureControl: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_camera) {
|
||||
ImagePicker(sourceType: .camera, pubkey: pubkey, imagesOnly: true) { img in
|
||||
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
|
||||
ImagePicker(sourceType: .camera, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
|
||||
handle_upload(media: .image(img))
|
||||
} onVideoPicked: { url in
|
||||
print("Cannot upload videos as profile image")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_library) {
|
||||
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, imagesOnly: true) { img in
|
||||
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
|
||||
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
|
||||
handle_upload(media: .image(img))
|
||||
} onVideoPicked: { url in
|
||||
print("Cannot upload videos as profile image")
|
||||
|
||||
@@ -119,6 +119,7 @@ struct ProfileView: View {
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var is_zoomed: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_qr_code: Bool = false
|
||||
@State var action_sheet_presented: Bool = false
|
||||
@State var filter_state : FilterState = .posts
|
||||
@State var yOffset: CGFloat = 0
|
||||
@@ -213,6 +214,10 @@ struct ProfileView: View {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
|
||||
show_qr_code = true
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
|
||||
@@ -266,8 +271,7 @@ struct ProfileView: View {
|
||||
|
||||
var dmButton: some View {
|
||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
|
||||
.environmentObject(dm_model)
|
||||
let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
|
||||
return NavigationLink(destination: dmview) {
|
||||
Image(systemName: "bubble.left.circle")
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
@@ -465,6 +469,9 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $show_qr_code) {
|
||||
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct QRCodeView: View {
|
||||
let damus_state: DamusState
|
||||
@State var pubkey: String
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var maybe_key: String? {
|
||||
guard let key = bech32_pubkey(damus_state.pubkey) else {
|
||||
guard let key = bech32_pubkey(pubkey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,10 +40,11 @@ struct QRCodeView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .center) {
|
||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||
|
||||
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
|
||||
if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
|
||||
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
|
||||
.padding(.top, 50)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
@@ -119,6 +121,6 @@ struct QRCodeView: View {
|
||||
|
||||
struct QRCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
QRCodeView(damus_state: test_damus_state())
|
||||
QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ struct RelayDetailView: View {
|
||||
|
||||
if let pubkey = nip11.pubkey {
|
||||
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
|
||||
UserView(damus_state: state, pubkey: pubkey)
|
||||
UserViewRow(damus_state: state, pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// SignalView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SignalView: View {
|
||||
let state: DamusState
|
||||
@ObservedObject var signal: SignalModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if signal.signal != signal.max_signal {
|
||||
NavigationLink(destination: RelayConfigView(state: state)) {
|
||||
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct SignalView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SignalView(state: test_damus_state(), signal: SignalModel(signal: 5, max_signal: 10))
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ struct SearchHomeView: View {
|
||||
@State var search: String = ""
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var SearchInput: some View {
|
||||
HStack {
|
||||
HStack{
|
||||
@@ -48,21 +50,20 @@ struct SearchHomeView: View {
|
||||
damus: damus_state,
|
||||
show_friend_icon: true,
|
||||
filter: {
|
||||
if damus_state.muted_threads.isMutedThread($0, privkey: self.damus_state.keypair.privkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if damus_state.settings.show_only_preferred_languages == false {
|
||||
return true
|
||||
}
|
||||
|
||||
// Always show your own posts.
|
||||
if $0.pubkey == damus_state.pubkey {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
|
||||
guard let noteLanguage = damus_state.translations.detectLanguage($0, state: damus_state) else {
|
||||
guard let noteLanguage = $0.note_language(damus_state.keypair.privkey) else {
|
||||
return true
|
||||
}
|
||||
|
||||
return damus_state.translations.preferredLanguages.contains(noteLanguage)
|
||||
return preferredLanguages.contains(noteLanguage)
|
||||
}
|
||||
)
|
||||
.refreshable {
|
||||
|
||||
@@ -11,7 +11,19 @@ struct NotificationSettingsView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
func indicator_binding(_ val: NewEventsBits) -> Binding<Bool> {
|
||||
return Binding.init(get: {
|
||||
(settings.notification_indicators & val.rawValue) > 0
|
||||
}, set: { v in
|
||||
if v {
|
||||
settings.notification_indicators |= val.rawValue
|
||||
} else {
|
||||
settings.notification_indicators &= ~val.rawValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
|
||||
@@ -31,6 +43,17 @@ struct NotificationSettingsView: View {
|
||||
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
Section(header: Text(NSLocalizedString("Notification Dots", comment: "Section header for notification indicator dot settings"))) {
|
||||
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps))
|
||||
.toggleStyle(.switch)
|
||||
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions))
|
||||
.toggleStyle(.switch)
|
||||
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: indicator_binding(.reposts))
|
||||
.toggleStyle(.switch)
|
||||
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: indicator_binding(.likes))
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Notifications")
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
|
||||
@@ -69,6 +69,11 @@ struct TranslationSettingsView: View {
|
||||
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
if settings.translation_service != .none {
|
||||
Toggle(NSLocalizedString("Translate DMs", comment: "Toggle to translate direct messages."), isOn: $settings.translate_dms)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Translation")
|
||||
|
||||
@@ -142,7 +142,7 @@ struct SideMenuView: View {
|
||||
.font(.title)
|
||||
.foregroundColor(textColor())
|
||||
}).fullScreenCover(isPresented: $showQRCode) {
|
||||
QRCodeView(damus_state: damus_state)
|
||||
QRCodeView(damus_state: damus_state, pubkey: damus_state.pubkey)
|
||||
}
|
||||
}
|
||||
.padding(.top, verticalSpacing)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -39,7 +39,7 @@
|
||||
<key>many</key>
|
||||
<string>Followers</string>
|
||||
<key>other</key>
|
||||
<string>Sledují</string>
|
||||
<string>Sledující</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>following_count</key>
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
<string>applinks:damus.io</string>
|
||||
<string>webcredentials:damus.io</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.photos-library</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||
|
||||
@@ -55,6 +55,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
// Display the notification in the foreground
|
||||
completionHandler([.banner, .list, .sound, .badge])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let notification = LossyLocalNotification.from_user_info(user_info: userInfo)
|
||||
notify(.local_notification, notification)
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
func needs_setup() -> Keypair? {
|
||||
|
||||
Binary file not shown.
@@ -61,7 +61,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r reagierten auf einen Beitrag in dem Du markiert warst</string>
|
||||
<string>%2$@ und %1$d andere*r reagierten auf einen Beitrag in dem Du markiert warst</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere reagierten auf einen Beitrag in dem Du markiert warst</string>
|
||||
</dict>
|
||||
@@ -77,7 +77,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r reagierten auf deinen Beitrag</string>
|
||||
<string>%2$@ und %1$d andere*r reagierten auf deinen Beitrag</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere reagierten auf deinen Beitrag</string>
|
||||
</dict>
|
||||
@@ -93,7 +93,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r reagierten auf dein Profil</string>
|
||||
<string>%2$@ und %1$d andere*r reagierten auf dein Profil</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere reagierten auf dein Profil</string>
|
||||
</dict>
|
||||
@@ -141,7 +141,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Antwort an %2$@, %3$@ & %1$d andere:r</string>
|
||||
<string>Antwort an %2$@, %3$@ & %1$d andere*r</string>
|
||||
<key>other</key>
|
||||
<string>Antwort an %2$@, %3$@ & %1$d andere</string>
|
||||
</dict>
|
||||
@@ -157,7 +157,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r teilten einen Beitrag in dem Du markiert warst</string>
|
||||
<string>%2$@ und %1$d andere*r teilten einen Beitrag in dem Du markiert warst</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere teilten ein Beitrag in dem Du markiert warst</string>
|
||||
</dict>
|
||||
@@ -173,7 +173,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r teilten deinen Beitrag</string>
|
||||
<string>%2$@ und %1$d andere*r teilten deinen Beitrag</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere teilten deinen Beitrag</string>
|
||||
</dict>
|
||||
@@ -189,7 +189,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ und %1$d andere:r teilten dein Profil</string>
|
||||
<string>%2$@ und %1$d andere*r teilten dein Profil</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ und %1$d andere teilten dein Profil</string>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user