Compare commits
663 Commits
reactions-
...
relay-hint
| Author | SHA1 | Date | |
|---|---|---|---|
|
eddd908fa3
|
|||
| 3ddb2625e9 | |||
|
|
f53ffae767 | ||
|
|
b9168f9914 | ||
|
|
63ff2b6f9e | ||
|
|
7d9468388b | ||
|
|
66b555e0ff
|
||
|
|
8df332472c
|
||
|
|
6072668438
|
||
|
|
6f26ddf7ac
|
||
|
df156df6d9
|
|||
|
|
11c367b541
|
||
|
|
4e1b23d1cb
|
||
|
|
2de3083dad
|
||
|
93149642db
|
|||
|
|
0b0d422b7a
|
||
|
|
036ea50a3a
|
||
|
|
073feccbbf | ||
|
|
eeea9d3266 | ||
|
|
b8bf5df7bc | ||
|
|
e9e68422d4 | ||
|
|
6f9a00d728 | ||
|
|
51e07df1b5 | ||
|
|
2a42723b81 | ||
| 839ef6a80d | |||
| c073dd8fea | |||
|
|
8d9f728cf0 | ||
| 2c62741e25 | |||
|
|
1f612f7fde | ||
|
|
0e9e102d0f | ||
|
|
b94e8765a1 | ||
|
|
53964f5c1a | ||
| bd574d93c3 | |||
| 47514ace79 | |||
|
|
298b43733f | ||
|
|
02116c0af5 | ||
| 92121e3b2d | |||
|
|
c92094823e | ||
|
|
f4b1a504a5 | ||
| 99ae7de5eb | |||
| b3d9ee3fc0 | |||
| e65219ee3e | |||
|
|
414c67a919 | ||
| f436291209 | |||
| a9196a39df | |||
|
|
6a8ee9c360 | ||
|
947e24864e
|
|||
|
b9198d6bd7
|
|||
|
|
14bf187a6e | ||
|
|
c996e5f8b3 | ||
|
b6dad349c9
|
|||
|
|
56dde30cf6
|
||
|
|
95bfbae131
|
||
|
|
3da0ff7ecc
|
||
|
|
b8f846ded8
|
||
|
|
e74c45ad39
|
||
|
|
e6a03522c6
|
||
|
|
dbc7d79ecd
|
||
|
|
d2b5a65eca
|
||
|
|
16b19d3a96
|
||
|
|
70edb8d7c5
|
||
|
ea04ebe95c
|
|||
|
|
44cf47faa4
|
||
|
612abfd862
|
|||
|
20af086273
|
|||
|
|
e9c1671d06 | ||
|
|
d02847d466 | ||
|
|
580fa954b2 | ||
|
|
aef516ae9f | ||
|
|
eb4e3b692b | ||
|
|
fe52381d63 | ||
|
|
ab8d52e685 | ||
|
|
1d32200ae3 | ||
|
|
309b00380d | ||
|
|
7fa2118480 | ||
|
|
1a6c17e308 | ||
|
|
82a6046620 | ||
|
|
241755c8c4 | ||
|
|
b26f66f15c | ||
|
|
28bd0c81e8 | ||
| 0bd1814877 | |||
| ee94f67b94 | |||
|
|
3a25075473 | ||
|
|
d16ff8f78f | ||
|
|
38dc90cb33 | ||
|
|
52bbc698b2 | ||
|
|
496a11f597 | ||
| 4a8a0ea1bd | |||
|
|
c424d4da99 | ||
|
|
69d5fc1553 | ||
| bcb59896db | |||
| e1e6d9eb3d | |||
| f1fdae5957 | |||
|
|
f96647fa40 | ||
|
|
5ea522d306 | ||
|
|
54d6161acd | ||
|
|
b1fd84fd75 | ||
|
|
9dbdf7928a | ||
|
|
67f0e3d296 | ||
|
|
e498418c2d | ||
| 33150a42c5 | |||
| e7fe4ab9b4 | |||
| c146bab08a | |||
|
|
d1cced8d54 | ||
|
|
8849b6105c | ||
|
|
3a0acfaba1 | ||
|
|
0ec2b05070 | ||
|
|
130bbfafb4 | ||
|
|
ffc75772f9 | ||
|
|
5b3fac70ed | ||
|
|
53e3f6d86b | ||
|
|
c28ab7a57c | ||
|
|
09ce3af11e | ||
| e42c09883a | |||
| 77e3924809 | |||
| 3511b1ee91 | |||
| 78a62c8ef0 | |||
|
|
8b96b9f4e6 | ||
|
|
649a857c3a | ||
|
|
cdae2c7558 | ||
|
|
3639110c51 | ||
|
186668512e
|
|||
|
f63666fae2
|
|||
|
|
68d25059b1
|
||
|
|
9aef6b7f5b
|
||
|
|
d2e712575f
|
||
|
|
bf9674e6e4
|
||
|
|
4815390cbe
|
||
|
|
6ce903f1f6
|
||
|
|
b2c91ffce4
|
||
|
|
ae335b18bf
|
||
|
|
6391819fb2
|
||
|
|
5d0e56b7c7
|
||
|
|
50ccc7bd7f
|
||
|
|
b3a6bcf3b2
|
||
|
|
38b2988bbe
|
||
|
|
446c541dcb
|
||
|
|
31fd48ee52
|
||
| b35cc33c32 | |||
|
|
9510290c29 | ||
|
|
3b1238b9c7 | ||
|
|
3bec23ecac | ||
|
|
7b678228b6 | ||
|
|
b1292d4562 | ||
|
|
a62d782fe5 | ||
|
|
81b07eb339 | ||
|
|
02f88398b9 | ||
|
|
e80961cc09 | ||
|
|
bd7721dc26 | ||
|
|
6d974bf71c | ||
|
|
aeeb817735 | ||
|
|
b5e7033958 | ||
|
|
bdc843f30f | ||
|
|
a823fa8e14 | ||
|
|
9232386c15 | ||
|
|
3c1547718c | ||
|
|
b67a7f3e9e | ||
|
|
92850d4f64 | ||
|
|
6323eafd7e | ||
|
|
b8fe826b58 | ||
|
|
841c49238f | ||
|
|
7ab612e3d9 | ||
|
|
6d8a27688f | ||
|
|
765385319a | ||
|
|
342c49a3e5 | ||
|
|
25860e7bb2 | ||
|
|
6962f2b462 | ||
|
e48ce4c6c5
|
|||
|
1cb311cc2c
|
|||
|
|
401846abe4
|
||
|
|
16ef393350
|
||
|
|
d5742f8e4c
|
||
|
|
319063f823
|
||
|
|
5b13cf5634
|
||
|
|
da10b908b3
|
||
|
|
4568935bc5
|
||
|
|
467404a55e
|
||
|
|
fcfe1e4558
|
||
|
|
c9d87a1b9a
|
||
|
|
35ebf4dfc2
|
||
|
|
bc3c256d22
|
||
|
|
0e10e74496
|
||
|
|
ebe9097f73
|
||
|
|
d61a11b647
|
||
|
|
d980cc1f8e
|
||
|
|
bd6056ce2e
|
||
|
|
cf48fda8d0
|
||
|
|
dc344cd28c
|
||
|
|
358610575f
|
||
|
|
a7869fccbb
|
||
|
|
a50903f90a
|
||
|
|
9243705995
|
||
|
|
db4dd9eee9
|
||
|
|
22f2aba969 | ||
|
|
98f2777fda | ||
|
|
102ce43216 | ||
|
|
0c148c8a1f | ||
|
|
3cccb2eb6b | ||
|
|
af4949e26a | ||
|
|
5bb7e95624 | ||
| 814bcf694f | |||
|
|
b0382c61b1 | ||
| e2650a8bfc | |||
|
|
ac39a53b33 | ||
|
|
fb356cdf0b | ||
|
|
238e89ce16 | ||
|
|
6e041c79f7 | ||
|
|
6ef4b60d14 | ||
|
|
054bec2d9a
|
||
|
|
943a46a343
|
||
|
|
17381f6b94
|
||
|
|
18c88de407
|
||
|
|
99d21fc89b
|
||
|
|
db5c86a0d1
|
||
|
|
736ec6fb9e
|
||
|
|
fa2327325a
|
||
|
|
4fdf048040
|
||
|
|
273538bd36
|
||
|
|
0980c8c040
|
||
|
|
f0bfdeaa5a
|
||
|
|
ab7c5c18e3
|
||
|
|
6ae95ab5ec
|
||
|
eec630b2b0
|
|||
|
|
2b3d86968d | ||
|
|
935a6cae7a | ||
|
|
d4940d8386 | ||
|
71ec18f6c6
|
|||
|
caa4bfe864
|
|||
|
a87ba73160
|
|||
|
|
4324b185fe | ||
|
|
1ab9b30b85 | ||
|
|
81cf6ad297 | ||
|
|
1b3be3a13b | ||
|
|
3a2ce04d6b | ||
|
|
981821a6bc | ||
|
|
98f83769bd | ||
|
|
7684f53281 | ||
|
|
15af686a58 | ||
|
|
aad8f9e8d4 | ||
| b2ee44c0ab | |||
|
|
a696ac5084 | ||
|
|
28237c3a63 | ||
|
|
1cae4640c0 | ||
|
|
21a07d54cb | ||
|
|
1efd07b852 | ||
|
|
e5eb7d44a2 | ||
|
|
ec9a89ee4d | ||
|
|
4741c2a3e8 | ||
|
|
0111c5e2dc | ||
|
|
bed4e00b53 | ||
|
|
bf14d7138a | ||
|
|
0c5da08a42 | ||
|
|
a6e123e928 | ||
|
|
69b1173e08 | ||
|
|
c3326213e9 | ||
|
325109d7b8
|
|||
|
|
f16d76605b
|
||
|
|
3eee1b205a
|
||
|
|
9545c6446d
|
||
|
|
40a75f65ab
|
||
|
|
98f42c9896
|
||
|
|
5c22989675
|
||
|
999f16f6a4
|
|||
|
|
3f5fd6eee8
|
||
|
|
7c195aa75c
|
||
|
|
2071efc129
|
||
|
|
9db2e9b464
|
||
|
|
5f6cb568ff
|
||
|
|
045399a065
|
||
|
|
1b526143d0
|
||
|
|
8a046c0d1b
|
||
|
|
2893e4234d
|
||
|
|
973a5ce2cb
|
||
|
|
1e81e90341
|
||
| 9e7943e0e9 | |||
|
|
bb7ac4fea5 | ||
|
|
05d0e15359 | ||
|
|
d4d17fcbad | ||
|
|
c21d29a897 | ||
|
|
6e117ac39c | ||
|
|
79407f17e8 | ||
|
|
72c19fc411 | ||
|
|
24c3e61a4b | ||
|
|
74d5bee1f6 | ||
|
|
8066fa1bf8 | ||
| 26df547605 | |||
| a97532b90d | |||
|
|
e8ba1ec806 | ||
|
|
e8c265a4d8 | ||
|
|
b33dc63fe4 | ||
|
|
c4852f1309 | ||
|
|
39a4be7076 | ||
|
|
50c7edc420 | ||
|
|
67fa3c1ce5 | ||
|
|
cd671da3e7 | ||
|
|
3b60ca04f1 | ||
|
|
e2e58499f5 | ||
|
5cadf09665
|
|||
|
|
1ca7b3462f
|
||
|
|
8a552d2b0f
|
||
|
|
9fa0f18f78
|
||
|
|
db672ca048
|
||
|
|
18ad73cd35
|
||
|
|
5719e9b37e
|
||
|
|
9fb2b3c0e5
|
||
|
|
5ec66feb06
|
||
|
|
ccc301cfcc
|
||
|
|
c1b9d0b55e
|
||
|
|
d9daa27016
|
||
|
|
fa3b5d57ed
|
||
|
|
7c3e598ca6
|
||
|
|
563d5c7881
|
||
|
|
b8cba0ee17
|
||
|
|
8556586af4
|
||
|
|
5fc52bb31b
|
||
|
|
a92c9f2c38
|
||
|
|
61e137696e
|
||
|
|
8fc3b124da
|
||
|
|
7852822295
|
||
|
|
85e55953b3
|
||
|
|
077f633f33
|
||
|
|
1c3d1598a3
|
||
|
|
314608627e
|
||
|
|
aeecc04b29
|
||
|
|
341389d438
|
||
|
|
fbeae64123
|
||
|
|
7a4af31859 | ||
|
|
e106be1412 | ||
|
|
282bf80daa | ||
|
|
bcb861a61b | ||
|
|
bb0ad18913 | ||
|
|
81830c7540 | ||
|
|
68128b5ff1 | ||
|
|
aebeb26bc6 | ||
|
|
79cf3db279 | ||
|
|
dcae0d2cc7 | ||
|
|
2b12dc5920 | ||
|
|
51930e7a12 | ||
|
|
b04e09d2e0 | ||
| b6c4213515 | |||
|
|
8230c6eded | ||
|
|
e79590f795 | ||
|
|
79bced1246 | ||
|
|
896f4b55e3 | ||
|
|
52e65f9429 | ||
|
|
a22cc532e2 | ||
|
|
823227920c | ||
|
|
3e2bbce25e | ||
|
|
e05b2d9ecf | ||
|
|
d7b31a1cd8 | ||
|
|
70f01c0880 | ||
|
|
2cf5f21f78 | ||
|
|
96e8f8b6b2 | ||
|
|
370cfd1b08 | ||
|
|
046af15734 | ||
|
|
9e4ab2d54c | ||
|
|
7cf12e2e0d | ||
|
|
a63a81b387 | ||
|
|
d994cd13dc | ||
|
|
95e985cfce | ||
|
|
3a69de9274 | ||
|
|
64f5acf98c | ||
|
|
5167ab264d | ||
|
|
e02895b29f | ||
|
|
0009d11025 | ||
|
|
afc317bb52 | ||
|
|
629212ea23 | ||
|
|
ec1252200f | ||
|
|
54ea1ab803 | ||
|
|
4cf8097de4 | ||
|
|
2c7384b0a9 | ||
|
|
19e312a8fb | ||
|
|
3986308638 | ||
| fa7740948b | |||
| 892a1420f3 | |||
| ee4cbf7363 | |||
| a1b1ce949b | |||
| 902e8c3950 | |||
| b776788b38 | |||
|
|
78066773f4 | ||
|
|
0bac284eee | ||
|
|
07c95d1003 | ||
|
|
1072c5a384 | ||
|
|
5ed6e85ad8 | ||
|
|
f948dd81ca | ||
|
|
391818f230 | ||
|
|
dc74ad37a1 | ||
|
|
e1c94b7ff9 | ||
|
|
ec933452d3 | ||
|
|
977b268023 | ||
|
|
0c778af833 | ||
|
|
e75e7950b5 | ||
|
|
8d68297cce | ||
|
|
e0fd24aff5 | ||
|
|
b5c3ff45e4 | ||
|
|
786dbb21c4 | ||
|
|
5a17c330da | ||
|
|
975be63ce1 | ||
|
|
d9796bd63c | ||
|
|
25a835624a | ||
|
|
1d06683bb3 | ||
|
|
8e15a86c0a | ||
|
|
51a3008e5a | ||
|
|
f71c1b9848 | ||
|
|
151e23d524 | ||
|
|
7619891c86 | ||
|
|
cc98525f59 | ||
|
|
8cf9549981 | ||
|
|
9ebf27cd37 | ||
|
|
16a1a9f37f | ||
|
|
08d28b0f00 | ||
| d0ae3ca08a | |||
| 6e2f770876 | |||
|
|
4079bea912 | ||
| 4d01340b90 | |||
|
|
61b89c2f54 | ||
|
|
cbdff4a5f8 | ||
|
|
866afe970b | ||
|
|
87efc91527 | ||
| 4121526588 | |||
| 4adcb738a2 | |||
| 1f17f19a6e | |||
|
|
fb54115286 | ||
|
|
50dd35d089 | ||
|
|
1205b2a0e2 | ||
|
|
0a93e909ed | ||
|
|
6f3f928ac3 | ||
|
|
637ceabede | ||
|
|
c25f54f7e7 | ||
|
|
d4ae0b1346 | ||
|
|
7773618547 | ||
|
|
a4199fa299 | ||
|
|
5b9ccc4ee5 | ||
|
|
f538e03093 | ||
|
|
e625297a2e | ||
|
|
e603678872 | ||
|
|
9d77f1b2f7 | ||
| c4f7d25793 | |||
|
|
100f195a03 | ||
|
|
e599ef1ac9 | ||
|
|
033c69b92e | ||
|
|
184e566b1b | ||
|
|
8c321e479b | ||
|
|
960c84d02e | ||
|
|
02f1c2d342 | ||
|
|
c8ca3c93f6 | ||
|
|
5c6e5ca2de | ||
|
|
e3105a90c5 | ||
|
|
38dc7b046a | ||
|
|
da76ad9b66 | ||
|
|
177c55cf3d | ||
|
|
eeb6547d3e | ||
|
|
50ef6600a8 | ||
|
|
be43819de2 | ||
|
|
58017952bc | ||
|
|
409be7fc58 | ||
|
|
1bc660c9cd | ||
|
|
a56a59f81d | ||
|
|
1d5af6ca5c | ||
|
|
f81b2b677f | ||
|
|
290152c859 | ||
|
|
c4ee52fdac | ||
|
|
6f04455350 | ||
|
|
3b62945e5b | ||
|
|
b1b032d905 | ||
|
|
7c805f7f23 | ||
|
|
a2b0620175 | ||
|
|
8c6bee3d90 | ||
|
|
bba651b37c | ||
|
|
f657af275a | ||
|
|
03ded7d39f | ||
|
|
53edc7eb0b | ||
|
|
8b969021d5 | ||
|
|
1e52982a5d | ||
|
|
0038d42f71 | ||
|
|
d94b387fb9 | ||
|
|
8a2dbc95ca | ||
|
|
5865b000c0 | ||
|
|
6efb512a64 | ||
|
|
b7b8c7f175 | ||
|
|
a76e2aa677 | ||
|
|
d9f2317728 | ||
|
|
f1339e835b | ||
|
|
64f2362be3 | ||
|
5b184a40fd
|
|||
|
|
17e6191a92
|
||
|
|
1ff065d4c7
|
||
|
|
94e2c76284 | ||
|
|
1925af6897 | ||
|
|
4effaa4324 | ||
|
|
e2a4443a9c | ||
|
|
7c0e1c5ded | ||
|
|
f6e34ad999 | ||
|
|
ca5da7b5cd | ||
|
|
c08e4a2fdd | ||
|
|
37a50f6087 | ||
|
|
2040e79165 | ||
|
|
a6449020b6 | ||
|
|
75a46f4ab4 | ||
|
|
c948c7e230 | ||
|
|
c83b0fba21 | ||
|
|
b7053e8680 | ||
|
|
17183632c8 | ||
|
|
686d6d6e92 | ||
|
|
847ae7b396 | ||
|
|
ba9780fb17 | ||
|
|
42f5af0ffd | ||
|
|
55f1330fc1 | ||
|
|
4b326340a3 | ||
|
|
83f7766833 | ||
|
|
1e3b20f5b3 | ||
|
|
e508f28f7d | ||
|
|
2c139863b8 | ||
|
|
c699409129 | ||
|
|
74e6d8781a | ||
|
|
876f9c742f | ||
|
|
1e7b57eaf3 | ||
| 5615b1e1ec | |||
|
|
9d66a5ed4f | ||
|
|
5555f1afec | ||
|
|
77b1b895a5 | ||
|
|
6751bc15cc | ||
|
|
735fa97089 | ||
|
|
314774f032 | ||
|
|
262bbf26ea | ||
|
|
cdf8d043c9 | ||
|
|
1eb7c94a5a | ||
|
|
5228d8cf4d | ||
|
|
782779f0d7 | ||
|
|
18ec8e6b6c | ||
|
|
7d82d8b76f | ||
|
|
f957756df7 | ||
|
|
2cb0553723 | ||
|
|
8464e151cc | ||
|
|
2da444e7c2 | ||
| 2a19d5d831 | |||
|
|
f92509fddf | ||
|
|
51ee4046a0 | ||
|
|
1e85bb946d | ||
|
|
6639c002ed | ||
|
|
2a61440aed | ||
|
|
823c2565da | ||
|
|
b5a81e2586 | ||
|
|
6254cea600 | ||
|
|
ce63f6a96b | ||
|
|
6fa2e8b5c6 | ||
|
|
2278ab09a4 | ||
|
|
dfa72fceb1 | ||
|
|
62772615b6 | ||
|
|
9e0b9debb4 | ||
|
|
3902fe7b30 | ||
|
|
471bb4638a | ||
|
|
8c5b0ed5c4 | ||
|
|
379de6ff8e | ||
|
|
cb241741e3 | ||
|
|
1dbf7101b9 | ||
|
|
d9bbca1005 | ||
|
|
d2acf61e5a | ||
|
|
d6898c77d8 | ||
|
|
dd1fdf159b | ||
|
|
51b1b81c0e | ||
|
|
da7af491d0 | ||
|
|
90b284fb6e | ||
|
|
c1a89bd617 | ||
|
|
a20f3ab2ab | ||
|
|
7b9d0edef4 | ||
|
|
c22fc8613d | ||
|
|
f61308e573 | ||
|
|
d93b04a54c | ||
|
|
4b881e6839 | ||
|
|
63b0661728 | ||
|
|
46a66bc69d | ||
|
|
c09018be48 | ||
|
|
d71d448ac8 | ||
|
|
5834e1ee9b | ||
|
|
d51179189c | ||
|
|
b01243b101 | ||
|
|
d2a80cce4e | ||
|
|
0cc9fc1670 | ||
|
|
1279791d65 | ||
|
|
5d2fc0ed54 | ||
|
|
dcafcd9184 | ||
|
|
8481ab85de | ||
|
|
881d3a3aa1 | ||
|
|
878509090f | ||
|
|
24657ecc75 | ||
|
|
cf16a9cd10 | ||
|
|
3a9dda5eb3 | ||
|
|
c69ddd7241 | ||
|
|
bfcb3e4c88 | ||
|
|
27083669fa | ||
|
|
aaddbd847a | ||
|
|
1537501127 | ||
|
|
8b020e2bd6 | ||
|
|
ad614f3e42 | ||
|
|
01497d0288 | ||
|
|
eaad552273 | ||
|
83ecc3142e
|
|||
|
|
ef4afbc720 | ||
|
|
a5cc3aec92 | ||
|
|
881ece214d | ||
|
|
2b140d4279 | ||
|
|
b43dcd2bc7 | ||
|
|
c67a75d740 | ||
|
|
7f00ef5d9d | ||
|
|
d663155941 | ||
|
|
abfe0f642f | ||
| f0b5162205 | |||
|
|
a9bb2ef98b | ||
|
|
eff4525720 | ||
|
|
858d9dc6f0 | ||
|
|
55090bc102 | ||
|
|
40d3d273f0 | ||
|
|
f9271da11c | ||
|
|
4f881a5667 | ||
|
|
9d97886e3f | ||
|
|
e70cfbbe63 | ||
|
|
8a75537ea3 | ||
|
|
49c8d63d0b | ||
|
|
2519b0ee9f | ||
|
|
ba1589e2e2 | ||
|
|
e9a2473bad | ||
|
|
6480023c96 | ||
|
|
774da239b9 | ||
|
|
90c80645ec | ||
|
|
613ec23f7f | ||
|
|
1d73ae1d32 | ||
|
63e364ce5b
|
|||
|
|
ee5f53e4eb
|
||
|
|
9de21a730a
|
||
|
|
36c09c8657
|
||
|
|
e8ac143192
|
||
|
|
93f44939e3
|
||
|
|
48078b9b6a
|
||
|
d6d6858e0b
|
|||
|
|
0187ff1dc0
|
||
|
|
4f9fef8515
|
||
|
|
1ebadd42f0
|
||
|
|
4fb4f3a2de
|
||
|
|
f49169c03c
|
||
|
|
740c10c9b2 | ||
|
|
653f9fbcbe | ||
|
1767a677bb
|
|||
|
|
dba1799df0 | ||
|
|
2db3d7310f | ||
|
|
b2ba1e0e3b | ||
|
|
10b1cf64ae | ||
|
|
afdd3f1d43 | ||
|
|
1b8e3fe184 | ||
|
|
8ab1c6a899 | ||
|
|
e8fae19b97 | ||
|
|
63e70605fc | ||
|
|
35df9f7ab7 | ||
|
|
605d88add1 | ||
|
|
2b0a7d126d | ||
|
|
6e2c133faa | ||
|
|
9885ff1912 | ||
|
|
abb818bbd4 | ||
|
|
f1dc023e18 | ||
|
|
4a332c7ffa | ||
|
|
616f730ae5 | ||
|
|
164cea96f3 | ||
|
|
fa70c376b1 |
52
.github/ISSUE_TEMPLATE/app_release.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: App release process
|
||||
about: Begin preparing for a new app release
|
||||
title: 'Release: '
|
||||
labels: release-tasks
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
A new version release. Please attempt to follow the release process steps below in the order they are shown.
|
||||
|
||||
## TestFlight release candidates
|
||||
|
||||
### Release candidate 1
|
||||
|
||||
**Version:** _[Enter full build information for the release candidate, including major and minor version number, build number, and commit hash]_
|
||||
|
||||
1. [ ] Merge in all needed changes to `master`
|
||||
2. [ ] Check CI, make sure it is passing
|
||||
3. [ ] Prepare preliminary changelog as a draft PR: _[Enter PR link to changelog here]_
|
||||
4. [ ] Make a _release_ build and submit to the internal TestFlight group via our new Release candidate workflow in Xcode Cloud.
|
||||
5. [ ] Prepare short screencast style video with main changes for the announcement
|
||||
6. [ ] Publish release build to these TestFlight groups:
|
||||
- [ ] Alpha testers group
|
||||
- [ ] Translators group
|
||||
- [ ] Purple group
|
||||
7. [ ] Publish announcement on Nostr
|
||||
|
||||
|
||||
_[Duplicate this release candidate section if there is more than one release candidate]_
|
||||
|
||||
|
||||
## App Store release
|
||||
|
||||
1. [ ] Release candidate checks:
|
||||
- [ ] Release candidate has been on Purple TestFlight for at least one week
|
||||
- [ ] No blocker issues came from feedback from Purple users (double-check)
|
||||
- [ ] Check with stakeholders
|
||||
- [ ] Check with developers & product for any release showstoppers (e.g., critical newfound bugs)
|
||||
2. [ ] Thorough check on release notes
|
||||
3. [ ] Submit to App Store review (with manual publishing setting enabled)
|
||||
4. [ ] Get App Store approval from Apple
|
||||
5. [ ] Prepare announcement
|
||||
7. [ ] Publish on the App Store and make announcement
|
||||
8. [ ] Publish changelog and tag commit hash corresponding to the release
|
||||
9. [ ] Perform a version bump on the repository, in preparation for the next release
|
||||
|
||||
|
||||
## Notes/others
|
||||
|
||||
_Enter any relevant notes here_
|
||||
|
||||
37
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
## Summary
|
||||
|
||||
_[Please provide a summary of the changes in this PR.]_
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
||||
- [ ] I have tested the changes in this PR
|
||||
- [ ] I have opened or referred to an existing github issue related to this change.
|
||||
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
||||
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
||||
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
||||
- [ ] I do not need to add a changelog entry. Reason: _[Please provide a reason]_
|
||||
- [ ] I have added appropriate `Closes:` or `Fixes:` tags in the commit messages wherever applicable, or made sure those are not needed. See [Submitting patches](https://github.com/damus-io/damus/blob/master/docs/CONTRIBUTING.md#submitting-patches)
|
||||
|
||||
## Test report
|
||||
|
||||
_Please provide a test report for the changes in this PR. You can use the template below, but feel free to modify it as needed._
|
||||
|
||||
**Device:** _[Please specify the device you used for testing]_
|
||||
|
||||
**iOS:** _[Please specify the iOS version you used for testing]_
|
||||
|
||||
**Damus:** _[Please specify the Damus version or commit hash you used for testing]_
|
||||
|
||||
**Setup:** _[Please provide a brief description of the setup you used for testing, if applicable]_
|
||||
|
||||
**Steps:** _[Please provide a list of steps you took to test the changes in this PR]_
|
||||
|
||||
**Results:**
|
||||
- [ ] PASS
|
||||
- [ ] Partial PASS
|
||||
- Details: _[Please provide details of the partial pass]_
|
||||
|
||||
## Other notes
|
||||
|
||||
_[Please provide any other information that you think is relevant to this PR.]_
|
||||
1
.gitignore
vendored
@@ -6,3 +6,4 @@ damus.xcodeproj/xcshareddata/xcbaselines
|
||||
TODO.bak
|
||||
tags
|
||||
build-git-hash.txt
|
||||
.build
|
||||
|
||||
5
ACKNOWLEDGEMENTS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### Acknowledgements and licenses
|
||||
|
||||
1. This product contains code derived from [Nostr SDK iOS](https://github.com/nostr-sdk/nostr-sdk-ios). [License](https://github.com/nostr-sdk/nostr-sdk-ios/blob/40df800c6749d7ce0b6fd7328e76cbc0dc71c87b/LICENSE)
|
||||
2. This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/). [License](https://github.com/krzyzanowskim/CryptoSwift/blob/e74bbbfbef939224b242ae7c342a90e60b88b5ce/LICENSE)
|
||||
|
||||
340
CHANGELOG.md
@@ -1,3 +1,343 @@
|
||||
## [1.14] - 2025-05-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added safety reminder to wallets with higher balance (Daniel D’Aquino)
|
||||
- Added one-click Coinos wallet setup (Daniel D’Aquino)
|
||||
- Add notification setting to hide hellthreads (Terry Yiu)
|
||||
- Added separated first aid option for relay lists that does not need a contact list reset (Daniel D’Aquino)
|
||||
- Added NIP-65 relay list support (Daniel D’Aquino)
|
||||
- Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker (Terry Yiu)
|
||||
- Added a search interface to the settings screen (SanjaySiddharth)
|
||||
- Added view introducing users to Zaps (ericholguin)
|
||||
- Added new wallet view with balance and transactions list (ericholguin)
|
||||
- Added copy technical info button to user visible errors, so that users can more easily share errors with developers (Daniel D’Aquino)
|
||||
- Add dismiss button to wallet high balance reminders (Daniel D’Aquino)
|
||||
- Zap receiver information now included for outgoing zaps (Daniel D’Aquino)
|
||||
- Added inline note rendering of invoices to pull up wallet selector sheet (Terry Yiu)
|
||||
- Added route to profile page from wallet tx list (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added additional information on top of blurred images (SanjaySiddharth)
|
||||
- Improved robustness of relay list handling (Daniel D’Aquino)
|
||||
- Updated image cache for better stability (Daniel D’Aquino)
|
||||
- Improved integration with Nostr Wallet Connect wallets (ericholguin)
|
||||
- Added relay connectivity information to NWC settings (Daniel D’Aquino)
|
||||
- Improved handling around NWC responses (Daniel D’Aquino)
|
||||
- Added more human visible errors on NWC wallets to aid with troubleshooting (Daniel D’Aquino)
|
||||
- Re-enabled note zaps as permitted by the new App Store guidelines (Daniel D’Aquino)
|
||||
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hide future notes from timeline (Terry Yiu)
|
||||
- Fixed issue where profiles with a NIP-65 relay list would not display on Damus (Daniel D’Aquino)
|
||||
- Fix quote notes to include missing q tag (Terry Yiu)
|
||||
- Fixed issue where the side menu would close when copying the npub (SanjaySiddharth)
|
||||
- Fixed issue where cached images would be backed up to iCloud (Daniel D’Aquino)
|
||||
- Optimized classify_url function (Terry Yiu)
|
||||
- Fixed note rendering for those that contain previewable items or leading and trailing whitespaces (Terry Yiu)
|
||||
- Fixed issue where some videos would become unplayable after some time using the app (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.14]: https://github.com/damus-io/damus/releases/tag/v1.14
|
||||
|
||||
|
||||
## [1.13.1] - 2025-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where threads would not load properly (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.13.1]: https://github.com/damus-io/damus/releases/tag/v1.13.1
|
||||
|
||||
|
||||
## [1.13] - 2025-03-14
|
||||
|
||||
### Added
|
||||
|
||||
- Added local persistence of note drafts (Daniel D’Aquino)
|
||||
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel D’Aquino)
|
||||
- Coinos connection button in Wallet view (ericholguin)
|
||||
- Added Alby Go to mobile wallets selection menu (Tomek ⚡ K)
|
||||
- Minor accessibility improvements around picture editing and onboarding (Daniel D’Aquino)
|
||||
- Profile image cropping tools (Daniel D’Aquino)
|
||||
- Added Conversations tab to profiles (Terry Yiu)
|
||||
- Added profile pictures to push notifications (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Don't show reposts for the same note more than once in your home feed (William Casarin)
|
||||
- Improved profile image bandwidth optimization (Daniel D’Aquino)
|
||||
- Improved reliability of picture selector (Daniel D’Aquino)
|
||||
- Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes (Terry Yiu)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where some push notifications would not open in the app and leave users confused (Daniel D’Aquino)
|
||||
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel D’Aquino)
|
||||
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel D’Aquino)
|
||||
- Trim whitespaces from Lightning addresses (Terry Yiu)
|
||||
- Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst (Terry Yiu)
|
||||
- Fixed issue where users continue to receive push notifications after logout (Daniel D’Aquino)
|
||||
- Fixed an issue where events on a thread view would occasionally disappear (Daniel D’Aquino)
|
||||
- Improved robustness of the URL handler (Daniel D’Aquino)
|
||||
- Translate notes even if they are in a preferred language but not the current language as that is what users expect (Terry Yiu)
|
||||
- Cancel ongoing uploading operations after the user cancels the post (Swift Coder)
|
||||
- Fixed link and photo sharing support on macOS (Swift Coder)
|
||||
- Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs (Terry Yiu)
|
||||
- Fixed reposts banner to be localizable (Terry Yiu)
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed language filtering from Universe feed because language detection can be inaccurate (Terry Yiu)
|
||||
- Removed mystery tabs meant to fix tab switching bug that no longer exists (Terry Yiu)
|
||||
|
||||
|
||||
|
||||
[1.13](https://github.com/damus-io/damus/releases/tag/v1.13): https://github.com/damus-io/damus/releases/tag/v1.13
|
||||
|
||||
|
||||
## [1.12.3] - 2025-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- Purple members who have been active for more than a year now get a special badge (Daniel D’Aquino)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved clarity of the mute button to indicate it can be used for blocking a user (Daniel D’Aquino)
|
||||
- Made the microphone access request message more clear to users (Daniel D’Aquino)
|
||||
|
||||
[v1.12.3]: https://github.com/damus-io/damus/releases/tag/v1.12.3
|
||||
|
||||
|
||||
## [1.12](https://github.com/damus-io/damus/releases/tag/v1.12) - 2024-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- Render Gif and video files while composing posts (Swift Coder)
|
||||
- Add profile info text in stretchable banner with follow button (Swift Coder)
|
||||
- Paste Gif image similar to jpeg and png files (Swift Coder)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved UX around the label for searching words (Daniel D’Aquino)
|
||||
- Improved accessibility support on some elements (Daniel D’Aquino)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where the "next" button would appear hidden and hard to click on the create account view (Daniel D’Aquino)
|
||||
- Fix non scrollable wallet screen (Swift Coder)
|
||||
- Fixed suggested users category titles to be localizable (Terry Yiu)
|
||||
- Fixed GradientFollowButton to have consistent width and autoscale text limited to 1 line (Terry Yiu)
|
||||
- Fixed right-to-left localization issues (Terry Yiu)
|
||||
- Fixed AddMuteItemView to trim leading and trailing whitespaces from mute text and disallow adding text with only whitespaces (Terry Yiu)
|
||||
- Fixed SideMenuView text to autoscale and limit to 1 line (Terry Yiu)
|
||||
- Fixed an issue where a profile would need to be input twice in the search to be found (Daniel D’Aquino)
|
||||
- Fixed non-breaking spaces in localized strings (Terry Yiu)
|
||||
- Fixed localization issue on Add mute item button (Terry Yiu)
|
||||
- Replace non-breaking spaces with regular spaces as Apple's NSLocalizedString macro does not seem to work with it (Terry Yiu)
|
||||
- Fixed localization issues in RelayConfigView (Terry Yiu)
|
||||
- Fix duplicate uploads (Swift Coder)
|
||||
- Remove duplicate pubkey from Follow Suggestion list (Swift Coder)
|
||||
- Fix Page control indicator (Swift Coder)
|
||||
- Fix damus sharing issues (Swift Coder)
|
||||
- Fixed issue where banner edit button is unclickable (Daniel D’Aquino)
|
||||
- Handle empty notification pages by displaying suitable text (Swift Coder)
|
||||
|
||||
[v1.12](https://github.com/damus-io/damus/releases/tag/v1.12): [https://github.com/damus-io/damus/releases/tag/v1.12]
|
||||
|
||||
|
||||
## [v1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10) - 2024-11-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add Damus Share Feature (Swift)
|
||||
- Added new easy to use video controls for full screen video (Daniel D’Aquino)
|
||||
- Add Edit, Share, and Tap-gesture in Profile pic image viewer (Swift Coder)
|
||||
- Disappearing header, tabbar, and post button on scroll (ericholguin)
|
||||
- Add Apple translation popovers for notes for iOS 17.4+ and macOS 14.4+ (Terry Yiu)
|
||||
- Added NDB search functionality to the universe view (ericholguin)
|
||||
- Added mute button to ProfileActionSheet (chungwwei)
|
||||
- Added mute action to selected text menu (ericholguin)
|
||||
- Added support for pasting images from the clipboard to the post composer (Swift Coder)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved image carousel image fill behavior (Daniel D’Aquino)
|
||||
- Improved video syncing and bandwidth usage when switching between timeline video and full screen mode (Daniel D’Aquino)
|
||||
- Swipe to dismiss on full screen carousel now shows an opacity effect for improved UX (Daniel D’Aquino)
|
||||
- Removed event contents from full screen media carousel for cleaner view (Daniel D’Aquino)
|
||||
- Add share button for images on full screen image carousel view (Swift)
|
||||
- Changed boldness of font in side menu labels. (ericholguin)
|
||||
- Changed search notes button with searched keyword (ericholguin)
|
||||
- Changed opacity of tabbar and post button (ericholguin)
|
||||
- Allow multiple images to be uploaded at the same time (swiftcoder) (William Casarin)
|
||||
- Changed side menu design (ericholguin)
|
||||
- Truncate fulltext search results (William Casarin)
|
||||
- Expanded profile search results to 128 (William Casarin)
|
||||
- Expand nostrdb text search results to 128 items (William Casarin)
|
||||
- Use LazyVStack in text search results (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed missing tab bar on navigation (Swift Coder)
|
||||
- Fixed some issues where QR code would not work, and improved UX (Daniel D’Aquino)
|
||||
- Fixed iOS 18 gesture issues that would take user to the thread view when clicking on a video or unmuting it (Daniel D’Aquino)
|
||||
- Fixed several issues that would cause video to automatically play or pause incorrectly (Daniel D’Aquino)
|
||||
- Fixed issue where full screen video would disappear when going to landscape mode (Daniel D’Aquino)
|
||||
- Fixed portrait video size on full screen carousel (Daniel D’Aquino)
|
||||
- Fix avatar image on qrcode view (Swift Coder)
|
||||
- Fix banner image upload (Swift Coder)
|
||||
- Fix dismiss button visibility (Swift Coder)
|
||||
- Fix quote repost counting (William Casarin)
|
||||
- Fixed overlapping text in Universe View (ericholguin)
|
||||
- Fixed localization issues and exported strings (Terry Yiu)
|
||||
- Fix sensitive long-press gesture on event chat bubble in iOS 18 (Daniel D’Aquino)
|
||||
- Fixed bottom padding for tabbar (ericholguin)
|
||||
- Fixed localization build failures (Terry Yiu)
|
||||
- Fixed back nav button placement in profile edit view (ericholguin)
|
||||
- Friend profiles will now more likely show up in profile search (William Casarin)
|
||||
- Fix broken QR code scanner and fix landscape mode (Terry Yiu)
|
||||
|
||||
[1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10): https://github.com/damus-io/damus/releases/tag/v1.11-10
|
||||
|
||||
## [1.10.1] - 2024-09-22
|
||||
|
||||
### Added
|
||||
|
||||
- Push notification support (Daniel D’Aquino)
|
||||
- Added profile edit safe guards (Eric Holguin)
|
||||
- Tor relay icon (ericholguin)
|
||||
- Add highlighter for web pages (Daniel D’Aquino)
|
||||
- Add support for adding comments when creating a highlight (Daniel D’Aquino)
|
||||
- Add support for rendering highlights with comments (Daniel D’Aquino)
|
||||
- Ability to create highlights (ericholguin)
|
||||
- Highlights (NIP-84) (ericholguin)
|
||||
- Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve notification view filtering UX (Daniel D’Aquino)
|
||||
- Improve visibility of friends filter button (Daniel D’Aquino)
|
||||
- Changed the default banner from ostriches to damoose (Eric Holguin)
|
||||
- Changed image and banner url text fields to new sheet view (Eric Holguin)
|
||||
- Onboarding design (ericholguin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix items that became unclickable on iOS 18 (Daniel D’Aquino)
|
||||
- Fix many reconnection issues (William Casarin)
|
||||
- Fixed issue where theme would be changed to black and can't be switched back on iOS 18 (cr0bar)
|
||||
- Fixed some scenarios where the contact list would never be saved locally and cause issues when switching relays. (Daniel D’Aquino)
|
||||
- Fix albyhub zaps not appearing (William Casarin)
|
||||
- Fix inadvertent escape from mention suggestion menu when typing a space character (Daniel D’Aquino)
|
||||
- Fix profile view toolbar alignment bug in iOS 18 (Terry Yiu)
|
||||
- Create Account model now uses correct metadata (ericholguin)
|
||||
- Restore localization for custom tabs (William Casarin)
|
||||
- Fix iOS 18 reflection runtime error for custom picker (William Casarin)
|
||||
|
||||
|
||||
[1.10.1]: https://github.com/damus-io/damus/releases/tag/v1.10.1
|
||||
|
||||
|
||||
## [1.9.1 (4)] - 2024-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when viewing notes with invalid image dimension metadata (Daniel D’Aquino)
|
||||
|
||||
[1.9.1 (4)]: https://github.com/damus-io/damus/releases/tag/v1.9.1-4
|
||||
|
||||
|
||||
## [1.9 (14)] - 2024-07-14
|
||||
|
||||
### Added
|
||||
|
||||
- Completely new threads experience that is easier and more pleasant to use (Daniel D’Aquino)
|
||||
- Add emoji search to emoji picker (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added first aid contact damus support email (alltheseas)
|
||||
- Disable mutiny wallet button (William Casarin)
|
||||
- Make friends show up first when searching for profiles (Terry Yiu)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash on profile page when there are profile updates (William Casarin)
|
||||
- Fix crash when adding duplicate mute items (William Casarin)
|
||||
- Fix pretty bad crash when building flatbuffer profiles (William Casarin)
|
||||
- Fix reactions view to not show reactions from replies on parent note (Terry Yiu)
|
||||
- Fix missing Mute button in profile view menu (Terry Yiu)
|
||||
- Fixed wallet not disconnecting when a user logs out (ericholguin)
|
||||
- Fix stale feed issue when follow list is too big (Daniel D’Aquino)
|
||||
|
||||
[1.9 (14)]: https://github.com/damus-io/damus/releases/tag/v1.9-14
|
||||
|
||||
## [1.8] - 2024-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added nip10 marker replies (William Casarin)
|
||||
- Add marker nip10 support when reading notes (William Casarin)
|
||||
- Added title image and tags to longform events (ericholguin)
|
||||
- Add First Aid solution for users who do not have a contact list created for their account (Daniel D’Aquino)
|
||||
- Relay fees metadata (ericholguin)
|
||||
- Added callbackuri for a better ux when connecting mutiny wallet nwc (ericholguin)
|
||||
- Add event content preview to the full screen carousel (Daniel D’Aquino)
|
||||
- Show list of quoted reposts in threads (William Casarin)
|
||||
- Proxy Tags are now viewable on Selected Events (ericholguin)
|
||||
- Connect to Mutiny Wallet Button (ericholguin)
|
||||
- Add ability to mute words, add new mutelist interface (Charlie) (William Casarin)
|
||||
- Add ability to mute hashtag from SearchView (Charlie Fish)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Change reactions to use a native looking emoji picker (Terry Yiu)
|
||||
- Relay detail design (ericholguin)
|
||||
- Updated Zeus logo (ericholguin)
|
||||
- Improve UX around video playback (Daniel D’Aquino)
|
||||
- Moved paste nwc button to main wallet view (ericholguin)
|
||||
- Errors with an NWC will show as an alert (ericholguin)
|
||||
- Relay config view user interface (ericholguin)
|
||||
- Always strip GPS data from images (kernelkind)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix thread bug where a quote isn't picked up as a reply (William Casarin)
|
||||
- Fixed threads not loading sometimes (William Casarin)
|
||||
- Fixed issue where some replies were including the q tag (William Casarin)
|
||||
- Fixed issue where timeline was scrolling when it isn't supposed to (William Casarin)
|
||||
- Fix issue where bootstrap relays would inadvertently be added to the user's list on connectivity issues (Daniel D’Aquino)
|
||||
- Fix broken GIF uploads (Daniel D’Aquino)
|
||||
- Fix ghost notifications caused by Purple impending expiration notifications (Daniel D’Aquino)
|
||||
- Improve reliability of contact list creation during onboarding (Daniel D’Aquino)
|
||||
- Fix emoji reactions being cut off (ericholguin)
|
||||
- Fix image indicators to limit number of dots to not spill screen beyond visible margins (ericholguin)
|
||||
- Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them. (Daniel D’Aquino)
|
||||
- Issue where NWC Scanner view would not dismiss after a failed scan/paste (ericholguin)
|
||||
|
||||
|
||||
|
||||
[1.8]: https://github.com/damus-io/damus/releases/tag/v1.8
|
||||
|
||||
## [1.7-rc2] - 2024-02-28
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
@@ -10,5 +14,9 @@
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
||||
let lnurls: LNUrls
|
||||
|
||||
init?() {
|
||||
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
|
||||
guard let ndb = Ndb(owns_db_file: false) else { return nil }
|
||||
self.ndb = ndb
|
||||
|
||||
guard let keypair = get_saved_keypair() else { return nil }
|
||||
|
||||
@@ -55,6 +55,9 @@ struct NotificationFormatter {
|
||||
var identifier = ""
|
||||
|
||||
switch notify.type {
|
||||
case .tagged:
|
||||
title = String(format: NSLocalizedString("Tagged by %@", comment: "Tagged by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
case .mention:
|
||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
@@ -70,6 +73,9 @@ struct NotificationFormatter {
|
||||
case .zap, .profile_zap:
|
||||
// not handled here. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
||||
return nil
|
||||
case .reply:
|
||||
title = String(format: NSLocalizedString("%@ replied to your note", comment: "Heading for local notification indicating a new reply"), displayName)
|
||||
identifier = "myReplyNotification"
|
||||
}
|
||||
content.title = title
|
||||
content.body = notify.content
|
||||
@@ -87,10 +93,11 @@ struct NotificationFormatter {
|
||||
|
||||
// If it does not work, try async formatting methods
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
|
||||
switch notify.type {
|
||||
case .zap, .profile_zap:
|
||||
guard let zap = await get_zap(from: notify.event, state: state) else {
|
||||
Log.debug("format_message: async get_zap failed", for: .push_notifications)
|
||||
return nil
|
||||
}
|
||||
content.title = Self.zap_notification_title(zap)
|
||||
|
||||
@@ -5,15 +5,32 @@
|
||||
// Created by Daniel D’Aquino on 2023-11-10.
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import ImageIO
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import Intents
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
private func configureKingfisherCache() {
|
||||
guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
|
||||
return
|
||||
}
|
||||
|
||||
let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
|
||||
if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
|
||||
KingfisherManager.shared.cache = cache
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
configureKingfisherCache()
|
||||
|
||||
self.contentHandler = contentHandler
|
||||
|
||||
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
|
||||
@@ -27,9 +44,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// Log that we got a push notification
|
||||
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
|
||||
|
||||
guard let state = NotificationExtensionState(),
|
||||
let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here.
|
||||
else {
|
||||
guard let state = NotificationExtensionState() else {
|
||||
Log.debug("Failed to open nostrdb", for: .push_notifications)
|
||||
|
||||
// Something failed to initialize so let's go for the next best thing
|
||||
guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else {
|
||||
// We cannot format this nostr event. Suppress notification.
|
||||
@@ -39,7 +56,18 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(improved_content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let sender_profile = {
|
||||
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
||||
let profile = txn?.unsafeUnownedValue?.profile
|
||||
let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))!
|
||||
return ProfileBuf(picture: picture,
|
||||
name: profile?.name,
|
||||
display_name: profile?.display_name,
|
||||
nip05: profile?.nip05)
|
||||
}()
|
||||
let sender_pubkey = nostr_event.pubkey
|
||||
|
||||
// Don't show notification details that match mute list.
|
||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||
if state.mutelist_manager.is_event_muted(nostr_event) {
|
||||
@@ -52,25 +80,74 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||
Log.debug("should_display_notification failed", for: .push_notifications)
|
||||
// We should not display notification for this event. Suppress notification.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
||||
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
Task {
|
||||
if let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: display_name, notify: notification_object, state: state) {
|
||||
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
||||
|
||||
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
var options: [AnyHashable: Any] = [:]
|
||||
if let imageSource = CGImageSourceCreateWithURL(sender_profile.picture as CFURL, nil),
|
||||
let uti = CGImageSourceGetType(imageSource) {
|
||||
options[UNNotificationAttachmentOptionsTypeHintKey] = uti
|
||||
}
|
||||
|
||||
let attachment = try UNNotificationAttachment(identifier: sender_profile.picture.absoluteString, url: sender_profile.picture, options: options)
|
||||
improvedContent.attachments = [attachment]
|
||||
} catch {
|
||||
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
||||
}
|
||||
|
||||
let kind = nostr_event.known_kind
|
||||
|
||||
// these aren't supported yet
|
||||
if !(kind == .text || kind == .dm) {
|
||||
contentHandler(improvedContent)
|
||||
return
|
||||
}
|
||||
|
||||
// rich communication notifications for kind1, dms, etc
|
||||
|
||||
let message_intent = await message_intent_from_note(ndb: state.ndb,
|
||||
sender_profile: sender_profile,
|
||||
content: improvedContent.body,
|
||||
note: nostr_event,
|
||||
our_pubkey: state.keypair.pubkey)
|
||||
|
||||
improvedContent.threadIdentifier = nostr_event.thread_id().hex()
|
||||
improvedContent.categoryIdentifier = "COMMUNICATION"
|
||||
|
||||
let interaction = INInteraction(intent: message_intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
do {
|
||||
try await interaction.donate()
|
||||
let updated = try improvedContent.updating(from: message_intent)
|
||||
contentHandler(updated)
|
||||
} catch {
|
||||
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
||||
contentHandler(improvedContent)
|
||||
}
|
||||
}
|
||||
@@ -85,3 +162,162 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ProfileBuf {
|
||||
let picture: URL
|
||||
let name: String?
|
||||
let display_name: String?
|
||||
let nip05: String?
|
||||
}
|
||||
|
||||
func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: String, note: NdbNote, our_pubkey: Pubkey) async -> INSendMessageIntent {
|
||||
let sender_pk = note.pubkey
|
||||
let sender = await profile_to_inperson(name: sender_profile.name,
|
||||
display_name: sender_profile.display_name,
|
||||
picture: sender_profile.picture.absoluteString,
|
||||
nip05: sender_profile.nip05,
|
||||
pubkey: sender_pk,
|
||||
our_pubkey: our_pubkey)
|
||||
|
||||
let conversationIdentifier = note.thread_id().hex()
|
||||
var recipients: [INPerson] = []
|
||||
var pks: [Pubkey] = []
|
||||
let meta = INSendMessageIntentDonationMetadata()
|
||||
|
||||
// gather recipients
|
||||
if let recipient_note_id = note.direct_replies() {
|
||||
let replying_to = ndb.lookup_note(recipient_note_id)
|
||||
if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey {
|
||||
meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
|
||||
|
||||
if replying_to_pk != sender_pk {
|
||||
// we push the actual person being replied to first
|
||||
pks.append(replying_to_pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pubkeys = Array(note.referenced_pubkeys)
|
||||
meta.recipientCount = pubkeys.count
|
||||
if pubkeys.contains(sender_pk) {
|
||||
meta.recipientCount -= 1
|
||||
}
|
||||
|
||||
for pk in pubkeys.prefix(3) {
|
||||
if pk == sender_pk || pks.contains(pk) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !meta.isReplyToCurrentUser && pk == our_pubkey {
|
||||
meta.mentionsCurrentUser = true
|
||||
}
|
||||
|
||||
pks.append(pk)
|
||||
}
|
||||
|
||||
for pk in pks {
|
||||
let recipient = await pubkey_to_inperson(ndb: ndb, pubkey: pk, our_pubkey: our_pubkey)
|
||||
recipients.append(recipient)
|
||||
}
|
||||
|
||||
// we enable default formatting this way
|
||||
var groupName = INSpeakableString(spokenPhrase: "")
|
||||
|
||||
// otherwise we just say its a DM
|
||||
if note.known_kind == .dm {
|
||||
groupName = INSpeakableString(spokenPhrase: "DM")
|
||||
}
|
||||
|
||||
let intent = INSendMessageIntent(recipients: recipients,
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: content,
|
||||
speakableGroupName: groupName,
|
||||
conversationIdentifier: conversationIdentifier,
|
||||
serviceName: "kind\(note.kind)",
|
||||
sender: sender,
|
||||
attachments: nil)
|
||||
intent.donationMetadata = meta
|
||||
|
||||
// this is needed for recipients > 0
|
||||
if let img = sender.image {
|
||||
intent.setImage(img, forParameterNamed: \.speakableGroupName)
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let profile_txn = ndb.lookup_profile(pubkey)
|
||||
let profile = profile_txn?.unsafeUnownedValue?.profile
|
||||
let name = profile?.name
|
||||
let display_name = profile?.display_name
|
||||
let nip05 = profile?.nip05
|
||||
let picture = profile?.picture
|
||||
|
||||
return await profile_to_inperson(name: name,
|
||||
display_name: display_name,
|
||||
picture: picture,
|
||||
nip05: nip05,
|
||||
pubkey: pubkey,
|
||||
our_pubkey: our_pubkey)
|
||||
}
|
||||
|
||||
func fetch_pfp(picture: URL) async throws -> RetrieveImageResult {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: picture)) { result in
|
||||
switch result {
|
||||
case .success(let img):
|
||||
continuation.resume(returning: img)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func profile_to_inperson(name: String?, display_name: String?, picture: String?, nip05: String?, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let npub = pubkey.npub
|
||||
let handle = INPersonHandle(value: npub, type: .unknown)
|
||||
var aliases: [INPersonHandle] = []
|
||||
|
||||
if let nip05 {
|
||||
aliases.append(INPersonHandle(value: nip05, type: .emailAddress))
|
||||
}
|
||||
|
||||
let nostrName = DisplayName(name: name, display_name: display_name, pubkey: pubkey)
|
||||
let nameComponents = nostrName.nameComponents()
|
||||
let displayName = nostrName.displayName
|
||||
let contactIdentifier = npub
|
||||
let customIdentifier = npub
|
||||
let suggestionType = INPersonSuggestionType.socialProfile
|
||||
|
||||
var image: INImage? = nil
|
||||
|
||||
if let picture,
|
||||
let url = URL(string: picture),
|
||||
let img = try? await fetch_pfp(picture: url),
|
||||
let imgdata = img.data()
|
||||
{
|
||||
image = INImage(imageData: imgdata)
|
||||
} else {
|
||||
Log.error("Failed to fetch pfp (%s) for %s", for: .push_notifications, picture ?? "nil", displayName)
|
||||
}
|
||||
|
||||
let person = INPerson(personHandle: handle,
|
||||
nameComponents: nameComponents,
|
||||
displayName: displayName,
|
||||
image: image,
|
||||
contactIdentifier: contactIdentifier,
|
||||
customIdentifier: customIdentifier,
|
||||
isMe: pubkey == our_pubkey,
|
||||
suggestionType: suggestionType
|
||||
)
|
||||
|
||||
return person
|
||||
}
|
||||
|
||||
func robohash(_ pk: Pubkey) -> String {
|
||||
return "https://robohash.org/" + pk.hex()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
]
|
||||
// swift-tools-version: 6.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "damus",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "damus",
|
||||
targets: ["damus"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "damus",
|
||||
dependencies: [
|
||||
.product(name: "secp256k1", package: "secp256k1.swift")
|
||||
],
|
||||
path: "damus"),
|
||||
.testTarget(
|
||||
name: "damusTests",
|
||||
dependencies: ["damus"],
|
||||
path: "damusTests"),
|
||||
]
|
||||
)
|
||||
|
||||
24
README.md
@@ -1,14 +1,30 @@
|
||||
[](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
|
||||
<div align="center">
|
||||
|
||||
# damus
|
||||
<img src="./damus/Assets.xcassets/damus-home.imageset/damus-home@2x.png" alt="Damus Logo" title="Damus logo" width=""/>
|
||||
|
||||
# Damus
|
||||
|
||||
The social network you control
|
||||
|
||||
A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
|
||||
|
||||
<img src="./ss.png" width="50%" height="50%" />
|
||||
[](/LICENSE)
|
||||
|
||||
## Download and Install
|
||||
|
||||
[](https://apps.apple.com/us/app/damus/id1628663131)
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
iOS 16.0+ • macOS 13.0+
|
||||
|
||||
<img src="./demo1.png" width="70%" height="50%" />
|
||||
|
||||
</div>
|
||||
|
||||
[nostr]: https://github.com/fiatjaf/nostr
|
||||
|
||||
## How is Damus better than twitter?
|
||||
## How is Damus better than X/Twitter?
|
||||
There are no toxic algorithms.\
|
||||
You can send or receive zaps (satoshis) without asking for permission.\
|
||||
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
{
|
||||
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
|
||||
"originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/twostraws/CodeScanner.git",
|
||||
"state" : {
|
||||
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cryptoswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
||||
"state" : {
|
||||
"revision" : "e74bbbfbef939224b242ae7c342a90e60b88b5ce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojikit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiKit",
|
||||
"state" : {
|
||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
||||
"version" : "0.1.2"
|
||||
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -15,8 +31,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||
"state" : {
|
||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
||||
"version" : "0.1.1"
|
||||
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "faviconfinder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state" : {
|
||||
"revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c",
|
||||
"version" : "5.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,8 +58,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||
"version" : "7.6.1"
|
||||
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
|
||||
"version" : "8.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -89,13 +114,29 @@
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "bba848db50462894e7fc0891d018dfecad4ef11e",
|
||||
"version" : "2.8.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftycrop",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/benedom/SwiftyCrop",
|
||||
"state" : {
|
||||
"revision" : "454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swipeactions",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/aheze/SwipeActions",
|
||||
"location" : "https://github.com/damus-io/SwipeActions.git",
|
||||
"state" : {
|
||||
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
|
||||
"version" : "1.1.0"
|
||||
"revision" : "33d99756c3112e1a07c1732e3cddc5ad5bd0c5f4"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D703D7162C66E47100A400EA"
|
||||
BuildableName = "HighlighterActionExtension.appex"
|
||||
BlueprintName = "HighlighterActionExtension"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<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"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.apple.mobilesafari"
|
||||
RemotePath = "/Library/Developer/CoreSimulator/Volumes/iOS_21F79/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.5.simruntime/Contents/Resources/RuntimeRoot/Applications/MobileSafari.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||
BuildableName = "damus.app"
|
||||
BlueprintName = "damus"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<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>
|
||||
@@ -40,7 +40,7 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "YES">
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 511 KiB After Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profile-banner.jpeg",
|
||||
"filename" : "damoose.jpeg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
damus/Assets.xcassets/Illustrations/damoose.imageset/damoose.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
6
damus/Assets.xcassets/Logos/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
23
damus/Assets.xcassets/Logos/alby.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bbw.jpg",
|
||||
"filename" : "blink.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
damus/Assets.xcassets/Logos/blink.imageset/blink.png
vendored
Normal file
|
After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
12
damus/Assets.xcassets/Logos/coinos.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "coinos.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/Logos/coinos.imageset/coinos.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
12
damus/Assets.xcassets/Logos/tor.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tor.svg.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/Logos/tor.imageset/tor.svg.png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 290 KiB |
12
damus/Assets.xcassets/alby-go.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby-go.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/alby-go.imageset/alby-go.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "alby.svg",
|
||||
"idiom": "universal",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "alby.svg"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "alby.svg"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/bbw.imageset/bbw.jpg
vendored
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 216 B |
@@ -12,31 +12,25 @@ let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||
DamusColors.blue
|
||||
]), startPoint: .leading, endPoint: .trailing)
|
||||
|
||||
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
struct CustomPicker<SelectionValue: Hashable>: View {
|
||||
|
||||
let tabs: [(String, SelectionValue)]
|
||||
@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
|
||||
|
||||
ForEach(tabs, id: \.1) { (text, tag) in
|
||||
Button {
|
||||
withAnimation(.spring()) {
|
||||
selection = tag
|
||||
}
|
||||
} label: {
|
||||
text
|
||||
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||
Text(text).padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||
.font(.system(size: 14, weight: .heavy))
|
||||
.tag(tag)
|
||||
}
|
||||
.background(
|
||||
Group {
|
||||
@@ -52,7 +46,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
.accentColor(tag == selection ? textColor() : .gray)
|
||||
}
|
||||
}
|
||||
.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
|
||||
@@ -20,6 +20,7 @@ struct DamusBackground: View {
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center)
|
||||
.ignoresSafeArea()
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// MutinyGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 3/9/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
|
||||
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
|
||||
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
|
||||
|
||||
let MutinyGradient: LinearGradient =
|
||||
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import Combine
|
||||
|
||||
// TODO: all this ShareSheet complexity can be replaced with ShareLink once we update to iOS 16
|
||||
struct ShareSheet: UIViewControllerRepresentable {
|
||||
@@ -95,64 +96,238 @@ enum ImageShape {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `CarouselModel` helps `ImageCarousel` with some state management logic, keeping track of media sizes, and the ideal display size
|
||||
///
|
||||
/// This model is necessary because the state management logic required to keep track of media sizes for each one of the carousel items,
|
||||
/// and the ideal display size at each moment is not a trivial task.
|
||||
///
|
||||
/// The rules for the media fill are as follows:
|
||||
/// 1. The media item should generally have a width that completely fills the width of its parent view
|
||||
/// 2. The height of the carousel should be adjusted accordingly
|
||||
/// 3. The only exception to rules 1 and 2 is when the total height would be 20% larger than the height of the device
|
||||
/// 4. If none of the above can be computed (e.g. due to missing information), default to a reasonable height, where the media item will fit into.
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// The view is has the following state management responsibilities:
|
||||
/// 1. Watching the size of the images (via the `.observe_image_size` modifier)
|
||||
/// 2. Notifying this class of geometry reader changes, by setting `geo_size`
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This class is organized in a way to reduce stateful behavior and the transiency bugs it can cause.
|
||||
///
|
||||
/// This is accomplished through the following pattern:
|
||||
/// 1. The `current_item_fill` is a published property so that any updates instantly re-render the view
|
||||
/// 2. However, `current_item_fill` has a mathematical dependency on other members of this class
|
||||
/// 3. Therefore, the members on which the fill property depends on all have `didSet` observers that will cause the `current_item_fill` to be recalculated and published.
|
||||
///
|
||||
/// This pattern helps ensure that the state is always consistent and that the view is always up-to-date.
|
||||
///
|
||||
/// This class is marked as `@MainActor` since most of its properties are published and should be accessed from the main thread to avoid inconsistent SwiftUI state during renders
|
||||
@MainActor
|
||||
class CarouselModel: ObservableObject {
|
||||
var current_url: URL?
|
||||
var fillHeight: CGFloat
|
||||
var maxHeight: CGFloat
|
||||
var firstImageHeight: CGFloat?
|
||||
// MARK: Immutable object attributes
|
||||
// These are some attributes that are not expected to change throughout the lifecycle of this object
|
||||
// These should not be modified after initialization to avoid state inconsistency
|
||||
|
||||
/// The state of the app
|
||||
let damus_state: DamusState
|
||||
/// All urls in the carousel
|
||||
let urls: [MediaUrl]
|
||||
/// The default fill height for the carousel, if we cannot calculate a more appropriate height
|
||||
/// **Usage note:** Default to this when `current_item_fill` is nil
|
||||
let default_fill_height: CGFloat
|
||||
/// The maximum height for any carousel item
|
||||
let max_height: CGFloat
|
||||
|
||||
|
||||
// MARK: Miscellaneous
|
||||
|
||||
/// Holds items that allows us to cancel video size observers during de-initialization
|
||||
private var all_cancellables: [AnyCancellable] = []
|
||||
|
||||
|
||||
// MARK: State management properties
|
||||
/// Properties relevant to state management.
|
||||
/// These should be made into computed/functional properties when possible to avoid stateful behavior
|
||||
/// When that is not possible (e.g. when dealing with an observed published property), establish its mathematical dependencies,
|
||||
/// and use `didSet` observers to ensure that the state is always re-computed when necessary.
|
||||
|
||||
@Published var open_sheet: Bool
|
||||
@Published var selectedIndex: Int
|
||||
@Published var video_size: CGSize?
|
||||
@Published var image_fill: ImageFill?
|
||||
/// Stores information about the size of each media item in `urls`.
|
||||
/// **Usage note:** The view is responsible for setting the size of image urls
|
||||
var media_size_information: [URL: CGSize] {
|
||||
didSet {
|
||||
guard let current_url else { return }
|
||||
// Upon updating information, update the carousel fill size if the size for the current url has changed
|
||||
if oldValue[current_url] != media_size_information[current_url] {
|
||||
self.refresh_current_item_fill()
|
||||
self.refresh_first_item_height()
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Stores information about the geometry reader
|
||||
/// **Usage note:** The view is responsible for setting this value
|
||||
var geo_size: CGSize? {
|
||||
didSet { self.refresh_current_item_fill() }
|
||||
}
|
||||
/// The index of the currently selected item
|
||||
/// **Usage note:** The view is responsible for setting this value
|
||||
@Published var selectedIndex: Int {
|
||||
didSet { self.refresh_current_item_fill() }
|
||||
}
|
||||
/// The current fill for the media item.
|
||||
/// **Usage note:** This property is read-only and should not be set directly. Update `selectedIndex` to update the current item being viewed.
|
||||
var current_url: URL? {
|
||||
return urls[safe: selectedIndex]?.url
|
||||
}
|
||||
/// Holds the ideal fill dimensions for the current item.
|
||||
/// **Usage note:** This property is automatically updated when other properties are set, and should not be set directly
|
||||
/// **Implementation note:** This property is mathematically dependent on geo_size, media_size_information, and `selectedIndex`,
|
||||
/// and is automatically updated upon changes to these properties.
|
||||
@Published private(set) var current_item_fill: ImageFill?
|
||||
|
||||
/// Holds the ideal fill dimensions for the first item in the carousel.
|
||||
/// This is used to maintain a consistent height for the carousel when swiping between images.
|
||||
/// **Usage note:** This property is automatically updated when other properties are set, and should not be set directly.
|
||||
/// **Implementation note:** This property ensures the carousel maintains a consistent height based on the first image,
|
||||
/// preventing the UI from "jumping" when swiping between images of different aspect ratios.
|
||||
@Published private(set) var first_image_fill: ImageFill?
|
||||
|
||||
|
||||
// MARK: Initialization and de-initialization
|
||||
|
||||
init(image_fill: ImageFill?) {
|
||||
self.current_url = nil
|
||||
self.fillHeight = 350
|
||||
self.maxHeight = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
self.firstImageHeight = nil
|
||||
self.open_sheet = false
|
||||
/// Initializes the `CarouselModel` with the given `DamusState` and `MediaUrl` array
|
||||
init(damus_state: DamusState, urls: [MediaUrl]) {
|
||||
// Immutable object attributes
|
||||
self.damus_state = damus_state
|
||||
self.urls = urls
|
||||
self.default_fill_height = 350
|
||||
self.max_height = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
|
||||
// State management properties
|
||||
self.selectedIndex = 0
|
||||
self.video_size = nil
|
||||
self.image_fill = image_fill
|
||||
self.current_item_fill = nil
|
||||
self.geo_size = nil
|
||||
self.media_size_information = [:]
|
||||
|
||||
// Setup the rest of the state management logic
|
||||
self.observe_video_sizes()
|
||||
Task {
|
||||
self.refresh_current_item_fill()
|
||||
self.refresh_first_item_height()
|
||||
}
|
||||
}
|
||||
|
||||
/// This private function observes the video sizes for all videos
|
||||
private func observe_video_sizes() {
|
||||
for media_url in urls {
|
||||
switch media_url {
|
||||
case .video(let url):
|
||||
let video_player = damus_state.video.get_player(for: url)
|
||||
if let video_size = video_player.video_size {
|
||||
self.media_size_information[url] = video_size // Set the initial size if available
|
||||
}
|
||||
let observer_cancellable = video_player.$video_size.sink(receiveValue: { new_size in
|
||||
self.media_size_information[url] = new_size // Update the size when it changes
|
||||
})
|
||||
all_cancellables.append(observer_cancellable) // Store the cancellable to cancel it later
|
||||
case .image(_):
|
||||
break; // Observing an image size needs to be done on the view directly, through the `.observe_image_size` modifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
for cancellable_item in all_cancellables {
|
||||
cancellable_item.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: State management and logic
|
||||
|
||||
/// This function refreshes the current item fill based on the current state of the model
|
||||
/// **Usage note:** This is private, do not call this directly from outside the class.
|
||||
/// **Implementation note:** This should be called using `didSet` observers on properties that affect the fill
|
||||
private func refresh_current_item_fill() {
|
||||
self.current_item_fill = self.compute_item_fill(url: current_url)
|
||||
}
|
||||
|
||||
/// Computes the image fill properties for a given URL without side effects.
|
||||
/// This is a pure function that calculates the appropriate fill dimensions based on image size and container constraints.
|
||||
/// **Usage note:** This is a helper method used by both `refresh_current_item_fill` and `refresh_first_item_height`.
|
||||
private func compute_item_fill(url: URL?) -> ImageFill? {
|
||||
if let url,
|
||||
let item_size = self.media_size_information[url],
|
||||
let geo_size {
|
||||
return ImageFill.calculate_image_fill(
|
||||
geo_size: geo_size,
|
||||
img_size: item_size,
|
||||
maxHeight: self.max_height,
|
||||
fillHeight: self.default_fill_height
|
||||
)
|
||||
}
|
||||
else {
|
||||
return nil // Not enough information to compute the proper fill. Default to nil
|
||||
}
|
||||
}
|
||||
|
||||
/// This function refreshes the first item height based on the current state of the model
|
||||
/// **Usage note:** This is private, do not call this directly from outside the class.
|
||||
/// **Implementation note:** This should be called using `didSet` observers on properties that affect the height.
|
||||
/// When the first image dimensions change, this ensures the carousel maintains consistent dimensions.
|
||||
private func refresh_first_item_height() {
|
||||
self.first_image_fill = self.compute_first_item_fill()
|
||||
}
|
||||
|
||||
/// Computes the first item fill with no side-effects.
|
||||
/// **Usage note:** Not to be used outside the class. Use the `first_image_fill` property instead.
|
||||
/// **Implementation note:** This retrieves the first URL from the carousel and computes its fill properties
|
||||
/// to establish a consistent height for the entire carousel.
|
||||
private func compute_first_item_fill() -> ImageFill? {
|
||||
guard let first_url = urls[safe: 0] else { return nil }
|
||||
return self.compute_item_fill(url: first_url.url)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Carousel
|
||||
|
||||
/// A carousel that displays images and videos
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - State management logic is mostly handled by `CarouselModel`, as it is complex, and becomes difficult to manage in a view
|
||||
///
|
||||
@MainActor
|
||||
struct ImageCarousel<Content: View>: View {
|
||||
var urls: [MediaUrl]
|
||||
|
||||
/// The event id of the note that this carousel is displaying
|
||||
let evid: NoteId
|
||||
|
||||
let state: DamusState
|
||||
/// The model that holds information and state of this carousel
|
||||
/// This is observed to update the view when the model changes
|
||||
@ObservedObject var model: CarouselModel
|
||||
let content: ((_ dismiss: @escaping (() -> Void)) -> Content)?
|
||||
|
||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
||||
self._model = ObservedObject(initialValue: CarouselModel(damus_state: state, urls: urls))
|
||||
self.content = nil
|
||||
}
|
||||
|
||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl], @ViewBuilder content: @escaping (_ dismiss: @escaping (() -> Void)) -> Content) {
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
||||
self._model = ObservedObject(initialValue: CarouselModel(damus_state: state, urls: urls))
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var filling: Bool {
|
||||
model.image_fill?.filling == true
|
||||
}
|
||||
/// Determines if the image should fill its container.
|
||||
/// Always returns true to ensure images consistently fill the width of the container.
|
||||
/// This simplifies the layout behavior and prevents inconsistent sizing between carousel items.
|
||||
var filling: Bool { true }
|
||||
|
||||
var height: CGFloat {
|
||||
model.firstImageHeight ?? model.image_fill?.height ?? model.fillHeight
|
||||
// Use the first image height (to prevent height from jumping when swiping), then default to the default fill height
|
||||
// This prioritization ensures consistent carousel height regardless of which image is currently displayed
|
||||
model.first_image_fill?.height ?? model.default_fill_height
|
||||
}
|
||||
|
||||
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
||||
@@ -160,7 +335,7 @@ struct ImageCarousel<Content: View>: View {
|
||||
if num_urls > 1 {
|
||||
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
|
||||
Color.clear
|
||||
} else if let meta = state.events.lookup_img_metadata(url: url),
|
||||
} else if let meta = model.damus_state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
@@ -169,12 +344,6 @@ struct ImageCarousel<Content: View>: View {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.model.image_fill == nil, let size = state.video.size_for_url(url) {
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
||||
self.model.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
@@ -183,24 +352,17 @@ struct ImageCarousel<Content: View>: View {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
model.open_sheet = true
|
||||
present(full_screen_item: .full_screen_carousel(urls: model.urls, selectedIndex: $model.selectedIndex))
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video, style: .preview(on_tap: { model.open_sheet = true }))
|
||||
.onChange(of: model.video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.model.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
self.model.firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.model.image_fill = fill
|
||||
}
|
||||
let video_model = model.damus_state.video.get_player(for: url)
|
||||
DamusVideoPlayerView(
|
||||
model: video_model,
|
||||
coordinator: model.damus_state.video,
|
||||
style: .preview(on_tap: {
|
||||
present(full_screen_item: .full_screen_carousel(urls: model.urls, selectedIndex: $model.selectedIndex))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,33 +371,21 @@ struct ImageCarousel<Content: View>: View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.imageContext(.note, disable_animation: model.damus_state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: model.maxHeight, fill: model.fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
self.model.image_fill = fill
|
||||
if index == 0 {
|
||||
self.model.firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.observe_image_size(size_changed: { size in
|
||||
// Observe the image size to update the model when the size changes, so we can calculate the fill
|
||||
model.media_size_information[url] = size
|
||||
})
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: model.urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.kfClickable()
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
@@ -247,26 +397,21 @@ struct ImageCarousel<Content: View>: View {
|
||||
|
||||
var Medias: some View {
|
||||
TabView(selection: $model.selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ForEach(model.urls.indices, id: \.self) { index in
|
||||
GeometryReader { geo in
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
Media(geo: geo, url: model.urls[index], index: index)
|
||||
.onChange(of: geo.size, perform: { new_size in
|
||||
model.geo_size = new_size
|
||||
})
|
||||
.onAppear {
|
||||
model.geo_size = geo.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $model.open_sheet) {
|
||||
if let content {
|
||||
FullScreenCarouselView<Content>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) {
|
||||
content({ // Dismiss closure
|
||||
model.open_sheet = false
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
FullScreenCarouselView<AnyView>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
|
||||
}
|
||||
}
|
||||
.frame(height: height)
|
||||
.clipped() // Prevents content from overflowing the frame, ensuring clean edges in the carousel
|
||||
.onChange(of: model.selectedIndex) { value in
|
||||
model.selectedIndex = value
|
||||
}
|
||||
@@ -274,11 +419,17 @@ struct ImageCarousel<Content: View>: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Medias
|
||||
.onTapGesture { }
|
||||
if #available(iOS 18.0, *) {
|
||||
Medias
|
||||
} else {
|
||||
// An empty tap gesture recognizer is needed on iOS 17 and below to suppress other overlapping tap recognizers
|
||||
// Otherwise it will both open the carousel and go to a note at the same time
|
||||
Medias.onTapGesture { }
|
||||
}
|
||||
|
||||
if urls.count > 1 {
|
||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
||||
|
||||
if model.urls.count > 1 {
|
||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: model.urls.count)
|
||||
.frame(maxWidth: 0, maxHeight: 0)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
@@ -286,27 +437,6 @@ struct ImageCarousel<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Modifier
|
||||
extension KFOptionSetter {
|
||||
/// 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?
|
||||
@@ -343,4 +473,3 @@ struct ImageCarousel_Previews: PreviewProvider {
|
||||
.environmentObject(OrientationTracker())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,26 +94,30 @@ enum OpenWalletError: Error {
|
||||
}
|
||||
|
||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
let url = try getUrlToOpen(invoice: invoice, with: wallet)
|
||||
this_app.open(url)
|
||||
}
|
||||
|
||||
func getUrlToOpen(invoice: String, with wallet: Wallet.Model) throws(OpenWalletError) -> URL {
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
|
||||
return url
|
||||
} else {
|
||||
guard let store_link = wallet.appStoreLink else {
|
||||
throw OpenWalletError.no_wallet_to_open
|
||||
throw .no_wallet_to_open
|
||||
}
|
||||
|
||||
guard let url = URL(string: store_link) else {
|
||||
throw OpenWalletError.store_link_invalid
|
||||
throw .store_link_invalid
|
||||
}
|
||||
|
||||
guard UIApplication.shared.canOpenURL(url) else {
|
||||
throw OpenWalletError.system_cannot_open_store_link
|
||||
guard this_app.canOpenURL(url) else {
|
||||
throw .system_cannot_open_store_link
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
@@ -122,8 +126,3 @@ struct InvoiceView_Previews: PreviewProvider {
|
||||
.frame(width: 300, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet(sheet))
|
||||
}
|
||||
|
||||
@@ -5,27 +5,27 @@
|
||||
// Created by William Casarin on 2023-01-11.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05Badge: View {
|
||||
let nip05: NIP05
|
||||
let pubkey: Pubkey
|
||||
let contacts: Contacts
|
||||
let damus_state: DamusState
|
||||
let show_domain: Bool
|
||||
let profiles: Profiles
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
|
||||
init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) {
|
||||
self.nip05 = nip05
|
||||
self.pubkey = pubkey
|
||||
self.contacts = contacts
|
||||
self.damus_state = damus_state
|
||||
self.show_domain = show_domain
|
||||
self.profiles = profiles
|
||||
self.nip05_domain_favicon = nip05_domain_favicon
|
||||
}
|
||||
|
||||
var nip05_color: Bool {
|
||||
return use_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||
return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
|
||||
}
|
||||
|
||||
var Seal: some View {
|
||||
@@ -44,8 +44,23 @@ struct NIP05Badge: View {
|
||||
}
|
||||
}
|
||||
|
||||
var domainBadge: some View {
|
||||
Group {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var username_matches_nip05: Bool {
|
||||
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -65,14 +80,18 @@ struct NIP05Badge: View {
|
||||
HStack(spacing: 2) {
|
||||
Seal
|
||||
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
}
|
||||
|
||||
if nip05_domain_favicon != nil {
|
||||
domainBadge
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
VStack {
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||