Compare commits
924 Commits
tx_transla
...
tyiu/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
74e099de8e
|
|||
|
85d5c4eda5
|
|||
|
|
7bf18336d0 | ||
|
|
b131c74ee3 | ||
|
|
b9fed6e4eb | ||
|
|
6910e84fd8 | ||
|
|
5f0c1a59e6 | ||
|
|
d8a9efc6e2 | ||
|
|
185732a633 | ||
|
|
b585e8c21c | ||
|
|
b14858b4b1 | ||
|
|
f236ea52e1 | ||
|
|
9c8391b33b | ||
|
|
89b2382ad7 | ||
|
|
2ce0a771ea | ||
|
|
0436c68ec5 | ||
|
|
88740a2bc7 | ||
|
|
e3f578e48a | ||
|
|
39788bd590 | ||
|
|
8c2c8e283f | ||
|
|
8a473885c7 | ||
|
|
ba1c2cd2b9 | ||
|
|
bedf7e0648 | ||
|
|
14802e334d | ||
|
|
98d11fb71e | ||
|
|
9ca959d8d3 | ||
|
|
584a20ade1 | ||
|
|
67041b22f4 | ||
|
|
825a2f944f | ||
|
|
1aa70efee0 | ||
|
|
e2812a9aa1 | ||
|
|
679779ab3e | ||
|
|
98a630248e | ||
|
2f4e33fc9f
|
|||
|
|
ddf60e78b9 | ||
|
|
08badfc746
|
||
|
|
6dfc39e1ed
|
||
|
|
6655b6ac9d
|
||
|
|
c81c14a761
|
||
|
|
3da12e708f | ||
|
|
3d97dc593e | ||
|
|
c6e5193111 | ||
|
|
61ef709d91 | ||
|
|
f6133814e8 | ||
|
|
1701dbdfb9 | ||
|
|
8fe8611527 | ||
|
|
41f692a0c4 | ||
|
|
e0d4841147 | ||
|
|
1bf171a09e | ||
|
|
c5f3a8e509 | ||
|
|
02c973fbb2 | ||
|
|
4996272942 | ||
|
|
094f63bcff | ||
|
|
f9339440f2 | ||
|
|
997bc38885 | ||
|
|
b9d62e300b | ||
|
|
1282b8f5f6 | ||
|
|
127f1e07e3 | ||
|
|
cc190c3618 | ||
|
|
c9c51c6d4a | ||
|
|
b63159a29f | ||
|
|
647c6f8428 | ||
|
|
c8f5a2cffc | ||
|
|
65e22ea0dc
|
||
|
|
e6ce0ef8a3
|
||
|
|
5c949ea0c2
|
||
|
|
4881d41831
|
||
|
|
af7177c436
|
||
|
|
26cf0380c0
|
||
|
|
499e3f6fd4
|
||
|
|
82b237754a
|
||
|
|
0608222cb0 | ||
|
|
b83204898b | ||
|
|
0dd74fde7f | ||
|
|
7a55ea13e3 | ||
|
|
2815827b9f | ||
|
|
4ac3e71039 | ||
|
|
2596542cb6 | ||
|
|
915f3901a7 | ||
|
|
39f39c7382 | ||
|
|
f2ce146e98 | ||
|
|
9ba3543d91 | ||
|
|
dec5de83b9 | ||
|
|
ab6ec18e35 | ||
| 35ee2f5744 | |||
|
|
42e6281e9b | ||
| ae82114a33 | |||
|
|
87d1cd9095 | ||
|
|
2d679bf087 | ||
|
|
aaa6eb013a | ||
|
|
fce689f0ef | ||
|
|
89f33be4cf | ||
|
|
96b6929235 | ||
|
|
a811a12da3 | ||
|
|
8edb1f289d | ||
|
|
cdbd8f2722 | ||
|
|
069a8f52ca | ||
|
|
7664985768 | ||
|
|
a77b0e1bbc | ||
|
|
d8e516841e | ||
|
|
dc33ad962d | ||
|
|
f7eae9275f | ||
|
|
e5adbb2fee | ||
|
|
88d26dc4c2 | ||
|
|
4d71297941 | ||
|
|
21ba936c72 | ||
|
|
04fa8f1805 | ||
|
|
3076e6ee7f | ||
|
08b7e50bd8
|
|||
|
|
5a238502cb | ||
|
|
b0aac1fc42 | ||
|
|
72b51a81de | ||
|
|
8ec1fa29b1 | ||
|
|
81683f980a | ||
|
|
b9fc3f90d1 | ||
|
|
695699aa10 | ||
|
|
0a4e75bfec | ||
|
|
9fef2f071a | ||
|
|
c03b4cac11 | ||
|
|
b773df1204 | ||
|
|
c7a34379dd | ||
|
|
eabf37e35c | ||
|
|
e11147b217 | ||
|
|
7674f42596 | ||
|
|
8c37c8f008 | ||
|
|
74dbbcf1a2 | ||
|
|
e3283fc8f8 | ||
|
|
54fdcd1c84 | ||
|
|
5e0ff1a6a0 | ||
| 6517dcba3f | |||
|
|
63e28d4d79 | ||
| e5c0400b54 | |||
|
|
c6c47e824a | ||
|
866e93d338
|
|||
|
f75fc7eebe
|
|||
|
|
d19596c17e | ||
|
|
0b40cd127c | ||
|
|
754ee254e9 | ||
|
|
963cb37762 | ||
|
|
00da97307e | ||
|
|
312c798bb5 | ||
|
|
7110650267 | ||
|
|
242c1011d9 | ||
|
|
e203eece85 | ||
|
|
1b60524070 | ||
|
|
d15a2f0401 | ||
|
|
159d0fa2b5 | ||
|
|
61fddf800e | ||
|
|
b6d5b6f45e | ||
|
|
f5ed9cd5d4 | ||
|
|
57006b928b | ||
|
fd596241a2
|
|||
|
|
98f0b2f2d2 | ||
|
|
9a4d93824a | ||
|
|
f76563b354 | ||
|
|
b2ee924692 | ||
|
|
6fc70748fe | ||
|
|
5e972dbf2d | ||
|
|
4ebdd01b6c | ||
|
|
13c0c0d679 | ||
|
|
8297859f18 | ||
|
|
e996d5703b | ||
|
|
dfc397337b | ||
|
|
f84d4516db | ||
|
|
2e34230119 | ||
|
|
cad89525b7 | ||
|
|
d2cf18aeee | ||
|
|
a8ce39fc96 | ||
|
|
ed90139b0c | ||
|
|
022045d916 | ||
|
|
4bda490010 | ||
|
|
97382adb63 | ||
|
|
c582755246 | ||
|
|
44a59e8d57 | ||
|
|
98685645d3 | ||
|
|
14f71f1a1d | ||
|
|
91cb6a6763 | ||
|
|
a65351154b | ||
|
|
2e2b33e21d | ||
|
|
c24b0afb8f | ||
|
|
a357bbe4a6 | ||
|
|
b687006b64 | ||
|
|
1f095b0896 | ||
|
|
4f7ed36a7c | ||
|
|
393809c7d7 | ||
|
|
9091cb1aae | ||
|
|
e78a82e5b7 | ||
|
|
7b0ef5f4a7 | ||
|
|
66a5df68b3 | ||
|
|
fa2344b9ba | ||
|
|
68c018cf44 | ||
|
f367df2225
|
|||
|
|
e0984aab34 | ||
|
|
eabbb12195 | ||
|
|
7b1f4b7701 | ||
|
|
7b6d3ef9df | ||
|
|
bc58686016 | ||
|
|
a574dcb27c | ||
|
|
761982e359 | ||
|
|
57d48a0395 | ||
|
|
4f96c88b9b | ||
|
|
da11bc575a | ||
|
|
cc9532d958 | ||
|
|
35f4e7c78d | ||
|
|
d8c822858a | ||
|
|
ca0c837231 | ||
|
|
38fc5afa44 | ||
| 9b76afae4f | |||
|
|
f911f1646d | ||
|
|
20fd061293 | ||
|
|
3f5262cd5d | ||
|
|
982d15ab4a | ||
|
|
074b6efc0f | ||
|
|
ad0ca6ca1a | ||
|
|
e140cacfdf | ||
|
|
b825aa80d8 | ||
|
|
9ee91553c1 | ||
|
|
7ce862f552 | ||
|
|
231f9d1853 | ||
|
|
63acf11065 | ||
|
|
0502f06ef8 | ||
|
|
d921a40f24 | ||
| 2e82b349b7 | |||
|
|
b0007af030 | ||
|
|
dd5c2d7301 | ||
|
|
27c0fbf453 | ||
|
|
d1ad4dc9ff | ||
|
|
4c58c4ffef | ||
|
|
cb3603fb35 | ||
|
|
6df5288294 | ||
|
|
9e02dac5d0 | ||
|
|
b7d9db5cec | ||
|
|
e46792e596 | ||
|
|
fc65da3473 | ||
|
|
4f15469320 | ||
|
|
3ea3595902 | ||
|
|
3caebd9c63 | ||
|
|
4d4f340ab0 | ||
|
|
6a549e5019 | ||
|
|
52bf47a494 | ||
|
|
aee243d3e0 | ||
|
|
18745403ce | ||
|
|
07a20040a4 | ||
|
|
ef3ef03b7f | ||
|
|
71e3ee4867 | ||
|
|
252a77fd97 | ||
|
|
a611a5d252 | ||
|
|
1533be77d8 | ||
|
|
c05223ca2b | ||
|
|
5d441d3192 | ||
|
|
04bce34297 | ||
|
|
af8ce3d32d | ||
|
|
cabe584938 | ||
|
|
dd511c3061 | ||
|
|
18449c8c0d | ||
|
|
044631b324 | ||
|
|
318b254b5d | ||
|
|
487419d098 | ||
|
|
ba82f19a11 | ||
|
|
cba6b3aef7 | ||
|
|
6872382bb7 | ||
|
|
42ea150d45 | ||
|
|
85f86ee31f | ||
|
|
96decd2392 | ||
|
|
73f7b69654 | ||
|
|
d982bb886e | ||
|
|
9766653969 | ||
|
|
5d91e7e595 | ||
|
|
ae00c103ad | ||
|
|
88aa713729 | ||
|
|
be1c03ad0e | ||
|
|
b2b62828e3 | ||
|
|
d1a77891c7 | ||
|
|
20505236ae | ||
|
|
094ac34135 | ||
|
|
6b6743fcbb | ||
|
|
8059408d5f | ||
|
|
04fa4edad8 | ||
|
|
6fffe250c2 | ||
|
|
1e7d9a6373 | ||
|
|
21989719fc | ||
|
|
d5e4866c55 | ||
|
|
f305df3471 | ||
|
|
21320367b1 | ||
|
|
82723faf33 | ||
|
|
48434f83ae | ||
|
|
083d0fa0e5 | ||
| d5a646f9ce | |||
|
38a1ad7611
|
|||
|
|
9bc3860f00 | ||
|
|
35f5ac04b4 | ||
|
|
75b73718d1 | ||
|
|
29cacebe58 | ||
|
|
84ae914bcc | ||
|
|
ef5f3ae649 | ||
|
|
f8068a42e5 | ||
|
|
bdde33bb51 | ||
|
|
e3b602df13 | ||
|
|
38b17f1acd | ||
|
|
575b91554c | ||
|
|
f36bc84618 | ||
|
|
d54c9b7d12 | ||
|
2c6647c95a
|
|||
|
|
3c2f281c6d | ||
|
|
4ba63b0dbd | ||
|
|
e2df7d5df6 | ||
|
|
0dfea0680f | ||
|
|
6cc34632fd | ||
|
|
dffb60a601 | ||
|
|
df076b03fd | ||
|
|
fc83cd4db7 | ||
|
|
e01761ce72 | ||
|
|
efc50f5b18 | ||
|
|
10c9e8ddbc | ||
|
|
f88718d56e | ||
|
|
b6a7f52596 | ||
|
|
cff98161ee | ||
|
|
8a70240968 | ||
|
|
a4855775ef | ||
|
|
06c2741bf4 | ||
|
|
721bb9abf5 | ||
|
|
89bb293acd | ||
|
|
f9c330aebf | ||
|
|
ffbfcd36f5 | ||
|
|
52f568f9b3 | ||
|
|
1c2a7db328 | ||
|
|
3110abc65b | ||
|
|
a9f62960ec | ||
|
|
150bbb1eb2 | ||
|
|
0aff41d384 | ||
|
|
3fec9dd209 | ||
|
|
a560d50366 | ||
|
|
174f7f6cc5 | ||
| a325a3c064 | |||
|
|
d0a6c2e2e4 | ||
|
|
b58baca227 | ||
|
|
5423704980 | ||
|
|
241ed1041d | ||
|
|
5134004ff7 | ||
|
|
071a4209ea | ||
|
|
7f385b2e7e | ||
|
|
502c4daf6f | ||
|
|
ffe2c7284a | ||
|
|
6b1f57d6d0 | ||
|
|
77f5268336 | ||
| c72c0079cc | |||
|
|
5ab1d6294c | ||
|
|
2f90f2d4b7 | ||
|
|
7c2e8a6cc5 | ||
|
|
1288732e5d | ||
|
|
4a6c6a65ab | ||
|
|
0f29d67e1f | ||
|
|
9fd2f51971 | ||
|
|
386bae64ca | ||
|
|
4b5c217213 | ||
|
240fda2429
|
|||
|
bacd9b3c38
|
|||
|
0152286859
|
|||
|
|
06e9a1b392
|
||
|
|
483730af18
|
||
|
|
23229015a6
|
||
|
|
7ab95583df
|
||
|
|
b7a48a24e9
|
||
|
04028d9cff
|
|||
|
6918fb46cf
|
|||
|
|
2b854ef9b7
|
||
|
|
5eb61f1ac1
|
||
|
|
c3bbf7aa8f
|
||
|
|
1e52d958ac
|
||
|
|
5252e5f5bb
|
||
|
|
71d5625f04
|
||
|
|
990e783c30
|
||
|
|
3602189133
|
||
|
|
3ca9acdf34
|
||
|
|
2036d5843b
|
||
|
|
2d3bd11d56
|
||
|
|
a715987e71
|
||
|
|
0303031445
|
||
|
|
d6ae9a5d79
|
||
|
|
356bd06e6a
|
||
|
|
e757bdca90
|
||
|
|
ff1b4d724d
|
||
|
|
b8614f055c
|
||
|
|
63ab151a5e
|
||
|
|
fd9d4deb44
|
||
|
|
a5c719673e
|
||
|
|
2bf3c6718d
|
||
|
|
3f3e59488a
|
||
|
|
b1b4b5b6c9
|
||
|
|
7455665672
|
||
|
|
9f22234926
|
||
|
|
21a8a4e96f
|
||
|
|
c635f3d77a
|
||
|
|
2e9a4388b9
|
||
|
|
4a1949eeb8
|
||
|
|
b4fcb58bcb
|
||
|
|
7d852eb33b
|
||
|
|
a448f610c0
|
||
|
|
8cc561b8c6
|
||
|
|
01630d0a4c
|
||
|
16156f4d9a
|
|||
|
f840fe9c80
|
|||
|
77bcd1b715
|
|||
|
|
75fd8de456 | ||
|
|
71f7ea47df | ||
|
|
64b1a57918 | ||
|
|
6c63f8f22a | ||
|
|
673358408a | ||
|
|
e4dd585754 | ||
|
|
436d20dfbd | ||
|
|
810b3e1fa5 | ||
|
|
75fb0d19e2 | ||
|
|
a2749eaaaa | ||
|
|
83c9289345 | ||
|
|
4c3a83772e | ||
| 5cd4c2d75e | |||
|
|
85e797a054 | ||
|
|
9f52e2c246 | ||
|
|
0210ae5d61 | ||
|
|
e5749c8748 | ||
|
|
8b9958a4ad | ||
|
|
87a0bdac94 | ||
|
|
37b964c296 | ||
|
|
b1a2b47116 | ||
|
|
af6f88ab17 | ||
|
|
647495dbc0 | ||
|
|
826fd1ef33 | ||
|
|
54dd2035a1 | ||
|
|
587819c8eb | ||
|
|
8954c1c245 | ||
|
|
19a421604c | ||
|
|
68b57d8b99 | ||
|
|
f3056653db | ||
|
|
6196279d2b | ||
|
|
f213420b41 | ||
|
|
b4140dc5f2 | ||
|
1b27e9041f
|
|||
|
|
795577a0a1 | ||
|
|
d5c45dc8ba | ||
|
|
603a5a1814 | ||
| 06a1a9aba6 | |||
|
|
ff1815cce0 | ||
|
|
0bdec912f8 | ||
|
|
6d8312fa57 | ||
|
|
f6d56179eb | ||
|
|
193e922c9c | ||
|
|
a1a89dc98e | ||
|
|
3e764e75e4 | ||
|
|
7c563cb0ae | ||
|
a328b0d1a8
|
|||
|
|
5018b9aa1e | ||
|
|
1f6657e471 | ||
|
|
062b5dc040 | ||
|
|
390c9162ae | ||
|
|
94f66adf8d | ||
|
|
d547dade04 | ||
|
|
94a67adff9 | ||
|
|
29f192c377 | ||
|
4e67c88607
|
|||
|
|
42200c347b | ||
|
36f05ccaed
|
|||
|
98a1b95d12
|
|||
|
|
4cdef502e9 | ||
|
|
ae2e70ba7d | ||
|
|
1b4e54582f | ||
|
|
909148f0be | ||
|
|
c100c6db47 | ||
|
|
8d3fb397f7 | ||
|
|
f8742a609c | ||
|
|
d55d0d61ed | ||
|
|
cf90480501 | ||
|
|
f0075904c2 | ||
|
a41acc12e7
|
|||
|
|
1e22984d52 | ||
| 9080e4efae | |||
|
6488634eda
|
|||
|
355cd1283c
|
|||
|
|
6ed9c408f9 | ||
|
|
5f52e6f62f | ||
|
|
59211bb4fd | ||
|
|
6d634763c5 | ||
|
|
49cf56f4c2 | ||
|
|
98c7bf5afc | ||
| 70a7239cfd | |||
|
|
0e83632896 | ||
|
|
f0df4aa218 | ||
|
|
bb9fc6f905 | ||
| f69e0c660a | |||
|
|
9089246b6b | ||
|
|
237c939639 | ||
|
|
4f86361b63 | ||
|
|
209ad71ff3 | ||
|
|
c728850524 | ||
| bc638f79f6 | |||
|
|
47a6f7ff38 | ||
|
|
6653798d27 | ||
|
|
e9ea96ffb6 | ||
|
59ccde9c38
|
|||
|
f9be7b166c
|
|||
|
|
2366089896 | ||
|
|
4f2bacfaab | ||
|
|
5a8b29b5cc | ||
|
|
679c0ac424 | ||
|
|
48050f5e69 | ||
|
|
b5c967e161 | ||
|
|
715d4aa35d | ||
|
|
4b54278378 | ||
|
|
6e700e5726 | ||
|
|
18ad113198 | ||
|
|
7415671900 | ||
|
|
d5b6d935e8 | ||
| 543fd67f35 | |||
|
|
c24689d3ef | ||
| bf7120dc08 | |||
|
|
dbe938ad9b | ||
|
|
289d55b918 | ||
|
|
771fa845e3 | ||
|
|
5f1545b86a | ||
|
|
9a95967a81 | ||
|
|
fe444228e6 | ||
|
|
504108da75 | ||
|
|
d43a2ff92d | ||
|
|
10596ddb09 | ||
|
|
989684cd37 | ||
|
|
9ab03034a2 | ||
| c602c754f8 | |||
|
c5db887ab1
|
|||
|
|
0563ec8bf8 | ||
|
|
29c4170833 | ||
|
|
3c629621eb | ||
|
|
09f12845c0 | ||
|
|
b882a96206 | ||
|
|
fb82cc0531 | ||
|
|
90cd48ead7 | ||
|
|
f71b67f036 | ||
|
4406e44424
|
|||
|
|
ae6608cf7d | ||
|
|
04759107a2 | ||
|
|
552402f2b5 | ||
| 852609ee30 | |||
|
|
1e44d97a97 | ||
| 567303e680 | |||
| 7d1bac4028 | |||
|
|
eae844e081 | ||
|
|
140b0e4fc4 | ||
| 0b476faff7 | |||
|
|
53ec89551b | ||
|
|
638052492d | ||
|
|
45f8c37498 | ||
|
|
f96ad99790 | ||
|
|
1f79c20973 | ||
| e8b23daa3d | |||
|
|
a2eb77a5e9 | ||
|
|
29a8206586 | ||
|
|
07676a1f95 | ||
|
|
79ca3b2262 | ||
|
|
ba8425dedb | ||
|
|
4faf63f29d | ||
|
|
84ad0e03d0 | ||
|
|
7d3d23def3 | ||
|
|
ac1a5d237e | ||
|
|
cfcd799d63 | ||
|
|
351b32308f | ||
|
|
5a4299edaa | ||
|
|
99b619e011 | ||
|
|
d5ee9e4780 | ||
|
|
ced5b4974f | ||
| 9be55b08fd | |||
|
|
ac5f39a922 | ||
|
|
0e9691ae7a | ||
| 1441d339a7 | |||
|
|
2517132041 | ||
|
|
71acb16387 | ||
|
|
9e2e8595e8 | ||
|
|
1a2e9464af | ||
|
|
63dd39c7e4 | ||
|
|
40be9885c5 | ||
|
|
331d7e9792 | ||
|
|
d21613a765 | ||
|
|
7780120504 | ||
|
|
1696e0365e | ||
|
|
006f8d79e0 | ||
|
|
135432e03c | ||
|
|
1fd4d4d950 | ||
| 7d406fd75f | |||
|
|
0902548336 | ||
|
|
09547529ad | ||
|
|
6bd7e7563c | ||
|
|
5ec77bf8d2 | ||
|
|
33368c3ac4 | ||
|
|
99d282ee20 | ||
|
|
a9009049c9 | ||
|
|
e64abca1f0 | ||
|
|
e90408027b | ||
|
|
58a74af25b | ||
|
|
0a33f4ca1c | ||
|
|
960ed8158c | ||
|
0cff4dc194
|
|||
|
|
03822418c7 | ||
|
de510423f6
|
|||
|
|
264fbac16c | ||
|
2cd508c4c2
|
|||
|
5e0b4583c0
|
|||
|
4d2a670c72
|
|||
|
73d17ac708
|
|||
|
c2e955faa5
|
|||
|
58d95a0c15
|
|||
|
d86a6a9e16
|
|||
|
1269c00485
|
|||
|
|
98183cb4a8 | ||
|
|
537100d923 | ||
|
|
ca3c65496a | ||
|
|
9b2fb867b4 | ||
|
|
52f6dff4e9 | ||
|
|
94811b3737 | ||
|
|
921b5a2a31 | ||
|
|
116825b556 | ||
|
|
e40cc9a50a | ||
|
|
43f6053429 | ||
|
|
1e8d8120ac | ||
|
|
dfb681cc02 | ||
|
|
889c584487 | ||
|
|
72f00fb413 | ||
|
|
d6694fac40 | ||
|
|
d4068f8d52 | ||
| 7d410bff34 | |||
|
|
b25e2ff6c0 | ||
| eddff1a579 | |||
| 387e1bcf22 | |||
| 4da002e1b4 | |||
|
|
139a2455a5 | ||
|
|
e058f7e8e1 | ||
|
|
ec3f0b3c5d | ||
|
|
20b1697e40 | ||
|
|
159f00e466 | ||
|
|
57635b3c17 | ||
|
900094fae4
|
|||
|
4fbc9882ce
|
|||
|
|
e1578c0337 | ||
|
|
9fa11118d3 | ||
|
3aac4e2f7f
|
|||
|
133c237105
|
|||
|
f59d267863
|
|||
|
|
78b4035d51 | ||
|
|
dcc4b7b5e4 | ||
|
|
1af12e5e81 | ||
|
|
2eeeb081fd | ||
|
|
7affc5ae4b | ||
|
|
f283519a0d | ||
|
|
3317f23618 | ||
| 2ed17a2509 | |||
| 08ca484d54 | |||
|
|
2feaa207d7 | ||
| bb6a09179e | |||
| 49f64e7f49 | |||
|
a65a6966ac
|
|||
|
6f15746b8a
|
|||
|
|
13066a8fa2 | ||
|
|
c647daf9b9 | ||
|
|
7bcc345038 | ||
|
|
bf0f879d66 | ||
|
|
3af9131afe | ||
|
|
b6b6d033a8 | ||
|
|
819d7496b2 | ||
|
|
4c58e73e18 | ||
|
|
6e38707aaa | ||
|
|
0f08612b79 | ||
|
|
ef89c4b33b | ||
|
|
5c9bc02ac6 | ||
|
|
b57d2a3a6e | ||
|
|
0e8c94b668 | ||
|
|
3e6c8c47a7 | ||
|
|
e4beb872a5 | ||
|
|
552bd9cae5 | ||
|
|
059a16a8dc | ||
|
|
b6ea17a0eb | ||
|
|
a9e9f0dc8f | ||
|
|
5edb7df5c4 | ||
|
|
d559dd3a13 | ||
|
|
b9c2473a2d | ||
|
|
196081cd38 | ||
|
3e02cc6889
|
|||
|
51f94cf135
|
|||
|
a20fa08030
|
|||
|
|
203203a706 | ||
|
|
92239eae69 | ||
|
|
5de745fb19 | ||
|
|
1baae90beb | ||
|
|
2b832120ec | ||
|
|
255668c17a | ||
| c046c7cf45 | |||
|
|
5daaec35a8 | ||
|
|
abfbc8c9aa | ||
|
|
44b1136b86 | ||
|
|
dc28456122 | ||
|
|
0dd804f61c | ||
|
|
a3e7abc85d | ||
|
|
d61d7df91b | ||
|
|
5e3ce4e454 | ||
|
|
59abc7b608 | ||
|
|
74d8d57542 | ||
|
|
214e45a98b | ||
|
|
2a8b9f75c1 | ||
|
|
7d323b65e4 | ||
|
|
b69116e685 | ||
|
|
561e2cd3ad | ||
|
|
ad87a62486 | ||
|
|
5793db4053 | ||
|
e736f8f837
|
|||
|
|
81c1993156 | ||
|
4d97dbcacf
|
|||
|
af72cf4e06
|
|||
|
55ba3f8c1b
|
|||
|
d7ab33e731
|
|||
|
1203b1d7fc
|
|||
|
|
a62f3e2737 | ||
|
|
209a1c3213 | ||
|
|
675903b768 | ||
|
|
92035e17d3 | ||
|
|
8df5bf04ae | ||
|
|
cf79fd9491 | ||
|
|
56d43f1ad1 | ||
|
|
7e1daf7816 | ||
|
|
0ead583bda | ||
|
|
dd44bd779b | ||
|
|
c31374fc0a | ||
|
|
984a1b916d | ||
|
|
b8cefb9392 | ||
|
|
f3730630b5 | ||
|
d5f2a17249
|
|||
|
4526ed01fe
|
|||
|
|
a590fb099d | ||
|
|
135814737c | ||
|
e2e60639d9
|
|||
|
83909c8fc9
|
|||
|
|
3e4914462b | ||
|
|
7a11433a98 | ||
|
|
03e1c1903f | ||
|
|
acdee6a326 | ||
|
|
f5e03f145c | ||
|
|
2a9ddd10c8 | ||
| 5e9580377d | |||
|
|
9d2ff2fe65 | ||
|
|
13ea42a2e2 | ||
|
|
d9e22ce7bf | ||
|
|
2335a65b78 | ||
|
|
566cd141ce | ||
|
|
55f7f8c072 | ||
|
|
4b4addd215 | ||
|
|
255a0c55ba | ||
|
|
ced6e2488f | ||
|
|
77c2abc524 | ||
|
|
e4ad15ced1 | ||
|
|
904a6e960a | ||
|
da8a82954a
|
|||
|
383f45fe96
|
|||
|
9dc0f3baf6
|
|||
|
abc857582f
|
|||
|
|
4816b57dcd | ||
|
|
06b1953b49 | ||
|
|
d658d1d987 | ||
|
|
e14cd99c85 | ||
|
|
0258ef792f | ||
|
|
52524e00a2 | ||
|
|
342883fbb0 | ||
|
|
1e6505abe3 | ||
|
|
98c24147e8 | ||
|
|
d1e7de5dcb | ||
|
|
11e0a87f06 | ||
|
|
1154cec719 | ||
|
|
1ecfb0487e | ||
|
|
8ffa8446b6 | ||
|
|
fb1f99e728 | ||
|
|
031408dec3 | ||
|
|
16b6d029fa | ||
|
|
3e093e8572 | ||
|
|
fa11af4b1d | ||
|
|
91159d70ca | ||
|
|
a57d654f32 | ||
|
|
0313480685 | ||
|
|
e07b31e0a1 | ||
|
538a0ae5ea
|
|||
|
0f82db2440
|
|||
|
|
92ae2c7754 | ||
|
|
00c819140b | ||
|
|
cbc3c46c9d | ||
|
|
4b5c34b4e2 | ||
|
|
9eb39f7e0a | ||
| 173b22b772 | |||
|
|
18c7cba53c | ||
|
|
9a40fd595d | ||
|
|
a71c35a6b0 | ||
|
|
d69d3cc74e | ||
|
|
6e220ac4c1 | ||
|
|
bcb40a6ec7 | ||
|
|
2f1063b49f | ||
|
|
73110952e5 | ||
| d01e7c0595 | |||
|
|
4f9cef541b | ||
|
|
2dfbc4b57e | ||
|
|
b9750dab77 | ||
|
|
d59331bc3c | ||
|
|
abd5856f21 | ||
|
|
42a475bd72 | ||
|
|
65c1325935 | ||
|
|
db64a73f87 | ||
|
|
9d44ed0bfe | ||
|
|
33383265c8 | ||
|
|
76d1ee34d8 | ||
|
|
3b0a84bd43 | ||
|
|
5a1daebeca | ||
| 08666ff90d | |||
|
|
b2b790a969 | ||
|
|
907f0d236f | ||
|
|
4b8f536a9b | ||
|
|
c76f92c6ed | ||
|
|
a165f4281c | ||
|
|
5dbf8029da | ||
|
|
b1885700ca | ||
|
|
9e5209b48d | ||
|
|
6bcea13d5a | ||
|
|
9944a7bf2a | ||
|
|
424fb55343 | ||
|
|
901931443f | ||
|
|
40d11ad680 | ||
|
|
4d8088d0d0 | ||
|
|
aee29f145a | ||
|
|
c9463804a8 | ||
|
|
dccaf35410 | ||
|
|
45e64dc42c | ||
|
|
5a32a384c4 | ||
|
|
35ae69740a | ||
|
|
43cc3b6b6b | ||
|
b367382aae
|
|||
|
bf1866056c
|
|||
|
016dfa54f3
|
|||
|
887eb902bf
|
|||
|
|
966f330317 | ||
|
|
cb54ac0494 | ||
|
|
fac1911524 | ||
|
dcf328e7ac
|
|||
|
|
38211a74f5 | ||
|
|
9b236f9583 | ||
|
|
4378397c4c | ||
|
|
392281ca40 | ||
|
|
3bf39dc04b | ||
|
|
8161d55d05 | ||
|
|
30151e14fa | ||
|
|
c6ca0a058e | ||
|
|
c10fcc52e3 | ||
|
|
08dd2d5414 | ||
|
|
d9b60c0052 | ||
|
|
fba29d4454 | ||
|
|
421245a2f8 | ||
|
|
ec4790a8fe | ||
|
|
5f22a7691f | ||
|
|
d878ff6fdb | ||
|
|
7081c4ac69 | ||
|
|
b03bef2f8b | ||
|
|
5733f782d9 | ||
|
|
a153515366 | ||
|
|
d5857325b1 | ||
|
|
04e4bc7985 | ||
|
|
e7d32d9ea7 | ||
|
|
442a50f9ae | ||
|
|
aba758b143 | ||
|
|
b7c7b0b3bf | ||
|
|
2d10d4592b | ||
|
|
9f8379cb1e | ||
|
|
d0eb632ce6 | ||
|
|
cd62418dda | ||
|
|
ba32d15a49 | ||
|
|
b688fa98a5 | ||
|
|
61a451184b | ||
|
|
96741af97b | ||
|
|
553dcb785c | ||
|
|
22008aeabc | ||
|
|
64bb28e017 | ||
|
|
c58c349053 | ||
|
|
4d358415bd | ||
|
|
5ba5a68628 | ||
|
|
89c857d3e9 | ||
|
|
4d3a3184b4 | ||
|
|
71b1a9d14f | ||
|
|
8785f31834 | ||
|
|
104205394f | ||
|
|
6593c9456d | ||
|
|
186954195d | ||
|
|
dba31a9d33 | ||
|
|
a69fb5306c | ||
|
|
fb1bcdd31f | ||
|
|
2ccc7e9a30 | ||
|
|
e9380c3821 | ||
|
|
39fa973a80 | ||
|
|
b70ce53b88 | ||
|
|
16e3c4e1cf | ||
|
|
b3b8a708f3 | ||
|
|
d89e3d32f8 | ||
|
|
b99e9b9acc | ||
|
|
61974ca696 | ||
|
|
42f484bc64 | ||
|
|
43c6084620 | ||
|
|
501412e75c | ||
|
|
5789cc0097 | ||
|
|
d4995aa4bf | ||
|
|
57dbb6a487 | ||
|
|
7f71ddce1d | ||
|
|
abf736ec2a | ||
|
|
2cfcc82b2d | ||
|
|
aaa21bf1bf | ||
|
|
ba03be5b91 | ||
|
|
3f3b78f9bc | ||
|
|
2348f64dff | ||
| 8428f0af43 | |||
|
|
8c91ce3e10 | ||
|
|
5fd5593595 | ||
|
|
2e99e5acaa | ||
|
|
0b7a600c67 | ||
|
|
bd5390fbc0 | ||
|
|
068099c5a7 | ||
|
|
7372c4847d | ||
|
|
b42f0ec5eb | ||
|
|
0d2ab6aff3 | ||
|
|
e3732b3adc |
31
.github/workflows/run-tests.yaml
vendored
Normal file
31
.github/workflows/run-tests.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Run Test Suite
|
||||||
|
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_tests:
|
||||||
|
runs-on: macos-12
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- xcode: "14.2"
|
||||||
|
ios: "16.2"
|
||||||
|
|
||||||
|
name: Test iOS (${{ matrix.ios }})
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Select Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.xcode }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=${{ matrix.ios }}' | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
656
CHANGELOG.md
656
CHANGELOG.md
@@ -1,3 +1,659 @@
|
|||||||
|
## [1.4.1-3] - 2023-04-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added text truncation settings (William Casarin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rename block to mute (William Casarin)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Reduce chopping of images (mainvolume)
|
||||||
|
- Fix some notification settings not saving (William Casarin)
|
||||||
|
- Fix broken camera uploads (again) (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
[1.4.1-3]: https://github.com/damus-io/damus/releases/tag/v1.4.1-3
|
||||||
|
|
||||||
|
## [1.4.1-2] - 2023-04-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Reply counts (William Casarin)
|
||||||
|
- Add option to only show notification from people you follow (Swift)
|
||||||
|
- Added local notifications for other events (Swift)
|
||||||
|
- Show a custom view when tagged user isn't found (ericholguin)
|
||||||
|
- Show referenced notes in DMs (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Show full bleed images on selected events in threads (William Casarin)
|
||||||
|
- Improvement to square image displaying (mainvolume)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix broken website links that have missing https:// prefixes (William Casarin)
|
||||||
|
- Get around CCP bootstrap relay banning by caching user's relays as their bootstrap relays (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.4.1-2]: https://github.com/damus-io/damus/releases/tag/v1.4.1-2
|
||||||
|
|
||||||
|
## [1.4.1] - 2023-04-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Profile Picture Upload (Joel Klabo)
|
||||||
|
- Enable offline posting (William Casarin)
|
||||||
|
- Add auto-translation caching to ruduce api usage (Terry Yiu)
|
||||||
|
- Added support for gif uploads (Swift)
|
||||||
|
- Add a Divider in the Follows List for Large Screens (Joel Klabo)
|
||||||
|
- Upload Photos and Videos from Camera (Joel Klabo)
|
||||||
|
- Added ability to lookup users by nip05 identifiers (William Casarin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Only truncate timeline text if enabled in settings (William Casarin)
|
||||||
|
- Make mentions wide in notifications like in timeline (William Casarin)
|
||||||
|
- Broadcast events you are replying to (William Casarin)
|
||||||
|
- Broadcast now also broadcasts event user's profile (William Casarin)
|
||||||
|
- Improved look of reply view (ericholguin)
|
||||||
|
- Remove gradient in some places for visibility (ericholguin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix cropped images (mainvolume)
|
||||||
|
- Truncate long text in notification items (William Casarin)
|
||||||
|
- Restore missing reply description on selected events (William Casarin)
|
||||||
|
- Show sent DMs immediately (William Casarin)
|
||||||
|
- Fixed size of translated text (William Casarin)
|
||||||
|
- Fix crash when reposting (William Casarin)
|
||||||
|
- Fix unclickable image dismiss button (OlegAba)
|
||||||
|
|
||||||
|
|
||||||
|
[1.4.1]: https://github.com/damus-io/damus/releases/tag/v1.4.1
|
||||||
|
## [1.4.0] - 2023-03-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Local zap notifications (Swift)
|
||||||
|
- Add support for video uploads (Swift)
|
||||||
|
- Auto Translation (Terry Yiu)
|
||||||
|
- Portuguese (Brazil) translations (Andressa Munturo)
|
||||||
|
- Spanish (Spain) translations (Max Pleb)
|
||||||
|
- Vietnamese translations (ShiryoRyo)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed small notification hit boxes (Terry Yiu)
|
||||||
|
|
||||||
|
[1.4.0]: https://github.com/damus-io/damus/releases/tag/v1.4.0
|
||||||
|
|
||||||
|
## [1.3.0-7] - 2023-03-24
|
||||||
|
|
||||||
|
- New experimental timeline view
|
||||||
|
|
||||||
|
[1.3.0-7]: https://github.com/damus-io/damus/releases/tag/v1.3.0-7
|
||||||
|
|
||||||
|
## [1.3.0-6] - 2023-03-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix bug where nostr: links and QRs stopped working (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.3.0-6]: https://github.com/damus-io/damus/releases/tag/v1.3.0-6
|
||||||
|
|
||||||
|
## [1.3.0-5] - 2023-03-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add Time Ago to DM View (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed internal links opening in other nostr clients (William Casarin)
|
||||||
|
- Remove authentication for copying npub (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
[1.3.0-5]: https://github.com/damus-io/damus/releases/tag/v1.3.0-5
|
||||||
|
|
||||||
|
## [1.3.0-4] - 2023-03-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- It's much easier to tag users in replies and posts (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix bug where small black text appears during image upload (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.3.0-4]: https://github.com/damus-io/damus/releases/tag/v1.3.0-4
|
||||||
|
|
||||||
|
## [1.3.0-3] - 2023-03-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix image upload url delay after progress bar disappears (William Casarin)
|
||||||
|
- Fix issue where damus stops trying to reconnect (William Casarin)
|
||||||
|
|
||||||
|
[1.3.0-3]: https://github.com/damus-io/damus/releases/tag/v1.3.0-3
|
||||||
|
|
||||||
|
## [1.3.0-2] - 2023-03-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add image uploader (Swift)
|
||||||
|
- Add option to always show images (never blur) (William Casarin)
|
||||||
|
- Canadian French (Pierre - synoptic_okubo)
|
||||||
|
- Hungarian translations (Zoltan)
|
||||||
|
- Korean translations (sogoagain)
|
||||||
|
- Swedish translations (Pextar)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixed embedded note popping (William Casarin)
|
||||||
|
- Bump notification limit from 100 to 500 (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix zap button preventing scrolling (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.3.0-2]: https://github.com/damus-io/damus/releases/tag/v1.3.0-2
|
||||||
|
|
||||||
|
## [1.3.0] - 2023-03-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extend user tagging search to all local profiles (William Casarin)
|
||||||
|
- Vibrate when a zap is received (Swift)
|
||||||
|
- New and Improved Share sheet (ericholguin)
|
||||||
|
- Bulgarian translations (elsat)
|
||||||
|
- Persian translations (Mahdi Taghizadeh)
|
||||||
|
- Ukrainian translations (Valeriia Khudiakova, Tony B)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reduce battery usage by using exp backoff on connections (Bryan Montz)
|
||||||
|
- Don't show both realname and username if they are the same (William Casarin)
|
||||||
|
- Show error on invalid lightning tip address (Swift)
|
||||||
|
- Make DM Content More Visible (Joel Klabo)
|
||||||
|
- Remove spaces from hashtag searches (gladiusKatana)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Show @ mentions for users with display_names and no username (William Casarin)
|
||||||
|
- Make user search case insensitive (William Casarin)
|
||||||
|
- Fix repost button sometimes not working (OlegAba)
|
||||||
|
- Don't show follows you for your own profile (benthecarman)
|
||||||
|
- Fix json appearing in profile searches (gladiusKatana)
|
||||||
|
- Fix unexpected font size when posting (Bryan Montz)
|
||||||
|
- Fix keyboard sticking issues (OlegAba)
|
||||||
|
- Fixed tab bar background color on macOS (Joel Klabo)
|
||||||
|
- Fix some links getting interpreted as images (gladiusKatana)
|
||||||
|
|
||||||
|
|
||||||
|
[1.3.0]: https://github.com/damus-io/damus/releases/tag/v1.3.0
|
||||||
|
|
||||||
|
## [1.2.0-4] - 2023-03-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add ellipsis button to notes (ericholguin)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Immediately search for events and profiles (William Casarin)
|
||||||
|
- Use long-press for custom zaps (William Casarin)
|
||||||
|
- Make shaka animation smoother (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed hit detection bugs on profile page (OlegAba)
|
||||||
|
- Fix disappearing text on Thread view (Bryan Montz)
|
||||||
|
- Render links in notification summaries (Joel Klabo)
|
||||||
|
- Don't show notifications from ourselves (William Casarin)
|
||||||
|
- Fix issue where navbar back button would show the wrong text (Jack Chakany)
|
||||||
|
- Fix case sensitivity when searching hashtags (randymcmillan)
|
||||||
|
- Fix issue where opening reposts shows json (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.2.0-4]: https://github.com/damus-io/damus/releases/tag/v1.2.0-4
|
||||||
|
|
||||||
|
## [1.2.0-3] - 2023-03-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add additional info to recommended relay view (ericholguin)
|
||||||
|
- Add shaka animation (Swift)
|
||||||
|
- Add option to disable image animation (OlegAba)
|
||||||
|
- Add additional warning when deleting account (ericholguin)
|
||||||
|
- Threads now load instantly and are cached (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Wrap long profile display names (OlegAba)
|
||||||
|
- Fixed weird scaling on profile pictures (OlegAba)
|
||||||
|
- Fixed width of copy pubkey on profile page (Joel Klabo)
|
||||||
|
- Make damus purple use more consistent in mentions (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.2.0-3]: https://github.com/damus-io/damus/releases/tag/v1.2.0-3
|
||||||
|
|
||||||
|
## [1.1.0-10] - 2023-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Truncate large posts and add a show more button (OlegAba)
|
||||||
|
- Private Zaps (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix default zap amount setting not getting updated (William Casarin)
|
||||||
|
- Fix issue where keyboard covers custom zap comment (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
|
||||||
|
|
||||||
|
## [1.1.0-9] - 2023-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Customized zaps (William Casarin)
|
||||||
|
- Add new Notifications View (William Casarin)
|
||||||
|
- Bookmarking (Joel Klabo)
|
||||||
|
- Chinese, Traditional (Hong Kong) translations (rasputin)
|
||||||
|
- Chinese, Traditional (Taiwan) translations (rasputin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- No more inline npubs when tagging users (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix alignment of side menu labels (Joel Klabo)
|
||||||
|
- Fix duplicated participants in reply-to view (Joel Klabo)
|
||||||
|
- Load missing profiles in Zaps view (William Casarin)
|
||||||
|
- Fix memory leak with inline videos (William Casarin)
|
||||||
|
- Eliminate popping when scrolling (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
|
||||||
|
|
||||||
|
## [1.1.0-3] - 2023-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add a "load more" button instead of always inserting events in timelines (William Casarin)
|
||||||
|
- Added the ability to select text on posts (OlegAba)
|
||||||
|
- Added Posts or Post & Replies selector to Profile (ericholguin)
|
||||||
|
- Improved profile navbar (OlegAba)
|
||||||
|
- Czech translations (Martin Gabrhel)
|
||||||
|
- Indonesian translations (johnybergzy)
|
||||||
|
- Russian translations (Tony B)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rename global feed to universe (William Casarin)
|
||||||
|
- Improve look of post view (ericholguin)
|
||||||
|
- Added a 20MB content length limit for all image files (OlegAba)
|
||||||
|
- Improved EventActionBar button spacing (Bryan Montz)
|
||||||
|
- Polished profile key copy buttons, added animation (Bryan Montz)
|
||||||
|
- Format large numbers of action bar actions (Joel Klabo)
|
||||||
|
- Improved blur on images, especially in dark mode (Bryan Montz)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Remove trailing slash when adding a relay (middlingphys)
|
||||||
|
- Scroll to top of events instead of the bottom (OlegAba)
|
||||||
|
- Fix lag on startup when you have lots of DMs (William Casarin)
|
||||||
|
- Fix an issues where dm notifications appear without any new events (William Casarin)
|
||||||
|
- Fix some hangs when scrolling by images (OlegAba)
|
||||||
|
- Force default zap amount text field to accept only numbers (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.1.0-3]: https://github.com/damus-io/damus/releases/tag/v1.1.0-3
|
||||||
|
|
||||||
|
## [1.1.0-2] - 2023-02-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Save drafts to posts, replies and DMs (Terry Yiu)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure stats get updated in realtime on action bars (William Casarin)
|
||||||
|
- Fix reposts not getting counted properly (William Casarin)
|
||||||
|
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
|
||||||
|
- Fix punctuation getting included in some urls (Gert Goet)
|
||||||
|
- Improve language detection (Terry Yiu)
|
||||||
|
- Fix some animated image crashes (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
|
||||||
|
## [1.0.0-15] - 2023-02-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Relay Filtering (William Casarin)
|
||||||
|
- Add password autofill on account login and creation (Terry Yiu)
|
||||||
|
- Show if relay is paid (William Casarin)
|
||||||
|
- Add "Follows You" indicator on profile (William Casarin)
|
||||||
|
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
|
||||||
|
- Relay Detail View (Joel Klabo)
|
||||||
|
- Warn when attempting to post an nsec key (Terry Yiu)
|
||||||
|
- DeepL translation integration (Terry Yiu)
|
||||||
|
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
|
||||||
|
- Add accessibility labels to action bar (Bryan Montz)
|
||||||
|
- Copy invoice button (Joel Klabo)
|
||||||
|
- Receive Lightning Zaps (William Casarin)
|
||||||
|
- Allow text selection in bio (Suhail Saqan)
|
||||||
|
- Chinese, Simplified (China mainland) translations (haolong, rasputin)
|
||||||
|
- Dutch translations (Heimen Stoffels - Vistaus)
|
||||||
|
- Greek translations (milicode)
|
||||||
|
- Japanese translations (akiomik, foxytanuki, Guetsu Ren - Nighthaven, h3y6e, middlingphys)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Show "Follow Back" button on profile page (William Casarin)
|
||||||
|
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
|
||||||
|
- Updated QR code view, include profile image, etc (ericholguin)
|
||||||
|
- Clicking relay numbers now goes to relay config (radixrat)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Load zaps, likes and reposts when you open a thread (William Casarin)
|
||||||
|
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
|
||||||
|
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
|
||||||
|
- Fix sidebar navigation bugs (OlegAba)
|
||||||
|
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
|
||||||
|
- Make @ mentions case insensitive (William Casarin)
|
||||||
|
- Fix some lnurls not getting decoded properly (William Casarin)
|
||||||
|
- Hide incoming DMs from blocked users (William Casarin)
|
||||||
|
- Hide blocked users from search results (William Casarin)
|
||||||
|
- Fix Cash App invoice payments (Rob Seward)
|
||||||
|
- DM Padding (OlegAba)
|
||||||
|
- Check for broken lnurls (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
|
||||||
|
## [1.0.0-13] - 2023-01-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- LibreTranslate note translations (Terry Yiu)
|
||||||
|
- Added support for account deletion (William Casarin)
|
||||||
|
- User tagging and autocompletion in posts (Swift)
|
||||||
|
- Polish translations (pysiak)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove redundant logout button from settings (Jonathan Milligan)
|
||||||
|
- Moved relay config to its own sidebar entry (William Casarin)
|
||||||
|
- New stylized tabs (ericholguin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix hidden profile action sheet when clicking ... (William Casarin)
|
||||||
|
- Fixed height of DM input (Terry Yiu)
|
||||||
|
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
|
||||||
|
## [1.0.0-12] - 2023-01-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Arabic translations (Barodane)
|
||||||
|
- Portuguese translations (Antonio Chagas)
|
||||||
|
- Add QRCode view for sharing your pubkey (ericholguin)
|
||||||
|
- Added nostr: uri handling (William Casarin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove markdown link support from posts (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed crash on some SVG profile pictures (OlegAba)
|
||||||
|
- Localization fixes
|
||||||
|
- Don't allow blocking yourself (Terry)
|
||||||
|
- Hide muted users from global (William Casarin)
|
||||||
|
- Fixed profiles sometimes not loading from other clients (William Casarin)
|
||||||
|
- Fixed bug where `spam` was always the report type (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
|
||||||
|
|
||||||
|
## [1.0.0-11] - 2023-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Reposts view (Terry Yiu)
|
||||||
|
- Italian translations (Nicolò Carcagnì)
|
||||||
|
- Latvian translations (SYX)
|
||||||
|
- Added ability to block users (William Casarin)
|
||||||
|
- Added a way to report content (William Casarin)
|
||||||
|
- Stretchable profile cover header (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump pfp/banner animated fize size limit to 5MiB/20MiB (William Casarin)
|
||||||
|
- Updated default boostrap relays (Ricardo Arturo Cabral Mejía)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- allow ws:// relays again (Steven Briscoe)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-11]: https://github.com/damus-io/damus/releases/tag/v1.0.0-11
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-8] - 2023-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Show website on profiles (William Casarin)
|
||||||
|
- Add the ability to choose participants when replying (Joel Klabo)
|
||||||
|
- German translations (Gregor, Peter Gerstbach)
|
||||||
|
- Turkish translations (Taylan Benli)
|
||||||
|
- French (France) translations (Solobalbo)
|
||||||
|
- Add DM Message Requests (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix commands and emojis getting included in hashtags (William Casarin)
|
||||||
|
- Fix duplicate post buttons when swiping tabs (Thomas Rademaker)
|
||||||
|
- Show embedded note references (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-8]: https://github.com/damus-io/damus/releases/tag/v1.0.0-8
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-7] - 2023-01-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Drastically improved image viewer (OlegAba)
|
||||||
|
- Added pinch to zoom on images (Swift)
|
||||||
|
- Add Latin American Spanish translations (Nicolás Valencia)
|
||||||
|
- Added SVG profile picture support (OlegAba)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Makes both name and username clickable in sidebar to go to profile (Zach Hendel)
|
||||||
|
- Clicking pfp in sidebar opens profile as well (radixrat)
|
||||||
|
- Don't blur images if your friend boosted it (ericholguin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix ... when too many likes/reposts (Joel Klabo)
|
||||||
|
- Don't show report alert if logged in as a pubkey (Swift)
|
||||||
|
- Fix padding issue at top of home timeline (Ben Weeks)
|
||||||
|
- Fix absurdly large sidebar on Mac/iPad (John Bethancourt)
|
||||||
|
- Fix tab views moving after selecting from search result (OlegAba)
|
||||||
|
- Make follow/unfollow button a consistent width (OlegAba)
|
||||||
|
- Don't add events to notifications from buggy relays (William Casarin)
|
||||||
|
- Fixed some crashes with large images (OlegAba)
|
||||||
|
- Fix DM sorting on incoming messages (William Casarin)
|
||||||
|
- Fix text getting truncated next to link previews (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-7]: https://github.com/damus-io/damus/releases/tag/v1.0.0-7
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-6] - 2023-01-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Profile banner images (Jason Jōb)
|
||||||
|
- Added Reactions View (William Casarin)
|
||||||
|
- Left hand option for post button (Jonathan Milligan)
|
||||||
|
- Damus icon at the top (Ben Weeks)
|
||||||
|
- Make purple badges on profile page tappable (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Make Shaka button purple when liked (Joel Klabo)
|
||||||
|
- Move counts to right side like Birdsite (Joel Klabo)
|
||||||
|
- Use custom icon for shaka button (Joel Klabo)
|
||||||
|
- Renamed boost to repost (William Casarin)
|
||||||
|
- Removed nip05 domain from boosts/reposts (William Casarin)
|
||||||
|
- Make DMs only take up 80% of screen width (Jonathan Milligan)
|
||||||
|
- Hide Recommended Relays Section if Empty (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed shaka moving when you press it (Joel Klabo)
|
||||||
|
- Fixed issue with relays not keeping in sync when adding (Fredrik Olofsson)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-6]: https://github.com/damus-io/damus/releases/tag/v1.0.0-6
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-5] - 2023-01-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added share button to profile (William Casarin)
|
||||||
|
- Added universal link sharing of notes (William Casarin)
|
||||||
|
- Added clear cache button to wipe pfp/image cache (OlegAba)
|
||||||
|
- Allow Adding Relay Without wss:// Prefix (Joel Klabo)
|
||||||
|
- Allow Saving Images to Library (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added damus gradient to post button (Ben Weeks)
|
||||||
|
- Center the Post Button (Thomas)
|
||||||
|
- Switch yellow nip05 check to gray (William Casarin)
|
||||||
|
- Switch from bluecheck to purplecheck (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Add system background color to profile pics (OlegAba)
|
||||||
|
- High res color pubkey on profile page (William Casarin)
|
||||||
|
- Don't spin forever if we're temporarily disconnected (William Casarin)
|
||||||
|
- Fixed a few issues with avatars not animating (OlegAba)
|
||||||
|
- Scroll to bottom when new DM received (Aidan O'Loan)
|
||||||
|
- Make reply view scrollable (Joel Klabo)
|
||||||
|
- Hide profile edit button when logged in with pubkey (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-5]: https://github.com/damus-io/damus/releases/tag/v1.0.0-5
|
||||||
|
|
||||||
|
## [1.0.0-4] - 2023-01-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added NIP05 Verification (William Casarin)
|
||||||
|
- Downscale images if they are unreasonably large (OlegAba)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Revert to old style ln/dm buttons (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix ascii shrug guy (Lionello Lunesu)
|
||||||
|
- Fix navigation popping in threads (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-4]: https://github.com/damus-io/damus/releases/tag/v1.0.0-4
|
||||||
|
|
||||||
|
## [1.0.0-2] - 2023-01-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Cache link previews (William Casarin)
|
||||||
|
- Added brb.io to recommended relay list (William Casarin)
|
||||||
|
- Add Blixt Wallet to Wallet Selector (Benjamin Hakes)
|
||||||
|
- Add River Wallet to Wallet Selector (Benjamin Hakes)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added muted shaka images instead of thumbs up (CutClout)
|
||||||
|
- Updated profile page look and feel (Ben Weeks)
|
||||||
|
- Filter replies from global feed (Nitesh Balusu)
|
||||||
|
- Show non-image links inline (William Casarin)
|
||||||
|
- Add swipe gesture to switch between tabs (Thomas Rademaker)
|
||||||
|
- Parse links in profiles (Lionello Lunesu) (Lio李歐)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix detection of email addresses in profiles (Lionello Lunesu)
|
||||||
|
- Fix padding on search results view (OlegAba)
|
||||||
|
- Fix home view moving after selecting from search result (OlegAba)
|
||||||
|
- Fix bug where boost event is loaded in the thread instead of the boosted event (William Casarin)
|
||||||
|
- Hide edit button on profile page when no private key (Swift)
|
||||||
|
- Fixed follows and relays getting out of sync on profile pages (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-2]: https://github.com/damus-io/damus/releases/tag/v1.0.0-2
|
||||||
## [1.0.0] - 2023-01-01
|
## [1.0.0] - 2023-01-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -1,3 +1,4 @@
|
|||||||
|
[](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
|
||||||
|
|
||||||
# damus
|
# damus
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
## Getting Started on Damus
|
## Getting Started on Damus
|
||||||
|
|
||||||
### Damus iOS
|
### Damus iOS
|
||||||
1) Get the Damus app on Testflight: https://testflight.apple.com/join/CLwjLxWl
|
1) Get the Damus app on the iOS App Store: https://apps.apple.com/ca/app/damus/id1628663131
|
||||||
|
|
||||||
#### ⚙️ Settings (gear icon, top right)
|
#### ⚙️ Settings (gear icon, top right)
|
||||||
- Relays: You can add more relays to send your notes to by tapping the "+".
|
- Relays: You can add more relays to send your notes to by tapping the "+".
|
||||||
@@ -48,7 +49,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
|
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
|
||||||
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
|
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
|
||||||
- Currently you can't delete your Notes in the iOS app
|
- Currently you can't delete your Notes in the iOS app
|
||||||
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
|
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `https://i.ibb.co/2SHZbwm/alpha60.jpg`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
|
||||||
- Engaging with Notes
|
- Engaging with Notes
|
||||||
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users’ notifications and in your 🏠 Personal and 🔍 Global Feeds
|
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users’ notifications and in your 🏠 Personal and 🔍 Global Feeds
|
||||||
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
|
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
|
||||||
@@ -56,8 +57,8 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
- Formatting Notes (may not format as intended in other web clients)
|
- Formatting Notes (may not format as intended in other web clients)
|
||||||
- Italics: 1 asterisk `*italic*`
|
- Italics: 1 asterisk `*italic*`
|
||||||
- Bold: 2 asterisk `**bold**`
|
- Bold: 2 asterisk `**bold**`
|
||||||
- Strikethrough: 2 tildes `~~strikethrough~~`
|
- Strikethrough: 1 tildes `~strikethrough~`
|
||||||
- Code: 1 back-tick ``code``
|
- Code: 1 back-tick `` `code` ``
|
||||||
|
|
||||||
#### 💬 Encrypted DMs (chat app, bottom navigation)
|
#### 💬 Encrypted DMs (chat app, bottom navigation)
|
||||||
- Tap the chat icon and you'll notice there's nothing to see at first. Go to a user profile and tap the 💬 chat icon next to the follow button to begin a DM
|
- Tap the chat icon and you'll notice there's nothing to see at first. Go to a user profile and tap the 💬 chat icon next to the follow button to begin a DM
|
||||||
@@ -91,15 +92,23 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
|
Contributors welcome!
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
|
||||||
|
|
||||||
[git-send-email]: http://git-send-email.io
|
[git-send-email]: http://git-send-email.io
|
||||||
|
|
||||||
## git log bot
|
### Translations
|
||||||
|
|
||||||
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
|
Translators welcome! Join the [Transifex][transifex] project.
|
||||||
|
|
||||||
### Awards
|
All user-facing strings must have a comment in order to provide context to translators. If a SwiftUI component has a `comment` parameter, use that. Otherwise, wrap your string with `NSLocalizedString` with the `comment` field populated.
|
||||||
|
|
||||||
|
[transifex]: https://explore.transifex.com/damus/damus-ios/
|
||||||
|
|
||||||
|
### Awards
|
||||||
|
|
||||||
There may be nostr badges awarded for contributors in the future... :)
|
There may be nostr badges awarded for contributors in the future... :)
|
||||||
|
|
||||||
@@ -107,3 +116,7 @@ First contributors:
|
|||||||
|
|
||||||
1. @randymcmillan
|
1. @randymcmillan
|
||||||
2. @jcarucci27
|
2. @jcarucci27
|
||||||
|
|
||||||
|
### git log bot
|
||||||
|
|
||||||
|
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
|
||||||
|
|||||||
@@ -1,804 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
|
||||||
<file original="damus/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
|
|
||||||
<header>
|
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
|
|
||||||
</header>
|
|
||||||
<body>
|
|
||||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
|
||||||
<source>Damus</source>
|
|
||||||
<target>Damus</target>
|
|
||||||
<note>Bundle display name</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
|
||||||
<source>damus</source>
|
|
||||||
<target>damus</target>
|
|
||||||
<note>Bundle name</note>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
<file original="damus/en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext">
|
|
||||||
<header>
|
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
|
|
||||||
</header>
|
|
||||||
<body>
|
|
||||||
<trans-unit id=" " xml:space="preserve">
|
|
||||||
<source> </source>
|
|
||||||
<target> </target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@" xml:space="preserve">
|
|
||||||
<source>%@</source>
|
|
||||||
<target>%@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@ following" xml:space="preserve">
|
|
||||||
<source>%@ following</source>
|
|
||||||
<target>%@ following</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@'s Followers" xml:space="preserve">
|
|
||||||
<source>%@'s Followers</source>
|
|
||||||
<target>%@'s Followers</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
|
|
||||||
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
|
|
||||||
<target>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" xml:space="preserve">
|
|
||||||
<source>%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs</source>
|
|
||||||
<target>%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." xml:space="preserve">
|
|
||||||
<source>%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.</source>
|
|
||||||
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%lld" xml:space="preserve">
|
|
||||||
<source>%lld</source>
|
|
||||||
<target>%lld</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%lld/%lld" xml:space="preserve">
|
|
||||||
<source>%lld/%lld</source>
|
|
||||||
<target>%lld/%lld</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="&nbsp;" xml:space="preserve">
|
|
||||||
<source>&nbsp;</source>
|
|
||||||
<target>&nbsp;</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="+" xml:space="preserve">
|
|
||||||
<source>+</source>
|
|
||||||
<target>+</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="< e >" xml:space="preserve">
|
|
||||||
<source>< e ></source>
|
|
||||||
<target>< e ></target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="@" xml:space="preserve">
|
|
||||||
<source>@</source>
|
|
||||||
<target>@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="About" xml:space="preserve">
|
|
||||||
<source>About</source>
|
|
||||||
<target>About</target>
|
|
||||||
<note>Label to prompt for about text entry for user to describe about themself.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="About Me" xml:space="preserve">
|
|
||||||
<source>About Me</source>
|
|
||||||
<target>About Me</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Absolute Boss" xml:space="preserve">
|
|
||||||
<source>Absolute Boss</source>
|
|
||||||
<target>Absolute Boss</target>
|
|
||||||
<note>Placeholder text for About Me description.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Account ID" xml:space="preserve">
|
|
||||||
<source>Account ID</source>
|
|
||||||
<target>Account ID</target>
|
|
||||||
<note>Label to indicate the public ID of the account.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Add" xml:space="preserve">
|
|
||||||
<source>Add</source>
|
|
||||||
<target>Add</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Add Relay" xml:space="preserve">
|
|
||||||
<source>Add Relay</source>
|
|
||||||
<target>Add Relay</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Are you sure you want to boost this post?" xml:space="preserve">
|
|
||||||
<source>Are you sure you want to boost this post?</source>
|
|
||||||
<target>Are you sure you want to boost this post?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." xml:space="preserve">
|
|
||||||
<source>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</source>
|
|
||||||
<target>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Bitcoin Beach" xml:space="preserve">
|
|
||||||
<source>Bitcoin Beach</source>
|
|
||||||
<target>Bitcoin Beach</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Bitcoin Beach.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
|
|
||||||
<source>Bitcoin Lightning Tips</source>
|
|
||||||
<target>Bitcoin Lightning Tips</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Blixt Wallet" xml:space="preserve">
|
|
||||||
<source>Blixt Wallet</source>
|
|
||||||
<target>Blixt Wallet</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Blue Wallet" xml:space="preserve">
|
|
||||||
<source>Blue Wallet</source>
|
|
||||||
<target>Blue Wallet</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Blue Wallet.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Boost" xml:space="preserve">
|
|
||||||
<source>Boost</source>
|
|
||||||
<target>Boost</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Boosted" xml:space="preserve">
|
|
||||||
<source>Boosted</source>
|
|
||||||
<target>Boosted</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Breez" xml:space="preserve">
|
|
||||||
<source>Breez</source>
|
|
||||||
<target>Breez</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Breez.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Broadcast" xml:space="preserve">
|
|
||||||
<source>Broadcast</source>
|
|
||||||
<target>Broadcast</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Cancel" xml:space="preserve">
|
|
||||||
<source>Cancel</source>
|
|
||||||
<target>Cancel</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Cash App" xml:space="preserve">
|
|
||||||
<source>Cash App</source>
|
|
||||||
<target>Cash App</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Cash App.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copied" xml:space="preserve">
|
|
||||||
<source>Copied</source>
|
|
||||||
<target>Copied</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy" xml:space="preserve">
|
|
||||||
<source>Copy</source>
|
|
||||||
<target>Copy</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Account ID" xml:space="preserve">
|
|
||||||
<source>Copy Account ID</source>
|
|
||||||
<target>Copy Account ID</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Image" xml:space="preserve">
|
|
||||||
<source>Copy Image</source>
|
|
||||||
<target>Copy Image</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Image URL" xml:space="preserve">
|
|
||||||
<source>Copy Image URL</source>
|
|
||||||
<target>Copy Image URL</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy LNURL" xml:space="preserve">
|
|
||||||
<source>Copy LNURL</source>
|
|
||||||
<target>Copy LNURL</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Note ID" xml:space="preserve">
|
|
||||||
<source>Copy Note ID</source>
|
|
||||||
<target>Copy Note ID</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Note JSON" xml:space="preserve">
|
|
||||||
<source>Copy Note JSON</source>
|
|
||||||
<target>Copy Note JSON</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy Text" xml:space="preserve">
|
|
||||||
<source>Copy Text</source>
|
|
||||||
<target>Copy Text</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy User ID" xml:space="preserve">
|
|
||||||
<source>Copy User ID</source>
|
|
||||||
<target>Copy User ID</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Copy invoice" xml:space="preserve">
|
|
||||||
<source>Copy invoice</source>
|
|
||||||
<target>Copy invoice</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Create" xml:space="preserve">
|
|
||||||
<source>Create</source>
|
|
||||||
<target>Create</target>
|
|
||||||
<note>Button to create account.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Create Account" xml:space="preserve">
|
|
||||||
<source>Create Account</source>
|
|
||||||
<target>Create Account</target>
|
|
||||||
<note>Button to create an account.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
|
|
||||||
<source>Creator(s) of Bitcoin. Absolute legend.</source>
|
|
||||||
<target>Creator(s) of Bitcoin. Absolute legend.</target>
|
|
||||||
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="DM" xml:space="preserve">
|
|
||||||
<source>DM</source>
|
|
||||||
<target>DM</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Damus" xml:space="preserve">
|
|
||||||
<source>Damus</source>
|
|
||||||
<target>Damus</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Default Wallet" xml:space="preserve">
|
|
||||||
<source>Default Wallet</source>
|
|
||||||
<target>Default Wallet</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Delete" xml:space="preserve">
|
|
||||||
<source>Delete</source>
|
|
||||||
<target>Delete</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Dismiss" xml:space="preserve">
|
|
||||||
<source>Dismiss</source>
|
|
||||||
<target>Dismiss</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Display Name" xml:space="preserve">
|
|
||||||
<source>Display Name</source>
|
|
||||||
<target>Display Name</target>
|
|
||||||
<note>Label to prompt display name entry.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Done" xml:space="preserve">
|
|
||||||
<source>Done</source>
|
|
||||||
<target>Done</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Earn Money" xml:space="preserve">
|
|
||||||
<source>Earn Money</source>
|
|
||||||
<target>Earn Money</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Edit" xml:space="preserve">
|
|
||||||
<source>Edit</source>
|
|
||||||
<target>Edit</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Edit Profile" xml:space="preserve">
|
|
||||||
<source>Edit Profile</source>
|
|
||||||
<target>Edit Profile</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Encrypted" xml:space="preserve">
|
|
||||||
<source>Encrypted</source>
|
|
||||||
<target>Encrypted</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Encrypted DMs" xml:space="preserve">
|
|
||||||
<source>Encrypted DMs</source>
|
|
||||||
<target>Encrypted DMs</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Enter your account key to login:" xml:space="preserve">
|
|
||||||
<source>Enter your account key to login:</source>
|
|
||||||
<target>Enter your account key to login:</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Error: %@" xml:space="preserve">
|
|
||||||
<source>Error: %@</source>
|
|
||||||
<target>Error: %@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Filter State" xml:space="preserve">
|
|
||||||
<source>Filter State</source>
|
|
||||||
<target>Filter State</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Follow" xml:space="preserve">
|
|
||||||
<source>Follow</source>
|
|
||||||
<target>Follow</target>
|
|
||||||
<note>Button to follow a user.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Followers" xml:space="preserve">
|
|
||||||
<source>Followers</source>
|
|
||||||
<target>Followers</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Following" xml:space="preserve">
|
|
||||||
<source>Following</source>
|
|
||||||
<target>Following</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Following..." xml:space="preserve">
|
|
||||||
<source>Following...</source>
|
|
||||||
<target>Following...</target>
|
|
||||||
<note>Label to indicate that the user is in the process of following another user.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Follows" xml:space="preserve">
|
|
||||||
<source>Follows</source>
|
|
||||||
<target>Follows</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Global" xml:space="preserve">
|
|
||||||
<source>Global</source>
|
|
||||||
<target>Global</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Goto post %@" xml:space="preserve">
|
|
||||||
<source>Goto post %@</source>
|
|
||||||
<target>Goto post %@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Goto profile %@" xml:space="preserve">
|
|
||||||
<source>Goto profile %@</source>
|
|
||||||
<target>Goto profile %@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Home" xml:space="preserve">
|
|
||||||
<source>Home</source>
|
|
||||||
<target>Home</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="LNLink" xml:space="preserve">
|
|
||||||
<source>LNLink</source>
|
|
||||||
<target>LNLink</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, LNLink.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Let's go!" xml:space="preserve">
|
|
||||||
<source>Let's go!</source>
|
|
||||||
<target>Let's go!</target>
|
|
||||||
<note>Button to complete account creation and start using the app.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Lightning Address or LNURL" xml:space="preserve">
|
|
||||||
<source>Lightning Address or LNURL</source>
|
|
||||||
<target>Lightning Address or LNURL</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Lightning Invoice" xml:space="preserve">
|
|
||||||
<source>Lightning Invoice</source>
|
|
||||||
<target>Lightning Invoice</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Local default" xml:space="preserve">
|
|
||||||
<source>Local default</source>
|
|
||||||
<target>Local default</target>
|
|
||||||
<note>Dropdown option label for system default for Lightning wallet.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Login" xml:space="preserve">
|
|
||||||
<source>Login</source>
|
|
||||||
<target>Login</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Logout" xml:space="preserve">
|
|
||||||
<source>Logout</source>
|
|
||||||
<target>Logout</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Make sure your nsec account key is saved before you logout or you will lose access to this account" xml:space="preserve">
|
|
||||||
<source>Make sure your nsec account key is saved before you logout or you will lose access to this account</source>
|
|
||||||
<target>Make sure your nsec account key is saved before you logout or you will lose access to this account</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Muun" xml:space="preserve">
|
|
||||||
<source>Muun</source>
|
|
||||||
<target>Muun</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Muun.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="NIP-05 Verification" xml:space="preserve">
|
|
||||||
<source>NIP-05 Verification</source>
|
|
||||||
<target>NIP-05 Verification</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Nothing to see here. Check back later!" xml:space="preserve">
|
|
||||||
<source>Nothing to see here. Check back later!</source>
|
|
||||||
<target>Nothing to see here. Check back later!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Notifications" xml:space="preserve">
|
|
||||||
<source>Notifications</source>
|
|
||||||
<target>Notifications</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Pay" xml:space="preserve">
|
|
||||||
<source>Pay</source>
|
|
||||||
<target>Pay</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Pay the lightning invoice" xml:space="preserve">
|
|
||||||
<source>Pay the lightning invoice</source>
|
|
||||||
<target>Pay the lightning invoice</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Phoenix" xml:space="preserve">
|
|
||||||
<source>Phoenix</source>
|
|
||||||
<target>Phoenix</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Phoenix.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Post" xml:space="preserve">
|
|
||||||
<source>Post</source>
|
|
||||||
<target>Post</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Posts" xml:space="preserve">
|
|
||||||
<source>Posts</source>
|
|
||||||
<target>Posts</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Posts & Replies" xml:space="preserve">
|
|
||||||
<source>Posts & Replies</source>
|
|
||||||
<target>Posts & Replies</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Private" xml:space="preserve">
|
|
||||||
<source>Private</source>
|
|
||||||
<target>Private</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Private Key" xml:space="preserve">
|
|
||||||
<source>Private Key</source>
|
|
||||||
<target>Private Key</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="PrivateKey" xml:space="preserve">
|
|
||||||
<source>PrivateKey</source>
|
|
||||||
<target>PrivateKey</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Profile Picture" xml:space="preserve">
|
|
||||||
<source>Profile Picture</source>
|
|
||||||
<target>Profile Picture</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Public Account ID" xml:space="preserve">
|
|
||||||
<source>Public Account ID</source>
|
|
||||||
<target>Public Account ID</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Public Key" xml:space="preserve">
|
|
||||||
<source>Public Key</source>
|
|
||||||
<target>Public Key</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Public Key?" xml:space="preserve">
|
|
||||||
<source>Public Key?</source>
|
|
||||||
<target>Public Key?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Recommended Relays" xml:space="preserve">
|
|
||||||
<source>Recommended Relays</source>
|
|
||||||
<target>Recommended Relays</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Relay" xml:space="preserve">
|
|
||||||
<source>Relay</source>
|
|
||||||
<target>Relay</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Relays" xml:space="preserve">
|
|
||||||
<source>Relays</source>
|
|
||||||
<target>Relays</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Reply to self" xml:space="preserve">
|
|
||||||
<source>Reply to self</source>
|
|
||||||
<target>Reply to self</target>
|
|
||||||
<note>Label to indicate that the user is replying to themself.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Replying to:" xml:space="preserve">
|
|
||||||
<source>Replying to:</source>
|
|
||||||
<target>Replying to:</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Reset" xml:space="preserve">
|
|
||||||
<source>Reset</source>
|
|
||||||
<target>Reset</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="River" xml:space="preserve">
|
|
||||||
<source>River</source>
|
|
||||||
<target>River</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, River</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Satoshi Nakamoto" xml:space="preserve">
|
|
||||||
<source>Satoshi Nakamoto</source>
|
|
||||||
<target>Satoshi Nakamoto</target>
|
|
||||||
<note>Name of Bitcoin creator(s).</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Save" xml:space="preserve">
|
|
||||||
<source>Save</source>
|
|
||||||
<target>Save</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Search hashtag: #%@" xml:space="preserve">
|
|
||||||
<source>Search hashtag: #%@</source>
|
|
||||||
<target>Search hashtag: #%@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Search..." xml:space="preserve">
|
|
||||||
<source>Search...</source>
|
|
||||||
<target>Search...</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Secret Account Login Key" xml:space="preserve">
|
|
||||||
<source>Secret Account Login Key</source>
|
|
||||||
<target>Secret Account Login Key</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Select a lightning wallet" xml:space="preserve">
|
|
||||||
<source>Select a lightning wallet</source>
|
|
||||||
<target>Select a lightning wallet</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Select default wallet" xml:space="preserve">
|
|
||||||
<source>Select default wallet</source>
|
|
||||||
<target>Select default wallet</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Send a message to start the conversation..." xml:space="preserve">
|
|
||||||
<source>Send a message to start the conversation...</source>
|
|
||||||
<target>Send a message to start the conversation...</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Settings" xml:space="preserve">
|
|
||||||
<source>Settings</source>
|
|
||||||
<target>Settings</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Share" xml:space="preserve">
|
|
||||||
<source>Share</source>
|
|
||||||
<target>Share</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Show" xml:space="preserve">
|
|
||||||
<source>Show</source>
|
|
||||||
<target>Show</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Show wallet selector" xml:space="preserve">
|
|
||||||
<source>Show wallet selector</source>
|
|
||||||
<target>Show wallet selector</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Strike" xml:space="preserve">
|
|
||||||
<source>Strike</source>
|
|
||||||
<target>Strike</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Strike.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
|
|
||||||
<source>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</source>
|
|
||||||
<target>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." xml:space="preserve">
|
|
||||||
<source>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</source>
|
|
||||||
<target>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="This is your account ID, you can give this to your friends so that they can follow you. Click to copy." xml:space="preserve">
|
|
||||||
<source>This is your account ID, you can give this to your friends so that they can follow you. Click to copy.</source>
|
|
||||||
<target>This is your account ID, you can give this to your friends so that they can follow you. Click to copy.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
|
|
||||||
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
|
|
||||||
<target>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Thread" xml:space="preserve">
|
|
||||||
<source>Thread</source>
|
|
||||||
<target>Thread</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Type your post here..." xml:space="preserve">
|
|
||||||
<source>Type your post here...</source>
|
|
||||||
<target>Type your post here...</target>
|
|
||||||
<note>Text box prompt to ask user to type their post.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Unfollow" xml:space="preserve">
|
|
||||||
<source>Unfollow</source>
|
|
||||||
<target>Unfollow</target>
|
|
||||||
<note>Button to unfollow a user.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Unfollowing" xml:space="preserve">
|
|
||||||
<source>Unfollowing</source>
|
|
||||||
<target>Unfollowing</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Unfollowing..." xml:space="preserve">
|
|
||||||
<source>Unfollowing...</source>
|
|
||||||
<target>Unfollowing...</target>
|
|
||||||
<note>Label to indicate that the user is in the process of unfollowing another user.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Unfollows" xml:space="preserve">
|
|
||||||
<source>Unfollows</source>
|
|
||||||
<target>Unfollows</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Username" xml:space="preserve">
|
|
||||||
<source>Username</source>
|
|
||||||
<target>Username</target>
|
|
||||||
<note>Label to prompt username entry.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Wallet Of Satoshi" xml:space="preserve">
|
|
||||||
<source>Wallet Of Satoshi</source>
|
|
||||||
<target>Wallet Of Satoshi</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Wallet Of Satoshi.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Wallet Selector" xml:space="preserve">
|
|
||||||
<source>Wallet Selector</source>
|
|
||||||
<target>Wallet Selector</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Website" xml:space="preserve">
|
|
||||||
<source>Website</source>
|
|
||||||
<target>Website</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Welcome to the social network %@ control." xml:space="preserve">
|
|
||||||
<source>Welcome to the social network %@ control.</source>
|
|
||||||
<target>Welcome to the social network %@ control.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Welcome, %@!" xml:space="preserve">
|
|
||||||
<source>Welcome, %@!</source>
|
|
||||||
<target>Welcome, %@!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Your Name" xml:space="preserve">
|
|
||||||
<source>Your Name</source>
|
|
||||||
<target>Your Name</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Zebedee" xml:space="preserve">
|
|
||||||
<source>Zebedee</source>
|
|
||||||
<target>Zebedee</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Zebedee.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Zeus LN" xml:space="preserve">
|
|
||||||
<source>Zeus LN</source>
|
|
||||||
<target>Zeus LN</target>
|
|
||||||
<note>Dropdown option label for Lightning wallet, Zeus LN.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
|
|
||||||
<source>https://example.com/pic.jpg</source>
|
|
||||||
<target>https://example.com/pic.jpg</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="https://jb55.com" xml:space="preserve">
|
|
||||||
<source>https://jb55.com</source>
|
|
||||||
<target>https://jb55.com</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="jb55@jb55.com" xml:space="preserve">
|
|
||||||
<source>jb55@jb55.com</source>
|
|
||||||
<target>jb55@jb55.com</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="none" xml:space="preserve">
|
|
||||||
<source>none</source>
|
|
||||||
<target>none</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="now" xml:space="preserve">
|
|
||||||
<source>now</source>
|
|
||||||
<target>now</target>
|
|
||||||
<note>String indicating that a given timestamp just occurred</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="optional" xml:space="preserve">
|
|
||||||
<source>optional</source>
|
|
||||||
<target>optional</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="replying_to_one_and_others" translate="no" xml:space="preserve">
|
|
||||||
<source>replying_to_one_and_others</source>
|
|
||||||
<target>replying_to_one_and_others</target>
|
|
||||||
<note>Label to indicate that the user is replying to 1 user and others. (Key in .stringsdict)</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="replying_to_two_and_others" translate="no" xml:space="preserve">
|
|
||||||
<source>replying_to_two_and_others</source>
|
|
||||||
<target>replying_to_two_and_others</target>
|
|
||||||
<note>Label to indicate that the user is replying to 2 users and others. (Key in .stringsdict)</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="satoshi" xml:space="preserve">
|
|
||||||
<source>satoshi</source>
|
|
||||||
<target>satoshi</target>
|
|
||||||
<note>Example username of Bitcoin creator(s), Satoshi Nakamoto.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="wss://some.relay.com" xml:space="preserve">
|
|
||||||
<source>wss://some.relay.com</source>
|
|
||||||
<target>wss://some.relay.com</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="you" xml:space="preserve">
|
|
||||||
<source>you</source>
|
|
||||||
<target>you</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="··· %lld other notes ···" xml:space="preserve">
|
|
||||||
<source>··· %lld other notes ···</source>
|
|
||||||
<target>··· %lld other notes ···</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="🤙" xml:space="preserve">
|
|
||||||
<source>🤙</source>
|
|
||||||
<target>🤙</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
<file original="damus/en.lproj/Localizable.stringsdict" source-language="en" target-language="en" datatype="plaintext">
|
|
||||||
<header>
|
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
|
|
||||||
</header>
|
|
||||||
<body>
|
|
||||||
<trans-unit id="/replying_to_one_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
|
||||||
<source>Replying to %@%#@others@</source>
|
|
||||||
<target>Replying to %@%#@others@</target>
|
|
||||||
<note>Label to indicate that the user is replying to 1 user and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_one_and_others:dict/others:dict/one:dict/:string" xml:space="preserve">
|
|
||||||
<source> & 1 other</source>
|
|
||||||
<target> & 1 other</target>
|
|
||||||
<note>Label to indicate that the user is replying to 1 user and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_one_and_others:dict/others:dict/other:dict/:string" xml:space="preserve">
|
|
||||||
<source> & %d others</source>
|
|
||||||
<target> & %d others</target>
|
|
||||||
<note>Label to indicate that the user is replying to 1 user and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_one_and_others:dict/others:dict/zero:dict/:string" xml:space="preserve">
|
|
||||||
<source/>
|
|
||||||
<target/>
|
|
||||||
<note>Label to indicate that the user is replying to 1 user and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_two_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
|
||||||
<source>Replying to %@, %@%#@others@</source>
|
|
||||||
<target>Replying to %@, %@%#@others@</target>
|
|
||||||
<note>Label to indicate that the user is replying to 2 users and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_two_and_others:dict/others:dict/one:dict/:string" xml:space="preserve">
|
|
||||||
<source> & 1 other</source>
|
|
||||||
<target> & 1 other</target>
|
|
||||||
<note>Label to indicate that the user is replying to 2 users and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_two_and_others:dict/others:dict/other:dict/:string" xml:space="preserve">
|
|
||||||
<source> & %d others</source>
|
|
||||||
<target> & %d others</target>
|
|
||||||
<note>Label to indicate that the user is replying to 2 users and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="/replying_to_two_and_others:dict/others:dict/zero:dict/:string" xml:space="preserve">
|
|
||||||
<source/>
|
|
||||||
<target/>
|
|
||||||
<note>Label to indicate that the user is replying to 2 users and others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/* Bundle display name */
|
|
||||||
"CFBundleDisplayName" = "Damus";
|
|
||||||
/* Bundle name */
|
|
||||||
"CFBundleName" = "damus";
|
|
||||||
Binary file not shown.
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>replying_to_one_and_others</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
|
||||||
<string>Replying to %@%#@others@</string>
|
|
||||||
<key>others</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringFormatSpecTypeKey</key>
|
|
||||||
<string>NSStringPluralRuleType</string>
|
|
||||||
<key>NSStringFormatValueTypeKey</key>
|
|
||||||
<string>d</string>
|
|
||||||
<key>zero</key>
|
|
||||||
<string></string>
|
|
||||||
<key>one</key>
|
|
||||||
<string> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %d others</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>replying_to_two_and_others</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
|
||||||
<string>Replying to %@, %@%#@others@</string>
|
|
||||||
<key>others</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSStringFormatSpecTypeKey</key>
|
|
||||||
<string>NSStringPluralRuleType</string>
|
|
||||||
<key>NSStringFormatValueTypeKey</key>
|
|
||||||
<string>d</string>
|
|
||||||
<key>zero</key>
|
|
||||||
<string></string>
|
|
||||||
<key>one</key>
|
|
||||||
<string> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %d others</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -22,6 +22,14 @@ static inline int is_whitespace(char c) {
|
|||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
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)
|
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||||
{
|
{
|
||||||
c->start = content;
|
c->start = content;
|
||||||
@@ -29,18 +37,35 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
|||||||
c->p = content;
|
c->p = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
static int consume_until_boundary(struct cursor *cur) {
|
||||||
char c;
|
char c;
|
||||||
|
|
||||||
while (cur->p < cur->end) {
|
while (cur->p < cur->end) {
|
||||||
c = *cur->p;
|
c = *cur->p;
|
||||||
|
|
||||||
if (is_whitespace(c))
|
if (is_boundary(c))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
cur->p++;
|
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;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +170,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
consume_until_whitespace(cur, 1);
|
consume_until_boundary(cur);
|
||||||
|
|
||||||
block->type = BLOCK_HASHTAG;
|
block->type = BLOCK_HASHTAG;
|
||||||
block->block.str.start = (const char*)(start + 1);
|
block->block.str.start = (const char*)(start + 1);
|
||||||
@@ -200,6 +225,9 @@ static int parse_url(struct cursor *cur, struct block *block) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strip any unwanted characters
|
||||||
|
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
|
||||||
|
|
||||||
block->type = BLOCK_URL;
|
block->type = BLOCK_URL;
|
||||||
block->block.str.start = (const char *)start;
|
block->block.str.start = (const char *)start;
|
||||||
block->block.str.end = (const char *)cur->p;
|
block->block.str.end = (const char *)cur->p;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/onevcat/Kingfisher",
|
"location" : "https://github.com/onevcat/Kingfisher",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
|
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||||
"version" : "7.4.0"
|
"version" : "7.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -17,15 +17,6 @@
|
|||||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "shimmer",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/joshuajhomann/Shimmer",
|
|
||||||
"state" : {
|
|
||||||
"branch" : "master",
|
|
||||||
"revision" : "2fde687b3f1d9c5409c53da095d3686361e41343"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "starscream",
|
"identity" : "starscream",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
98
damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme
Normal file
98
damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1420"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEF227F7A08200C66700"
|
||||||
|
BuildableName = "damusTests.xctest"
|
||||||
|
BlueprintName = "damusTests"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
||||||
|
BuildableName = "damusUITests.xctest"
|
||||||
|
BlueprintName = "damusUITests"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xC5",
|
||||||
|
"green" : "0x43",
|
||||||
|
"red" : "0xCC"
|
||||||
|
}
|
||||||
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
6
damus/Assets.xcassets/Colors/Contents.json
Normal file
6
damus/Assets.xcassets/Colors/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF4",
|
||||||
|
"green" : "0xEE",
|
||||||
|
"red" : "0xEE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1E",
|
||||||
|
"green" : "0x1C",
|
||||||
|
"red" : "0x1C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x00",
|
||||||
|
"green" : "0x00",
|
||||||
|
"red" : "0x00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFF",
|
||||||
|
"green" : "0x4D",
|
||||||
|
"red" : "0x4B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1E",
|
||||||
|
"green" : "0x1C",
|
||||||
|
"red" : "0x1C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x4F",
|
||||||
|
"green" : "0xC3",
|
||||||
|
"red" : "0x66"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF4",
|
||||||
|
"green" : "0xEE",
|
||||||
|
"red" : "0xEE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x5F",
|
||||||
|
"green" : "0x5F",
|
||||||
|
"red" : "0x5F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xC5",
|
||||||
|
"green" : "0x43",
|
||||||
|
"red" : "0xCC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFF",
|
||||||
|
"green" : "0xFF",
|
||||||
|
"red" : "0xFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
6
damus/Assets.xcassets/Profile/Contents.json
Normal file
6
damus/Assets.xcassets/Profile/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
12
damus/Assets.xcassets/Profile/profile-banner.imageset/Contents.json
vendored
Normal file
12
damus/Assets.xcassets/Profile/profile-banner.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "profile-banner.jpeg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
damus/Assets.xcassets/Profile/profile-banner.imageset/profile-banner.jpeg
vendored
Normal file
BIN
damus/Assets.xcassets/Profile/profile-banner.imageset/profile-banner.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
11
damus/Assets.xcassets/bbw.imageset/Contents.json
vendored
11
damus/Assets.xcassets/bbw.imageset/Contents.json
vendored
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "bbw.jpg",
|
"filename" : "bbw.jpg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
21
damus/Assets.xcassets/bitcoin-logo.imageset/Contents.json
vendored
Normal file
21
damus/Assets.xcassets/bitcoin-logo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "bitcoin-logo.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
7
damus/Assets.xcassets/bitcoin-logo.imageset/bitcoin-logo.svg
vendored
Normal file
7
damus/Assets.xcassets/bitcoin-logo.imageset/bitcoin-logo.svg
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 20 20" version="1.1">
|
||||||
|
<g id="surface1">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.862745%,57.647059%,10.196078%);fill-opacity:1;" d="M 19.699219 12.417969 C 18.363281 17.777344 12.9375 21.035156 7.582031 19.699219 C 2.226562 18.363281 -1.035156 12.9375 0.300781 7.582031 C 1.636719 2.222656 7.0625 -1.035156 12.417969 0.300781 C 17.773438 1.632812 21.035156 7.0625 19.699219 12.417969 Z M 19.699219 12.417969 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.410156 8.574219 C 14.609375 7.246094 13.59375 6.53125 12.210938 6.050781 L 12.660156 4.25 L 11.5625 3.976562 L 11.125 5.730469 C 10.835938 5.660156 10.539062 5.589844 10.246094 5.523438 L 10.6875 3.757812 L 9.589844 3.484375 L 9.140625 5.285156 C 8.902344 5.230469 8.667969 5.179688 8.4375 5.121094 L 8.441406 5.117188 L 6.925781 4.738281 L 6.636719 5.910156 C 6.636719 5.910156 7.449219 6.097656 7.433594 6.109375 C 7.875 6.21875 7.957031 6.511719 7.941406 6.746094 L 7.429688 8.800781 C 7.460938 8.808594 7.5 8.820312 7.546875 8.835938 L 7.429688 8.808594 L 6.710938 11.683594 C 6.65625 11.820312 6.519531 12.023438 6.210938 11.945312 C 6.21875 11.960938 5.410156 11.746094 5.410156 11.746094 L 4.867188 13 L 6.296875 13.359375 C 6.5625 13.425781 6.820312 13.492188 7.078125 13.558594 L 6.621094 15.382812 L 7.71875 15.65625 L 8.167969 13.851562 C 8.46875 13.933594 8.757812 14.007812 9.042969 14.078125 L 8.59375 15.875 L 9.691406 16.148438 L 10.144531 14.328125 C 12.015625 14.683594 13.425781 14.539062 14.015625 12.847656 C 14.492188 11.484375 13.992188 10.699219 13.007812 10.1875 C 13.726562 10.019531 14.265625 9.550781 14.410156 8.574219 Z M 11.902344 12.089844 C 11.5625 13.453125 9.269531 12.71875 8.523438 12.53125 L 9.128906 10.117188 C 9.871094 10.300781 12.253906 10.667969 11.902344 12.089844 Z M 12.242188 8.554688 C 11.933594 9.796875 10.023438 9.164062 9.402344 9.011719 L 9.949219 6.820312 C 10.570312 6.976562 12.5625 7.261719 12.242188 8.554688 Z M 12.242188 8.554688 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "bitcoin-p2p.png",
|
"filename" : "bitcoin-p2p.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "blixt-wallet.png",
|
"filename" : "blixt-wallet.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "bluewallet.png",
|
"filename" : "bluewallet.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "breez.jpg",
|
"filename" : "breez.jpg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "cashapp.png",
|
"filename" : "cashapp.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
23
damus/Assets.xcassets/damus-home.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/damus-home.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "damus-home@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "damus-home@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "damus-home@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@1x.png
vendored
Normal file
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@1x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@2x.png
vendored
Normal file
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@3x.png
vendored
Normal file
BIN
damus/Assets.xcassets/damus-home.imageset/damus-home@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "digital-nomad.png",
|
"filename" : "digital-nomad.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "encrypted-message.png",
|
"filename" : "encrypted-message.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "lnlink.png",
|
"filename" : "lnlink.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "damus-nobg.png",
|
"filename" : "damus-nobg.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "muun.png",
|
"filename" : "muun.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "phoenix.png",
|
"filename" : "phoenix.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "river.png",
|
"filename" : "river.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
15
damus/Assets.xcassets/shaka-full.imageset/Contents.json
vendored
Normal file
15
damus/Assets.xcassets/shaka-full.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "shaka-full.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
88
damus/Assets.xcassets/shaka-full.imageset/shaka-full.pdf
vendored
Normal file
88
damus/Assets.xcassets/shaka-full.imageset/shaka-full.pdf
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.073975 cm
|
||||||
|
1.000000 1.000000 1.000000 scn
|
||||||
|
1.295334 8.661732 m
|
||||||
|
3.613694 8.367855 l
|
||||||
|
4.475733 8.733568 5.268113 9.771931 5.474915 10.327032 c
|
||||||
|
6.083156 11.959681 5.507567 14.604573 5.474915 15.061715 c
|
||||||
|
5.448792 15.427428 6.008246 15.693006 6.291239 15.780080 c
|
||||||
|
7.571236 15.858447 8.508359 14.876789 8.642253 13.984165 c
|
||||||
|
8.740212 13.331103 8.576948 11.752880 8.381030 10.849482 c
|
||||||
|
8.979668 10.936556 10.980525 10.901726 11.868687 10.849482 c
|
||||||
|
12.756847 10.797236 13.474895 10.196423 14.193260 9.412750 c
|
||||||
|
14.767952 8.237244 13.953805 7.725680 13.474895 7.616838 c
|
||||||
|
13.834077 7.257654 l
|
||||||
|
14.781013 5.918882 13.649043 5.178749 13.115711 5.004600 c
|
||||||
|
13.474895 4.743376 l
|
||||||
|
14.487136 3.763786 13.246323 2.751544 13.017752 2.882155 c
|
||||||
|
11.058574 3.176033 l
|
||||||
|
15.499378 1.673996 l
|
||||||
|
16.054478 0.400530 15.074889 0.073999 14.781013 0.073999 c
|
||||||
|
8.576947 1.673996 l
|
||||||
|
6.291239 1.673996 5.311650 1.869914 4.299407 2.163791 c
|
||||||
|
4.157911 2.131138 3.659409 1.987464 2.797370 1.673996 c
|
||||||
|
1.935332 1.360527 1.219143 2.087601 0.968804 2.490320 c
|
||||||
|
-0.285071 4.083785 -0.467927 7.257655 1.295334 8.661732 c
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
1149
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 15.666626 15.710510 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001239 00000 n
|
||||||
|
0000001262 00000 n
|
||||||
|
0000001435 00000 n
|
||||||
|
0000001509 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1568
|
||||||
|
%%EOF
|
||||||
15
damus/Assets.xcassets/shaka-line.imageset/Contents.json
vendored
Normal file
15
damus/Assets.xcassets/shaka-line.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "shaka-line.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
323
damus/Assets.xcassets/shaka-line.imageset/shaka-line.pdf
vendored
Normal file
323
damus/Assets.xcassets/shaka-line.imageset/shaka-line.pdf
vendored
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.474731 -0.563965 cm
|
||||||
|
1.000000 1.000000 1.000000 scn
|
||||||
|
3.613694 9.332577 m
|
||||||
|
3.553993 8.861599 l
|
||||||
|
3.637261 8.851044 3.721838 8.862753 3.799107 8.895533 c
|
||||||
|
3.613694 9.332577 l
|
||||||
|
h
|
||||||
|
1.295334 9.626453 m
|
||||||
|
1.355035 10.097433 l
|
||||||
|
1.227973 10.113539 1.099794 10.077623 0.999601 9.997839 c
|
||||||
|
1.295334 9.626453 l
|
||||||
|
h
|
||||||
|
0.968804 3.455042 m
|
||||||
|
1.372000 3.705677 l
|
||||||
|
1.362764 3.720535 1.352713 3.734872 1.341894 3.748621 c
|
||||||
|
0.968804 3.455042 l
|
||||||
|
h
|
||||||
|
4.299407 3.128512 m
|
||||||
|
4.431771 3.584435 l
|
||||||
|
4.353942 3.607030 4.271623 3.609325 4.192656 3.591103 c
|
||||||
|
4.299407 3.128512 l
|
||||||
|
h
|
||||||
|
8.576947 2.638718 m
|
||||||
|
8.695503 3.098424 l
|
||||||
|
8.656776 3.108411 8.616942 3.113465 8.576947 3.113465 c
|
||||||
|
8.576947 2.638718 l
|
||||||
|
h
|
||||||
|
14.781013 1.038721 m
|
||||||
|
14.662457 0.579016 l
|
||||||
|
14.701184 0.569027 14.741018 0.563974 14.781013 0.563974 c
|
||||||
|
14.781013 1.038721 l
|
||||||
|
h
|
||||||
|
15.499378 2.638718 m
|
||||||
|
15.934578 2.828420 l
|
||||||
|
15.881091 2.951125 15.778289 3.045548 15.651489 3.088437 c
|
||||||
|
15.499378 2.638718 l
|
||||||
|
h
|
||||||
|
11.058574 4.140755 m
|
||||||
|
11.128998 4.610250 l
|
||||||
|
10.885809 4.646729 10.655017 4.491467 10.597156 4.252461 c
|
||||||
|
10.539293 4.013455 10.673516 3.769826 10.906463 3.691035 c
|
||||||
|
11.058574 4.140755 l
|
||||||
|
h
|
||||||
|
13.017752 3.846877 m
|
||||||
|
13.253292 4.259073 l
|
||||||
|
13.202273 4.288227 13.146286 4.307655 13.088176 4.316372 c
|
||||||
|
13.017752 3.846877 l
|
||||||
|
h
|
||||||
|
13.474895 5.708097 m
|
||||||
|
13.805044 6.049252 l
|
||||||
|
13.789093 6.064689 13.772079 6.078987 13.754128 6.092043 c
|
||||||
|
13.474895 5.708097 l
|
||||||
|
h
|
||||||
|
13.115711 5.969321 m
|
||||||
|
12.968349 6.420619 l
|
||||||
|
12.798800 6.365256 12.674588 6.219535 12.646772 6.043359 c
|
||||||
|
12.618958 5.867183 12.692234 5.690281 12.836478 5.585376 c
|
||||||
|
13.115711 5.969321 l
|
||||||
|
h
|
||||||
|
13.834077 8.222376 m
|
||||||
|
14.221668 8.496526 l
|
||||||
|
14.206144 8.518474 14.188784 8.539063 14.169774 8.558073 c
|
||||||
|
13.834077 8.222376 l
|
||||||
|
h
|
||||||
|
13.474895 8.581559 m
|
||||||
|
13.369680 9.044500 l
|
||||||
|
13.201114 9.006190 13.066693 8.879284 13.018762 8.713197 c
|
||||||
|
12.970830 8.547110 13.016963 8.368095 13.139197 8.245862 c
|
||||||
|
13.474895 8.581559 l
|
||||||
|
h
|
||||||
|
14.193260 10.377472 m
|
||||||
|
14.619765 10.585986 l
|
||||||
|
14.599768 10.626891 14.573989 10.664707 14.543221 10.698271 c
|
||||||
|
14.193260 10.377472 l
|
||||||
|
h
|
||||||
|
8.381030 11.814203 m
|
||||||
|
7.917068 11.914822 l
|
||||||
|
7.884080 11.762714 7.927746 11.604099 8.033934 11.490305 c
|
||||||
|
8.140121 11.376513 8.295343 11.321997 8.449365 11.344399 c
|
||||||
|
8.381030 11.814203 l
|
||||||
|
h
|
||||||
|
8.642253 14.948887 m
|
||||||
|
9.111748 15.019311 l
|
||||||
|
8.642253 14.948887 l
|
||||||
|
h
|
||||||
|
6.291239 16.744801 m
|
||||||
|
6.262227 17.218662 l
|
||||||
|
6.224693 17.216364 6.187564 17.209614 6.151623 17.198555 c
|
||||||
|
6.291239 16.744801 l
|
||||||
|
h
|
||||||
|
5.474915 16.026436 m
|
||||||
|
5.948456 16.060261 l
|
||||||
|
5.474915 16.026436 l
|
||||||
|
h
|
||||||
|
5.474915 11.291754 m
|
||||||
|
5.030037 11.457493 l
|
||||||
|
5.474915 11.291754 l
|
||||||
|
h
|
||||||
|
3.673396 9.803555 m
|
||||||
|
1.355035 10.097433 l
|
||||||
|
1.235632 9.155476 l
|
||||||
|
3.553993 8.861599 l
|
||||||
|
3.673396 9.803555 l
|
||||||
|
h
|
||||||
|
0.999601 9.997839 m
|
||||||
|
-0.029049 9.178730 -0.454726 7.875908 -0.474048 6.619066 c
|
||||||
|
-0.493367 5.362488 -0.110331 4.058727 0.595713 3.161463 c
|
||||||
|
1.341894 3.748621 l
|
||||||
|
0.794064 4.444821 0.458734 5.524729 0.475334 6.604470 c
|
||||||
|
0.491930 7.683949 0.856455 8.670100 1.591066 9.255068 c
|
||||||
|
0.999601 9.997839 l
|
||||||
|
h
|
||||||
|
0.565608 3.204407 m
|
||||||
|
0.721970 2.952868 1.013515 2.611341 1.407507 2.372385 c
|
||||||
|
1.811404 2.127421 2.357187 1.973489 2.959612 2.192553 c
|
||||||
|
2.635129 3.084882 l
|
||||||
|
2.375515 2.990478 2.132184 3.043347 1.899893 3.184233 c
|
||||||
|
1.657696 3.331126 1.465977 3.554496 1.372000 3.705677 c
|
||||||
|
0.565608 3.204407 l
|
||||||
|
h
|
||||||
|
2.959612 2.192553 m
|
||||||
|
3.816493 2.504146 4.293336 2.639887 4.406158 2.665923 c
|
||||||
|
4.192656 3.591103 l
|
||||||
|
4.022485 3.551832 3.502325 3.400227 2.635129 3.084882 c
|
||||||
|
2.959612 2.192553 l
|
||||||
|
h
|
||||||
|
4.167043 2.672591 m
|
||||||
|
5.229115 2.364247 6.254152 2.163970 8.576947 2.163970 c
|
||||||
|
8.576947 3.113465 l
|
||||||
|
6.328326 3.113465 5.394184 3.305025 4.431771 3.584435 c
|
||||||
|
4.167043 2.672591 l
|
||||||
|
h
|
||||||
|
8.458392 2.179011 m
|
||||||
|
14.662457 0.579016 l
|
||||||
|
14.899569 1.498427 l
|
||||||
|
8.695503 3.098424 l
|
||||||
|
8.458392 2.179011 l
|
||||||
|
h
|
||||||
|
14.781013 0.563974 m
|
||||||
|
15.036198 0.563974 15.495326 0.684875 15.814721 1.047266 c
|
||||||
|
16.180891 1.462728 16.264221 2.072176 15.934578 2.828420 c
|
||||||
|
15.064179 2.449016 l
|
||||||
|
15.289635 1.931793 15.160722 1.741243 15.102402 1.675073 c
|
||||||
|
15.055794 1.622190 14.990156 1.579316 14.916806 1.549556 c
|
||||||
|
14.881134 1.535082 14.847747 1.525430 14.820526 1.519657 c
|
||||||
|
14.791491 1.513498 14.777695 1.513469 14.781013 1.513469 c
|
||||||
|
14.781013 0.563974 l
|
||||||
|
h
|
||||||
|
15.651489 3.088437 m
|
||||||
|
11.210685 4.590474 l
|
||||||
|
10.906463 3.691035 l
|
||||||
|
15.347267 2.188998 l
|
||||||
|
15.651489 3.088437 l
|
||||||
|
h
|
||||||
|
10.988150 3.671260 m
|
||||||
|
12.947328 3.377382 l
|
||||||
|
13.088176 4.316372 l
|
||||||
|
11.128998 4.610250 l
|
||||||
|
10.988150 3.671260 l
|
||||||
|
h
|
||||||
|
12.782211 3.434681 m
|
||||||
|
12.991495 3.315090 13.204453 3.370091 13.288217 3.396689 c
|
||||||
|
13.400116 3.432221 13.506123 3.490767 13.598186 3.554502 c
|
||||||
|
13.783985 3.683133 13.977411 3.877748 14.120350 4.119644 c
|
||||||
|
14.264680 4.363894 14.369576 4.678114 14.335162 5.031647 c
|
||||||
|
14.300108 5.391746 14.125634 5.739002 13.805044 6.049252 c
|
||||||
|
13.144745 5.366943 l
|
||||||
|
13.330275 5.187398 13.380290 5.040778 13.390134 4.939653 c
|
||||||
|
13.400617 4.831963 13.370820 4.717613 13.302905 4.602680 c
|
||||||
|
13.233600 4.485394 13.137231 4.390213 13.057724 4.335170 c
|
||||||
|
13.017135 4.307070 12.996612 4.300308 13.000857 4.301657 c
|
||||||
|
13.003194 4.302399 13.024761 4.309311 13.061064 4.310122 c
|
||||||
|
13.095938 4.310902 13.170414 4.306433 13.253292 4.259073 c
|
||||||
|
12.782211 3.434681 l
|
||||||
|
h
|
||||||
|
13.754128 6.092043 m
|
||||||
|
13.394944 6.353267 l
|
||||||
|
12.836478 5.585376 l
|
||||||
|
13.195662 5.324152 l
|
||||||
|
13.754128 6.092043 l
|
||||||
|
h
|
||||||
|
13.263074 5.518023 m
|
||||||
|
13.593105 5.625790 14.123367 5.907292 14.433812 6.409482 c
|
||||||
|
14.595931 6.671733 14.696482 6.993351 14.669847 7.364054 c
|
||||||
|
14.643518 7.730516 14.495621 8.109214 14.221668 8.496526 c
|
||||||
|
13.446486 7.948226 l
|
||||||
|
13.646002 7.666152 13.711709 7.450294 13.722795 7.296009 c
|
||||||
|
13.733575 7.145966 13.695351 7.020646 13.626177 6.908748 c
|
||||||
|
13.474038 6.662641 13.171650 6.487002 12.968349 6.420619 c
|
||||||
|
13.263074 5.518023 l
|
||||||
|
h
|
||||||
|
14.169774 8.558073 m
|
||||||
|
13.810592 8.917255 l
|
||||||
|
13.139197 8.245862 l
|
||||||
|
13.498380 7.886679 l
|
||||||
|
14.169774 8.558073 l
|
||||||
|
h
|
||||||
|
13.580109 8.118617 m
|
||||||
|
13.896242 8.190466 14.344993 8.395787 14.624650 8.816864 c
|
||||||
|
14.929440 9.275781 14.963785 9.882310 14.619765 10.585986 c
|
||||||
|
13.766754 10.168959 l
|
||||||
|
13.997427 9.697128 13.912044 9.460121 13.833706 9.342171 c
|
||||||
|
13.730235 9.186377 13.532457 9.081495 13.369680 9.044500 c
|
||||||
|
13.580109 8.118617 l
|
||||||
|
h
|
||||||
|
14.543221 10.698271 m
|
||||||
|
13.820906 11.486253 12.989320 12.223852 11.896564 12.288132 c
|
||||||
|
11.840808 11.340275 l
|
||||||
|
12.524374 11.300065 13.128883 10.836036 13.843298 10.056674 c
|
||||||
|
14.543221 10.698271 l
|
||||||
|
h
|
||||||
|
11.896564 12.288132 m
|
||||||
|
11.441970 12.314873 10.711069 12.336796 10.019300 12.341186 c
|
||||||
|
9.341933 12.345484 8.654247 12.333687 8.312695 12.284006 c
|
||||||
|
8.449365 11.344399 l
|
||||||
|
8.706450 11.381794 9.318512 11.396118 10.013274 11.391710 c
|
||||||
|
10.693633 11.387392 11.407242 11.365778 11.840808 11.340275 c
|
||||||
|
11.896564 12.288132 l
|
||||||
|
h
|
||||||
|
8.844993 11.713585 m
|
||||||
|
8.948084 12.188952 9.040332 12.829445 9.094679 13.432834 c
|
||||||
|
9.147870 14.023395 9.169946 14.631327 9.111748 15.019311 c
|
||||||
|
8.172758 14.878462 l
|
||||||
|
8.212520 14.613384 8.201942 14.105675 8.149012 13.518009 c
|
||||||
|
8.097237 12.943172 8.009893 12.342852 7.917068 11.914822 c
|
||||||
|
8.844993 11.713585 l
|
||||||
|
h
|
||||||
|
9.111748 15.019311 m
|
||||||
|
8.944062 16.137217 7.805658 17.313158 6.262227 17.218662 c
|
||||||
|
6.320251 16.270941 l
|
||||||
|
7.336813 16.333179 8.072657 15.545805 8.172758 14.878462 c
|
||||||
|
9.111748 15.019311 l
|
||||||
|
h
|
||||||
|
6.151623 17.198555 m
|
||||||
|
5.976391 17.144638 5.715709 17.036982 5.490986 16.876261 c
|
||||||
|
5.292936 16.734617 4.969444 16.439627 5.001374 15.992612 c
|
||||||
|
5.948456 16.060261 l
|
||||||
|
5.951383 16.019283 5.934667 15.999795 5.943361 16.012491 c
|
||||||
|
5.954769 16.029152 5.984430 16.061831 6.043331 16.103956 c
|
||||||
|
6.162553 16.189222 6.323094 16.257891 6.430855 16.291048 c
|
||||||
|
6.151623 17.198555 l
|
||||||
|
h
|
||||||
|
5.001374 15.992612 m
|
||||||
|
5.011176 15.855374 5.059216 15.566318 5.104405 15.255149 c
|
||||||
|
5.152757 14.922197 5.207128 14.509316 5.241940 14.062993 c
|
||||||
|
5.312967 13.152368 5.295928 12.171200 5.030037 11.457493 c
|
||||||
|
5.919792 11.126015 l
|
||||||
|
6.262142 12.044956 6.261431 13.202559 6.188560 14.136827 c
|
||||||
|
6.151423 14.612950 6.093790 15.049047 6.044043 15.391605 c
|
||||||
|
5.991133 15.755945 5.954979 15.968927 5.948456 16.060261 c
|
||||||
|
5.001374 15.992612 l
|
||||||
|
h
|
||||||
|
5.030037 11.457493 m
|
||||||
|
4.953650 11.252455 4.742510 10.903708 4.434547 10.555828 c
|
||||||
|
4.127778 10.209298 3.769400 9.914337 3.428282 9.769621 c
|
||||||
|
3.799107 8.895533 l
|
||||||
|
4.320028 9.116529 4.788858 9.523607 5.145489 9.926461 c
|
||||||
|
5.500926 10.327968 5.789377 10.775953 5.919792 11.126015 c
|
||||||
|
5.030037 11.457493 l
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
7995
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 16.615845 16.660034 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000008085 00000 n
|
||||||
|
0000008108 00000 n
|
||||||
|
0000008281 00000 n
|
||||||
|
0000008355 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
8414
|
||||||
|
%%EOF
|
||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "strike.png",
|
"filename" : "strike.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "undercover.png",
|
"filename" : "undercover.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "walletofsatoshi.png",
|
"filename" : "walletofsatoshi.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "zebedee.png",
|
"filename" : "zebedee.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "zeus.png",
|
"filename" : "zeus.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
61
damus/Components/CustomPicker.swift
Normal file
61
damus/Components/CustomPicker.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// CustomPicker.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Eric Holguin on 1/22/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||||
|
DamusColors.purple,
|
||||||
|
DamusColors.blue
|
||||||
|
]), startPoint: .leading, endPoint: .trailing)
|
||||||
|
|
||||||
|
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@Namespace var picker
|
||||||
|
@Binding var selection: SelectionValue
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
let contentMirror = Mirror(reflecting: content)
|
||||||
|
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
|
||||||
|
HStack {
|
||||||
|
ForEach(0..<blocksCount, id: \.self) { index in
|
||||||
|
let tupleBlock = contentMirror.descendant("value", ".\(index)")
|
||||||
|
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
|
||||||
|
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
|
||||||
|
|
||||||
|
Button {
|
||||||
|
withAnimation(.spring()) {
|
||||||
|
selection = tag
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
text
|
||||||
|
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||||
|
.font(.system(size: 14, weight: .heavy))
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
Group {
|
||||||
|
if tag == selection {
|
||||||
|
Rectangle().fill(RECTANGLE_GRADIENT).frame(height: 2.5)
|
||||||
|
.matchedGeometryEffect(id: "selector", in: picker)
|
||||||
|
.cornerRadius(2.5)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alignment: .bottom
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.accentColor(tag == selection ? textColor() : .gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color(UIColor.systemBackground))
|
||||||
|
}
|
||||||
|
|
||||||
|
func textColor() -> Color {
|
||||||
|
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||||
|
}
|
||||||
|
}
|
||||||
22
damus/Components/DamusColors.swift
Normal file
22
damus/Components/DamusColors.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// DamusColors.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-03-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class DamusColors {
|
||||||
|
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||||
|
static let white = Color("DamusWhite")
|
||||||
|
static let black = Color("DamusBlack")
|
||||||
|
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 blue = Color("DamusBlue")
|
||||||
|
}
|
||||||
|
|
||||||
39
damus/Components/Highlight.swift
Normal file
39
damus/Components/Highlight.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// Highlight.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
enum Highlight {
|
||||||
|
case none
|
||||||
|
case main
|
||||||
|
case reply
|
||||||
|
case custom(Color, Float)
|
||||||
|
|
||||||
|
var is_main: Bool {
|
||||||
|
if case .main = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_none: Bool {
|
||||||
|
if case .none = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_replied_to: Bool {
|
||||||
|
switch self {
|
||||||
|
case .reply: return true
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
44
damus/Components/IconLabel.swift
Normal file
44
damus/Components/IconLabel.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// IconLabel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-04-05.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct IconLabel: View {
|
||||||
|
let text: String
|
||||||
|
let img_name: String
|
||||||
|
let img_color: Color
|
||||||
|
|
||||||
|
init(_ text: String, img_name: String, color: Color) {
|
||||||
|
self.text = text
|
||||||
|
self.img_name = img_name
|
||||||
|
self.img_color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image(systemName: img_name)
|
||||||
|
.foregroundColor(img_color)
|
||||||
|
.frame(width: 20)
|
||||||
|
.padding([.trailing], 20)
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
struct IconLabel_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange)
|
||||||
|
|
||||||
|
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
|
||||||
|
|
||||||
|
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,14 @@ import Kingfisher
|
|||||||
struct ShareSheet: UIViewControllerRepresentable {
|
struct ShareSheet: UIViewControllerRepresentable {
|
||||||
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
|
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
|
||||||
|
|
||||||
let activityItems: [URL]
|
let activityItems: [URL?]
|
||||||
let callback: Callback? = nil
|
let callback: Callback? = nil
|
||||||
let applicationActivities: [UIActivity]? = nil
|
let applicationActivities: [UIActivity]? = nil
|
||||||
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
|
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||||
let controller = UIActivityViewController(
|
let controller = UIActivityViewController(
|
||||||
activityItems: activityItems,
|
activityItems: activityItems as [Any],
|
||||||
applicationActivities: applicationActivities)
|
applicationActivities: applicationActivities)
|
||||||
controller.excludedActivityTypes = excludedActivityTypes
|
controller.excludedActivityTypes = excludedActivityTypes
|
||||||
controller.completionWithItemsHandler = callback
|
controller.completionWithItemsHandler = callback
|
||||||
@@ -31,92 +31,43 @@ struct ShareSheet: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageContextMenuModifier: ViewModifier {
|
|
||||||
let url: URL
|
enum ImageShape {
|
||||||
let image: UIImage?
|
case square
|
||||||
@Binding var showShareSheet: Bool
|
case landscape
|
||||||
|
case portrait
|
||||||
func body(content: Content) -> some View {
|
case unknown
|
||||||
return content.contextMenu {
|
|
||||||
Button {
|
|
||||||
UIPasteboard.general.url = url
|
|
||||||
} label: {
|
|
||||||
Label("Copy Image URL", systemImage: "doc.on.doc")
|
|
||||||
}
|
|
||||||
if let someImage = image {
|
|
||||||
Button {
|
|
||||||
UIPasteboard.general.image = someImage
|
|
||||||
} label: {
|
|
||||||
Label("Copy Image", systemImage: "photo.on.rectangle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
showShareSheet = true
|
|
||||||
} label: {
|
|
||||||
Label("Share", systemImage: "square.and.arrow.up")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageViewer: View {
|
|
||||||
let urls: [URL]
|
|
||||||
|
|
||||||
private struct ImageHandler: ImageModifier {
|
|
||||||
@Binding var handler: UIImage?
|
|
||||||
|
|
||||||
func modify(_ image: UIImage) -> UIImage {
|
|
||||||
handler = image
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@State private var image: UIImage?
|
|
||||||
@State private var showShareSheet = false
|
|
||||||
|
|
||||||
func onShared(completed: Bool) -> Void {
|
|
||||||
if (completed) {
|
|
||||||
showShareSheet = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
TabView {
|
|
||||||
ForEach(urls, id: \.absoluteString) { url in
|
|
||||||
VStack{
|
|
||||||
Text(url.lastPathComponent)
|
|
||||||
|
|
||||||
KFAnimatedImage(url)
|
|
||||||
.configure { view in
|
|
||||||
view.framePreloadCount = 3
|
|
||||||
}
|
|
||||||
.cacheOriginalImage()
|
|
||||||
.imageModifier(ImageHandler(handler: $image))
|
|
||||||
.loadDiskFileSynchronously()
|
|
||||||
.scaleFactor(UIScreen.main.scale)
|
|
||||||
.fade(duration: 0.1)
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.tabItem {
|
|
||||||
Text(url.absoluteString)
|
|
||||||
}
|
|
||||||
.id(url.absoluteString)
|
|
||||||
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
|
|
||||||
.sheet(isPresented: $showShareSheet) {
|
|
||||||
ShareSheet(activityItems: [url])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tabViewStyle(PageTabViewStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ImageCarousel: View {
|
struct ImageCarousel: View {
|
||||||
var urls: [URL]
|
var urls: [URL]
|
||||||
|
|
||||||
@State var open_sheet: Bool = false
|
let evid: String
|
||||||
@State var current_url: URL? = nil
|
let previews: PreviewCache
|
||||||
|
|
||||||
|
@State private var open_sheet: Bool = false
|
||||||
|
@State private var current_url: URL? = nil
|
||||||
|
@State private var image_fill: ImageFill? = nil
|
||||||
|
@State private var fillHeight: CGFloat = 350
|
||||||
|
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
|
||||||
|
|
||||||
|
init(previews: PreviewCache, evid: String, urls: [URL]) {
|
||||||
|
_open_sheet = State(initialValue: false)
|
||||||
|
_current_url = State(initialValue: nil)
|
||||||
|
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
|
||||||
|
self.urls = urls
|
||||||
|
self.evid = evid
|
||||||
|
self.previews = previews
|
||||||
|
}
|
||||||
|
|
||||||
|
var filling: Bool {
|
||||||
|
image_fill?.filling == true
|
||||||
|
}
|
||||||
|
|
||||||
|
var height: CGFloat {
|
||||||
|
image_fill?.height ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView {
|
||||||
@@ -124,32 +75,32 @@ struct ImageCarousel: View {
|
|||||||
Rectangle()
|
Rectangle()
|
||||||
.foregroundColor(Color.clear)
|
.foregroundColor(Color.clear)
|
||||||
.overlay {
|
.overlay {
|
||||||
KFAnimatedImage(url)
|
GeometryReader { geo in
|
||||||
.configure { view in
|
KFAnimatedImage(url)
|
||||||
view.framePreloadCount = 3
|
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||||
}
|
.backgroundDecode(true)
|
||||||
.cacheOriginalImage()
|
.imageContext(.note)
|
||||||
.loadDiskFileSynchronously()
|
.cancelOnDisappear(true)
|
||||||
.scaleFactor(UIScreen.main.scale)
|
.configure { view in
|
||||||
.fade(duration: 0.1)
|
view.framePreloadCount = 3
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.tabItem {
|
|
||||||
Text(url.absoluteString)
|
|
||||||
}
|
|
||||||
.id(url.absoluteString)
|
|
||||||
.contextMenu {
|
|
||||||
Button("Copy Image") {
|
|
||||||
UIPasteboard.general.string = url.absoluteString
|
|
||||||
}
|
}
|
||||||
}
|
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||||
|
previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||||
|
image_fill = fill
|
||||||
|
}
|
||||||
|
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||||
|
.tabItem {
|
||||||
|
Text(url.absoluteString)
|
||||||
|
}
|
||||||
|
.id(url.absoluteString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cornerRadius(10)
|
.fullScreenCover(isPresented: $open_sheet) {
|
||||||
.sheet(isPresented: $open_sheet) {
|
ImageView(urls: urls)
|
||||||
ImageViewer(urls: urls)
|
|
||||||
}
|
}
|
||||||
.frame(height: 200)
|
.frame(height: height)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
open_sheet = true
|
open_sheet = true
|
||||||
}
|
}
|
||||||
@@ -157,8 +108,71 @@ struct ImageCarousel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageCarousel_Previews: PreviewProvider {
|
// MARK: - Image Modifier
|
||||||
static var previews: some View {
|
extension KFOptionSetter {
|
||||||
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!])
|
/// Sets a block to get image size
|
||||||
|
///
|
||||||
|
/// - Parameter block: The block which is used to read the image object.
|
||||||
|
/// - Returns: `Self` value after read size
|
||||||
|
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
|
||||||
|
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||||
|
let img_size = image.size
|
||||||
|
let geo_size = size
|
||||||
|
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
|
||||||
|
img_size: img_size,
|
||||||
|
maxHeight: max,
|
||||||
|
fillHeight: fill)
|
||||||
|
DispatchQueue.main.async { [block, fill] in
|
||||||
|
try? block(fill)
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
options.imageModifier = modifier
|
||||||
|
return self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct ImageFill {
|
||||||
|
let filling: Bool?
|
||||||
|
let height: CGFloat
|
||||||
|
|
||||||
|
|
||||||
|
static func determine_image_shape(_ size: CGSize) -> ImageShape {
|
||||||
|
guard size.height > 0 else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
let imageRatio = size.width / size.height
|
||||||
|
switch imageRatio {
|
||||||
|
case 1.0: return .square
|
||||||
|
case ..<1.0: return .portrait
|
||||||
|
case 1.0...: return .landscape
|
||||||
|
default: return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
|
||||||
|
let shape = determine_image_shape(img_size)
|
||||||
|
|
||||||
|
let xfactor = geo_size.width / img_size.width
|
||||||
|
let scaled = img_size.height * xfactor
|
||||||
|
// calculate scaled image height
|
||||||
|
// set scale factor and constrain images to minimum 150
|
||||||
|
// and animations to scaled factor for dynamic size adjustment
|
||||||
|
switch shape {
|
||||||
|
case .portrait, .landscape:
|
||||||
|
let filling = scaled > maxHeight
|
||||||
|
let height = filling ? fillHeight : scaled
|
||||||
|
return ImageFill(filling: filling, height: height)
|
||||||
|
case .square, .unknown:
|
||||||
|
return ImageFill(filling: nil, height: scaled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageCarousel_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,84 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InvoiceView: View {
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
let our_pubkey: String
|
||||||
|
let invoice: Invoice
|
||||||
|
@State var showing_select_wallet: Bool = false
|
||||||
|
@State var copied = false
|
||||||
|
|
||||||
|
var CopyButton: some View {
|
||||||
|
Button {
|
||||||
|
copied = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
copied = false
|
||||||
|
}
|
||||||
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
UIPasteboard.general.string = invoice.string
|
||||||
|
} label: {
|
||||||
|
if !copied {
|
||||||
|
Image(systemName: "doc.on.clipboard")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "checkmark.circle")
|
||||||
|
.foregroundColor(DamusColors.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var PayButton: some View {
|
||||||
|
Button {
|
||||||
|
if should_show_wallet_selector(our_pubkey) {
|
||||||
|
showing_select_wallet = true
|
||||||
|
} else {
|
||||||
|
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||||
|
.foregroundColor(colorScheme == .light ? .black : .white)
|
||||||
|
.overlay {
|
||||||
|
Text("Pay", comment: "Button to pay a Lightning invoice.")
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(colorScheme == .light ? .white : .black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
|
||||||
|
print("pay button tap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.foregroundColor(.secondary.opacity(0.1))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Label("", systemImage: "bolt.fill")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||||
|
Spacer()
|
||||||
|
CopyButton
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Text(invoice.description_string)
|
||||||
|
Text(invoice.amount.amount_sats_str())
|
||||||
|
.font(.title)
|
||||||
|
PayButton
|
||||||
|
.frame(height: 50)
|
||||||
|
.zIndex(10.0)
|
||||||
|
}
|
||||||
|
.padding(30)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||||
|
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
||||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
@@ -28,68 +106,12 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InvoiceView: View {
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@Environment(\.openURL) private var openURL
|
|
||||||
|
|
||||||
let invoice: Invoice
|
|
||||||
@State var showing_select_wallet: Bool = false
|
|
||||||
@ObservedObject var user_settings = UserSettingsStore()
|
|
||||||
|
|
||||||
var PayButton: some View {
|
|
||||||
Button {
|
|
||||||
if user_settings.show_wallet_selector {
|
|
||||||
showing_select_wallet = true
|
|
||||||
} else {
|
|
||||||
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
RoundedRectangle(cornerRadius: 20)
|
|
||||||
.foregroundColor(colorScheme == .light ? .black : .white)
|
|
||||||
.overlay {
|
|
||||||
Text("Pay")
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(colorScheme == .light ? .white : .black)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//.buttonStyle(.bordered)
|
|
||||||
.onTapGesture {
|
|
||||||
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.foregroundColor(.secondary.opacity(0.1))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Label("", systemImage: "bolt.fill")
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
Text("Lightning Invoice")
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
Text(invoice.description)
|
|
||||||
Text(invoice.amount.amount_sats_str())
|
|
||||||
.font(.title)
|
|
||||||
PayButton
|
|
||||||
.frame(height: 50)
|
|
||||||
.zIndex(10.0)
|
|
||||||
}
|
|
||||||
.padding(30)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
|
||||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
|
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
|
||||||
|
|
||||||
struct InvoiceView_Previews: PreviewProvider {
|
struct InvoiceView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoiceView(invoice: test_invoice)
|
InvoiceView(our_pubkey: "", invoice: test_invoice)
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 300, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InvoicesView: View {
|
struct InvoicesView: View {
|
||||||
|
let our_pubkey: String
|
||||||
var invoices: [Invoice]
|
var invoices: [Invoice]
|
||||||
|
|
||||||
@State var open_sheet: Bool = false
|
@State var open_sheet: Bool = false
|
||||||
@@ -16,7 +17,7 @@ struct InvoicesView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView {
|
||||||
ForEach(invoices, id: \.string) { invoice in
|
ForEach(invoices, id: \.string) { invoice in
|
||||||
InvoiceView(invoice: invoice)
|
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Text(invoice.string)
|
Text(invoice.string)
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ struct InvoicesView: View {
|
|||||||
|
|
||||||
struct InvoicesView_Previews: PreviewProvider {
|
struct InvoicesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
|
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
damus/Components/NIP05Badge.swift
Normal file
90
damus/Components/NIP05Badge.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// NIP05Badge.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NIP05Badge: View {
|
||||||
|
let nip05: NIP05
|
||||||
|
let pubkey: String
|
||||||
|
let contacts: Contacts
|
||||||
|
let show_domain: Bool
|
||||||
|
let clickable: Bool
|
||||||
|
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
|
init (nip05: NIP05, pubkey: String, contacts: Contacts, show_domain: Bool, clickable: Bool) {
|
||||||
|
self.nip05 = nip05
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.contacts = contacts
|
||||||
|
self.show_domain = show_domain
|
||||||
|
self.clickable = clickable
|
||||||
|
}
|
||||||
|
|
||||||
|
var nip05_color: Bool {
|
||||||
|
return use_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Seal: some View {
|
||||||
|
Group {
|
||||||
|
if nip05_color {
|
||||||
|
LINEAR_GRADIENT
|
||||||
|
.mask(Image(systemName: "checkmark.seal.fill")
|
||||||
|
.resizable()
|
||||||
|
).frame(width: 14, height: 14)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "checkmark.seal.fill")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Seal
|
||||||
|
|
||||||
|
if show_domain {
|
||||||
|
if clickable {
|
||||||
|
Text(nip05.host)
|
||||||
|
.nip05_colorized(gradient: nip05_color)
|
||||||
|
.onTapGesture {
|
||||||
|
if let nip5url = nip05.siteUrl {
|
||||||
|
openURL(nip5url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(nip05.host)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func nip05_colorized(gradient: Bool) -> some View {
|
||||||
|
if gradient {
|
||||||
|
return AnyView(self.foregroundStyle(LINEAR_GRADIENT))
|
||||||
|
} else {
|
||||||
|
return AnyView(self.foregroundColor(.gray))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
|
||||||
|
return contacts.is_friend_or_self(pubkey) ? true : false
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NIP05Badge_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let test_state = test_damus_state()
|
||||||
|
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, clickable: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
32
damus/Components/Reposted.swift
Normal file
32
damus/Components/Reposted.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// Reposted.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Reposted: View {
|
||||||
|
let damus: DamusState
|
||||||
|
let pubkey: String
|
||||||
|
let profile: Profile?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Image(systemName: "arrow.2.squarepath")
|
||||||
|
.foregroundColor(Color.gray)
|
||||||
|
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
|
||||||
|
.foregroundColor(Color.gray)
|
||||||
|
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
|
||||||
|
.foregroundColor(Color.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Reposted_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let test_state = test_damus_state()
|
||||||
|
Reposted(damus: test_state, pubkey: test_state.pubkey, profile: make_test_profile())
|
||||||
|
}
|
||||||
|
}
|
||||||
102
damus/Components/SelectableText.swift
Normal file
102
damus/Components/SelectableText.swift
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//
|
||||||
|
// SelectableText.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Oleg Abalonski on 2/16/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SelectableText: View {
|
||||||
|
|
||||||
|
let attributedString: AttributedString
|
||||||
|
|
||||||
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
|
let size: EventViewKind
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
TextViewRepresentable(
|
||||||
|
attributedString: attributedString,
|
||||||
|
textColor: UIColor.label,
|
||||||
|
font: eventviewsize_to_uifont(size),
|
||||||
|
fixedWidth: selectedTextWidth,
|
||||||
|
height: $selectedTextHeight
|
||||||
|
)
|
||||||
|
.padding([.leading, .trailing], -1.0)
|
||||||
|
.onAppear {
|
||||||
|
self.selectedTextWidth = geo.size.width
|
||||||
|
}
|
||||||
|
.onChange(of: geo.size) { newSize in
|
||||||
|
self.selectedTextWidth = newSize.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: selectedTextHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
|
|
||||||
|
let attributedString: AttributedString
|
||||||
|
let textColor: UIColor
|
||||||
|
let font: UIFont
|
||||||
|
let fixedWidth: CGFloat
|
||||||
|
|
||||||
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||||
|
let view = UITextView()
|
||||||
|
view.isEditable = false
|
||||||
|
view.dataDetectorTypes = .all
|
||||||
|
view.isSelectable = true
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.textContainer.lineFragmentPadding = 0
|
||||||
|
view.textContainerInset = .zero
|
||||||
|
view.textContainerInset.left = 1.0
|
||||||
|
view.textContainerInset.right = 1.0
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||||
|
let mutableAttributedString = createNSAttributedString()
|
||||||
|
uiView.attributedText = mutableAttributedString
|
||||||
|
|
||||||
|
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
height = newHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNSAttributedString() -> NSMutableAttributedString {
|
||||||
|
let mutableAttributedString = NSMutableAttributedString(attributedString)
|
||||||
|
let myAttribute = [
|
||||||
|
NSAttributedString.Key.font: font,
|
||||||
|
NSAttributedString.Key.foregroundColor: textColor
|
||||||
|
]
|
||||||
|
|
||||||
|
mutableAttributedString.addAttributes(
|
||||||
|
myAttribute,
|
||||||
|
range: NSRange.init(location: 0, length: mutableAttributedString.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
return mutableAttributedString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension NSAttributedString {
|
||||||
|
|
||||||
|
func height(containerWidth: CGFloat) -> CGFloat {
|
||||||
|
|
||||||
|
let rect = self.boundingRect(
|
||||||
|
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||||
|
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
||||||
|
context: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return ceil(rect.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ struct TextFieldAlert<Presenting>: View where Presenting: View {
|
|||||||
.disabled(isShowing)
|
.disabled(isShowing)
|
||||||
VStack {
|
VStack {
|
||||||
Text(self.title)
|
Text(self.title)
|
||||||
TextField("Relay", text: self.$text)
|
TextField(NSLocalizedString("Relay", comment: "Text field for relay server. Used for testing purposes."), text: self.$text)
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -28,7 +28,7 @@ struct TextFieldAlert<Presenting>: View where Presenting: View {
|
|||||||
self.isShowing.toggle()
|
self.isShowing.toggle()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Dismiss")
|
Text("Dismiss", comment: "Button to dismiss a text field alert.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
damus/Components/ThiccDivider.swift
Normal file
22
damus/Components/ThiccDivider.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// ThiccDivider.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-04-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ThiccDivider: View {
|
||||||
|
var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 4)
|
||||||
|
.foregroundColor(DamusColors.adaptableGrey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThiccDivider_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ThiccDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
141
damus/Components/TranslateView.swift
Normal file
141
damus/Components/TranslateView.swift
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
//
|
||||||
|
// TranslateButton.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-02-02.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import NaturalLanguage
|
||||||
|
|
||||||
|
struct TranslateView: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
|
let size: EventViewKind
|
||||||
|
|
||||||
|
@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))
|
||||||
|
} else {
|
||||||
|
self._translated_artifacts = State(initialValue: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var TranslateButton: some View {
|
||||||
|
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||||
|
show_translated_note = true
|
||||||
|
processTranslation()
|
||||||
|
}
|
||||||
|
.translate_button_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTranslation() {
|
||||||
|
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Text("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func translate_button_style() -> some View {
|
||||||
|
return self
|
||||||
|
.font(.footnote)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.padding([.top, .bottom], 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TranslateView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let ds = test_damus_state()
|
||||||
|
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
damus/Components/UserView.swift
Normal file
38
damus/Components/UserView.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// UserView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UserView(damus_state: test_damus_state(), pubkey: "pk")
|
||||||
|
}
|
||||||
|
}
|
||||||
39
damus/Components/WebsiteLink.swift
Normal file
39
damus/Components/WebsiteLink.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// WebsiteLink.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WebsiteLink: View {
|
||||||
|
let url: URL
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "link")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.footnote)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
openURL(url)
|
||||||
|
}, label: {
|
||||||
|
Text(link_text)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var link_text: String {
|
||||||
|
url.host ?? url.absoluteString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebsiteLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
WebsiteLink(url: URL(string: "https://jb55.com")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
194
damus/Components/ZapButton.swift
Normal file
194
damus/Components/ZapButton.swift
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
//
|
||||||
|
// ZapButton.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum ZappingEventType {
|
||||||
|
case failed(ZappingError)
|
||||||
|
case got_zap_invoice(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ZappingError {
|
||||||
|
case fetching_invoice
|
||||||
|
case bad_lnurl
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ZappingEvent {
|
||||||
|
let is_custom: Bool
|
||||||
|
let type: ZappingEventType
|
||||||
|
let event: NostrEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ZapButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
|
let lnurl: String
|
||||||
|
|
||||||
|
@ObservedObject var bar: ActionBarModel
|
||||||
|
|
||||||
|
@State var zapping: Bool = false
|
||||||
|
@State var invoice: String = ""
|
||||||
|
@State var slider_value: Double = 0.0
|
||||||
|
@State var slider_visible: Bool = false
|
||||||
|
@State var showing_select_wallet: Bool = false
|
||||||
|
@State var showing_zap_customizer: Bool = false
|
||||||
|
@State var is_charging: Bool = false
|
||||||
|
|
||||||
|
var zap_img: String {
|
||||||
|
if bar.zapped {
|
||||||
|
return "bolt.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zapping {
|
||||||
|
return "bolt"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "bolt.horizontal.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
var zap_color: Color? {
|
||||||
|
if bar.zapped {
|
||||||
|
return Color.orange
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_charging {
|
||||||
|
return Color.yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zapping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color.yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Button(action: {
|
||||||
|
}, label: {
|
||||||
|
Image(systemName: zap_img)
|
||||||
|
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
|
||||||
|
.font(.footnote.weight(.medium))
|
||||||
|
})
|
||||||
|
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||||
|
guard !zapping else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showing_zap_customizer = true
|
||||||
|
})
|
||||||
|
.highPriorityGesture(TapGesture().onEnded {_ in
|
||||||
|
guard !zapping else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
|
||||||
|
self.zapping = true
|
||||||
|
})
|
||||||
|
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||||
|
|
||||||
|
if bar.zap_total > 0 {
|
||||||
|
Text(verbatim: format_msats_abbrev(bar.zap_total))
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showing_zap_customizer) {
|
||||||
|
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||||
|
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.zapping)) { notif in
|
||||||
|
let zap_ev = notif.object as! ZappingEvent
|
||||||
|
|
||||||
|
guard zap_ev.event.id == self.event.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !zap_ev.is_custom else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch zap_ev.type {
|
||||||
|
case .failed:
|
||||||
|
break
|
||||||
|
case .got_zap_invoice(let inv):
|
||||||
|
if should_show_wallet_selector(damus_state.pubkey) {
|
||||||
|
self.invoice = inv
|
||||||
|
self.showing_select_wallet = true
|
||||||
|
} else {
|
||||||
|
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.zapping = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct ZapButton_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
|
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||||
|
guard let keypair = damus_state.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only take the first 10 because reasons
|
||||||
|
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||||
|
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||||
|
let content = comment ?? ""
|
||||||
|
|
||||||
|
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||||
|
if mpayreq == nil {
|
||||||
|
mpayreq = await fetch_static_payreq(lnurl)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let payreq = mpayreq else {
|
||||||
|
// TODO: show error
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
|
notify(.zapping, ev)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||||
|
}
|
||||||
|
|
||||||
|
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||||
|
|
||||||
|
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
|
notify(.zapping, ev)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||||
|
notify(.zapping, ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
152
damus/Components/ZoomableScrollView.swift
Normal file
152
damus/Components/ZoomableScrollView.swift
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
//
|
||||||
|
// ZoomableScrollView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Oleg Abalonski on 1/25/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
|
||||||
|
|
||||||
|
private var content: Content
|
||||||
|
|
||||||
|
init(@ViewBuilder content: () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIScrollView {
|
||||||
|
let scrollView = GesturedScrollView()
|
||||||
|
scrollView.delegate = context.coordinator
|
||||||
|
scrollView.maximumZoomScale = 20
|
||||||
|
scrollView.minimumZoomScale = 1
|
||||||
|
scrollView.bouncesZoom = true
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
|
||||||
|
let hostedView = context.coordinator.hostingController.view!
|
||||||
|
hostedView.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
hostedView.frame = scrollView.bounds
|
||||||
|
hostedView.backgroundColor = .clear
|
||||||
|
scrollView.addSubview(hostedView)
|
||||||
|
|
||||||
|
return scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIScrollView, context: Context) {
|
||||||
|
context.coordinator.hostingController.rootView = self.content
|
||||||
|
assert(context.coordinator.hostingController.view.superview == uiView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIScrollViewDelegate {
|
||||||
|
var hostingController: UIHostingController<Content>
|
||||||
|
|
||||||
|
init(hostingController: UIHostingController<Content>) {
|
||||||
|
self.hostingController = hostingController
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
|
return hostingController.view
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
|
let viewSize = hostingController.view.frame.size
|
||||||
|
guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return }
|
||||||
|
|
||||||
|
if scrollView.zoomScale > 1 {
|
||||||
|
|
||||||
|
let ratioW = viewSize.width / imageSize.width
|
||||||
|
let ratioH = viewSize.height / imageSize.height
|
||||||
|
|
||||||
|
let ratio = ratioW < ratioH ? ratioW:ratioH
|
||||||
|
|
||||||
|
let newWidth = imageSize.width * ratio
|
||||||
|
let newHeight = imageSize.height * ratio
|
||||||
|
|
||||||
|
let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width))
|
||||||
|
let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height))
|
||||||
|
|
||||||
|
scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
|
||||||
|
} else {
|
||||||
|
scrollView.contentInset = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate {
|
||||||
|
|
||||||
|
let doubleTapGesture: UITapGestureRecognizer
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
doubleTapGesture = UITapGestureRecognizer()
|
||||||
|
super.init(frame: frame)
|
||||||
|
doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap))
|
||||||
|
doubleTapGesture.numberOfTapsRequired = 2
|
||||||
|
addGestureRecognizer(doubleTapGesture)
|
||||||
|
doubleTapGesture.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||||
|
if self.zoomScale == 1 {
|
||||||
|
let pointInView = gesture.location(in: self.subviews.first)
|
||||||
|
let newZoomScale = self.maximumZoomScale / 4.0
|
||||||
|
let scrollViewSize = self.bounds.size
|
||||||
|
let width = scrollViewSize.width / newZoomScale
|
||||||
|
let height = scrollViewSize.height / newZoomScale
|
||||||
|
let originX = pointInView.x - (width / 2.0)
|
||||||
|
let originY = pointInView.y - (height / 2.0)
|
||||||
|
let zoomRect = CGRect(x: originX, y: originY, width: width, height: height)
|
||||||
|
self.zoom(to: zoomRect, animated: true)
|
||||||
|
} else {
|
||||||
|
self.setZoomScale(self.minimumZoomScale, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return gestureRecognizer == doubleTapGesture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension UIHostingController {
|
||||||
|
|
||||||
|
convenience init(rootView: Content, ignoreSafeArea: Bool) {
|
||||||
|
self.init(rootView: rootView)
|
||||||
|
|
||||||
|
if ignoreSafeArea {
|
||||||
|
disableSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableSafeArea() {
|
||||||
|
guard let viewClass = object_getClass(view) else { return }
|
||||||
|
|
||||||
|
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
|
||||||
|
if let viewSubclass = NSClassFromString(viewSubclassName) {
|
||||||
|
object_setClass(view, viewSubclass)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
|
||||||
|
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
|
||||||
|
|
||||||
|
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
|
||||||
|
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
|
||||||
|
}
|
||||||
|
|
||||||
|
objc_registerClassPair(viewSubclass)
|
||||||
|
object_setClass(view, viewSubclass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,31 +7,27 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Starscream
|
import Starscream
|
||||||
import Kingfisher
|
|
||||||
|
|
||||||
var BOOTSTRAP_RELAYS = [
|
|
||||||
"wss://relay.damus.io",
|
|
||||||
"wss://nostr-relay.wlvs.space",
|
|
||||||
"wss://nostr.fmt.wiz.biz",
|
|
||||||
"wss://relay.nostr.bg",
|
|
||||||
"wss://nostr.oxtr.dev",
|
|
||||||
"wss://nostr.v0l.io",
|
|
||||||
"wss://brb.io",
|
|
||||||
]
|
|
||||||
|
|
||||||
struct TimestampedProfile {
|
struct TimestampedProfile {
|
||||||
let profile: Profile
|
let profile: Profile
|
||||||
let timestamp: Int64
|
let timestamp: Int64
|
||||||
|
let event: NostrEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Sheets: Identifiable {
|
enum Sheets: Identifiable {
|
||||||
case post
|
case post
|
||||||
|
case report(ReportTarget)
|
||||||
case reply(NostrEvent)
|
case reply(NostrEvent)
|
||||||
|
case event(NostrEvent)
|
||||||
|
case filter
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .report: return "report"
|
||||||
case .post: return "post"
|
case .post: return "post"
|
||||||
case .reply(let ev): return "reply-" + ev.id
|
case .reply(let ev): return "reply-" + ev.id
|
||||||
|
case .event(let ev): return "event-" + ev.id
|
||||||
|
case .filter: return "filter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +40,15 @@ enum ThreadState {
|
|||||||
enum FilterState : Int {
|
enum FilterState : Int {
|
||||||
case posts_and_replies = 1
|
case posts_and_replies = 1
|
||||||
case posts = 0
|
case posts = 0
|
||||||
|
|
||||||
|
func filter(ev: NostrEvent) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .posts:
|
||||||
|
return !ev.is_reply(nil)
|
||||||
|
case .posts_and_replies:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@@ -62,18 +67,24 @@ struct ContentView: View {
|
|||||||
@State var damus_state: DamusState? = nil
|
@State var damus_state: DamusState? = nil
|
||||||
@State var selected_timeline: Timeline? = .home
|
@State var selected_timeline: Timeline? = .home
|
||||||
@State var is_thread_open: Bool = false
|
@State var is_thread_open: Bool = false
|
||||||
|
@State var is_deleted_account: Bool = false
|
||||||
@State var is_profile_open: Bool = false
|
@State var is_profile_open: Bool = false
|
||||||
@State var event: NostrEvent? = nil
|
@State var event: NostrEvent? = nil
|
||||||
@State var active_profile: String? = nil
|
@State var active_profile: String? = nil
|
||||||
@State var active_search: NostrFilter? = nil
|
@State var active_search: NostrFilter? = nil
|
||||||
@State var active_event_id: String? = nil
|
@State var active_event: NostrEvent? = nil
|
||||||
@State var profile_open: Bool = false
|
@State var profile_open: Bool = false
|
||||||
@State var thread_open: Bool = false
|
@State var thread_open: Bool = false
|
||||||
@State var search_open: Bool = false
|
@State var search_open: Bool = false
|
||||||
|
@State var muting: String? = nil
|
||||||
|
@State var confirm_mute: Bool = false
|
||||||
|
@State var user_muted_confirm: Bool = false
|
||||||
|
@State var confirm_overwrite_mutelist: Bool = false
|
||||||
|
@State var current_boost: NostrEvent? = nil
|
||||||
@State var filter_state : FilterState = .posts_and_replies
|
@State var filter_state : FilterState = .posts_and_replies
|
||||||
|
@State private var isSideBarOpened = false
|
||||||
@StateObject var home: HomeModel = HomeModel()
|
@StateObject var home: HomeModel = HomeModel()
|
||||||
@StateObject var user_settings = UserSettingsStore()
|
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
@@ -83,19 +94,30 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var PostingTimelineView: some View {
|
var PostingTimelineView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
TabView(selection: $filter_state) {
|
ZStack {
|
||||||
ContentTimelineView
|
TabView(selection: $filter_state) {
|
||||||
.tag(FilterState.posts)
|
contentTimelineView(filter: FilterState.posts.filter)
|
||||||
ContentTimelineView
|
.tag(FilterState.posts)
|
||||||
.tag(FilterState.posts_and_replies)
|
.id(FilterState.posts)
|
||||||
|
contentTimelineView(filter: FilterState.posts_and_replies.filter)
|
||||||
|
.tag(FilterState.posts_and_replies)
|
||||||
|
.id(FilterState.posts_and_replies)
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
|
|
||||||
|
if privkey != nil {
|
||||||
|
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||||
|
self.active_sheet = .post
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .top) {
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
FiltersView
|
CustomPicker(selection: $filter_state, content: {
|
||||||
//.frame(maxWidth: 275)
|
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
||||||
.padding()
|
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
||||||
|
})
|
||||||
Divider()
|
Divider()
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
}
|
}
|
||||||
@@ -103,35 +125,24 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ContentTimelineView: some View {
|
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let damus = self.damus_state {
|
if let damus = self.damus_state {
|
||||||
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter_event)
|
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
|
||||||
}
|
|
||||||
if privkey != nil {
|
|
||||||
PostButtonContainer {
|
|
||||||
self.active_sheet = .post
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var FiltersView: some View {
|
func popToRoot() {
|
||||||
VStack{
|
profile_open = false
|
||||||
Picker("Filter State", selection: $filter_state) {
|
thread_open = false
|
||||||
Text("Posts").tag(FilterState.posts)
|
search_open = false
|
||||||
Text("Posts & Replies").tag(FilterState.posts_and_replies)
|
isSideBarOpened = false
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func filter_event(_ ev: NostrEvent) -> Bool {
|
var timelineNavItem: Text {
|
||||||
if self.filter_state == .posts {
|
return Text(timeline_name(selected_timeline))
|
||||||
return !ev.is_reply(nil)
|
.bold()
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainContent(damus: DamusState) -> some View {
|
func MainContent(damus: DamusState) -> some View {
|
||||||
@@ -139,22 +150,30 @@ struct ContentView: View {
|
|||||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
|
if let active_event {
|
||||||
EmptyView()
|
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
|
||||||
|
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
switch selected_timeline {
|
switch selected_timeline {
|
||||||
case .search:
|
case .search:
|
||||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
if #available(iOS 16.0, *) {
|
||||||
|
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||||
|
.scrollDismissesKeyboard(.immediately)
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||||
|
}
|
||||||
|
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView
|
PostingTimelineView
|
||||||
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
|
NotificationsView(state: damus, notifications: home.notifications)
|
||||||
.navigationTitle("Notifications")
|
|
||||||
|
|
||||||
case .dms:
|
case .dms:
|
||||||
DirectMessagesView(damus_state: damus_state!)
|
DirectMessagesView(damus_state: damus_state!)
|
||||||
@@ -164,23 +183,31 @@ struct ContentView: View {
|
|||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle(selected_timeline == .home ? "Home" : "Global", displayMode: .inline)
|
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
VStack {
|
||||||
|
if selected_timeline == .home {
|
||||||
|
Image("damus-home")
|
||||||
|
.resizable()
|
||||||
|
.frame(width:30,height:30)
|
||||||
|
.shadow(color: DamusColors.purple, radius: 2)
|
||||||
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
|
} else {
|
||||||
|
timelineNavItem
|
||||||
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var MaybeSearchView: some View {
|
var MaybeSearchView: some View {
|
||||||
Group {
|
Group {
|
||||||
if let search = self.active_search {
|
if let search = self.active_search {
|
||||||
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
|
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var MaybeThreadView: some View {
|
|
||||||
Group {
|
|
||||||
if let evid = self.active_event_id {
|
|
||||||
BuildThreadV2View(damus: damus_state!, event_id: evid)
|
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@@ -198,63 +225,101 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MaybeReportView(target: ReportTarget) -> some View {
|
||||||
|
Group {
|
||||||
|
if let damus_state {
|
||||||
|
if let sec = damus_state.keypair.privkey {
|
||||||
|
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if let damus = self.damus_state {
|
if let damus = self.damus_state {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
MainContent(damus: damus)
|
TabView { // Prevents navbar appearance change on scroll
|
||||||
.toolbar {
|
MainContent(damus: damus)
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
.toolbar() {
|
||||||
let profile_model = ProfileModel(pubkey: damus_state!.pubkey, damus: damus_state!)
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
let followers_model = FollowersModel(damus_state: damus_state!, target: damus_state!.pubkey)
|
Button {
|
||||||
let prof_dest = ProfileView(damus_state: damus_state!, profile: profile_model, followers: followers_model)
|
isSideBarOpened.toggle()
|
||||||
|
} label: {
|
||||||
NavigationLink(destination: prof_dest) {
|
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||||
/// Verify that the user has a profile picture, if not display a generic SF Symbol
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
/// (Resolves an in-app error where ``Robohash`` pictures are not generated so the button dissapears
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
if let picture = damus_state?.profiles.lookup(id: pubkey)?.picture {
|
}
|
||||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, picture: picture)
|
.disabled(isSideBarOpened)
|
||||||
} else {
|
}
|
||||||
Image(systemName: "person.fill")
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe expand this to other timelines in the future
|
||||||
|
if selected_timeline == .search {
|
||||||
|
Button(action: {
|
||||||
|
//isFilterVisible.toggle()
|
||||||
|
self.active_sheet = .filter
|
||||||
|
}) {
|
||||||
|
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||||
|
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
//.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
HStack(alignment: .center) {
|
.overlay(
|
||||||
if home.signal.signal != home.signal.max_signal {
|
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||||
Text("\(home.signal.signal)/\(home.signal.max_signal)")
|
)
|
||||||
.font(.callout)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(destination: ConfigView(state: damus_state!).environmentObject(user_settings)) {
|
|
||||||
Label("", systemImage: "gear")
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
|
|
||||||
|
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
|
||||||
|
.padding([.bottom], 8)
|
||||||
|
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||||
}
|
}
|
||||||
|
|
||||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, action: switch_timeline)
|
|
||||||
.padding([.bottom], 8)
|
|
||||||
}
|
}
|
||||||
|
.ignoresSafeArea(.keyboard)
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.connect()
|
self.connect()
|
||||||
//KingfisherManager.shared.cache.clearDiskCache()
|
|
||||||
setup_notifications()
|
setup_notifications()
|
||||||
}
|
}
|
||||||
.sheet(item: $active_sheet) { item in
|
.sheet(item: $active_sheet) { item in
|
||||||
switch item {
|
switch item {
|
||||||
|
case .report(let target):
|
||||||
|
MaybeReportView(target: target)
|
||||||
case .post:
|
case .post:
|
||||||
PostView(replying_to: nil, references: [])
|
PostView(replying_to: nil, damus_state: damus_state!)
|
||||||
case .reply(let event):
|
case .reply(let event):
|
||||||
ReplyView(replying_to: event, damus: damus_state!)
|
PostView(replying_to: event, damus_state: damus_state!)
|
||||||
|
case .event:
|
||||||
|
EventDetailView()
|
||||||
|
case .filter:
|
||||||
|
let timeline = selected_timeline ?? .home
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||||
|
.presentationDetents([.height(550)])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
} else {
|
||||||
|
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
@@ -268,7 +333,11 @@ struct ContentView: View {
|
|||||||
active_profile = ref.ref_id
|
active_profile = ref.ref_id
|
||||||
profile_open = true
|
profile_open = true
|
||||||
} else if ref.key == "e" {
|
} else if ref.key == "e" {
|
||||||
active_event_id = ref.ref_id
|
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||||
|
if let ev {
|
||||||
|
active_event = ev
|
||||||
|
}
|
||||||
|
}
|
||||||
thread_open = true
|
thread_open = true
|
||||||
}
|
}
|
||||||
case .filter(let filt):
|
case .filter(let filt):
|
||||||
@@ -280,13 +349,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.boost)) { notif in
|
.onReceive(handle_notify(.boost)) { notif in
|
||||||
guard let privkey = self.privkey else {
|
current_boost = (notif.object as? NostrEvent)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let ev = notif.object as! NostrEvent
|
|
||||||
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
|
|
||||||
self.damus_state?.pool.send(.event(boost))
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.open_thread)) { obj in
|
.onReceive(handle_notify(.open_thread)) { obj in
|
||||||
//let ev = obj.object as! NostrEvent
|
//let ev = obj.object as! NostrEvent
|
||||||
@@ -299,9 +362,27 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.like)) { like in
|
.onReceive(handle_notify(.like)) { like in
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||||
|
self.is_deleted_account = true
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.report)) { notif in
|
||||||
|
let target = notif.object as! ReportTarget
|
||||||
|
self.active_sheet = .report(target)
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.mute)) { notif in
|
||||||
|
let pubkey = notif.object as! String
|
||||||
|
self.muting = pubkey
|
||||||
|
self.confirm_mute = true
|
||||||
|
}
|
||||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
.onReceive(handle_notify(.broadcast_event)) { obj in
|
||||||
let ev = obj.object as! NostrEvent
|
let ev = obj.object as! NostrEvent
|
||||||
self.damus_state?.pool.send(.event(ev))
|
guard let ds = self.damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ds.postbox.send(ev)
|
||||||
|
if let profile = ds.profiles.profiles[ev.pubkey] {
|
||||||
|
ds.postbox.send(profile.event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollow)) { notif in
|
.onReceive(handle_notify(.unfollow)) { notif in
|
||||||
guard let privkey = self.privkey else {
|
guard let privkey = self.privkey else {
|
||||||
@@ -315,11 +396,11 @@ struct ContentView: View {
|
|||||||
let target = notif.object as! FollowTarget
|
let target = notif.object as! FollowTarget
|
||||||
let pk = target.pubkey
|
let pk = target.pubkey
|
||||||
|
|
||||||
if let ev = unfollow_user(pool: damus.pool,
|
if let ev = unfollow_user(postbox: damus.postbox,
|
||||||
our_contacts: damus.contacts.event,
|
our_contacts: damus.contacts.event,
|
||||||
pubkey: damus.pubkey,
|
pubkey: damus.pubkey,
|
||||||
privkey: privkey,
|
privkey: privkey,
|
||||||
unfollow: pk) {
|
unfollow: pk) {
|
||||||
notify(.unfollowed, pk)
|
notify(.unfollowed, pk)
|
||||||
|
|
||||||
damus.contacts.event = ev
|
damus.contacts.event = ev
|
||||||
@@ -338,10 +419,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let ev = follow_user(pool: damus.pool,
|
if let ev = follow_user(pool: damus.pool,
|
||||||
our_contacts: damus.contacts.event,
|
our_contacts: damus.contacts.event,
|
||||||
pubkey: damus.pubkey,
|
pubkey: damus.pubkey,
|
||||||
privkey: privkey,
|
privkey: privkey,
|
||||||
follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
|
follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
|
||||||
notify(.followed, fnotify.pubkey)
|
notify(.followed, fnotify.pubkey)
|
||||||
|
|
||||||
damus_state?.contacts.event = ev
|
damus_state?.contacts.event = ev
|
||||||
@@ -362,9 +443,20 @@ struct ContentView: View {
|
|||||||
let post_res = obj.object as! NostrPostResult
|
let post_res = obj.object as! NostrPostResult
|
||||||
switch post_res {
|
switch post_res {
|
||||||
case .post(let post):
|
case .post(let post):
|
||||||
|
//let post = tup.0
|
||||||
|
//let to_relays = tup.1
|
||||||
print("post \(post.content)")
|
print("post \(post.content)")
|
||||||
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
||||||
self.damus_state?.pool.send(.event(new_ev))
|
guard let ds = self.damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ds.postbox.send(new_ev)
|
||||||
|
for eref in new_ev.referenced_ids.prefix(3) {
|
||||||
|
// also broadcast at most 3 referenced events
|
||||||
|
if let ev = ds.events.lookup(eref.ref_id) {
|
||||||
|
ds.postbox.send(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
case .cancel:
|
case .cancel:
|
||||||
active_sheet = nil
|
active_sheet = nil
|
||||||
print("post cancelled")
|
print("post cancelled")
|
||||||
@@ -373,9 +465,112 @@ struct ContentView: View {
|
|||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.pool.connect_to_disconnected()
|
self.damus_state?.pool.connect_to_disconnected()
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
|
home.filter_muted()
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||||
|
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||||
|
is_deleted_account = false
|
||||||
|
notify(.logout, ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||||
|
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||||
|
user_muted_confirm = false
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
if let pubkey = self.muting {
|
||||||
|
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||||
|
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||||
|
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||||
|
} else {
|
||||||
|
Text("User has been muted", comment: "Alert message that informs a user was d.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
|
||||||
|
confirm_overwrite_mutelist = false
|
||||||
|
confirm_mute = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||||
|
guard let ds = damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let keypair = ds.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pubkey = muting else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state?.contacts.set_mutelist(mutelist)
|
||||||
|
ds.postbox.send(mutelist)
|
||||||
|
|
||||||
|
confirm_overwrite_mutelist = false
|
||||||
|
confirm_mute = false
|
||||||
|
user_muted_confirm = true
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
|
||||||
|
})
|
||||||
|
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
|
||||||
|
confirm_mute = false
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Mute", comment: "Alert button to mute a user."), role: .destructive) {
|
||||||
|
guard let ds = damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds.contacts.mutelist == nil {
|
||||||
|
confirm_overwrite_mutelist = true
|
||||||
|
} else {
|
||||||
|
guard let keypair = ds.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let pubkey = muting else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
damus_state?.contacts.set_mutelist(ev)
|
||||||
|
ds.postbox.send(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
if let pubkey = muting {
|
||||||
|
let profile = damus_state?.profiles.lookup(id: pubkey)
|
||||||
|
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||||
|
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||||
|
} else {
|
||||||
|
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||||
|
current_boost = nil
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||||
|
if let current_boost {
|
||||||
|
self.damus_state?.pool.send(.event(current_boost))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func switch_timeline(_ timeline: Timeline) {
|
func switch_timeline(_ timeline: Timeline) {
|
||||||
|
self.popToRoot()
|
||||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||||
|
|
||||||
if timeline == self.selected_timeline {
|
if timeline == self.selected_timeline {
|
||||||
@@ -401,20 +596,42 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func connect() {
|
func connect() {
|
||||||
let pool = RelayPool()
|
let pool = RelayPool()
|
||||||
|
let metadatas = RelayMetadatas()
|
||||||
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
|
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||||
|
|
||||||
for relay in BOOTSTRAP_RELAYS {
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
add_relay(pool, relay)
|
for relay in bootstrap_relays {
|
||||||
|
if let url = URL(string: relay) {
|
||||||
|
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||||
|
|
||||||
self.damus_state = DamusState(pool: pool, keypair: keypair,
|
let settings = UserSettingsStore()
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
self.damus_state = DamusState(pool: pool,
|
||||||
contacts: Contacts(),
|
keypair: keypair,
|
||||||
tips: TipCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
profiles: Profiles(),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
dms: home.dms
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
|
tips: TipCounter(our_pubkey: pubkey),
|
||||||
|
profiles: Profiles(),
|
||||||
|
dms: home.dms,
|
||||||
|
previews: PreviewCache(),
|
||||||
|
zaps: Zaps(our_pubkey: pubkey),
|
||||||
|
lnurls: LNUrls(),
|
||||||
|
settings: settings,
|
||||||
|
relay_filters: relay_filters,
|
||||||
|
relay_metadata: metadatas,
|
||||||
|
drafts: Drafts(),
|
||||||
|
events: EventCache(),
|
||||||
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
|
postbox: PostBox(pool: pool),
|
||||||
|
bootstrap_relays: bootstrap_relays,
|
||||||
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
|
translations: Translations(settings)
|
||||||
)
|
)
|
||||||
home.damus_state = self.damus_state!
|
home.damus_state = self.damus_state!
|
||||||
|
|
||||||
@@ -430,7 +647,6 @@ struct ContentView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func get_since_time(last_event: NostrEvent?) -> Int64? {
|
func get_since_time(last_event: NostrEvent?) -> Int64? {
|
||||||
if let last_event = last_event {
|
if let last_event = last_event {
|
||||||
return last_event.created_at - 60 * 10
|
return last_event.created_at - 60 * 10
|
||||||
@@ -564,3 +780,69 @@ func setup_notifications() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
|
||||||
|
if let ev = state.events.lookup(evid) {
|
||||||
|
callback(ev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let subid = UUID().description
|
||||||
|
|
||||||
|
var has_event = false
|
||||||
|
|
||||||
|
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||||
|
|
||||||
|
if search_type == .profile {
|
||||||
|
filter.kinds = [0]
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.limit = 1
|
||||||
|
var attempts = 0
|
||||||
|
|
||||||
|
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||||
|
guard case .nostr_event(let ev) = res else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard ev.subid == subid else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ev {
|
||||||
|
case .ok:
|
||||||
|
break
|
||||||
|
case .event(_, let ev):
|
||||||
|
has_event = true
|
||||||
|
callback(ev)
|
||||||
|
state.pool.unsubscribe(sub_id: subid)
|
||||||
|
case .eose:
|
||||||
|
if !has_event {
|
||||||
|
attempts += 1
|
||||||
|
if attempts == state.pool.descriptors.count / 2 {
|
||||||
|
callback(nil)
|
||||||
|
}
|
||||||
|
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||||
|
}
|
||||||
|
case .notice(_):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func timeline_name(_ timeline: Timeline?) -> String {
|
||||||
|
guard let timeline else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
|
||||||
|
case .notifications:
|
||||||
|
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
|
||||||
|
case .search:
|
||||||
|
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||||
|
case .dms:
|
||||||
|
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
<string>nostr</string>
|
<string>nostr</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>io.damus</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>damus</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
@@ -24,7 +34,6 @@
|
|||||||
<string>zeusln</string>
|
<string>zeusln</string>
|
||||||
<string>zebedee</string>
|
<string>zebedee</string>
|
||||||
<string>lightning</string>
|
<string>lightning</string>
|
||||||
<string>squarecash</string>
|
|
||||||
<string>phoenix</string>
|
<string>phoenix</string>
|
||||||
<string>lnlink</string>
|
<string>lnlink</string>
|
||||||
<string>strike</string>
|
<string>strike</string>
|
||||||
@@ -37,5 +46,9 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,28 +11,59 @@ import Foundation
|
|||||||
class ActionBarModel: ObservableObject {
|
class ActionBarModel: ObservableObject {
|
||||||
@Published var our_like: NostrEvent?
|
@Published var our_like: NostrEvent?
|
||||||
@Published var our_boost: NostrEvent?
|
@Published var our_boost: NostrEvent?
|
||||||
@Published var our_tip: NostrEvent?
|
@Published var our_reply: NostrEvent?
|
||||||
|
@Published var our_zap: Zap?
|
||||||
@Published var likes: Int
|
@Published var likes: Int
|
||||||
@Published var boosts: Int
|
@Published var boosts: Int
|
||||||
@Published var tips: Int64
|
@Published var zaps: Int
|
||||||
|
@Published var zap_total: Int64
|
||||||
|
@Published var replies: Int
|
||||||
|
|
||||||
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
|
static func empty() -> ActionBarModel {
|
||||||
self.likes = likes
|
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
self.boosts = boosts
|
|
||||||
self.tips = tips
|
|
||||||
self.our_like = our_like
|
|
||||||
self.our_boost = our_boost
|
|
||||||
self.our_tip = our_tip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tipped: Bool {
|
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
|
||||||
return our_tip != nil
|
self.likes = likes
|
||||||
|
self.boosts = boosts
|
||||||
|
self.zaps = zaps
|
||||||
|
self.replies = replies
|
||||||
|
self.zap_total = zap_total
|
||||||
|
self.our_like = our_like
|
||||||
|
self.our_boost = our_boost
|
||||||
|
self.our_zap = our_zap
|
||||||
|
self.our_reply = our_reply
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(damus: DamusState, evid: String) {
|
||||||
|
self.likes = damus.likes.counts[evid] ?? 0
|
||||||
|
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||||
|
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||||
|
self.replies = damus.replies.get_replies(evid)
|
||||||
|
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
||||||
|
self.our_like = damus.likes.our_events[evid]
|
||||||
|
self.our_boost = damus.boosts.our_events[evid]
|
||||||
|
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||||
|
self.our_reply = damus.replies.our_reply(evid)
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_empty: Bool {
|
||||||
|
return likes == 0 && boosts == 0 && zaps == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var zapped: Bool {
|
||||||
|
return our_zap != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var liked: Bool {
|
var liked: Bool {
|
||||||
return our_like != nil
|
return our_like != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var replied: Bool {
|
||||||
|
return our_reply != nil
|
||||||
|
}
|
||||||
|
|
||||||
var boosted: Bool {
|
var boosted: Bool {
|
||||||
return our_boost != nil
|
return our_boost != nil
|
||||||
}
|
}
|
||||||
|
|||||||
71
damus/Models/BookmarksManager.swift
Normal file
71
damus/Models/BookmarksManager.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// BookmarksManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Joel Klabo on 2/18/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
fileprivate func get_bookmarks_key(pubkey: String) -> String {
|
||||||
|
pk_setting_key(pubkey, key: "bookmarks")
|
||||||
|
}
|
||||||
|
|
||||||
|
func load_bookmarks(pubkey: String) -> [NostrEvent] {
|
||||||
|
let key = get_bookmarks_key(pubkey: pubkey)
|
||||||
|
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
|
||||||
|
event_from_json(dat: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||||
|
let uniq_bookmarks = Array(Set(value))
|
||||||
|
|
||||||
|
if uniq_bookmarks != current_value {
|
||||||
|
let encoded = uniq_bookmarks.map(event_to_json)
|
||||||
|
UserDefaults.standard.set(encoded, forKey: get_bookmarks_key(pubkey: pubkey))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookmarksManager: ObservableObject {
|
||||||
|
|
||||||
|
private let userDefaults = UserDefaults.standard
|
||||||
|
private let pubkey: String
|
||||||
|
|
||||||
|
private var _bookmarks: [NostrEvent]
|
||||||
|
var bookmarks: [NostrEvent] {
|
||||||
|
get {
|
||||||
|
return _bookmarks
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if save_bookmarks(pubkey: pubkey, current_value: _bookmarks, value: newValue) {
|
||||||
|
self._bookmarks = newValue
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(pubkey: String) {
|
||||||
|
self._bookmarks = load_bookmarks(pubkey: pubkey)
|
||||||
|
self.pubkey = pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBookmarked(_ ev: NostrEvent) -> Bool {
|
||||||
|
return bookmarks.contains(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBookmark(_ ev: NostrEvent) {
|
||||||
|
if isBookmarked(ev) {
|
||||||
|
bookmarks = bookmarks.filter { $0 != ev }
|
||||||
|
} else {
|
||||||
|
bookmarks.append(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearAll() {
|
||||||
|
bookmarks = []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,50 @@ import Foundation
|
|||||||
class Contacts {
|
class Contacts {
|
||||||
private var friends: Set<String> = Set()
|
private var friends: Set<String> = Set()
|
||||||
private var friend_of_friends: Set<String> = Set()
|
private var friend_of_friends: Set<String> = Set()
|
||||||
|
private var muted: Set<String> = Set()
|
||||||
|
|
||||||
|
let our_pubkey: String
|
||||||
var event: NostrEvent?
|
var event: NostrEvent?
|
||||||
|
var mutelist: NostrEvent?
|
||||||
|
|
||||||
|
init(our_pubkey: String) {
|
||||||
|
self.our_pubkey = our_pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_muted(_ pk: String) -> Bool {
|
||||||
|
return muted.contains(pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_mutelist(_ ev: NostrEvent) {
|
||||||
|
let oldlist = self.mutelist
|
||||||
|
self.mutelist = ev
|
||||||
|
|
||||||
|
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
|
||||||
|
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
||||||
|
let diff = old.symmetricDifference(new)
|
||||||
|
|
||||||
|
var new_mutes = Array<String>()
|
||||||
|
var new_unmutes = Array<String>()
|
||||||
|
|
||||||
|
for d in diff {
|
||||||
|
if new.contains(d) {
|
||||||
|
new_mutes.append(d)
|
||||||
|
} else {
|
||||||
|
new_unmutes.append(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: set local mutelist here
|
||||||
|
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
||||||
|
|
||||||
|
if new_mutes.count > 0 {
|
||||||
|
notify(.new_mutes, new_mutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_unmutes.count > 0 {
|
||||||
|
notify(.new_unmutes, new_unmutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func get_friendosphere() -> [String] {
|
func get_friendosphere() -> [String] {
|
||||||
var fs = get_friend_list()
|
var fs = get_friend_list()
|
||||||
@@ -56,6 +99,10 @@ class Contacts {
|
|||||||
return friends.contains(pubkey)
|
return friends.contains(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func is_friend_or_self(_ pubkey: String) -> Bool {
|
||||||
|
return pubkey == our_pubkey || is_friend(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
func follow_state(_ pubkey: String) -> FollowState {
|
func follow_state(_ pubkey: String) -> FollowState {
|
||||||
return is_friend(pubkey) ? .follows : .unfollows
|
return is_friend(pubkey) ? .follows : .unfollows
|
||||||
}
|
}
|
||||||
@@ -93,7 +140,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri
|
|||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
|
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
|
||||||
guard let cs = our_contacts else {
|
guard let cs = our_contacts else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -102,7 +149,7 @@ func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, p
|
|||||||
ev.calculate_id()
|
ev.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
|
|
||||||
pool.send(.event(ev))
|
postbox.send(ev)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class CreateAccountModel: ObservableObject {
|
|||||||
@Published var about: String = ""
|
@Published var about: String = ""
|
||||||
@Published var pubkey: String = ""
|
@Published var pubkey: String = ""
|
||||||
@Published var privkey: String = ""
|
@Published var privkey: String = ""
|
||||||
|
@Published var profile_image: String? = nil
|
||||||
|
|
||||||
var pubkey_bech32: String {
|
var pubkey_bech32: String {
|
||||||
return bech32_pubkey(self.pubkey) ?? ""
|
return bech32_pubkey(self.pubkey) ?? ""
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import LinkPresentation
|
||||||
|
|
||||||
struct DamusState {
|
struct DamusState {
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
@@ -16,12 +17,29 @@ struct DamusState {
|
|||||||
let tips: TipCounter
|
let tips: TipCounter
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
let dms: DirectMessagesModel
|
let dms: DirectMessagesModel
|
||||||
|
let previews: PreviewCache
|
||||||
|
let zaps: Zaps
|
||||||
|
let lnurls: LNUrls
|
||||||
|
let settings: UserSettingsStore
|
||||||
|
let relay_filters: RelayFilters
|
||||||
|
let relay_metadata: RelayMetadatas
|
||||||
|
let drafts: Drafts
|
||||||
|
let events: EventCache
|
||||||
|
let bookmarks: BookmarksManager
|
||||||
|
let postbox: PostBox
|
||||||
|
let bootstrap_relays: [String]
|
||||||
|
let replies: ReplyCounter
|
||||||
|
let translations: Translations
|
||||||
var pubkey: String {
|
var pubkey: String {
|
||||||
return keypair.pubkey
|
return keypair.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var is_privkey_user: Bool {
|
||||||
|
keypair.privkey != nil
|
||||||
|
}
|
||||||
|
|
||||||
static var empty: DamusState {
|
static var empty: DamusState {
|
||||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel())
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
damus/Models/DeepLPlan.swift
Normal file
35
damus/Models/DeepLPlan.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// DeepLPlan.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/3/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DeepLPlan: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
case free
|
||||||
|
case pro
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .free:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
|
||||||
|
case .pro:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,38 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class DirectMessageModel: ObservableObject {
|
class DirectMessageModel: ObservableObject {
|
||||||
@Published var events: [NostrEvent]
|
@Published var events: [NostrEvent] {
|
||||||
|
didSet {
|
||||||
|
is_request = determine_is_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var draft: String
|
||||||
|
|
||||||
init(events: [NostrEvent]) {
|
var is_request: Bool
|
||||||
self.events = events
|
var our_pubkey: String
|
||||||
|
|
||||||
|
func determine_is_request() -> Bool {
|
||||||
|
for event in events {
|
||||||
|
if event.pubkey == our_pubkey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(events: [NostrEvent], our_pubkey: String) {
|
||||||
|
self.events = events
|
||||||
|
self.is_request = false
|
||||||
|
self.our_pubkey = our_pubkey
|
||||||
|
self.draft = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
init(our_pubkey: String) {
|
||||||
self.events = []
|
self.events = []
|
||||||
|
self.is_request = false
|
||||||
|
self.our_pubkey = our_pubkey
|
||||||
|
self.draft = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,26 @@ import Foundation
|
|||||||
class DirectMessagesModel: ObservableObject {
|
class DirectMessagesModel: ObservableObject {
|
||||||
@Published var dms: [(String, DirectMessageModel)] = []
|
@Published var dms: [(String, DirectMessageModel)] = []
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
|
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 friend_dms: [(String, DirectMessageModel)] {
|
||||||
|
return dms.filter { dm in !dm.1.is_request }
|
||||||
|
}
|
||||||
|
|
||||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||||
if let dm = lookup(pubkey) {
|
if let dm = lookup(pubkey) {
|
||||||
return dm
|
return dm
|
||||||
}
|
}
|
||||||
|
|
||||||
let new = DirectMessageModel()
|
let new = DirectMessageModel(our_pubkey: our_pubkey)
|
||||||
dms.append((pubkey, new))
|
dms.append((pubkey, new))
|
||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|||||||
13
damus/Models/DraftsModel.swift
Normal file
13
damus/Models/DraftsModel.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// DraftModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/12/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Drafts: ObservableObject {
|
||||||
|
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||||
|
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||||
|
}
|
||||||
@@ -9,9 +9,65 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
class EventsModel: ObservableObject {
|
class EventsModel: ObservableObject {
|
||||||
var has_event: Set<String> = Set()
|
let state: DamusState
|
||||||
|
let target: String
|
||||||
|
let kind: NostrKind
|
||||||
|
let sub_id = UUID().uuidString
|
||||||
|
let profiles_id = UUID().uuidString
|
||||||
|
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
|
|
||||||
init() {
|
init(state: DamusState, target: String, kind: NostrKind) {
|
||||||
|
self.state = state
|
||||||
|
self.target = target
|
||||||
|
self.kind = kind
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_filter() -> NostrFilter {
|
||||||
|
var filter = NostrFilter.filter_kinds([kind.rawValue])
|
||||||
|
filter.referenced_ids = [target]
|
||||||
|
filter.limit = 500
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe() {
|
||||||
|
state.pool.subscribe(sub_id: sub_id,
|
||||||
|
filters: [get_filter()],
|
||||||
|
handler: handle_nostr_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe() {
|
||||||
|
state.pool.unsubscribe(sub_id: sub_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle_event(relay_id: String, ev: NostrEvent) {
|
||||||
|
guard ev.kind == kind.rawValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard last_etag(tags: ev.tags) == target else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
|
guard case .nostr_event(let nev) = ev else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch nev {
|
||||||
|
case .event(_, let ev):
|
||||||
|
handle_event(relay_id: relay_id, ev: ev)
|
||||||
|
case .notice(_):
|
||||||
|
break
|
||||||
|
case .ok:
|
||||||
|
break
|
||||||
|
case .eose(_):
|
||||||
|
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class FollowersModel: ObservableObject {
|
|||||||
let sub_id: String = UUID().description
|
let sub_id: String = UUID().description
|
||||||
let profiles_id: String = UUID().description
|
let profiles_id: String = UUID().description
|
||||||
|
|
||||||
var count_display: String {
|
var count: Int? {
|
||||||
guard let contacts = self.contacts else {
|
guard let contacts = self.contacts else {
|
||||||
return "?"
|
return nil
|
||||||
}
|
}
|
||||||
return "\(contacts.count)";
|
return contacts.count
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState, target: String) {
|
init(damus_state: DamusState, target: String) {
|
||||||
@@ -51,11 +51,7 @@ class FollowersModel: ObservableObject {
|
|||||||
if has_contact.contains(ev.pubkey) {
|
if has_contact.contains(ev.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
process_contact_event(
|
process_contact_event(state: damus_state, ev: ev)
|
||||||
pool: damus_state.pool,
|
|
||||||
contacts: damus_state.contacts,
|
|
||||||
pubkey: damus_state.pubkey, ev: ev
|
|
||||||
)
|
|
||||||
contacts?.append(ev.pubkey)
|
contacts?.append(ev.pubkey)
|
||||||
has_contact.insert(ev.pubkey)
|
has_contact.insert(ev.pubkey)
|
||||||
}
|
}
|
||||||
@@ -73,32 +69,34 @@ class FollowersModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
switch ev {
|
guard case .nostr_event(let nev) = ev else {
|
||||||
case .ws_event:
|
return
|
||||||
break
|
}
|
||||||
case .nostr_event(let nev):
|
|
||||||
switch nev {
|
switch nev {
|
||||||
case .event(let sub_id, let ev):
|
case .event(let sub_id, let ev):
|
||||||
guard sub_id == self.sub_id || sub_id == self.profiles_id else {
|
guard sub_id == self.sub_id || sub_id == self.profiles_id else {
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
if ev.known_kind == .contacts {
|
|
||||||
handle_contact_event(ev)
|
|
||||||
} else if ev.known_kind == .metadata {
|
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
case .notice(let msg):
|
|
||||||
print("followingmodel notice: \(msg)")
|
|
||||||
|
|
||||||
case .eose(let sub_id):
|
|
||||||
if sub_id == self.sub_id {
|
|
||||||
load_profiles(relay_id: relay_id)
|
|
||||||
} else if sub_id == self.profiles_id {
|
|
||||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .notice(let msg):
|
||||||
|
print("followingmodel notice: \(msg)")
|
||||||
|
|
||||||
|
case .eose(let sub_id):
|
||||||
|
if sub_id == self.sub_id {
|
||||||
|
load_profiles(relay_id: relay_id)
|
||||||
|
} else if sub_id == self.profiles_id {
|
||||||
|
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||||
|
}
|
||||||
|
|
||||||
|
case .ok:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ class FollowingModel {
|
|||||||
break
|
break
|
||||||
case .nostr_event(let nev):
|
case .nostr_event(let nev):
|
||||||
switch nev {
|
switch nev {
|
||||||
|
case .ok:
|
||||||
|
break
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
if ev.kind == 0 {
|
if ev.kind == 0 {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
print("followingmodel notice: \(msg)")
|
print("followingmodel notice: \(msg)")
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class HomeModel: ObservableObject {
|
|||||||
var channels: [String: NostrEvent] = [:]
|
var channels: [String: NostrEvent] = [:]
|
||||||
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
||||||
var done_init: Bool = false
|
var done_init: Bool = false
|
||||||
|
var incoming_dms: [NostrEvent] = []
|
||||||
|
let dm_debouncer = Debouncer(interval: 0.5)
|
||||||
|
var should_debounce_dms = true
|
||||||
|
|
||||||
let home_subid = UUID().description
|
let home_subid = UUID().description
|
||||||
let contacts_subid = UUID().description
|
let contacts_subid = UUID().description
|
||||||
@@ -47,23 +50,33 @@ class HomeModel: ObservableObject {
|
|||||||
let profiles_subid = UUID().description
|
let profiles_subid = UUID().description
|
||||||
|
|
||||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||||
@Published var notifications: [NostrEvent] = []
|
@Published var notifications = NotificationsModel()
|
||||||
@Published var dms: DirectMessagesModel = DirectMessagesModel()
|
@Published var dms: DirectMessagesModel
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events = EventHolder()
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
@Published var signal: SignalModel = SignalModel()
|
@Published var signal: SignalModel = SignalModel()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.damus_state = DamusState.empty
|
self.damus_state = DamusState.empty
|
||||||
|
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
|
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||||
|
self.setup_debouncer()
|
||||||
}
|
}
|
||||||
|
|
||||||
var pool: RelayPool {
|
var pool: RelayPool {
|
||||||
return damus_state.pool
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||||
if !has_event.keys.contains(sub_id) {
|
if !has_event.keys.contains(sub_id) {
|
||||||
@@ -96,6 +109,8 @@ class HomeModel: ObservableObject {
|
|||||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||||
case .metadata:
|
case .metadata:
|
||||||
handle_metadata_event(ev)
|
handle_metadata_event(ev)
|
||||||
|
case .list:
|
||||||
|
handle_list_event(ev)
|
||||||
case .boost:
|
case .boost:
|
||||||
handle_boost_event(sub_id: sub_id, ev)
|
handle_boost_event(sub_id: sub_id, ev)
|
||||||
case .like:
|
case .like:
|
||||||
@@ -108,9 +123,75 @@ class HomeModel: ObservableObject {
|
|||||||
handle_channel_create(ev)
|
handle_channel_create(ev)
|
||||||
case .channel_meta:
|
case .channel_meta:
|
||||||
handle_channel_meta(ev)
|
handle_channel_meta(ev)
|
||||||
|
case .zap:
|
||||||
|
handle_zap_event(ev)
|
||||||
|
case .zap_request:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||||
|
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state.zaps.add_zap(zap: zap)
|
||||||
|
|
||||||
|
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !notifications.insert_zap(zap) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||||
|
if damus_state.settings.zap_vibration {
|
||||||
|
// Generate zap vibration
|
||||||
|
zap_vibrate(zap_amount: zap.invoice.amount)
|
||||||
|
}
|
||||||
|
if damus_state.settings.zap_notification {
|
||||||
|
// Create in-app local notification for zap received.
|
||||||
|
create_in_app_zap_notification(profiles: profiles, zap: zap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_zap_event(_ ev: NostrEvent) {
|
||||||
|
// These are zap notifications
|
||||||
|
guard let ptag = event_tag(ev, name: "p") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let our_keypair = damus_state.keypair
|
||||||
|
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||||
|
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let lnurl = profile.lnurl else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.damus_state.profiles.zappers[ptag] = zapper
|
||||||
|
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func handle_channel_create(_ ev: NostrEvent) {
|
func handle_channel_create(_ ev: NostrEvent) {
|
||||||
guard ev.is_valid else {
|
guard ev.is_valid else {
|
||||||
return
|
return
|
||||||
@@ -122,6 +203,12 @@ class HomeModel: ObservableObject {
|
|||||||
func handle_channel_meta(_ ev: NostrEvent) {
|
func handle_channel_meta(_ ev: NostrEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
func handle_delete_event(_ ev: NostrEvent) {
|
func handle_delete_event(_ ev: NostrEvent) {
|
||||||
guard ev.is_valid else {
|
guard ev.is_valid else {
|
||||||
return
|
return
|
||||||
@@ -131,7 +218,7 @@ class HomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||||
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
|
process_contact_event(state: self.damus_state, ev: ev)
|
||||||
|
|
||||||
if sub_id == init_subid {
|
if sub_id == init_subid {
|
||||||
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
||||||
@@ -151,7 +238,7 @@ class HomeModel: ObservableObject {
|
|||||||
guard inner_ev.is_valid else {
|
guard inner_ev.is_valid else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if inner_ev.is_textlike {
|
if inner_ev.is_textlike {
|
||||||
handle_text_event(sub_id: sub_id, ev)
|
handle_text_event(sub_id: sub_id, ev)
|
||||||
}
|
}
|
||||||
@@ -167,6 +254,7 @@ class HomeModel: ObservableObject {
|
|||||||
case .success(let n):
|
case .success(let n):
|
||||||
let boosted = Counted(event: ev, id: e, total: n)
|
let boosted = Counted(event: ev, id: e, total: n)
|
||||||
notify(.boosted, boosted)
|
notify(.boosted, boosted)
|
||||||
|
notify(.update_stats, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +264,14 @@ class HomeModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK SIGS ON THESE
|
|
||||||
|
|
||||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||||
case .already_counted:
|
case .already_counted:
|
||||||
break
|
break
|
||||||
case .success(let n):
|
case .success(let n):
|
||||||
|
handle_notification(ev: ev)
|
||||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||||
notify(.liked, liked)
|
notify(.liked, liked)
|
||||||
|
notify(.update_stats, e.ref_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +312,7 @@ class HomeModel: ObservableObject {
|
|||||||
switch ev {
|
switch ev {
|
||||||
case .event(let sub_id, let ev):
|
case .event(let sub_id, let ev):
|
||||||
// globally handle likes
|
// globally handle likes
|
||||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
|
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||||
if !always_process {
|
if !always_process {
|
||||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||||
return
|
return
|
||||||
@@ -238,15 +326,20 @@ class HomeModel: ObservableObject {
|
|||||||
case .eose(let sub_id):
|
case .eose(let sub_id):
|
||||||
|
|
||||||
if sub_id == dms_subid {
|
if sub_id == dms_subid {
|
||||||
let dms = dms.dms.flatMap { $0.1.events }
|
var dms = dms.dms.flatMap { $0.1.events }
|
||||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
|
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 {
|
} else if sub_id == notifications_subid {
|
||||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
|
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loading = false
|
self.loading = false
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case .ok:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +365,11 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
||||||
our_contacts_filter.authors = [damus_state.pubkey]
|
our_contacts_filter.authors = [damus_state.pubkey]
|
||||||
|
|
||||||
|
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
|
||||||
|
our_blocklist_filter.parameter = ["mute"]
|
||||||
|
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||||
|
|
||||||
var dms_filter = NostrFilter.filter_kinds([
|
var dms_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.dm.rawValue,
|
NostrKind.dm.rawValue,
|
||||||
])
|
])
|
||||||
@@ -290,7 +387,6 @@ class HomeModel: ObservableObject {
|
|||||||
// TODO: separate likes?
|
// TODO: separate likes?
|
||||||
var home_filter = NostrFilter.filter_kinds([
|
var home_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.text.rawValue,
|
NostrKind.text.rawValue,
|
||||||
NostrKind.chat.rawValue,
|
|
||||||
NostrKind.like.rawValue,
|
NostrKind.like.rawValue,
|
||||||
NostrKind.boost.rawValue,
|
NostrKind.boost.rawValue,
|
||||||
])
|
])
|
||||||
@@ -300,16 +396,16 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
var notifications_filter = NostrFilter.filter_kinds([
|
var notifications_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.text.rawValue,
|
NostrKind.text.rawValue,
|
||||||
NostrKind.chat.rawValue,
|
|
||||||
NostrKind.like.rawValue,
|
NostrKind.like.rawValue,
|
||||||
NostrKind.boost.rawValue,
|
NostrKind.boost.rawValue,
|
||||||
|
NostrKind.zap.rawValue,
|
||||||
])
|
])
|
||||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||||
notifications_filter.limit = 100
|
notifications_filter.limit = 500
|
||||||
|
|
||||||
var home_filters = [home_filter]
|
var home_filters = [home_filter]
|
||||||
var notifications_filters = [notifications_filter]
|
var notifications_filters = [notifications_filter]
|
||||||
var contacts_filters = [contacts_filter, our_contacts_filter]
|
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
||||||
var dms_filters = [dms_filter, our_dms_filter]
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
|
|
||||||
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||||
@@ -333,9 +429,32 @@ class HomeModel: ObservableObject {
|
|||||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handle_list_event(_ ev: NostrEvent) {
|
||||||
|
// we only care about our lists
|
||||||
|
guard ev.pubkey == damus_state.pubkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mutelist = damus_state.contacts.mutelist {
|
||||||
|
if ev.created_at <= mutelist.created_at {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard name.ref_id == "mute" else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state.contacts.set_mutelist(ev)
|
||||||
|
}
|
||||||
|
|
||||||
func handle_metadata_event(_ ev: NostrEvent) {
|
func handle_metadata_event(_ ev: NostrEvent) {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||||
@@ -346,94 +465,93 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
return m[kind]
|
return m[kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
|
|
||||||
let last_ev = get_last_event(timeline)
|
|
||||||
|
|
||||||
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
|
||||||
save_last_event(ev, timeline: timeline)
|
|
||||||
if shouldNotify {
|
|
||||||
new_events = NewEventsBits(prev: new_events, setting: timeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle_notification(ev: NostrEvent) {
|
func handle_notification(ev: NostrEvent) {
|
||||||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
// don't show notifications from ourselves
|
||||||
|
guard ev.pubkey != damus_state.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_last_event(ev: ev, timeline: .notifications)
|
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard should_show_event(contacts: damus_state.contacts, ev: ev) 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) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||||
|
process_local_notification(damus_state: damus_state, event: ev)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert_home_event(_ ev: NostrEvent) -> Bool {
|
@discardableResult
|
||||||
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
|
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
|
||||||
if ok {
|
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||||
|
new_events = new_bits
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert_home_event(_ ev: NostrEvent) {
|
||||||
|
if events.insert(ev) {
|
||||||
handle_last_event(ev: ev, timeline: .home)
|
handle_last_event(ev: ev, timeline: .home)
|
||||||
}
|
}
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_hide_event(_ ev: NostrEvent) -> Bool {
|
|
||||||
return !ev.should_show_event
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||||
if should_hide_event(ev) {
|
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
damus_state.replies.count_replies(ev)
|
||||||
|
damus_state.events.insert(ev)
|
||||||
|
|
||||||
if sub_id == home_subid {
|
if sub_id == home_subid {
|
||||||
let _ = insert_home_event(ev)
|
insert_home_event(ev)
|
||||||
} else if sub_id == notifications_subid {
|
} else if sub_id == notifications_subid {
|
||||||
handle_notification(ev: ev)
|
handle_notification(ev: ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_dm(_ ev: NostrEvent) {
|
func handle_dm(_ ev: NostrEvent) {
|
||||||
|
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||||
var inserted = false
|
return
|
||||||
var found = false
|
|
||||||
let ours = ev.pubkey == self.damus_state.pubkey
|
|
||||||
var i = 0
|
|
||||||
|
|
||||||
var the_pk = ev.pubkey
|
|
||||||
if ours {
|
|
||||||
if let ref_pk = ev.referenced_pubkeys.first {
|
|
||||||
the_pk = ref_pk.ref_id
|
|
||||||
} else {
|
|
||||||
// self dm!?
|
|
||||||
print("TODO: handle self dm?")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pk, _) in dms.dms {
|
if !should_debounce_dms {
|
||||||
if pk == the_pk {
|
self.incoming_dms.append(ev)
|
||||||
found = true
|
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
|
self.new_events = notifs
|
||||||
$0.created_at < $1.created_at
|
}
|
||||||
|
self.incoming_dms = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
incoming_dms.append(ev)
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
inserted = true
|
|
||||||
let model = DirectMessageModel(events: [ev])
|
|
||||||
dms.dms.append((the_pk, model))
|
|
||||||
}
|
|
||||||
|
|
||||||
if inserted {
|
|
||||||
handle_last_event(ev: ev, timeline: .dms, shouldNotify: !ours)
|
|
||||||
|
|
||||||
dms.dms = dms.dms.sorted { a, b in
|
|
||||||
if a.1.events.count > 0 && b.1.events.count > 0 {
|
|
||||||
return a.1.events.last!.created_at > b.1.events.last!.created_at
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
self.incoming_dms = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,24 +657,55 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
|||||||
print("-----")
|
print("-----")
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
|
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
notify(.deleted_account, ())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var old_nip05: String? = nil
|
||||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||||
|
old_nip05 = mprof.profile.nip05
|
||||||
if mprof.timestamp > ev.created_at {
|
if mprof.timestamp > ev.created_at {
|
||||||
// skip if we already have an newer profile
|
// skip if we already have an newer profile
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
|
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
|
||||||
profiles.add(id: ev.pubkey, profile: tprof)
|
profiles.add(id: ev.pubkey, profile: tprof)
|
||||||
|
|
||||||
|
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
||||||
|
Task.detached(priority: .background) {
|
||||||
|
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
|
||||||
|
if validated != nil {
|
||||||
|
print("validated nip05 for '\(nip05)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
profiles.validated[ev.pubkey] = validated
|
||||||
|
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||||
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load pfps asap
|
// load pfps asap
|
||||||
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
||||||
if let _ = URL(string: picture) {
|
if URL(string: picture) != nil {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let banner = tprof.profile.banner ?? ""
|
||||||
|
if URL(string: banner) != nil {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
}
|
}
|
||||||
@@ -569,33 +718,33 @@ func robohash(_ pk: String) -> String {
|
|||||||
return "https://robohash.org/" + pk
|
return "https://robohash.org/" + pk
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||||
guard ev.pubkey == pubkey else {
|
guard ev.pubkey == state.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// only use new stuff
|
// only use new stuff
|
||||||
if let current_ev = contacts.event {
|
if let current_ev = state.contacts.event {
|
||||||
guard ev.created_at > current_ev.created_at else {
|
guard ev.created_at > current_ev.created_at else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let m_old_ev = contacts.event
|
let m_old_ev = state.contacts.event
|
||||||
contacts.event = ev
|
state.contacts.event = ev
|
||||||
|
|
||||||
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
|
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
|
||||||
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
|
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||||
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
|
load_our_stuff(state: state, ev: ev)
|
||||||
add_contact_if_friend(contacts: contacts, ev: ev)
|
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||||
let bootstrap_dict: [String: RelayInfo] = [:]
|
let bootstrap_dict: [String: RelayInfo] = [:]
|
||||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
|
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
|
||||||
d[r] = .rw
|
d[r] = .rw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,20 +766,305 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
|
|||||||
|
|
||||||
let diff = old.symmetricDifference(new)
|
let diff = old.symmetricDifference(new)
|
||||||
|
|
||||||
|
let new_relay_filters = load_relay_filters(state.pubkey) == nil
|
||||||
for d in diff {
|
for d in diff {
|
||||||
changed = true
|
changed = true
|
||||||
if new.contains(d) {
|
if new.contains(d) {
|
||||||
if let url = URL(string: d) {
|
if let url = URL(string: d) {
|
||||||
try? pool.add_relay(url, info: decoded[d] ?? .rw)
|
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pool.remove_relay(d)
|
state.pool.remove_relay(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
||||||
notify(.relays_changed, ())
|
notify(.relays_changed, ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
|
||||||
|
try? pool.add_relay(url, info: info)
|
||||||
|
|
||||||
|
let relay_id = url.absoluteString
|
||||||
|
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.detached(priority: .background) {
|
||||||
|
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
metadatas.insert(relay_id: relay_id, metadata: meta)
|
||||||
|
|
||||||
|
// if this is the first time adding filters, we should filter non-paid relays
|
||||||
|
if new_relay_filters && !meta.is_paid {
|
||||||
|
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
|
||||||
|
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
|
||||||
|
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
||||||
|
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
var res: (Data, URLResponse)? = nil
|
||||||
|
|
||||||
|
res = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
|
guard let data = res?.0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
|
||||||
|
return nip11
|
||||||
|
}
|
||||||
|
|
||||||
|
func process_relay_metadata() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
|
||||||
|
var inserted = false
|
||||||
|
var found = false
|
||||||
|
|
||||||
|
let ours = ev.pubkey == our_pubkey
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
var the_pk = ev.pubkey
|
||||||
|
if ours {
|
||||||
|
if let ref_pk = ev.referenced_pubkeys.first {
|
||||||
|
the_pk = ref_pk.ref_id
|
||||||
|
} else {
|
||||||
|
// self dm!?
|
||||||
|
print("TODO: handle self dm?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pk, _) in dms.dms {
|
||||||
|
if pk == the_pk {
|
||||||
|
found = true
|
||||||
|
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
|
||||||
|
$0.created_at < $1.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
|
||||||
|
dms.dms.append((the_pk, model))
|
||||||
|
inserted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var new_bits: NewEventsBits? = nil
|
||||||
|
if inserted {
|
||||||
|
new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (inserted, new_bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
|
||||||
|
var inserted = false
|
||||||
|
|
||||||
|
var new_events: NewEventsBits? = nil
|
||||||
|
|
||||||
|
for ev in evs {
|
||||||
|
let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
|
||||||
|
inserted = res.0 || inserted
|
||||||
|
if let new = res.1 {
|
||||||
|
new_events = new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_events
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 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? {
|
||||||
|
let last_ev = get_last_event(timeline)
|
||||||
|
|
||||||
|
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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
|
||||||
|
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
|
||||||
|
for tag in ev.tags {
|
||||||
|
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||||
|
if contacts.is_muted(ev.pubkey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ev.should_show_event
|
||||||
|
}
|
||||||
|
|
||||||
|
func zap_vibrate(zap_amount: Int64) {
|
||||||
|
let sats = zap_amount / 1000
|
||||||
|
var vibration_generator: UIImpactFeedbackGenerator
|
||||||
|
if sats >= 10000 {
|
||||||
|
vibration_generator = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
|
} else if sats >= 1000 {
|
||||||
|
vibration_generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
} else {
|
||||||
|
vibration_generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
}
|
||||||
|
vibration_generator.impactOccurred()
|
||||||
|
}
|
||||||
|
|
||||||
|
func zap_notification_title(_ zap: Zap) -> String {
|
||||||
|
if zap.private_request != nil {
|
||||||
|
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
||||||
|
let src = zap.private_request ?? zap.request.ev
|
||||||
|
let anon = event_is_anonymous(ev: src)
|
||||||
|
let pk = anon ? "anon" : src.pubkey
|
||||||
|
let profile = profiles.lookup(id: pk)
|
||||||
|
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
||||||
|
let formattedSats = format_msats_abbrev(zap.invoice.amount)
|
||||||
|
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
|
||||||
|
|
||||||
|
if src.content.isEmpty {
|
||||||
|
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||||||
|
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
|
||||||
|
} else {
|
||||||
|
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
|
||||||
|
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
content.title = zap_notification_title(zap)
|
||||||
|
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||||
|
content.sound = UNNotificationSound.default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("Error: \(error)")
|
||||||
|
} else {
|
||||||
|
print("Local notification scheduled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||||
|
guard let type = ev.known_kind else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if damus_state.settings.notification_only_from_following,
|
||||||
|
damus_state.contacts.follow_state(ev.pubkey) != .follows
|
||||||
|
{
|
||||||
|
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 {
|
||||||
|
|
||||||
|
if let inner_ev = ev.inner_event {
|
||||||
|
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
var title = ""
|
||||||
|
var identifier = ""
|
||||||
|
switch type {
|
||||||
|
case .text:
|
||||||
|
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||||
|
identifier = "myMentionNotification"
|
||||||
|
case .boost:
|
||||||
|
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)
|
||||||
|
identifier = "myDMNotification"
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
content.title = title
|
||||||
|
content.body = conversation
|
||||||
|
content.sound = UNNotificationSound.default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("Error: \(error)")
|
||||||
|
} else {
|
||||||
|
print("Local notification scheduled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
54
damus/Models/ImageUploadModel.swift
Normal file
54
damus/Models/ImageUploadModel.swift
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// ImageUploadModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-03-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
enum MediaUpload {
|
||||||
|
case image(URL)
|
||||||
|
case video(URL)
|
||||||
|
|
||||||
|
var genericFileName: String {
|
||||||
|
"damus_generic_filename.\(file_extension)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_extension: String {
|
||||||
|
switch self {
|
||||||
|
case .image(let url):
|
||||||
|
return url.pathExtension
|
||||||
|
case .video(let url):
|
||||||
|
return url.pathExtension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_image: Bool {
|
||||||
|
if case .image = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||||
|
@Published var progress: Double? = nil
|
||||||
|
|
||||||
|
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
|
||||||
|
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.progress = nil
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
damus/Models/LibreTranslateServer.swift
Normal file
41
damus/Models/LibreTranslateServer.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// LibreTranslateServer.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 1/21/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
case argosopentech
|
||||||
|
case terraprint
|
||||||
|
case vern
|
||||||
|
case custom
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .argosopentech:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||||
|
case .terraprint:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
|
||||||
|
case .vern:
|
||||||
|
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
|
||||||
|
case .custom:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum CountResult {
|
||||||
|
case already_counted
|
||||||
|
case success(Int)
|
||||||
|
}
|
||||||
|
|
||||||
class EventCounter {
|
class EventCounter {
|
||||||
var counts: [String: Int] = [:]
|
var counts: [String: Int] = [:]
|
||||||
@@ -14,11 +18,6 @@ class EventCounter {
|
|||||||
var our_events: [String: NostrEvent] = [:]
|
var our_events: [String: NostrEvent] = [:]
|
||||||
var our_pubkey: String
|
var our_pubkey: String
|
||||||
|
|
||||||
enum CountResult {
|
|
||||||
case already_counted
|
|
||||||
case success(Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
init (our_pubkey: String) {
|
init (our_pubkey: String) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
|
|||||||
let block: Block
|
let block: Block
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Invoice {
|
typealias Invoice = LightningInvoice<Amount>
|
||||||
let description: String
|
typealias ZapInvoice = LightningInvoice<Int64>
|
||||||
let amount: Amount
|
|
||||||
|
enum InvoiceDescription {
|
||||||
|
case description(String)
|
||||||
|
case description_hash(Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LightningInvoice<T> {
|
||||||
|
let description: InvoiceDescription
|
||||||
|
let amount: T
|
||||||
let string: String
|
let string: String
|
||||||
let expiry: UInt64
|
let expiry: UInt64
|
||||||
let payment_hash: Data
|
let payment_hash: Data
|
||||||
let created_at: UInt64
|
let created_at: UInt64
|
||||||
|
|
||||||
|
var description_string: String {
|
||||||
|
switch description {
|
||||||
|
case .description(let string):
|
||||||
|
return string
|
||||||
|
case .description_hash:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Block {
|
enum Block {
|
||||||
@@ -77,6 +94,14 @@ enum Block {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var is_note_mention: Bool {
|
||||||
|
guard case .mention(let mention) = self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return mention.type == .event
|
||||||
|
}
|
||||||
|
|
||||||
var is_mention: Bool {
|
var is_mention: Bool {
|
||||||
if case .mention = self {
|
if case .mention = self {
|
||||||
return true
|
return true
|
||||||
@@ -187,16 +212,80 @@ enum Amount: Equatable {
|
|||||||
func amount_sats_str() -> String {
|
func amount_sats_str() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .any:
|
case .any:
|
||||||
return "Any"
|
return NSLocalizedString("Any", comment: "Any amount of sats")
|
||||||
case .specific(let amt):
|
case .specific(let amt):
|
||||||
if amt < 1000 {
|
return format_msats(amt)
|
||||||
return "\(Double(amt) / 1000.0) sats"
|
|
||||||
}
|
|
||||||
return "\(amt / 1000) sats"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func format_actions_abbrev(_ actions: Int) -> String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.positivePrefix = ""
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
formatter.maximumFractionDigits = 3
|
||||||
|
formatter.roundingMode = .down
|
||||||
|
formatter.roundingIncrement = 0.1
|
||||||
|
formatter.multiplier = 1
|
||||||
|
|
||||||
|
if actions >= 1_000_000 {
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.multiplier = 0.000001
|
||||||
|
} else if actions >= 1000 {
|
||||||
|
formatter.positiveSuffix = "k"
|
||||||
|
formatter.multiplier = 0.001
|
||||||
|
} else {
|
||||||
|
return "\(actions)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let actions = NSNumber(value: actions)
|
||||||
|
|
||||||
|
return formatter.string(from: actions) ?? "\(actions)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func format_msats_abbrev(_ msats: Int64) -> String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.positivePrefix = ""
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
formatter.maximumFractionDigits = 3
|
||||||
|
formatter.roundingMode = .down
|
||||||
|
formatter.roundingIncrement = 0.1
|
||||||
|
formatter.multiplier = 1
|
||||||
|
|
||||||
|
let sats = NSNumber(value: (Double(msats) / 1000.0))
|
||||||
|
|
||||||
|
if msats >= 1_000_000*1000 {
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.multiplier = 0.000001
|
||||||
|
} else if msats >= 1000*1000 {
|
||||||
|
formatter.positiveSuffix = "k"
|
||||||
|
formatter.multiplier = 0.001
|
||||||
|
} else {
|
||||||
|
return sats.stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.string(from: sats) ?? sats.stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
|
||||||
|
let numberFormatter = NumberFormatter()
|
||||||
|
numberFormatter.numberStyle = .decimal
|
||||||
|
numberFormatter.minimumFractionDigits = 0
|
||||||
|
numberFormatter.maximumFractionDigits = 3
|
||||||
|
numberFormatter.roundingMode = .down
|
||||||
|
numberFormatter.locale = locale
|
||||||
|
|
||||||
|
let sats = NSNumber(value: (Double(msat) / 1000.0))
|
||||||
|
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
||||||
|
|
||||||
|
let format = localizedStringFormat(key: "sats_count", locale: locale)
|
||||||
|
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||||
|
}
|
||||||
|
|
||||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||||
guard let invstr = strblock_to_string(b.invstr) else {
|
guard let invstr = strblock_to_string(b.invstr) else {
|
||||||
return nil
|
return nil
|
||||||
@@ -206,9 +295,8 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var description = ""
|
guard let description = convert_invoice_description(b11: b11) else {
|
||||||
if b11.description != nil {
|
return nil
|
||||||
description = String(cString: b11.description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
||||||
@@ -219,6 +307,18 @@ 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))
|
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||||
|
if let desc = b11.description {
|
||||||
|
return .description(String(cString: desc))
|
||||||
|
}
|
||||||
|
|
||||||
|
if var deschash = maybe_pointee(b11.description_hash) {
|
||||||
|
return .description_hash(Data(bytes: &deschash, count: 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
||||||
{
|
{
|
||||||
let ind = Int(ind)
|
let ind = Int(ind)
|
||||||
|
|||||||
18
damus/Models/MutelistModel.swift
Normal file
18
damus/Models/MutelistModel.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// ListModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
class MutelistModel: ObservableObject {
|
||||||
|
let contacts: Contacts
|
||||||
|
|
||||||
|
@Published var users: [String]
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
32
damus/Models/Notifications/EventGroup.swift
Normal file
32
damus/Models/Notifications/EventGroup.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// ReactionGroup.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-02-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class EventGroup {
|
||||||
|
var events: [NostrEvent]
|
||||||
|
|
||||||
|
var last_event_at: Int64 {
|
||||||
|
guard let first = self.events.first else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return first.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.events = []
|
||||||
|
}
|
||||||
|
|
||||||
|
init(events: [NostrEvent]) {
|
||||||
|
self.events = events
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert(_ ev: NostrEvent) -> Bool {
|
||||||
|
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
damus/Models/Notifications/ZapGroup.swift
Normal file
59
damus/Models/Notifications/ZapGroup.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// ZapGroup.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-02-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ZapGroup {
|
||||||
|
var zaps: [Zap]
|
||||||
|
var msat_total: Int64
|
||||||
|
var zappers: Set<String>
|
||||||
|
|
||||||
|
var last_event_at: Int64 {
|
||||||
|
guard let first = zaps.first else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return first.event.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
func zap_requests() -> [NostrEvent] {
|
||||||
|
zaps.map { z in
|
||||||
|
if let priv = z.private_request {
|
||||||
|
return priv
|
||||||
|
} else {
|
||||||
|
return z.request.ev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(zaps: [Zap]) {
|
||||||
|
self.zaps = zaps
|
||||||
|
self.msat_total = 0
|
||||||
|
self.zappers = Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.zaps = []
|
||||||
|
self.msat_total = 0
|
||||||
|
self.zappers = Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert(_ zap: Zap) -> Bool {
|
||||||
|
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
msat_total += zap.invoice.amount
|
||||||
|
|
||||||
|
if !zappers.contains(zap.request.ev.pubkey) {
|
||||||
|
zappers.insert(zap.request.ev.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
320
damus/Models/NotificationsModel.swift
Normal file
320
damus/Models/NotificationsModel.swift
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
//
|
||||||
|
// NotificationsModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-02-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum NotificationItem {
|
||||||
|
case repost(String, EventGroup)
|
||||||
|
case reaction(String, EventGroup)
|
||||||
|
case profile_zap(ZapGroup)
|
||||||
|
case event_zap(String, ZapGroup)
|
||||||
|
case reply(NostrEvent)
|
||||||
|
|
||||||
|
var is_reply: NostrEvent? {
|
||||||
|
if case .reply(let ev) = self {
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_zap: ZapGroup? {
|
||||||
|
switch self {
|
||||||
|
case .profile_zap(let zapgrp):
|
||||||
|
return zapgrp
|
||||||
|
case .event_zap(_, let zapgrp):
|
||||||
|
return zapgrp
|
||||||
|
case .reaction:
|
||||||
|
return nil
|
||||||
|
case .reply:
|
||||||
|
return nil
|
||||||
|
case .repost:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .repost(let evid, _):
|
||||||
|
return "repost_" + evid
|
||||||
|
case .reaction(let evid, _):
|
||||||
|
return "reaction_" + evid
|
||||||
|
case .profile_zap:
|
||||||
|
return "profile_zap"
|
||||||
|
case .event_zap(let evid, _):
|
||||||
|
return "event_zap_" + evid
|
||||||
|
case .reply(let ev):
|
||||||
|
return "reply_" + ev.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var last_event_at: Int64 {
|
||||||
|
switch self {
|
||||||
|
case .reaction(_, let evgrp):
|
||||||
|
return evgrp.last_event_at
|
||||||
|
case .repost(_, let evgrp):
|
||||||
|
return evgrp.last_event_at
|
||||||
|
case .profile_zap(let zapgrp):
|
||||||
|
return zapgrp.last_event_at
|
||||||
|
case .event_zap(_, let zapgrp):
|
||||||
|
return zapgrp.last_event_at
|
||||||
|
case .reply(let reply):
|
||||||
|
return reply.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||||
|
var incoming_zaps: [Zap]
|
||||||
|
var incoming_events: [NostrEvent]
|
||||||
|
var should_queue: Bool
|
||||||
|
|
||||||
|
// mappings from events to
|
||||||
|
var zaps: [String: ZapGroup]
|
||||||
|
var profile_zaps: ZapGroup
|
||||||
|
var reactions: [String: EventGroup]
|
||||||
|
var reposts: [String: EventGroup]
|
||||||
|
var replies: [NostrEvent]
|
||||||
|
var has_reply: Set<String>
|
||||||
|
|
||||||
|
@Published var notifications: [NotificationItem]
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.zaps = [:]
|
||||||
|
self.reactions = [:]
|
||||||
|
self.reposts = [:]
|
||||||
|
self.replies = []
|
||||||
|
self.has_reply = Set()
|
||||||
|
self.should_queue = true
|
||||||
|
self.incoming_zaps = []
|
||||||
|
self.incoming_events = []
|
||||||
|
self.profile_zaps = ZapGroup()
|
||||||
|
self.notifications = []
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_should_queue(_ val: Bool) {
|
||||||
|
self.should_queue = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniq_pubkeys() -> [String] {
|
||||||
|
var pks = Set<String>()
|
||||||
|
|
||||||
|
for ev in incoming_events {
|
||||||
|
pks.insert(ev.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for grp in reposts {
|
||||||
|
for ev in grp.value.events {
|
||||||
|
pks.insert(ev.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ev in replies {
|
||||||
|
pks.insert(ev.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for zap in incoming_zaps {
|
||||||
|
pks.insert(zap.request.ev.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(pks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func build_notifications() -> [NotificationItem] {
|
||||||
|
var notifs: [NotificationItem] = []
|
||||||
|
|
||||||
|
for el in zaps {
|
||||||
|
let evid = el.key
|
||||||
|
let zapgrp = el.value
|
||||||
|
|
||||||
|
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
||||||
|
notifs.append(notif)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !profile_zaps.zaps.isEmpty {
|
||||||
|
notifs.append(.profile_zap(profile_zaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
for el in reposts {
|
||||||
|
let evid = el.key
|
||||||
|
let evgrp = el.value
|
||||||
|
|
||||||
|
notifs.append(.repost(evid, evgrp))
|
||||||
|
}
|
||||||
|
|
||||||
|
for el in reactions {
|
||||||
|
let evid = el.key
|
||||||
|
let evgrp = el.value
|
||||||
|
|
||||||
|
notifs.append(.reaction(evid, evgrp))
|
||||||
|
}
|
||||||
|
|
||||||
|
for reply in replies {
|
||||||
|
notifs.append(.reply(reply))
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs.sort { $0.last_event_at > $1.last_event_at }
|
||||||
|
return notifs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func insert_repost(_ ev: NostrEvent) -> Bool {
|
||||||
|
guard let reposted_ev = ev.inner_event else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = reposted_ev.id
|
||||||
|
|
||||||
|
if let evgrp = self.reposts[id] {
|
||||||
|
return evgrp.insert(ev)
|
||||||
|
} else {
|
||||||
|
let evgrp = EventGroup()
|
||||||
|
self.reposts[id] = evgrp
|
||||||
|
return evgrp.insert(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insert_text(_ ev: NostrEvent) -> Bool {
|
||||||
|
guard !has_reply.contains(ev.id) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
has_reply.insert(ev.id)
|
||||||
|
replies.append(ev)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insert_reaction(_ ev: NostrEvent) -> Bool {
|
||||||
|
guard let ref_id = ev.referenced_ids.last else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = ref_id.id
|
||||||
|
|
||||||
|
if let evgrp = self.reactions[id] {
|
||||||
|
return evgrp.insert(ev)
|
||||||
|
} else {
|
||||||
|
let evgrp = EventGroup()
|
||||||
|
self.reactions[id] = evgrp
|
||||||
|
return evgrp.insert(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
|
||||||
|
if ev.known_kind == .boost {
|
||||||
|
return insert_repost(ev)
|
||||||
|
} else if ev.known_kind == .like {
|
||||||
|
return insert_reaction(ev)
|
||||||
|
} else if ev.known_kind == .text {
|
||||||
|
return insert_text(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insert_zap_immediate(_ zap: Zap) -> Bool {
|
||||||
|
switch zap.target {
|
||||||
|
case .note(let notezt):
|
||||||
|
let id = notezt.note_id
|
||||||
|
if let zapgrp = self.zaps[notezt.note_id] {
|
||||||
|
return zapgrp.insert(zap)
|
||||||
|
} else {
|
||||||
|
let zapgrp = ZapGroup()
|
||||||
|
self.zaps[id] = zapgrp
|
||||||
|
return zapgrp.insert(zap)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .profile:
|
||||||
|
return profile_zaps.insert(zap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert_event(_ ev: NostrEvent) -> Bool {
|
||||||
|
if should_queue {
|
||||||
|
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
if insert_event_immediate(ev) {
|
||||||
|
self.notifications = build_notifications()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert_zap(_ zap: Zap) -> Bool {
|
||||||
|
if should_queue {
|
||||||
|
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if insert_zap_immediate(zap) {
|
||||||
|
self.notifications = build_notifications()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||||
|
var changed = false
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
count = incoming_events.count
|
||||||
|
incoming_events = incoming_events.filter(isIncluded)
|
||||||
|
changed = changed || incoming_events.count != count
|
||||||
|
|
||||||
|
count = profile_zaps.zaps.count
|
||||||
|
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||||
|
changed = changed || profile_zaps.zaps.count != count
|
||||||
|
|
||||||
|
for el in reactions {
|
||||||
|
count = el.value.events.count
|
||||||
|
el.value.events = el.value.events.filter(isIncluded)
|
||||||
|
changed = changed || el.value.events.count != count
|
||||||
|
}
|
||||||
|
|
||||||
|
for el in reposts {
|
||||||
|
count = el.value.events.count
|
||||||
|
el.value.events = el.value.events.filter(isIncluded)
|
||||||
|
changed = changed || el.value.events.count != count
|
||||||
|
}
|
||||||
|
|
||||||
|
for el in zaps {
|
||||||
|
count = el.value.zaps.count
|
||||||
|
el.value.zaps = el.value.zaps.filter {
|
||||||
|
isIncluded($0.request.ev)
|
||||||
|
}
|
||||||
|
changed = changed || el.value.zaps.count != count
|
||||||
|
}
|
||||||
|
|
||||||
|
count = replies.count
|
||||||
|
replies = replies.filter(isIncluded)
|
||||||
|
changed = changed || replies.count != count
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
self.notifications = build_notifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func flush() -> Bool {
|
||||||
|
var inserted = false
|
||||||
|
|
||||||
|
for zap in incoming_zaps {
|
||||||
|
inserted = insert_zap_immediate(zap) || inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
for event in incoming_events {
|
||||||
|
inserted = insert_event_immediate(event) || inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted {
|
||||||
|
self.notifications = build_notifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,38 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ProfileModel: ObservableObject, Equatable {
|
class ProfileModel: ObservableObject, Equatable {
|
||||||
@Published var events: [NostrEvent] = []
|
var events: EventHolder = EventHolder()
|
||||||
@Published var contacts: NostrEvent? = nil
|
@Published var contacts: NostrEvent? = nil
|
||||||
@Published var following: Int = 0
|
@Published var following: Int = 0
|
||||||
@Published var relays: [String: RelayInfo]? = nil
|
@Published var relays: [String: RelayInfo]? = nil
|
||||||
|
@Published var progress: Int = 0
|
||||||
|
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
|
|
||||||
|
|
||||||
var seen_event: Set<String> = Set()
|
var seen_event: Set<String> = Set()
|
||||||
var sub_id = UUID().description
|
var sub_id = UUID().description
|
||||||
var prof_subid = UUID().description
|
var prof_subid = UUID().description
|
||||||
|
|
||||||
|
func follows(pubkey: String) -> Bool {
|
||||||
|
guard let contacts = self.contacts else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in contacts.tags {
|
||||||
|
guard tag.count >= 2 && tag[0] == "p" else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag[1] == pubkey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func get_follow_target() -> FollowTarget {
|
func get_follow_target() -> FollowTarget {
|
||||||
if let contacts = contacts {
|
if let contacts = contacts {
|
||||||
return .contact(contacts)
|
return .contact(contacts)
|
||||||
@@ -70,7 +90,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||||
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
|
process_contact_event(state: damus, ev: ev)
|
||||||
|
|
||||||
// only use new stuff
|
// only use new stuff
|
||||||
if let current_ev = self.contacts {
|
if let current_ev = self.contacts {
|
||||||
@@ -93,11 +113,13 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ev.is_textlike || ev.known_kind == .boost {
|
if ev.is_textlike || ev.known_kind == .boost {
|
||||||
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
|
if self.events.insert(ev) {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
} else if ev.known_kind == .contacts {
|
} else if ev.known_kind == .contacts {
|
||||||
handle_profile_contact_event(ev)
|
handle_profile_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
} else if ev.known_kind == .metadata {
|
||||||
process_metadata_event(profiles: damus.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
seen_event.insert(ev.id)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
@@ -107,15 +129,18 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
case .ws_event:
|
case .ws_event:
|
||||||
return
|
return
|
||||||
case .nostr_event(let resp):
|
case .nostr_event(let resp):
|
||||||
|
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
|
||||||
|
return
|
||||||
|
}
|
||||||
switch resp {
|
switch resp {
|
||||||
case .event(let sid, let ev):
|
case .ok:
|
||||||
if sid != self.sub_id && sid != self.prof_subid {
|
break
|
||||||
return
|
case .event(_, let ev):
|
||||||
}
|
|
||||||
add_event(ev)
|
add_event(ev)
|
||||||
case .notice(let notice):
|
case .notice(let notice):
|
||||||
notify(.notice, notice)
|
notify(.notice, notice)
|
||||||
case .eose:
|
case .eose:
|
||||||
|
progress += 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
damus/Models/ReactionsModel.swift
Normal file
16
damus/Models/ReactionsModel.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// LikesModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
final class ReactionsModel: EventsModel {
|
||||||
|
|
||||||
|
init(state: DamusState, target: String) {
|
||||||
|
super.init(state: state, target: target, kind: .like)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,25 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ReplyMap {
|
class ReplyMap {
|
||||||
var replies: [String: String] = [:]
|
var replies: [String: Set<String>] = [:]
|
||||||
|
|
||||||
func lookup(_ id: String) -> String? {
|
func lookup(_ id: String) -> Set<String>? {
|
||||||
return replies[id]
|
return replies[id]
|
||||||
}
|
}
|
||||||
func add(id: String, reply_id: String) {
|
|
||||||
replies[id] = reply_id
|
private func ensure_set(id: String) {
|
||||||
|
if replies[id] == nil {
|
||||||
|
replies[id] = Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func add(id: String, reply_id: String) -> Bool {
|
||||||
|
ensure_set(id: id)
|
||||||
|
if (replies[id]!).contains(reply_id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
replies[id]!.insert(reply_id)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
damus/Models/Report.swift
Normal file
59
damus/Models/Report.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// Report.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ReportType: String {
|
||||||
|
case explicit
|
||||||
|
case illegal
|
||||||
|
case spam
|
||||||
|
case impersonation
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReportNoteTarget {
|
||||||
|
let pubkey: String
|
||||||
|
let note_id: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReportTarget {
|
||||||
|
case user(String)
|
||||||
|
case note(ReportNoteTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Report {
|
||||||
|
let type: ReportType
|
||||||
|
let target: ReportTarget
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
|
||||||
|
var tags: [[String]]
|
||||||
|
switch target {
|
||||||
|
case .user(let pubkey):
|
||||||
|
tags = [["p", pubkey]]
|
||||||
|
case .note(let notet):
|
||||||
|
tags = [["e", notet.note_id], ["p", notet.pubkey]]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.append(["report", type.rawValue])
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
|
||||||
|
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = 1984
|
||||||
|
let tags = create_report_tags(target: report.target, type: report.type)
|
||||||
|
let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags)
|
||||||
|
|
||||||
|
ev.id = calculate_event_id(ev: ev)
|
||||||
|
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
15
damus/Models/RepostsModel.swift
Normal file
15
damus/Models/RepostsModel.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// RepostsModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 1/22/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class RepostsModel: EventsModel {
|
||||||
|
|
||||||
|
init(state: DamusState, target: String) {
|
||||||
|
super.init(state: state, target: target, kind: .boost)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
damus/Models/Search/SearchResultsModel.swift
Normal file
12
damus/Models/Search/SearchResultsModel.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// SearchResultsModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-03-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
class SearchResultsModel: ObservableObject {
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user