Compare commits
513 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b2870a318 | |||
| 1076e8531c | |||
| 72f2effda4 | |||
| 7566f56858 | |||
| c1f9c9e25e | |||
| 380a52efb3 | |||
| 028c3ba92b | |||
| f80aba33f1 | |||
| bb15c9e2d0 | |||
| 518c80bb1d | |||
| 0067d049e6 | |||
| bf60dd24aa | |||
| 38d9cc4892 | |||
| c4f2f55617 | |||
| 8f73fb85e9 | |||
| 3bd126c11d | |||
| 7e7aba06a6 | |||
| 2bf00f7ddc | |||
| 24b88e4ac0 | |||
| 3df3130395 | |||
| c0c388d1b9 | |||
| 9f27cc61da | |||
| 8fa1987ec0 | |||
| 39eae42b05 | |||
| 4dfbb437f9 | |||
| f132d22308 | |||
| b7dd2ff8b4 | |||
| b6b78591bc | |||
| ec54a0dbce | |||
| 8793912b65 | |||
| 70c430ddc2 | |||
| 815dbba497 | |||
| dc5bac67aa | |||
| 5427fd7860 | |||
| 119c09d730 | |||
| 1da6833c71 | |||
| 4b8cf53731 | |||
| d646ddd91d | |||
| 764719afde | |||
| 75ec7688b1 | |||
| 7fc508603f | |||
| fb2d78fd57 | |||
| 4480132c74 | |||
| 38c0a9d403 | |||
| 4169db33e6 | |||
| ee48072137 | |||
| a3c1a5c731 | |||
| e74f922e8d | |||
| 16cd90f7b7 | |||
| e2ba10d224 | |||
| 459e9359db | |||
| d2a044f958 | |||
| 2fbcd644d0 | |||
| cf8e736f46 | |||
| d4378731ae | |||
| 000344a942 | |||
| bf6abd301c | |||
| 143a1dd39b | |||
| 9b3a8258ce | |||
| 646b8f8736 | |||
| 2528e4acad | |||
| 286d939097 | |||
| ca3d661830 | |||
| 63fee653e8 | |||
| 9da2473976 | |||
| 6d1eeacc49 | |||
| f85748fef9 | |||
| 9f34b33b7e | |||
| 1510f39a8a | |||
| bbbe011482 | |||
| 82ab7a043f | |||
| 798253a50e | |||
| 52432ca068 | |||
| b3f1d8464b | |||
| 87bb62b359 | |||
| 3f914d02cc | |||
| d1db77d0f5 | |||
| 6aa297c1a4 | |||
| f3647e9bc1 | |||
| 5b43c62f2d | |||
| 23ffb15a8d | |||
| adb2ce4846 | |||
| cdee6ca743 | |||
| fe30aa4af2 | |||
| 9943728eab | |||
| 8ae7cf05cc | |||
| a7c944e8ef | |||
| 102339d7e8 | |||
| 9a0ad0c663 | |||
| f86afc08fa | |||
| cd1b328b1b | |||
| 48f2bb1c75 | |||
| d416fe913e | |||
| 7f8744725c | |||
| e5d1b82a9d | |||
| 619cf2e134 | |||
| 28b522f015 | |||
| 39233f261e | |||
| 00f0127caf | |||
| f69b575381 | |||
| 986973a605 | |||
| 0d6b4591f1 | |||
| 2c62749d9b | |||
| 4be4288ef0 | |||
| c7eec167cf | |||
| 7bae956ffa | |||
| a2f59a5b1b | |||
| df56af9b0e | |||
| 83f7f9584f | |||
| a2d440e54f | |||
| 4132e8449b | |||
| ee444416e4 | |||
| 10c12c3c48 | |||
| db3775ae99 | |||
| 393acce884 | |||
| 68fe663730 | |||
| f65a4b0db0 | |||
| cdfb502e6e | |||
| 1a2c83e49b | |||
| e6c7a675a9 | |||
| 69c04f29f4 | |||
| 04c6f9b4fe | |||
| 86ec12a9db | |||
| 72b3111c64 | |||
| 6709c91779 | |||
| bb6e7495f5 | |||
| df17929681 | |||
| e083719ceb | |||
| bfdc69f18c | |||
| e7ae20afb7 | |||
| 229d92055f | |||
| 64c77cfd13 | |||
| 3a63894562 | |||
| 1d272f8b37 | |||
| bac433b640 | |||
| 62f573eac0 | |||
| b3ea62c53c | |||
| b0731503a8 | |||
| 2421c02c24 | |||
| 25e868118d | |||
| 2880044e0e | |||
| 5300404b46 | |||
| d949d58076 | |||
| 997b223e95 | |||
| ba52a97e26 | |||
| cc4c7b5fe9 | |||
| 8e2ebd11fc | |||
| 9cae4da9f4 | |||
| c05d7e99e2 | |||
| 2390599e8f | |||
| 1a4d45fa9c | |||
| 57447e5bf4 | |||
| 8e411daaef | |||
| 183aebf841 | |||
| e3e500ccc2 | |||
| e7a2535ece | |||
| 761e369313 | |||
| 5248275d73 | |||
| cb033279dd | |||
| 41d50d8c28 | |||
| a52c2b4c3c | |||
| b5917cb184 | |||
| 57348472f8 | |||
| 4b6223dc00 | |||
| 5525e45a15 | |||
| 80a2ae60b0 | |||
| d7e95f5d2f | |||
| ca4e5ae5ee | |||
| b673658c0c | |||
| 5c5c130700 | |||
| 2d89ca6c0e | |||
| 806a7c2609 | |||
| 501ce8067d | |||
| 6429f82829 | |||
| fe626218ea | |||
| b62b1bc063 | |||
| d980f36246 | |||
| b469addd29 | |||
| 6923c2a8b7 | |||
| 1d3f32fb99 | |||
| 42a550788a | |||
| b1c68972c9 | |||
| 3978e32d5f | |||
| ba2b5d182e | |||
| bef04fa899 | |||
| 4f8609421c | |||
| 07660c9d44 | |||
| a324dad2ba | |||
| dbaa0f5d49 | |||
| 478721d349 | |||
| a669a23dbc | |||
| cfeb6478cc | |||
| 64539c49c1 | |||
| 0399ae37ec | |||
| 173a411a36 | |||
| 62013a2ea2 | |||
| c82cf4a4c2 | |||
| df42085be6 | |||
| b09d3065ae | |||
| c050f5a9e3 | |||
| 78e6c0eca0 | |||
| da4da45348 | |||
| dc2af86db8 | |||
| 7502004aba | |||
| 2e8678e4c6 | |||
| 97569016fc | |||
| fe72798592 | |||
| 4583c4e028 | |||
| 0b98197a86 | |||
| 0e94a9c33f | |||
| 3dff1fcb4d | |||
| e163286dd4 | |||
| a99e12f12e | |||
| c3dd997e57 | |||
| a730384baf | |||
| 43cf91e877 | |||
| 75bee027e1 | |||
| 5cbf69a8bd | |||
| ecbb3086d8 | |||
| 7476767aa7 | |||
| e5b8987a9d | |||
| 6ca74c21bf | |||
| e0099141aa | |||
| d0491ed202 | |||
| cbc2137ced | |||
| f9ac13ba11 | |||
| b3533c285f | |||
| a636ae6592 | |||
| 69e3ee0aff | |||
| a39a87ba6d | |||
| 5b22d6ac01 | |||
| 7334cd26f8 | |||
| 44555215cf | |||
| 0cc25913c0 | |||
| 004b30b737 | |||
| 632f330b4c | |||
| 666433912f | |||
| db98ce8db7 | |||
| 71dcfae5ff | |||
| 04155f5b23 | |||
| b4058389ec | |||
| 483fa81b74 | |||
| a8d1c4bbbc | |||
| 0a8c2faa74 | |||
| dd3231e70f | |||
| 7ff9c00032 | |||
| 9ed483abf7 | |||
| b9aeaf29a4 | |||
| 65e3f81f36 | |||
| c6641dba31 | |||
| e48d1e0e59 | |||
| d1e5aee84e | |||
| 5cb22d0bed | |||
| d1c6f53d7c | |||
| 6e238f98c0 | |||
| 290274d6c8 | |||
| e1de0239c9 | |||
| bec77d59e8 | |||
| 84f8794d7c | |||
| 4cddf41bf3 | |||
| 125a7e238e | |||
| 468200b717 | |||
| bdfcb99781 | |||
| 38da650861 | |||
| dd006badfc | |||
| 87e4e3fe5b | |||
| af3e38254f | |||
| 70843f54d3 | |||
| bda75b29b4 | |||
| 750830d593 | |||
| 3c0f1a1d2f | |||
| 4253b0ed29 | |||
| 3c9b3f23df | |||
| e0d83608be | |||
| a0301d599b | |||
| 7dcaa0e8d7 | |||
| 129f49bcc7 | |||
| fc3b68c390 | |||
| 52c7df8a15 | |||
| ce1c4dd488 | |||
| fc6a1a3819 | |||
| 69bd6d0e70 | |||
| 6d383d54e8 | |||
| 998c48b1d3 | |||
| 7217d122b5 | |||
| 1c37c5bb3d | |||
| e8f785b558 | |||
| c94d314f6d | |||
| 2672a8f922 | |||
| 8a8d80d692 | |||
| 95698813c6 | |||
| 4001e877b4 | |||
| 99defc6d79 | |||
| a94883089e | |||
| 5ea4aeb75c | |||
| 456d111925 | |||
| 837ae4b38e | |||
| ffbcbf86c3 | |||
| bcda637192 | |||
| 72c7dd6126 | |||
| a2a4b3599f | |||
| 4955a4f16c | |||
| bb1ff4fb11 | |||
| b81f7c9ed3 | |||
| 689cfb6d45 | |||
| 9da3141650 | |||
| e4fe18df2f | |||
| ba80ebac63 | |||
| d4943daa82 | |||
| cde03ec0fe | |||
| 4f6c08f8a2 | |||
| 38e0fc53ad | |||
| 2a30ca5306 | |||
| 4a4ea13bef | |||
| 239bd3f31a | |||
| 831ec05012 | |||
| 0cc0598287 | |||
| 0a5bc618c2 | |||
| 069904f07a | |||
| 03b42c8276 | |||
| 8697cc23be | |||
| 69e1f97e72 | |||
| 3e832af3e4 | |||
| 84b8650fa4 | |||
| 83abb5aa94 | |||
| a12eddb47b | |||
| c87166247c | |||
| 037c8cb41b | |||
| 79de2e1176 | |||
| d4b026a3ad | |||
| 00f383ff2e | |||
| 6f6bb508db | |||
| e2a0672ca5 | |||
| e2a5fe7a79 | |||
| 5d02ae75dc | |||
| 2460bbbc83 | |||
| 084d8d931b | |||
| 6ee4ac1a89 | |||
| 1d07097350 | |||
| 63d6b362c7 | |||
| bfed277ea9 | |||
| 9e8aa2ef3a | |||
| 4bbc0878f7 | |||
| 16a3ba2a9b | |||
| 7c11eb8947 | |||
| 6bdc8d4d9f | |||
| b9048936ba | |||
| b9620f4443 | |||
| f2249fe592 | |||
| fd42a0e8d4 | |||
| 37d52ba35f | |||
| 3037323dc0 | |||
| 5301ef876d | |||
| aa054d8b1a | |||
| 3655790e5f | |||
| 6cca823ed4 | |||
| 18a383edab | |||
| 43da7d628e | |||
| 2fae3ca248 | |||
| d99ada44f5 | |||
| cb0119b9b8 | |||
| dac9ef8e4e | |||
| 528917b90e | |||
| a22db78967 | |||
| 5718510779 | |||
| f877dc7fbe | |||
| df03fb1116 | |||
| 7455b49f8d | |||
| ae00eb0b9c | |||
| b82e434c70 | |||
| 576c9c2c95 | |||
| cef046b3ae | |||
| 18ae6f6044 | |||
| 664f3c01e0 | |||
| 15e82c4e41 | |||
| 397ecf773e | |||
| 45397e7fb8 | |||
| 11aa841241 | |||
| cc1c18d55f | |||
| e3fbd69e6e | |||
| ac756bf266 | |||
| 8e28ff13e9 | |||
| d8b87db784 | |||
| 0b8c6c4a49 | |||
| 9f4f468bf0 | |||
| 7563dff621 | |||
| f782898b62 | |||
| d0601400cd | |||
| d262da39e5 | |||
| 7d617d8399 | |||
| d2b7db18af | |||
| 89c2690254 | |||
| 34945d1c42 | |||
| 43b207c4dc | |||
| 55efb3fdfd | |||
| c4a1ad2e33 | |||
| fd8442c632 | |||
| e0875eb9b9 | |||
| 962ac7d80c | |||
| 5338ee11bc | |||
| 6d2e9a037d | |||
| ac8530bd9a | |||
| f7d11cf124 | |||
| 72d85e5740 | |||
| e57b5721f6 | |||
| 4ba6c72459 | |||
| c33698c662 | |||
| cf4e40c4cf | |||
| 664da505cd | |||
| 573d4e3cfb | |||
| b2dc41f25b | |||
| b3bc0e4957 | |||
| 0e79e5b9cc | |||
| 34c7bfcffb | |||
| fd9fee8f50 | |||
| b14c3ab345 | |||
| 823058e335 | |||
| 60ec6924f3 | |||
| 18fc895fcb | |||
| 42295159a0 | |||
| db408ac30d | |||
| 1ced5689c3 | |||
| 263a803875 | |||
| 58afb8fa0c | |||
| 4aaa19ea1b | |||
| 2f9010cd13 | |||
| 12fcdfcd4f | |||
| 317ab57ed2 | |||
| 52ef67740a | |||
| 68ebd32e15 | |||
| e94aa3c119 | |||
| 4d10fe7cc0 | |||
| 841928783b | |||
| 6e5e1a0846 | |||
| d57f4747a6 | |||
| 94a0077b09 | |||
| f2eb04adff | |||
| d4d5979a35 | |||
| dde6e54657 | |||
| 698a7513b8 | |||
| ea3f5a6779 | |||
| f5fce8e2e7 | |||
| 46b5c01c49 | |||
| dd069329ee | |||
| c1b52b66ff | |||
| 5873e8aa60 | |||
| c582082816 | |||
| 6ddba63ff9 | |||
| 5a7750a91b | |||
| 8c71b7d9b9 | |||
| b5a28c71ad | |||
| ccdd18a863 | |||
| 2244ecad9b | |||
| da2457da9f | |||
| c18b29e7d6 | |||
| 3a954201ce | |||
| c8bc8ee8bf | |||
| 8c3e52ce8c | |||
| 303b8967e9 | |||
| f3debe6c02 | |||
| 374ca7f265 | |||
| 91689e5b90 | |||
| a64eaba45c | |||
| 394a1e7d30 | |||
| d5b1fab1e7 | |||
| 10a1e6e640 | |||
| 84af4d2d8e | |||
| acddff79f0 | |||
| 489707b9b2 | |||
| 33902dbefe | |||
| 1b318a7a52 | |||
| b6a4b38d14 | |||
| a3eb6d52c0 | |||
| d2c537d275 | |||
| 9eefd6600d | |||
| ad034b1641 | |||
| d94860014c | |||
| 33af39ee93 | |||
| 1d56a2193d | |||
| 75905e4652 | |||
| d07b9cde5f | |||
| d8a9cc5a7e | |||
| 863d11352f | |||
| b4cc770cdf | |||
| 901e56a625 | |||
| 479fed34f7 | |||
| 81d7b08aed | |||
| a582b1ea73 | |||
| 1c0b2a09df | |||
| 3a42a1b560 | |||
| db203bf00d | |||
| ffb36af734 | |||
| b399fa8dcc | |||
| 5bba5959f7 | |||
| 2ad65e394e | |||
| 345b20bf5d | |||
| b9fb251b32 | |||
| dd9a9c0df2 | |||
| 115b5f9fbe | |||
| 3ad7dcfeb4 | |||
| 60d107aed2 | |||
| 08d8d45ecb | |||
| c40e8ce1a7 | |||
| 993bf8d2e6 | |||
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 |
@@ -3,4 +3,7 @@
|
|||||||
--ignore-dir=build
|
--ignore-dir=build
|
||||||
--ignore-dir=ios/DerivedData
|
--ignore-dir=ios/DerivedData
|
||||||
--ignore-dir=ios/App/App/public
|
--ignore-dir=ios/App/App/public
|
||||||
|
--ignore-dir=ios/App/Pods
|
||||||
|
--ignore-file=match:.svg
|
||||||
|
--ignore-file=match:package-lock.json
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
android
|
||||||
|
ios
|
||||||
|
build
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
|
||||||
VITE_BURROW_URL=
|
|
||||||
VITE_PLATFORM_URL=https://flotilla.social
|
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
|
||||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
|
||||||
VITE_PLATFORM_NAME=Flotilla
|
|
||||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
|
||||||
VITE_PLATFORM_RELAY=
|
|
||||||
VITE_PLATFORM_ACCENT="#7161FF"
|
|
||||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
|
||||||
VITE_GLITCHTIP_API_KEY=
|
|
||||||
GLITCHTIP_AUTH_TOKEN=
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
|
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||||
|
VITE_POMADE_SIGNERS=
|
||||||
|
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||||
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
|
VITE_PLATFORM_NAME=Flotilla
|
||||||
|
VITE_PLATFORM_LOGO=static/logo.png
|
||||||
|
VITE_PLATFORM_RELAYS=
|
||||||
|
VITE_PLATFORM_ACCENT="#7161FF"
|
||||||
|
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||||
|
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||||
|
VITE_PUSH_SERVER=https://nps.flotilla.social/
|
||||||
|
VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
||||||
|
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
||||||
|
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||||
|
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||||
|
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
||||||
|
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||||
|
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||||
|
VITE_GLITCHTIP_API_KEY=
|
||||||
|
GLITCHTIP_AUTH_TOKEN=
|
||||||
@@ -1 +1 @@
|
|||||||
package-lock.json -diff
|
pnpm-lock.yaml -diff
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker-container
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Env
|
# Env
|
||||||
.env.local
|
.env
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
@@ -56,8 +56,11 @@ out/
|
|||||||
.gradle/
|
.gradle/
|
||||||
local.properties
|
local.properties
|
||||||
proguard/
|
proguard/
|
||||||
|
google-services.json
|
||||||
|
GoogleService-Info.plist
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
|
.roo
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
npm run lint
|
pnpm run lint
|
||||||
npm run check
|
pnpm run check
|
||||||
|
|
||||||
|
if [[ ! -z $(cat package.json | grep 'link:') ]]; then
|
||||||
|
echo "Some packages are linked to local files!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
"svelteSortOrder": "options-styles-scripts-markup",
|
"svelteSortOrder": "options-styles-scripts-markup",
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"bracketSpacing": false,
|
"bracketSpacing": false,
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-svelte"],
|
||||||
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-29 (relay-based groups) to create Discord-like spaces (servers) and rooms (channels).
|
||||||
|
|
||||||
|
**Tech Stack:**
|
||||||
|
|
||||||
|
- SvelteKit 5.48+ with TypeScript 5.9+
|
||||||
|
- Capacitor for cross-platform (Web/PWA, Android, iOS)
|
||||||
|
- TailwindCSS + DaisyUI for styling
|
||||||
|
- Welshman library suite for Nostr protocol
|
||||||
|
- IndexedDB for local storage
|
||||||
|
- Vite for building
|
||||||
|
|
||||||
|
**Key Concepts:**
|
||||||
|
|
||||||
|
- **Spaces** - Relays used as community groups (like Discord servers)
|
||||||
|
- **Rooms** - NIP-29 groups within spaces (like Discord channels), identified by `h`
|
||||||
|
- **Chats** - Direct message conversations (NIP-04/NIP-44 encrypted)
|
||||||
|
|
||||||
|
## Architecture & Dependency Graph
|
||||||
|
|
||||||
|
The project follows a **strict acyclic dependency hierarchy**:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/ (top layer - can depend on anything)
|
||||||
|
↓
|
||||||
|
app/components/ (can depend on app/* and lib/*)
|
||||||
|
↓
|
||||||
|
app/core/ & app/util/ (can only depend on lib/*)
|
||||||
|
↓
|
||||||
|
lib/ (can only depend on external libraries)
|
||||||
|
↓
|
||||||
|
external libraries (bottom layer)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import Ordering Convention (CRITICAL):**
|
||||||
|
Always sort imports by dependency level:
|
||||||
|
|
||||||
|
1. Third-party libraries first
|
||||||
|
2. Then `lib/` imports
|
||||||
|
3. Then `app/` imports
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {throttle} from "throttle-debounce"
|
||||||
|
import {Dialog} from "$lib/components"
|
||||||
|
import {repository} from "$app/core/state"
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── lib/ # Generic reusable code
|
||||||
|
│ ├── components/ # 38 UI components (Button, Dialog, etc.)
|
||||||
|
│ ├── html.ts # DOM utilities
|
||||||
|
│ ├── indexeddb.ts # IndexedDB helpers
|
||||||
|
│ └── util.ts # Generic utilities
|
||||||
|
│
|
||||||
|
├── app/
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── state.ts # State management, stores, constants (687 lines)
|
||||||
|
│ │ ├── commands.ts # Publishing events and other write operations (440+ lines)
|
||||||
|
│ │ ├── requests.ts # Loading data from network (191 lines)
|
||||||
|
│ │ ├── sync.ts # Data synchronization (296 lines)
|
||||||
|
│ │ └── storage.ts # IndexedDB setup
|
||||||
|
│ │
|
||||||
|
│ ├── util/
|
||||||
|
│ │ ├── notifications.ts # Push notifications (731 lines)
|
||||||
|
│ │ ├── policies.ts # Relay policies
|
||||||
|
│ │ ├── routes.ts # Routing helpers
|
||||||
|
│ │ ├── modal.ts # Modal management
|
||||||
|
│ │ ├── toast.ts # Toast notifications
|
||||||
|
│ │ ├── theme.ts # Theme switching
|
||||||
|
│ │ └── keyboard.ts # Keyboard handling
|
||||||
|
│ │
|
||||||
|
│ ├── editor/ # Rich text editor config
|
||||||
|
│ │ ├── index.ts # TipTap setup with Nostr integration
|
||||||
|
│ │ ├── EditorContent.svelte
|
||||||
|
│ │ └── MentionNodeView.ts
|
||||||
|
│ │
|
||||||
|
│ └── components/ # 188 app-specific components
|
||||||
|
│ ├── Space*.svelte # Space/relay management
|
||||||
|
│ ├── Room*.svelte # Room/channel management
|
||||||
|
│ ├── Chat*.svelte # Direct messaging
|
||||||
|
│ ├── Profile*.svelte # User profiles
|
||||||
|
│ ├── Thread*.svelte # Threaded posts
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── routes/ # SvelteKit file-based routing
|
||||||
|
│ ├── +layout.svelte # Root layout (sync logic here)
|
||||||
|
│ ├── spaces/ # Space management
|
||||||
|
│ │ └── [relay]/ # Specific space
|
||||||
|
│ │ ├── chat/ # Space chat
|
||||||
|
│ │ ├── threads/ # Thread posts
|
||||||
|
│ │ ├── calendar/ # Events
|
||||||
|
│ │ └── [h]/ # Specific room (h = room id)
|
||||||
|
│ ├── chat/ # Direct messages
|
||||||
|
│ ├── settings/ # User settings
|
||||||
|
│ └── [bech32]/ # Bech32 entity viewer
|
||||||
|
│
|
||||||
|
├── assets/icons/ # ~1,277 SVG icons
|
||||||
|
├── app.html # HTML template
|
||||||
|
├── app.css # Global styles
|
||||||
|
└── types.d.ts # Type definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
**Core Principles:**
|
||||||
|
|
||||||
|
- Use Svelte 4 **stores** for all state (NOT runes outside UI components)
|
||||||
|
- Most global state flows through Welshman's `repository` (unidirectional)
|
||||||
|
- Query state using `deriveEventsMapped` or `deriveProfile` etc
|
||||||
|
- Update state by publishing events via `publishThunk`
|
||||||
|
|
||||||
|
**Thunks:**
|
||||||
|
|
||||||
|
- Reduce UI latency by handling signatures and sending in background
|
||||||
|
- Return status that should be displayed to user
|
||||||
|
- Allow cancellation and error handling
|
||||||
|
- Immediately publish to local repository for optimistic updates
|
||||||
|
|
||||||
|
## Nostr Integration
|
||||||
|
|
||||||
|
**Welshman Library Suite:**
|
||||||
|
|
||||||
|
- `@welshman/app` - High-level state (pubkey, signer, repository, tracker)
|
||||||
|
- `@welshman/net` - Network layer (Pool, Socket, load, pull, request)
|
||||||
|
- `@welshman/store` - Svelte integration (deriveEventsMapped, etc.)
|
||||||
|
- `@welshman/util` - Event utilities (kinds, tags, validation)
|
||||||
|
- `@welshman/signer` - Signing abstraction (NIP-01, NIP-07, NIP-46)
|
||||||
|
- `@welshman/router` - Relay routing (inbox/outbox model)
|
||||||
|
- `@welshman/editor` - Rich text editor with Nostr
|
||||||
|
- `@welshman/content` - Content parsing
|
||||||
|
- `@welshman/feeds` - Feed management
|
||||||
|
|
||||||
|
**Key NIPs Implemented:**
|
||||||
|
|
||||||
|
- NIP-01: Basic protocol
|
||||||
|
- NIP-44/59/17: Encrypted DMs
|
||||||
|
- NIP-07: Browser extension signing
|
||||||
|
- NIP-19: Bech32 encoding
|
||||||
|
- NIP-29: Relay-based Groups
|
||||||
|
- NIP-42: Relay authentication
|
||||||
|
- NIP-43: Relay membership
|
||||||
|
- NIP-46: Nostr Connect (remote signing)
|
||||||
|
- NIP-57: Lightning Zaps
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
**Component Parameterization:**
|
||||||
|
|
||||||
|
- Only pass entity identifiers (`url` for spaces, `h` for rooms)
|
||||||
|
- Derive all other data inside the component from identifiers
|
||||||
|
- Example: Don't pass `members` prop, derive it from `h` inside component
|
||||||
|
|
||||||
|
**Code Style:**
|
||||||
|
|
||||||
|
- **No `null`** - only use `undefined`
|
||||||
|
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
||||||
|
- TailwindCSS and DaisyUI styling
|
||||||
|
- Only add comments for really weird stuff
|
||||||
|
- Do not call functions in components unless a parameter is reactive. Instead, use a svelte store or rune to make it reactive.
|
||||||
|
- Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect.
|
||||||
|
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
|
||||||
|
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
||||||
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Component
|
||||||
|
|
||||||
|
1. Determine if it's generic (`lib/components/`) or app-specific (`app/components/`)
|
||||||
|
2. Follow naming convention: `PascalCase.svelte`
|
||||||
|
3. Import in dependency order (3rd party → lib → app)
|
||||||
|
4. Use stores for state, runes only for UI reactivity
|
||||||
|
|
||||||
|
### Creating a New Route
|
||||||
|
|
||||||
|
1. Add to `src/routes/` following SvelteKit conventions
|
||||||
|
2. Use `+page.svelte` for page component
|
||||||
|
3. Use `+layout.svelte` for shared layouts
|
||||||
|
4. Top-level sync logic goes in root `+layout.svelte`
|
||||||
|
|
||||||
|
### Loading Data from Network
|
||||||
|
|
||||||
|
1. Use utilities from `app/core/requests.ts`
|
||||||
|
2. Or create derived stores in `app/core/state.ts`
|
||||||
|
3. Use `load`, `pull`, or `request` from `@welshman/net`
|
||||||
|
|
||||||
|
### Publishing Events
|
||||||
|
|
||||||
|
1. Create `make*` function to build event template
|
||||||
|
2. Create `publish*` function using `publishThunk`
|
||||||
|
3. Display thunk status to user (for cancel/error handling)
|
||||||
|
4. These go in in `app/core/commands.ts`
|
||||||
|
|
||||||
|
### Managing Modals/Toasts
|
||||||
|
|
||||||
|
- Import from `app/util/modal.ts` or `app/util/toast.ts`
|
||||||
|
- Pass component objects with parameters
|
||||||
|
- Use `$state.snapshot` if calling component might unmount
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
Agents should not run the dev server or build the app. Instead, use the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run format # Format changed files
|
||||||
|
pnpm run lint # Check formatting and linting
|
||||||
|
pnpm run check # Type check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Welshman Development:**
|
||||||
|
|
||||||
|
- Clone welshman to parent directory
|
||||||
|
- Use `./link_deps` script to link local welshman packages
|
||||||
|
- Avoid committing `pnpm.overrides` changes
|
||||||
|
|
||||||
|
**Git Workflow:**
|
||||||
|
|
||||||
|
- `master` branch auto-deploys to production
|
||||||
|
- Work on feature branches based on `dev` branch
|
||||||
|
- Pre-commit hooks run lint/typecheck automatically
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
See `.env.template` for all options.
|
||||||
|
|
||||||
|
## Important Files to Reference
|
||||||
|
|
||||||
|
- **src/app/core/state.ts** - All stores and constants
|
||||||
|
- **src/app/core/sync.ts** - Data synchronization
|
||||||
|
- **src/app/core/requests.ts** - Utilities for requesting data
|
||||||
|
- **src/app/core/commands.ts** - Publishing patterns
|
||||||
|
- **src/app/util/notifications.ts** - Notification badges and push notifications
|
||||||
|
- **src/routes/+layout.svelte** - Top-level sync logic
|
||||||
|
|
||||||
|
## Mobile Development
|
||||||
|
|
||||||
|
**Capacitor Integration:**
|
||||||
|
|
||||||
|
- Android: Full support, APK builds via `pnpm run release:android`
|
||||||
|
- iOS: Full support (zaps disabled due to App Store policy)
|
||||||
|
- PWA: Progressive Web App with service worker
|
||||||
|
|
||||||
|
**Native Features:**
|
||||||
|
|
||||||
|
- Push notifications (FCM/APNs)
|
||||||
|
- Deep linking (nostr: and https: URLs)
|
||||||
|
- Native signing plugin
|
||||||
|
- Keyboard management
|
||||||
|
- Safe area handling
|
||||||
|
- Badge management
|
||||||
@@ -1,5 +1,299 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.6.4
|
||||||
|
|
||||||
|
* Clean up modal design
|
||||||
|
* Fix overflowing popovers
|
||||||
|
* Use space urls for relay hints
|
||||||
|
* Re-work notification badges
|
||||||
|
* Add push notification support via NIP 9a
|
||||||
|
* Optimistically load messaging relays to avoid unnecessary warning
|
||||||
|
* Recover from indexeddb not being available
|
||||||
|
* Fix safe area inset support
|
||||||
|
* Show space URL in top bar on mobile
|
||||||
|
* Fix calendar detail page
|
||||||
|
* Improve relay synchronization, especially for pyramid and relay29
|
||||||
|
* Improve invite code error handling
|
||||||
|
* Add wallet receive flow
|
||||||
|
* Fix safari image uploads
|
||||||
|
* Re-work recent activity page
|
||||||
|
* Add classified listing content type
|
||||||
|
* Use address for page param for replaceable events
|
||||||
|
* Refine discover page to avoid slowness
|
||||||
|
* Upgrade som dependencies
|
||||||
|
* Tag event author when tagging parent event
|
||||||
|
* Disable macos build
|
||||||
|
* Add room muting
|
||||||
|
|
||||||
|
# 1.6.3
|
||||||
|
|
||||||
|
* Fix scroll down button z index
|
||||||
|
* Hide tooltips on mobile
|
||||||
|
* Sort comments ascending
|
||||||
|
* Make video embeds rounded
|
||||||
|
* Fix ProfileMultiSelect styling
|
||||||
|
* Accept hex pubkeys/npubs/nprofiles in ProfileMultiSelect
|
||||||
|
* Tweak room edit form design
|
||||||
|
* Report pending signer to user
|
||||||
|
* Update default relays
|
||||||
|
* Fix chat list responsiveness
|
||||||
|
* Fix memory leak, notification badge not showing
|
||||||
|
* Improve space join flow
|
||||||
|
* Fix opening images in fullscreen dialog
|
||||||
|
* Add support for blocked relays
|
||||||
|
* Add authentication policy setting
|
||||||
|
* Add login with key if no signer is detected
|
||||||
|
* Publish default relay selections on signup
|
||||||
|
|
||||||
|
# 1.6.2
|
||||||
|
|
||||||
|
* Fix modal scrolling and style
|
||||||
|
|
||||||
|
# 1.6.1
|
||||||
|
|
||||||
|
* Fix skinny profile images
|
||||||
|
* Custom handler for relay urls
|
||||||
|
* Improve time based chat partitioning
|
||||||
|
* Improve authenticated image access interop
|
||||||
|
* Fix image detail dialog
|
||||||
|
* Fix zapper loading
|
||||||
|
* Fix recent events missing in feeds
|
||||||
|
|
||||||
|
# 1.6.0
|
||||||
|
|
||||||
|
* Switch back to indexeddb to fix memory and performance
|
||||||
|
* Add pay invoice functionality
|
||||||
|
* Add space membership management and bans
|
||||||
|
* Add event info to profile dialog
|
||||||
|
* Add better room membership management
|
||||||
|
* Refactor stores for performance
|
||||||
|
* Hide nav when keyboard is open
|
||||||
|
* Handle flotilla links in-app
|
||||||
|
* Fix new messages indicator z-index
|
||||||
|
* Fix some display bugs
|
||||||
|
* Add date to chat items
|
||||||
|
* Refine data synchronization
|
||||||
|
* Hide nav when keyboard is open on mobile
|
||||||
|
|
||||||
|
# 1.5.3
|
||||||
|
|
||||||
|
* Add space edit form
|
||||||
|
* Improve room syncing
|
||||||
|
* Return better blossom errors
|
||||||
|
* Fix access restricted bugs
|
||||||
|
* Add room detail dialog
|
||||||
|
* Fix broken link to self hosting
|
||||||
|
* Tweak shadows
|
||||||
|
* Always join spaces when visiting them
|
||||||
|
|
||||||
|
# 1.5.2
|
||||||
|
|
||||||
|
* Fix negentropy room syncing
|
||||||
|
|
||||||
|
# 1.5.1
|
||||||
|
|
||||||
|
* Fix chat path link
|
||||||
|
|
||||||
|
# 1.5.0
|
||||||
|
|
||||||
|
* Restyle mobile dialogs
|
||||||
|
* Add room membership lists
|
||||||
|
* Add space membership lists
|
||||||
|
* Add edit room form
|
||||||
|
* Support closed/private/restricted/hidden rooms
|
||||||
|
* Add hosting services page
|
||||||
|
* Improve performance and UI
|
||||||
|
* Fix push notifications
|
||||||
|
* Improve error detection and handling
|
||||||
|
* Support invite links on discover page
|
||||||
|
* Add link to landlubber if user is admin
|
||||||
|
* Clear reply/share/edit on escape
|
||||||
|
|
||||||
|
# 1.4.1
|
||||||
|
|
||||||
|
* Improve data synchronization
|
||||||
|
* Fix app url on capacitor deployments
|
||||||
|
|
||||||
|
# 1.4.0
|
||||||
|
|
||||||
|
* Allow "editing" chat messages
|
||||||
|
* Check for room create permission
|
||||||
|
* Re-work space navigation
|
||||||
|
* Show all messages in non-nip29 chat
|
||||||
|
* Improve synchronization logic
|
||||||
|
* Add connection status to space menu
|
||||||
|
* Add icon picker to room create component
|
||||||
|
* Improve mention suggestions
|
||||||
|
* Improve storage adapter and relay list performance
|
||||||
|
* Fix modals
|
||||||
|
* Add room deletion
|
||||||
|
* Fix zapper loading
|
||||||
|
* Add support for relay/group member lists and join/leave events
|
||||||
|
|
||||||
|
# 1.3.1
|
||||||
|
|
||||||
|
* Fix memory leak in storage adapter
|
||||||
|
* Show fewer annoying toast messages
|
||||||
|
|
||||||
|
# 1.3.0
|
||||||
|
|
||||||
|
* Add optional badge and sound for notifications
|
||||||
|
* Improve link rendering
|
||||||
|
* Remove imgproxy
|
||||||
|
* Bring back blossom feature detection for spaces
|
||||||
|
* Improve light theme
|
||||||
|
* Add more info to signer status
|
||||||
|
* Simplify navigation for adding a space
|
||||||
|
* Add ability to scan QR code for invite links
|
||||||
|
* Streamline wallet setup and move receive address setting
|
||||||
|
* Remove indexeddb on mobile, use capacitor file storage API
|
||||||
|
* Fix duplicate DMs showing up
|
||||||
|
|
||||||
|
# 1.2.5
|
||||||
|
|
||||||
|
* Fix icons in build
|
||||||
|
|
||||||
|
# 1.2.4
|
||||||
|
|
||||||
|
* Add direct message alerts
|
||||||
|
* Add alert settings page
|
||||||
|
* Add instructions to key download
|
||||||
|
* Add option that allows relays to strip signatures
|
||||||
|
* Detect relays that mostly refuse to serve requests
|
||||||
|
* Compress and upload profile images
|
||||||
|
* Use system theme by default
|
||||||
|
* Switch icon set, refactor how they're included
|
||||||
|
* Use capacitor's preferences for storage instead of localStorage
|
||||||
|
|
||||||
|
# 1.2.3
|
||||||
|
|
||||||
|
* Add `created_at` to event info dialog
|
||||||
|
* Add signer status to profile page
|
||||||
|
* Re-work bunker login flow
|
||||||
|
* Add in-app onboarding flow
|
||||||
|
* Only protect events if relay authenticates
|
||||||
|
* Filter out non-global chats from global chat
|
||||||
|
* Improve publish status indicator
|
||||||
|
* Fix encrypted upload content type
|
||||||
|
* Add relays to event details dialog
|
||||||
|
* Add universal link handler for apps
|
||||||
|
|
||||||
|
# 1.2.2
|
||||||
|
|
||||||
|
* Fix phantom chat notifications
|
||||||
|
* Fix zaps on mobile
|
||||||
|
|
||||||
|
# 1.2.1
|
||||||
|
|
||||||
|
* Add zaps to chat, threads, and events
|
||||||
|
* Add funding goals
|
||||||
|
* Add NWC support
|
||||||
|
* Add wallet settings page
|
||||||
|
* Handle invalid bunker url
|
||||||
|
* Fix sidebar overflow
|
||||||
|
* Fix profile npub display
|
||||||
|
|
||||||
|
# 1.2.0
|
||||||
|
|
||||||
|
* Fix sort order of thread comments
|
||||||
|
* Fix link display when no title is available
|
||||||
|
* Fix making profiles non-protected
|
||||||
|
* Replace bunker url with relay claims for notifier auth
|
||||||
|
* Add push notifications on all platforms
|
||||||
|
* Add "mark all as read" on desktop
|
||||||
|
* Re-design space dashboard
|
||||||
|
|
||||||
|
# 1.1.1
|
||||||
|
|
||||||
|
* Add chat quick link
|
||||||
|
|
||||||
|
# 1.1.0
|
||||||
|
|
||||||
|
* Add better theming support
|
||||||
|
* Improve forms for entering invite codes
|
||||||
|
* Rely more heavily on NIP 29 for rooms
|
||||||
|
* Support multiple platform relays
|
||||||
|
* Remove default general room
|
||||||
|
* Remove room tag from threads/calendars
|
||||||
|
* Show pubkey on profile detail
|
||||||
|
* Support pasting pubkey into chat start dialog
|
||||||
|
* Add minimal style for quoted messages
|
||||||
|
|
||||||
|
# 1.0.4
|
||||||
|
|
||||||
|
* Fix thunk status click handler
|
||||||
|
* Remove duplicate dependencies
|
||||||
|
* Improve navigation on white-labeled instances
|
||||||
|
* Add setting for font size
|
||||||
|
|
||||||
|
# 1.0.3
|
||||||
|
|
||||||
|
* Add light theme
|
||||||
|
* Use correct alerts server
|
||||||
|
* Ignore relay errors for claims
|
||||||
|
* Fix inline code blocks
|
||||||
|
* Add custom emoji parsing and display
|
||||||
|
|
||||||
|
# 1.0.2
|
||||||
|
|
||||||
|
* Fix add relay button
|
||||||
|
* Fix safe inset areas
|
||||||
|
* Better rendering for errors from relays
|
||||||
|
* Improve remote signer login
|
||||||
|
|
||||||
|
# 1.0.1
|
||||||
|
|
||||||
|
* Fix relay images in nav
|
||||||
|
* Fix relay nav overflow
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
* Add alerts via Anchor
|
||||||
|
* Fix nip46 signer connect
|
||||||
|
* Allow use of cleartext relays on native builds
|
||||||
|
* Fix some modal state bugs caused by svelte 5
|
||||||
|
* Detect blossom support on community relays
|
||||||
|
* Use user blossom server list in settings
|
||||||
|
* Fix some feed bugs
|
||||||
|
* Improve thunk indicator
|
||||||
|
* Update storage adapters
|
||||||
|
* Fix modal flash
|
||||||
|
* Switch to pnpm
|
||||||
|
* Improve calendar windowing
|
||||||
|
|
||||||
|
# 0.2.14
|
||||||
|
|
||||||
|
* Add calendar event editing
|
||||||
|
|
||||||
|
# 0.2.13
|
||||||
|
|
||||||
|
* Fix android keyboard issue
|
||||||
|
|
||||||
|
# 0.2.12
|
||||||
|
|
||||||
|
* Fix keyboard covering chat input
|
||||||
|
* Fix thread replies
|
||||||
|
* Make error reporting and analytics optional
|
||||||
|
* Replace long press with tap target
|
||||||
|
* Fix time input
|
||||||
|
* Fix nevent hints for url-specific stuff
|
||||||
|
* Fix confirm and reactions on mobile
|
||||||
|
* Add reply to chat on mobile
|
||||||
|
* Fix profile suggestions
|
||||||
|
|
||||||
|
# 0.2.11
|
||||||
|
|
||||||
|
* Add in-app signup flow on ios
|
||||||
|
* Add profile deletion
|
||||||
|
|
||||||
|
# 0.2.10
|
||||||
|
|
||||||
|
* Improve space discovery
|
||||||
|
|
||||||
|
# 0.2.9
|
||||||
|
|
||||||
|
* Add NIP 01 signup flow on mobile
|
||||||
|
|
||||||
# 0.2.8
|
# 0.2.8
|
||||||
|
|
||||||
* Show spinner when joining a room
|
* Show spinner when joining a room
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm i
|
||||||
|
|
||||||
|
# Copy the rest of the application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Default to serving the build directory
|
||||||
|
CMD ["npx", "serve", "build"]
|
||||||
|
|
||||||
@@ -2,110 +2,45 @@
|
|||||||
|
|
||||||
A discord-like nostr client based on the idea of "relays as groups".
|
A discord-like nostr client based on the idea of "relays as groups".
|
||||||
|
|
||||||
If you would like to be interoperable with Flotilla, please check out this draft NIP: https://github.com/coracle-social/nips/blob/relay-chat/xx.md
|
If you would like to be interoperable with Flotilla, please check out this guide: https://habla.news/u/hodlbod@coracle.social/1741286140797
|
||||||
|
|
||||||
# Deploy
|
|
||||||
|
|
||||||
To run your own Flotilla, it's as simple as:
|
|
||||||
|
|
||||||
- `npm install`
|
|
||||||
- `npm run build`
|
|
||||||
- `npx serve build`
|
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env` for examples):
|
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
||||||
|
|
||||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust.
|
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||||
- `VITE_PLATFORM_NAME` - The name of the app
|
- `VITE_PLATFORM_NAME` - The name of the app
|
||||||
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
||||||
- `VITE_PLATFORM_RELAY` - A relay url that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the platform relay the home page.
|
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
||||||
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
|
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
|
||||||
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
|
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
|
||||||
- `VITE_GLITCHTIP_API_KEY` - A Sentry DSN for use with glitchtip (error reporting)
|
|
||||||
- `GLITCHTIP_AUTH_TOKEN` - A glitchtip auth token for error reporting
|
|
||||||
|
|
||||||
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
|
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
|
||||||
|
|
||||||
## Nginx/TLS (optional)
|
## Development
|
||||||
|
|
||||||
If you'd like to set up flotilla on a server you control, you'll want to set up a reverse proxy and provision a TSL certificate for the domain you'll be using. You should also make sure to add swap to your server.
|
See [./CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
There will be some parts of the following templates, for example `<SERVER NAME>`, which you'll need to fill in before running the code.
|
## Deployment
|
||||||
|
|
||||||
First, create an `A` record with your DNS provider pointing to the IP of your server. This will allow certbot to create your certificate later.
|
To run your own Flotilla, it's as simple as:
|
||||||
|
|
||||||
Next install `nginx`, `git`, and `certbot`. If you're on a debian- or ubuntu-based distro, run `sudo apt-get update && sudo apt-get install nginx git certbot python3-certbot-nginx`.
|
|
||||||
|
|
||||||
Now, create a new user where your code will be stored, clone the repository, fill in your `.env.local` file, and build the app.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Replace with your password
|
pnpm install
|
||||||
PASSWORD=<YOUR PASSWORD HERE>
|
pnpm run build
|
||||||
|
npx serve build
|
||||||
# Add the user and set a password
|
|
||||||
adduser flotilla
|
|
||||||
echo flotilla:$PASSWORD | chpasswd
|
|
||||||
|
|
||||||
# Login as flotilla
|
|
||||||
sudo su flotilla
|
|
||||||
|
|
||||||
# Go to flotilla's home directory
|
|
||||||
cd ~
|
|
||||||
|
|
||||||
# Install nvm, yarn, clone repos
|
|
||||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
||||||
|
|
||||||
# Update PATH
|
|
||||||
. ~/.bashrc
|
|
||||||
|
|
||||||
# Clone repository and install dependencies
|
|
||||||
git clone https://github.com/coracle-social/flotilla.git
|
|
||||||
cd ~/flotilla
|
|
||||||
nvm install
|
|
||||||
nvm use
|
|
||||||
npm i
|
|
||||||
|
|
||||||
# Optionally create and populate .env.local to suit your use case
|
|
||||||
|
|
||||||
# Build the app
|
|
||||||
NODE_OPTIONS=--max_old_space_size=16384 npm run build
|
|
||||||
|
|
||||||
# Exit back to root
|
|
||||||
exit
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you've exited back to root, you can set up nginx. Place the following in a file named after your domain in the `/etc/nginx/sites-available` directory, for example, `flotilla.example.com`. This should match the `A` record you registered above.
|
Or, if you prefer to use a container:
|
||||||
|
|
||||||
```conf
|
```sh
|
||||||
server {
|
podman run -d -p 3000:3000 ghcr.io/coracle-social/flotilla:latest
|
||||||
listen 80;
|
|
||||||
server_name <SERVER NAME>;
|
|
||||||
root /home/flotilla/flotilla/build;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can run `certbot`, which will provision a TLS certificate for your domain and update your nginx configuration.
|
Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir ./mount
|
||||||
|
podman run -v ./mount:/app/mount ghcr.io/coracle-social/flotilla:latest bash -c 'cp -r build/* mount'
|
||||||
```
|
```
|
||||||
certbot --nginx -d <SERVER NAME>
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, enable the site and restart nginx. If you want to be careful, run `nginx -t` before restarting nginx.
|
|
||||||
|
|
||||||
```
|
|
||||||
ln -s /etc/nginx/sites-{available,enabled}/<SERVER NAME>
|
|
||||||
service nginx restart
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, visit your domain. You should be all set up!
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
Run `npm run dev` to get a dev server, and `npm run check:watch` to watch for typescript errors. When you're ready to commit, run `npm run format && npm run lint` and fix any errors that come up.
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "social.flotilla"
|
namespace = "social.flotilla"
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 9
|
versionCode 40
|
||||||
versionName "0.2.7"
|
versionName "1.6.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-community-safe-area')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
|
implementation project(':capacitor-filesystem')
|
||||||
|
implementation project(':capacitor-keyboard')
|
||||||
|
implementation project(':capacitor-preferences')
|
||||||
|
implementation project(':capacitor-push-notifications')
|
||||||
|
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
||||||
|
implementation project(':capawesome-capacitor-badge')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,27 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https" android:host="app.flotilla.social" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
@@ -32,4 +41,5 @@
|
|||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 711 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:ignore="NewApi">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||||
classpath 'com.google.gms:google-services:4.4.2'
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-community-safe-area'
|
||||||
|
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android')
|
||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-filesystem'
|
||||||
|
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android')
|
||||||
|
|
||||||
|
include ':capacitor-keyboard'
|
||||||
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
|
include ':capacitor-preferences'
|
||||||
|
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences/android')
|
||||||
|
|
||||||
|
include ':capacitor-push-notifications'
|
||||||
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
|
include ':capawesome-capacitor-android-dark-mode-support'
|
||||||
|
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
||||||
|
|
||||||
|
include ':capawesome-capacitor-badge'
|
||||||
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -206,7 +205,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-classpath "$CLASSPATH" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
ext {
|
ext {
|
||||||
minSdkVersion = 23
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 35
|
compileSdkVersion = 36
|
||||||
targetSdkVersion = 35
|
targetSdkVersion = 36
|
||||||
androidxActivityVersion = '1.9.2'
|
androidxActivityVersion = '1.11.0'
|
||||||
androidxAppCompatVersion = '1.7.0'
|
//https://github.com/ionic-team/capacitor/issues/7866
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
// androidxAppCompatVersion = '1.7.1'
|
||||||
androidxCoreVersion = '1.15.0'
|
androidxAppCompatVersion = '1.7.1'
|
||||||
androidxFragmentVersion = '1.8.4'
|
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||||
coreSplashScreenVersion = '1.0.1'
|
androidxCoreVersion = '1.17.0'
|
||||||
androidxWebkitVersion = '1.12.1'
|
androidxFragmentVersion = '1.8.9'
|
||||||
|
coreSplashScreenVersion = '1.2.0'
|
||||||
|
androidxWebkitVersion = '1.14.0'
|
||||||
junitVersion = '4.13.2'
|
junitVersion = '4.13.2'
|
||||||
androidxJunitVersion = '1.2.1'
|
androidxJunitVersion = '1.3.0'
|
||||||
androidxEspressoCoreVersion = '3.6.1'
|
androidxEspressoCoreVersion = '3.7.0'
|
||||||
cordovaAndroidVersion = '10.1.1'
|
cordovaAndroidVersion = '14.0.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Fetch tags and set to env vars
|
||||||
|
git fetch --prune --unshallow --tags || true
|
||||||
|
git describe --tags --abbrev=0 || true
|
||||||
|
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
||||||
|
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
CI=0 pnpm i
|
||||||
|
|
||||||
|
# Rebuild sharp
|
||||||
|
pnpm rebuild
|
||||||
|
|
||||||
|
# The build runs out of memory at times
|
||||||
|
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
|
||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
temp_env=$(declare -p -x)
|
temp_env=$(declare -p -x)
|
||||||
|
|
||||||
if [ -f .env ]; then
|
if [ -f .env.template ]; then
|
||||||
source .env
|
source .env.template
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f .env.local ]; then
|
|
||||||
source .env.local
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Avoid overwriting env vars provided directly
|
# Avoid overwriting env vars provided directly
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import type { CapacitorConfig } from '@capacitor/cli';
|
import type {CapacitorConfig} from "@capacitor/cli"
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'social.flotilla',
|
appId: "social.flotilla",
|
||||||
appName: 'Flotilla',
|
appName: "Flotilla",
|
||||||
webDir: 'build'
|
webDir: "build",
|
||||||
server: {
|
android: {
|
||||||
androidScheme: "https"
|
adjustMarginsForEdgeToEdge: true,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
CapacitorHttp: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
SystemBars: {
|
||||||
|
insetsHandling: "enable",
|
||||||
|
},
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash",
|
||||||
}
|
},
|
||||||
|
Keyboard: {
|
||||||
|
style: "DARK",
|
||||||
|
resizeOnFullScreen: true,
|
||||||
|
},
|
||||||
|
Badge: {
|
||||||
|
persist: true,
|
||||||
|
autoClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
server: {
|
||||||
// server: {
|
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||||
// url: "http://192.168.1.250:1847",
|
// url: "http://192.168.1.17:1847",
|
||||||
// cleartext: true
|
// cleartext: true,
|
||||||
// },
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export default config;
|
export default config
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
|
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||||
@@ -18,8 +19,10 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
|
||||||
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
|
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
@@ -57,6 +60,8 @@
|
|||||||
504EC2FB1FED79650016851F = {
|
504EC2FB1FED79650016851F = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
|
||||||
|
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
@@ -160,6 +165,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||||
@@ -285,7 +291,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -336,7 +342,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
@@ -349,20 +355,22 @@
|
|||||||
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 1.6.4;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -374,19 +382,21 @@
|
|||||||
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 1.6.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
@@ -49,5 +49,9 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:app.flotilla.social</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
require_relative '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
platform :ios, '14.0'
|
platform :ios, '15.0'
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
|
|
||||||
# workaround to avoid Xcode caching of Pods that requires
|
# workaround to avoid Xcode caching of Pods that requires
|
||||||
@@ -9,10 +9,16 @@ use_frameworks!
|
|||||||
install! 'cocoapods', :disable_input_output_paths => true
|
install! 'cocoapods', :disable_input_output_paths => true
|
||||||
|
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/nostr-signer-capacitor-plugin'
|
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
|
||||||
|
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
|
||||||
|
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard'
|
||||||
|
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
|
||||||
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
||||||
|
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
||||||
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Flotilla Chat' do
|
target 'Flotilla Chat' do
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
|
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
|
||||||
|
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
|
||||||
|
|
||||||
|
pkg.pnpm.overrides = pkg.pnpm.overrides || {}
|
||||||
|
pkg.pnpm.overrides["@welshman/app"] = "link:../welshman/packages/app"
|
||||||
|
pkg.pnpm.overrides["@welshman/content"] = "link:../welshman/packages/content"
|
||||||
|
pkg.pnpm.overrides["@welshman/editor"] = "link:../welshman/packages/editor"
|
||||||
|
pkg.pnpm.overrides["@welshman/feeds"] = "link:../welshman/packages/feeds"
|
||||||
|
pkg.pnpm.overrides["@welshman/lib"] = "link:../welshman/packages/lib"
|
||||||
|
pkg.pnpm.overrides["@welshman/net"] = "link:../welshman/packages/net"
|
||||||
|
pkg.pnpm.overrides["@welshman/router"] = "link:../welshman/packages/router"
|
||||||
|
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
|
||||||
|
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
|
||||||
|
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
|
||||||
|
// pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
|
||||||
|
|
||||||
|
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
|
||||||
|
|
||||||
|
execSync('pnpm i', { stdio: 'inherit' })
|
||||||
|
|
||||||
|
execSync('git checkout -f pnpm-lock.yaml', { stdio: 'inherit' })
|
||||||
|
execSync('git checkout -f package.json', { stdio: 'inherit' })
|
||||||
@@ -1,75 +1,100 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "1.6.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"sourcemaps": "./build.sh && ./sourcemaps.sh",
|
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
"format": "prettier --write src",
|
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
||||||
|
"format:all": "prettier --write src",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@sentry/cli": "^2.40.0",
|
"@eslint/js": "^9.39.2",
|
||||||
"@sveltejs/kit": "^2.5.27",
|
"@sveltejs/kit": "^2.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.2",
|
||||||
"eslint-plugin-svelte": "^2.45.1",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.15.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.48.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.3.5",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.53.1",
|
||||||
"vite": "^5.4.4"
|
"vite": "^5.4.21"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor-community/safe-area": "^8.0.1",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/android": "^8.0.1",
|
||||||
"@capacitor/cli": "^7.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/cli": "^8.0.1",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/core": "^8.0.1",
|
||||||
"@noble/curves": "^1.5.0",
|
"@capacitor/filesystem": "^8.1.0",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@capacitor/ios": "^8.0.1",
|
||||||
|
"@capacitor/keyboard": "^8.0.0",
|
||||||
|
"@capacitor/preferences": "^8.0.0",
|
||||||
|
"@capacitor/push-notifications": "^8.0.0",
|
||||||
|
"@capawesome/capacitor-android-dark-mode-support": "^8.0.0",
|
||||||
|
"@capawesome/capacitor-badge": "^8.0.0",
|
||||||
|
"@getalby/lightning-tools": "^6.1.0",
|
||||||
|
"@getalby/sdk": "^5.1.2",
|
||||||
|
"@noble/curves": "^1.9.7",
|
||||||
|
"@pomade/core": "^0.0.12",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.35.0",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/adapter-static": "^3.0.4",
|
"@tiptap/core": "^2.27.2",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "~0.0.42",
|
"@welshman/app": "^0.8.4",
|
||||||
"@welshman/content": "~0.0.18",
|
"@welshman/content": "^0.8.4",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/editor": "^0.8.4",
|
||||||
"@welshman/editor": "~0.0.15",
|
"@welshman/feeds": "^0.8.4",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/lib": "^0.8.4",
|
||||||
"@welshman/lib": "~0.0.41",
|
"@welshman/net": "^0.8.4",
|
||||||
"@welshman/net": "~0.0.47",
|
"@welshman/router": "^0.8.4",
|
||||||
"@welshman/signer": "~0.0.20",
|
"@welshman/signer": "^0.8.4",
|
||||||
"@welshman/store": "~0.0.16",
|
"@welshman/store": "^0.8.4",
|
||||||
"@welshman/util": "~0.0.61",
|
"@welshman/util": "^0.8.4",
|
||||||
"daisyui": "^4.12.10",
|
"compressorjs": "^1.2.1",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"daisyui": "^4.12.24",
|
||||||
"dotenv": "^16.4.5",
|
"date-picker-svelte": "^2.17.0",
|
||||||
"emoji-picker-element": "^1.22.8",
|
"dotenv": "^16.6.1",
|
||||||
"fuse.js": "^7.0.0",
|
"emoji-picker-element": "^1.28.1",
|
||||||
"husky": "^9.1.6",
|
"fuse.js": "^7.1.0",
|
||||||
"idb": "^8.0.0",
|
"husky": "^9.1.7",
|
||||||
"nostr-signer-capacitor-plugin": "coracle-social/nostr-signer-capacitor-plugin#9fbe4f8",
|
"idb": "^8.0.3",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"nostr-tools": "^2.19.4",
|
||||||
"qrcode": "^1.5.4"
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"qr-scanner": "^1.4.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"throttle-debounce": "^5.0.2",
|
||||||
|
"tippy.js": "^6.3.7"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"ignoredBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"sharp"
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.35.0-rc.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
||||||
|
|
||||||
dotenv.config({path: ".env.local"})
|
|
||||||
dotenv.config({path: ".env"})
|
dotenv.config({path: ".env"})
|
||||||
|
dotenv.config({path: ".env.template"})
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
preset,
|
preset,
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
hash=$(find build -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
|
|
||||||
|
|
||||||
sentry-cli \
|
|
||||||
--url https://glitchtip.coracle.social \
|
|
||||||
--auth-token $GLITCHTIP_AUTH_TOKEN \
|
|
||||||
--api-key $VITE_GLITCHTIP_API_KEY \
|
|
||||||
sourcemaps \
|
|
||||||
--org coracle \
|
|
||||||
--project flotilla \
|
|
||||||
--release $hash \
|
|
||||||
upload \
|
|
||||||
--url-prefix /_app/immutable/ \
|
|
||||||
build/_app/immutable
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "@welshman/editor/index.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -44,6 +46,14 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
|
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||||
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
|
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||||
|
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme] {
|
||||||
|
@apply bg-base-300;
|
||||||
--base-100: oklch(var(--b1));
|
--base-100: oklch(var(--b1));
|
||||||
--base-200: oklch(var(--b2));
|
--base-200: oklch(var(--b2));
|
||||||
--base-300: oklch(var(--b3));
|
--base-300: oklch(var(--b3));
|
||||||
@@ -52,58 +62,88 @@
|
|||||||
--primary-content: oklch(var(--pc));
|
--primary-content: oklch(var(--pc));
|
||||||
--secondary: oklch(var(--s));
|
--secondary: oklch(var(--s));
|
||||||
--secondary-content: oklch(var(--sc));
|
--secondary-content: oklch(var(--sc));
|
||||||
|
--neutral: oklch(var(--n));
|
||||||
|
--neutral-content: oklch(var(--nc));
|
||||||
}
|
}
|
||||||
|
|
||||||
:root,
|
.mobile [data-tip]::before {
|
||||||
body,
|
display: none !important;
|
||||||
html {
|
|
||||||
@apply bg-base-300;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ios */
|
/* safe area insets */
|
||||||
|
|
||||||
.sait {
|
@layer components {
|
||||||
padding-top: env(safe-area-inset-top);
|
.pt-sai {
|
||||||
}
|
padding-top: var(--sait);
|
||||||
|
}
|
||||||
|
|
||||||
.sair {
|
.pr-sai {
|
||||||
padding-right: env(safe-area-inset-right);
|
padding-right: var(--sair);
|
||||||
}
|
}
|
||||||
|
|
||||||
.saib {
|
.pb-sai {
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: var(--saib);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sail {
|
.pl-sai {
|
||||||
padding-left: env(safe-area-inset-left);
|
padding-left: var(--sail);
|
||||||
}
|
}
|
||||||
|
|
||||||
.saix {
|
.px-sai {
|
||||||
@apply sail sair;
|
@apply pl-sai pr-sai;
|
||||||
}
|
}
|
||||||
|
|
||||||
.saiy {
|
.py-sai {
|
||||||
@apply sait saib;
|
@apply pt-sai pb-sai;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sai {
|
.p-sai {
|
||||||
@apply saiy saix;
|
@apply py-sai px-sai;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-sai {
|
.mt-sai {
|
||||||
top: env(safe-area-inset-top);
|
margin-top: var(--sait);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-sai {
|
.mr-sai {
|
||||||
right: env(safe-area-inset-right);
|
margin-right: var(--sair);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-sai {
|
.mb-sai {
|
||||||
bottom: env(safe-area-inset-bottom);
|
margin-bottom: var(--saib);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-sai {
|
.ml-sai {
|
||||||
left: env(safe-area-inset-left);
|
margin-left: var(--sail);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-sai {
|
||||||
|
@apply ml-sai mr-sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-sai {
|
||||||
|
@apply mt-sai mb-sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-sai {
|
||||||
|
@apply my-sai mx-sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-sai {
|
||||||
|
top: var(--sait);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-sai {
|
||||||
|
right: var(--sair);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sai {
|
||||||
|
bottom: var(--saib);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sai {
|
||||||
|
left: var(--sail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* utilities */
|
/* utilities */
|
||||||
@@ -126,11 +166,11 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card2 {
|
.card2 {
|
||||||
@apply rounded-box p-6 text-base-content;
|
@apply rounded-box p-4 text-base-content sm:p-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card2.card2-sm {
|
.card2.card2-sm {
|
||||||
@apply p-4 text-base-content;
|
@apply p-2 text-base-content sm:p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
@@ -181,12 +221,6 @@ html {
|
|||||||
@apply ellipsize;
|
@apply ellipsize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 639px) {
|
|
||||||
[data-tip]::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-padding-x {
|
.content-padding-x {
|
||||||
@apply px-4 sm:px-8 md:px-12;
|
@apply px-4 sm:px-8 md:px-12;
|
||||||
}
|
}
|
||||||
@@ -240,12 +274,12 @@ html {
|
|||||||
.input-editor,
|
.input-editor,
|
||||||
.chat-editor,
|
.chat-editor,
|
||||||
.note-editor {
|
.note-editor {
|
||||||
@apply -m-1 min-h-12 p-1;
|
@apply -m-1 min-h-12 p-1 text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap {
|
.tiptap {
|
||||||
--tiptap-object-bg: var(--base-100);
|
--tiptap-object-bg: var(--neutral);
|
||||||
--tiptap-object-fg: var(--base-content);
|
--tiptap-object-fg: var(--neutral-content);
|
||||||
--tiptap-active-bg: var(--primary);
|
--tiptap-active-bg: var(--primary);
|
||||||
--tiptap-active-fg: var(--primary-content);
|
--tiptap-active-fg: var(--primary-content);
|
||||||
}
|
}
|
||||||
@@ -257,6 +291,14 @@ html {
|
|||||||
--tiptap-active-fg: var(--base-content);
|
--tiptap-active-fg: var(--base-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__item {
|
||||||
|
@apply border-l-2 border-solid border-base-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__selected {
|
||||||
|
@apply border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
.tiptap {
|
.tiptap {
|
||||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
||||||
}
|
}
|
||||||
@@ -292,6 +334,16 @@ html {
|
|||||||
color: var(--base-content);
|
color: var(--base-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* content rendered by welshman/content */
|
||||||
|
|
||||||
|
.welshman-content a {
|
||||||
|
@apply link;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welshman-content-error a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|
||||||
/* date input */
|
/* date input */
|
||||||
|
|
||||||
.picker {
|
.picker {
|
||||||
@@ -310,6 +362,12 @@ html {
|
|||||||
@apply !h-full !w-full !rounded-lg !border-none !bg-inherit !px-4 !text-inherit;
|
@apply !h-full !w-full !rounded-lg !border-none !bg-inherit !px-4 !text-inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* tippy popover */
|
||||||
|
|
||||||
|
.tippy-box {
|
||||||
|
@apply rounded-box shadow-xl;
|
||||||
|
}
|
||||||
|
|
||||||
/* emoji picker */
|
/* emoji picker */
|
||||||
|
|
||||||
emoji-picker {
|
emoji-picker {
|
||||||
@@ -323,3 +381,43 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* progress */
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-value {
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content width for fixed elements */
|
||||||
|
|
||||||
|
.cw {
|
||||||
|
@apply w-full md:left-[18.5rem] md:w-[calc(100%-18.5rem-var(--sair))];
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-full {
|
||||||
|
@apply w-full md:left-[4rem] md:w-[calc(100%-4rem-var(--sair))];
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb {
|
||||||
|
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard open state adjustments */
|
||||||
|
|
||||||
|
body.keyboard-open .cb {
|
||||||
|
@apply bottom-sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.keyboard-open .hide-on-keyboard {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chat view */
|
||||||
|
|
||||||
|
.chat__compose {
|
||||||
|
@apply cb cw fixed z-compose;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__scroll-down {
|
||||||
|
@apply fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||||
<meta name="theme-color" content="{ACCENT}" />
|
<meta name="theme-color" content="{ACCENT}" />
|
||||||
<meta name="description" content="{DESCRIPTION}" />
|
<meta name="description" content="{DESCRIPTION}" />
|
||||||
<meta name="og:url" content="{URL}" />
|
<meta name="og:url" content="{URL}" />
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon-180x180.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon-180x180.png" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover" data-sveltekit-preload-code="eager">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
|
|||||||
@@ -1,506 +0,0 @@
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
|
||||||
import {get} from "svelte/store"
|
|
||||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
|
||||||
import {
|
|
||||||
DELETE,
|
|
||||||
REPORT,
|
|
||||||
PROFILE,
|
|
||||||
INBOX_RELAYS,
|
|
||||||
RELAYS,
|
|
||||||
FOLLOWS,
|
|
||||||
REACTION,
|
|
||||||
AUTH_JOIN,
|
|
||||||
GROUP_JOIN,
|
|
||||||
GROUP_LEAVE,
|
|
||||||
GROUP_CREATE,
|
|
||||||
GROUP_EDIT_META,
|
|
||||||
GROUPS,
|
|
||||||
COMMENT,
|
|
||||||
isSignedEvent,
|
|
||||||
createEvent,
|
|
||||||
displayProfile,
|
|
||||||
normalizeRelayUrl,
|
|
||||||
makeList,
|
|
||||||
addToListPublicly,
|
|
||||||
removeFromListByPredicate,
|
|
||||||
getTag,
|
|
||||||
getListTags,
|
|
||||||
getRelayTags,
|
|
||||||
isShareableRelayUrl,
|
|
||||||
getRelayTagValues,
|
|
||||||
toNostrURI,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
|
||||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
|
||||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
|
||||||
import {
|
|
||||||
pubkey,
|
|
||||||
signer,
|
|
||||||
repository,
|
|
||||||
publishThunk,
|
|
||||||
publishThunks,
|
|
||||||
loadProfile,
|
|
||||||
loadInboxRelaySelections,
|
|
||||||
profilesByPubkey,
|
|
||||||
relaySelectionsByPubkey,
|
|
||||||
getWriteRelayUrls,
|
|
||||||
loadFollows,
|
|
||||||
loadMutes,
|
|
||||||
tagEvent,
|
|
||||||
tagEventForReaction,
|
|
||||||
getRelayUrls,
|
|
||||||
userRelaySelections,
|
|
||||||
userInboxRelaySelections,
|
|
||||||
nip44EncryptToSelf,
|
|
||||||
loadRelay,
|
|
||||||
addSession,
|
|
||||||
clearStorage,
|
|
||||||
dropSession,
|
|
||||||
tagEventForComment,
|
|
||||||
tagEventForQuote,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import type {Thunk} from "@welshman/app"
|
|
||||||
import {
|
|
||||||
tagRoom,
|
|
||||||
PROTECTED,
|
|
||||||
userMembership,
|
|
||||||
INDEXER_RELAYS,
|
|
||||||
NIP46_PERMS,
|
|
||||||
loadMembership,
|
|
||||||
loadSettings,
|
|
||||||
getDefaultPubkeys,
|
|
||||||
userRoomsByUrl,
|
|
||||||
} from "@app/state"
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
|
|
||||||
export const getPubkeyHints = (pubkey: string) => {
|
|
||||||
const selections = relaySelectionsByPubkey.get().get(pubkey)
|
|
||||||
const relays = selections ? getWriteRelayUrls(selections) : []
|
|
||||||
const hints = relays.length ? relays : INDEXER_RELAYS
|
|
||||||
|
|
||||||
return hints
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getPubkeyPetname = (pubkey: string) => {
|
|
||||||
const profile = profilesByPubkey.get().get(pubkey)
|
|
||||||
const display = displayProfile(profile)
|
|
||||||
|
|
||||||
return display
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getThunkError = async (thunk: Thunk) => {
|
|
||||||
const result = await thunk.result
|
|
||||||
const [{status, message}] = Object.values(result) as any
|
|
||||||
|
|
||||||
if (status !== PublishStatus.Success) {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
|
|
||||||
if (parent) {
|
|
||||||
const nevent = nip19.neventEncode({
|
|
||||||
id: parent.id,
|
|
||||||
kind: parent.kind,
|
|
||||||
author: parent.pubkey,
|
|
||||||
relays: ctx.app.router.Event(parent).limit(3).getUrls(),
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = [...tags, tagEventForQuote(parent)]
|
|
||||||
content = toNostrURI(nevent) + "\n\n" + content
|
|
||||||
}
|
|
||||||
|
|
||||||
return {content, tags}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log in
|
|
||||||
|
|
||||||
export const loginWithNip46 = async ({
|
|
||||||
relays,
|
|
||||||
signerPubkey,
|
|
||||||
clientSecret = makeSecret(),
|
|
||||||
connectSecret = "",
|
|
||||||
}: {
|
|
||||||
relays: string[]
|
|
||||||
signerPubkey: string
|
|
||||||
clientSecret?: string
|
|
||||||
connectSecret?: string
|
|
||||||
}) => {
|
|
||||||
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
|
|
||||||
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
|
||||||
|
|
||||||
// TODO: remove ack result
|
|
||||||
if (!["ack", connectSecret].includes(result)) return false
|
|
||||||
|
|
||||||
const pubkey = await broker.getPublicKey()
|
|
||||||
|
|
||||||
if (!pubkey) return false
|
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
|
||||||
|
|
||||||
const handler = {relays, pubkey: signerPubkey}
|
|
||||||
|
|
||||||
addSession({method: "nip46", pubkey, secret: clientSecret, handler})
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log out
|
|
||||||
|
|
||||||
export const logout = async () => {
|
|
||||||
const $pubkey = pubkey.get()
|
|
||||||
|
|
||||||
if ($pubkey) {
|
|
||||||
dropSession($pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
await clearStorage()
|
|
||||||
|
|
||||||
localStorage.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loaders
|
|
||||||
|
|
||||||
export const loadUserData = (
|
|
||||||
pubkey: string,
|
|
||||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
|
||||||
) => {
|
|
||||||
const promise = Promise.race([
|
|
||||||
sleep(3000),
|
|
||||||
Promise.all([
|
|
||||||
loadInboxRelaySelections(pubkey, request),
|
|
||||||
loadMembership(pubkey, request),
|
|
||||||
loadSettings(pubkey, request),
|
|
||||||
loadProfile(pubkey, request),
|
|
||||||
loadFollows(pubkey, request),
|
|
||||||
loadMutes(pubkey, request),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
|
||||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
|
||||||
promise.then(async () => {
|
|
||||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
|
||||||
const relays = sample(1, INDEXER_RELAYS)
|
|
||||||
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
loadMembership(pubkey, {relays})
|
|
||||||
loadProfile(pubkey, {relays})
|
|
||||||
loadFollows(pubkey, {relays})
|
|
||||||
loadMutes(pubkey, {relays})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverRelays = (lists: List[]) =>
|
|
||||||
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
|
||||||
|
|
||||||
// Synchronization
|
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
|
||||||
const authors = [pubkey.get()!]
|
|
||||||
const kinds = [RELAYS, INBOX_RELAYS, FOLLOWS, PROFILE]
|
|
||||||
const events = repository.query([{kinds, authors}])
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
if (isSignedEvent(event)) {
|
|
||||||
await publishThunk({event, relays}).result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NIP 29 stuff
|
|
||||||
|
|
||||||
export const nip29 = {
|
|
||||||
createRoom: (url: string, room: string) => {
|
|
||||||
const event = createEvent(GROUP_CREATE, {tags: [tagRoom(room, url)]})
|
|
||||||
|
|
||||||
return publishThunk({event, relays: [url]})
|
|
||||||
},
|
|
||||||
editMeta: (url: string, room: string, meta: Record<string, string>) => {
|
|
||||||
const event = createEvent(GROUP_EDIT_META, {
|
|
||||||
tags: [tagRoom(room, url), ...Object.entries(meta)],
|
|
||||||
})
|
|
||||||
|
|
||||||
return publishThunk({event, relays: [url]})
|
|
||||||
},
|
|
||||||
joinRoom: (url: string, room: string) => {
|
|
||||||
const event = createEvent(GROUP_JOIN, {tags: [tagRoom(room, url)]})
|
|
||||||
|
|
||||||
return publishThunk({event, relays: [url]})
|
|
||||||
},
|
|
||||||
leaveRoom: (url: string, room: string) => {
|
|
||||||
const event = createEvent(GROUP_LEAVE, {tags: [tagRoom(room, url)]})
|
|
||||||
|
|
||||||
return publishThunk({event, relays: [url]})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// List updates
|
|
||||||
|
|
||||||
export const addSpaceMembership = async (url: string) => {
|
|
||||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
|
||||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
|
||||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeSpaceMembership = async (url: string) => {
|
|
||||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
|
||||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
|
||||||
const relays = uniq([
|
|
||||||
url,
|
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
|
||||||
...getRelayTagValues(event.tags),
|
|
||||||
])
|
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addRoomMembership = async (url: string, room: string, name: string) => {
|
|
||||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
|
||||||
const newTags = [
|
|
||||||
["r", url],
|
|
||||||
["group", room, url, name],
|
|
||||||
]
|
|
||||||
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
|
||||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeRoomMembership = async (url: string, room: string) => {
|
|
||||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
|
||||||
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
|
||||||
const relays = uniq([
|
|
||||||
url,
|
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
|
||||||
...getRelayTagValues(event.tags),
|
|
||||||
])
|
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
|
||||||
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
|
||||||
|
|
||||||
if (read && write) {
|
|
||||||
tags.push(["r", url])
|
|
||||||
} else if (read) {
|
|
||||||
tags.push(["r", url, "read"])
|
|
||||||
} else if (write) {
|
|
||||||
tags.push(["r", url, "write"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return publishThunk({
|
|
||||||
event: createEvent(list.kind, {tags}),
|
|
||||||
relays: [
|
|
||||||
url,
|
|
||||||
...INDEXER_RELAYS,
|
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
|
||||||
...userRoomsByUrl.get().keys(),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
|
||||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
|
||||||
|
|
||||||
// Only update inbox policies if they already exist or we're adding them
|
|
||||||
if (enabled || getRelayUrls(list).includes(url)) {
|
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
tags.push(["relay", url])
|
|
||||||
}
|
|
||||||
|
|
||||||
return publishThunk({
|
|
||||||
event: createEvent(list.kind, {tags}),
|
|
||||||
relays: [
|
|
||||||
...INDEXER_RELAYS,
|
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
|
||||||
...userRoomsByUrl.get().keys(),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relay access
|
|
||||||
|
|
||||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
|
||||||
const connection = ctx.net.pool.get(url)
|
|
||||||
|
|
||||||
await connection.auth.attempt(5000)
|
|
||||||
|
|
||||||
const thunk = publishThunk({
|
|
||||||
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
|
||||||
relays: [url],
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await thunk.result
|
|
||||||
|
|
||||||
if (result[url].status === PublishStatus.Failure) {
|
|
||||||
const message =
|
|
||||||
connection.auth.message?.replace(/^.*: /, "") ||
|
|
||||||
result[url].message?.replace(/^.*: /, "") ||
|
|
||||||
"join request rejected"
|
|
||||||
|
|
||||||
// If it's a strict NIP 29 relay don't worry about requesting access
|
|
||||||
// TODO: remove this if relay29 ever gets less strict
|
|
||||||
if (message !== "missing group (`h`) tag") {
|
|
||||||
return `Failed to join relay (${message})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkRelayProfile = async (url: string) => {
|
|
||||||
const relay = await loadRelay(url)
|
|
||||||
|
|
||||||
if (!relay?.profile) {
|
|
||||||
return "Sorry, we weren't able to find that relay."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkRelayConnection = async (url: string) => {
|
|
||||||
const connection = ctx.net.pool.get(url)
|
|
||||||
|
|
||||||
await connection.socket.open()
|
|
||||||
|
|
||||||
if (connection.socket.status !== SocketStatus.Open) {
|
|
||||||
return `Failed to connect`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
|
||||||
const connection = ctx.net.pool.get(url)
|
|
||||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
|
||||||
|
|
||||||
await connection.auth.attempt(timeout)
|
|
||||||
|
|
||||||
// Only raise an error if it's not a timeout.
|
|
||||||
// If it is, odds are the problem is with our signer, not the relay
|
|
||||||
if (!okStatuses.includes(connection.auth.status) && connection.auth.message) {
|
|
||||||
return `Failed to authenticate (${connection.auth.message})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
|
||||||
const checks = [
|
|
||||||
() => checkRelayConnection(url),
|
|
||||||
() => checkRelayAccess(url, claim),
|
|
||||||
() => checkRelayAuth(url),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const check of checks) {
|
|
||||||
const error = await check()
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
export const sendWrapped = async ({
|
|
||||||
template,
|
|
||||||
pubkeys,
|
|
||||||
delay,
|
|
||||||
}: {
|
|
||||||
template: EventTemplate
|
|
||||||
pubkeys: string[]
|
|
||||||
delay?: number
|
|
||||||
}) => {
|
|
||||||
const nip59 = Nip59.fromSigner(signer.get()!)
|
|
||||||
|
|
||||||
return publishThunks(
|
|
||||||
await Promise.all(
|
|
||||||
uniq(pubkeys).map(async recipient => ({
|
|
||||||
event: await nip59.wrap(recipient, stamp(template)),
|
|
||||||
relays: ctx.app.router.PubkeyInbox(recipient).getUrls(),
|
|
||||||
delay,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
|
||||||
const tags = [["k", String(event.kind)], ...tagEvent(event)]
|
|
||||||
const groupTag = getTag("h", event.tags)
|
|
||||||
|
|
||||||
if (groupTag) {
|
|
||||||
tags.push(PROTECTED)
|
|
||||||
tags.push(groupTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createEvent(DELETE, {tags})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
|
||||||
publishThunk({event: makeDelete({event}), relays})
|
|
||||||
|
|
||||||
export type ReportParams = {
|
|
||||||
event: TrustedEvent
|
|
||||||
content: string
|
|
||||||
reason: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeReport = ({event, reason, content}: ReportParams) => {
|
|
||||||
const tags = [
|
|
||||||
["p", event.pubkey],
|
|
||||||
["e", event.id, reason],
|
|
||||||
]
|
|
||||||
|
|
||||||
return createEvent(REPORT, {content, tags})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishReport = ({
|
|
||||||
relays,
|
|
||||||
event,
|
|
||||||
reason,
|
|
||||||
content,
|
|
||||||
}: ReportParams & {relays: string[]}) =>
|
|
||||||
publishThunk({event: makeReport({event, reason, content}), relays})
|
|
||||||
|
|
||||||
export type ReactionParams = {
|
|
||||||
event: TrustedEvent
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeReaction = ({event, content}: ReactionParams) => {
|
|
||||||
const tags = tagEventForReaction(event)
|
|
||||||
const groupTag = getTag("h", event.tags)
|
|
||||||
|
|
||||||
if (groupTag) {
|
|
||||||
tags.push(PROTECTED)
|
|
||||||
tags.push(groupTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createEvent(REACTION, {content, tags})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
|
||||||
publishThunk({event: makeReaction(params), relays})
|
|
||||||
|
|
||||||
export type CommentParams = {
|
|
||||||
event: TrustedEvent
|
|
||||||
content: string
|
|
||||||
tags?: string[][]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
|
||||||
createEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
|
|
||||||
|
|
||||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
|
||||||
publishThunk({event: makeComment(params), relays})
|
|
||||||
@@ -2,35 +2,17 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
import {modals} from "@app/util/modal"
|
||||||
import PasswordReset from "@app/components/PasswordReset.svelte"
|
|
||||||
import {BURROW_URL} from "@app/state"
|
|
||||||
import {modals, pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {children}: Props = $props()
|
const {children}: Props = $props()
|
||||||
|
|
||||||
if (BURROW_URL && !$pubkey) {
|
|
||||||
if ($page.url.pathname === "/confirm-email") {
|
|
||||||
pushModal(EmailConfirm, {
|
|
||||||
email: $page.url.searchParams.get("email"),
|
|
||||||
confirm_token: $page.url.searchParams.get("confirm_token"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($page.url.pathname === "/reset-password") {
|
|
||||||
pushModal(PasswordReset, {
|
|
||||||
email: $page.url.searchParams.get("email"),
|
|
||||||
reset_token: $page.url.searchParams.get("reset_token"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
@@ -39,7 +21,7 @@
|
|||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</PrimaryNav>
|
</PrimaryNav>
|
||||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
{:else if !$modals[$page.url.hash.slice(1)]}
|
||||||
<Landing />
|
<Dialog children={{component: Landing, props: {}}} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Toast />
|
<Toast />
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
|
import type {Nip46Controller} from "@app/util/nip46"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
controller: Nip46Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
const {controller}: Props = $props()
|
||||||
|
const {url, loading} = controller
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $url}
|
||||||
|
{#if $loading}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Spinner loading>Establishing connection...</Spinner>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<QRCode code={$url} />
|
||||||
|
<p class="text-sm opacity-75">Scan with your signer to log in, or click to copy.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {debounce} from "throttle-debounce"
|
||||||
|
import Scanner from "@lib/components/Scanner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import CpuBolt from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||||
|
import QrCode from "@assets/icons/qr-code.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||||
|
import type {Nip46Controller} from "@app/util/nip46"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
controller: Nip46Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
const {controller}: Props = $props()
|
||||||
|
const {loading, bunker} = controller
|
||||||
|
|
||||||
|
const toggleScanner = () => {
|
||||||
|
showScanner = !showScanner
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScan = debounce(1000, async (data: string) => {
|
||||||
|
showScanner = false
|
||||||
|
$bunker = data
|
||||||
|
})
|
||||||
|
|
||||||
|
let showScanner = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Bunker Link*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={CpuBolt} />
|
||||||
|
<input disabled={$loading} bind:value={$bunker} class="grow" placeholder="bunker://" />
|
||||||
|
<Button onclick={toggleScanner}>
|
||||||
|
<Icon icon={QrCode} />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
A login link provided by a nostr signing app.
|
||||||
|
<Button class="link" onclick={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{#if showScanner}
|
||||||
|
<Scanner onscan={onScan} />
|
||||||
|
{/if}
|
||||||
@@ -1,43 +1,64 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
import {getTagValue, getAddress} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||||
import {makeCalendarPath} from "@app/routes"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {makeCalendarPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {
|
type Props = {
|
||||||
url,
|
|
||||||
event,
|
|
||||||
showActivity = false,
|
|
||||||
}: {
|
|
||||||
url: string
|
url: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
showRoom?: boolean
|
||||||
showActivity?: boolean
|
showActivity?: boolean
|
||||||
} = $props()
|
|
||||||
|
|
||||||
const path = makeCalendarPath(url, event.id)
|
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
|
||||||
|
|
||||||
if (reaction) {
|
|
||||||
publishDelete({relays: [url], event: reaction})
|
|
||||||
} else {
|
|
||||||
publishReaction({event, content, relays: [url]})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
|
const h = getTagValue("h", event.tags)
|
||||||
|
const path = makeCalendarPath(url, getAddress(event))
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||||
|
|
||||||
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
|
const createReaction = async (template: EventContent) =>
|
||||||
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
{#if h && showRoom}
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
|
||||||
<ThunkStatusOrDeleted {event} />
|
Posted in #<RoomName {h} {url} />
|
||||||
{#if showActivity}
|
</Link>
|
||||||
<EventActivity {url} {path} {event} />
|
{/if}
|
||||||
{/if}
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<EventActions {url} {event} noun="Event" />
|
<ThunkStatusOrDeleted {event} />
|
||||||
</div>
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} noun="Event">
|
||||||
|
{#snippet customActions()}
|
||||||
|
{#if event.pubkey === $pubkey}
|
||||||
|
<li>
|
||||||
|
<Button onclick={editEvent}>
|
||||||
|
<Icon size={4} icon={Pen2} />
|
||||||
|
Edit Event
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</EventActions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,164 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {randomId, HOUR} from "@welshman/lib"
|
|
||||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
|
||||||
import {publishThunk} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import {daysBetween} from "@lib/util"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import CalendarEventForm from "@app/components/CalendarEventForm.svelte"
|
||||||
import {PROTECTED, GENERAL, tagRoom} from "@app/state"
|
|
||||||
import {makeEditor} from "@app/editor"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
const {url} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
const uploading = writable(false)
|
h?: string
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
if ($uploading) return
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide a title.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!start || !end) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide start and end times.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start >= end) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "End time must be later than start time.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = createEvent(EVENT_TIME, {
|
|
||||||
content: editor.getText({blockSeparator: "\n"}).trim(),
|
|
||||||
tags: [
|
|
||||||
["d", randomId()],
|
|
||||||
["title", title],
|
|
||||||
["location", location],
|
|
||||||
["start", start.toString()],
|
|
||||||
["end", end.toString()],
|
|
||||||
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
|
||||||
...editor.storage.nostr.getEditorTags(),
|
|
||||||
tagRoom(GENERAL, url),
|
|
||||||
PROTECTED,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
pushToast({message: "Your event has been published!"})
|
|
||||||
publishThunk({event, relays: [url]})
|
|
||||||
history.back()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({submit, uploading})
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
let title = $state("")
|
|
||||||
let location = $state("")
|
|
||||||
let start: number | undefined = $state()
|
|
||||||
let end: number | undefined = $state()
|
|
||||||
let endDirty = false
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!endDirty && start) {
|
|
||||||
end = start + HOUR
|
|
||||||
} else if (end) {
|
|
||||||
endDirty = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
<CalendarEventForm {url} {h}>
|
||||||
<ModalHeader>
|
{#snippet header()}
|
||||||
{#snippet title()}
|
<ModalHeader>
|
||||||
<div>Create an Event</div>
|
<ModalTitle>Create an Event</ModalTitle>
|
||||||
{/snippet}
|
<ModalSubtitle>Invite other group members to events online or in real life.</ModalSubtitle>
|
||||||
{#snippet info()}
|
</ModalHeader>
|
||||||
<div>Invite other group members to events online or in real life.</div>
|
{/snippet}
|
||||||
{/snippet}
|
</CalendarEventForm>
|
||||||
</ModalHeader>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Title*</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<input bind:value={title} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Summary</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<div class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
|
||||||
<div class="input-editor flex-grow overflow-hidden">
|
|
||||||
<EditorContent {editor} />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
data-tip="Add an image"
|
|
||||||
class="center btn tooltip"
|
|
||||||
onclick={() => editor.chain().selectFiles().run()}>
|
|
||||||
{#if $uploading}
|
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon="gallery-send" />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
Start*
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<DateTimeInput bind:value={start} />
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
End*
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<DateTimeInput bind:value={end} />
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Location (optional)</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="map-point" />
|
|
||||||
<input bind:value={location} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back}>
|
|
||||||
<Icon icon="alt-arrow-left" />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
|
||||||
<Spinner loading={$uploading}>Create Event</Spinner>
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {fromPairs} from "@welshman/lib"
|
import {fromPairs, LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {LOCALE, secondsToDate} from "@welshman/app"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
@@ -9,11 +8,16 @@
|
|||||||
|
|
||||||
const {event}: Props = $props()
|
const {event}: Props = $props()
|
||||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
const start = $derived(parseInt(meta.start))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if !isNaN(start)}
|
||||||
class="flex h-14 w-14 flex-col items-center justify-center gap-1 rounded-box border border-solid border-base-content p-2 sm:h-24 sm:w-24">
|
{@const startDate = secondsToDate(start)}
|
||||||
<span class="sm:text-lg">{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</span>
|
<div
|
||||||
<span class="sm:text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex">
|
||||||
</div>
|
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||||
|
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||||
|
<span class="text-xs opacity-75"
|
||||||
|
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import CalendarEventForm from "@app/components/CalendarEventForm.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
d: getTagValue("d", event.tags)!,
|
||||||
|
title: getTagValue("title", event.tags)!,
|
||||||
|
location: getTagValue("location", event.tags)!,
|
||||||
|
start: parseInt(getTagValue("start", event.tags)!),
|
||||||
|
end: parseInt(getTagValue("end", event.tags)!),
|
||||||
|
content: event.content,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarEventForm {url} {initialValues}>
|
||||||
|
{#snippet header()}
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Edit this Event</ModalTitle>
|
||||||
|
<ModalSubtitle>Invite other group members to events online or in real life.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarEventForm>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import {randomId, HOUR} from "@welshman/lib"
|
||||||
|
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||||
|
import {publishThunk} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {daysBetween} from "@lib/util"
|
||||||
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
|
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
|
import {PROTECTED} from "@app/core/state"
|
||||||
|
import {makeEditor} from "@app/editor"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h?: string
|
||||||
|
header: Snippet
|
||||||
|
initialValues?: {
|
||||||
|
d: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
location: string
|
||||||
|
start: number
|
||||||
|
end: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if ($uploading) return
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide a title.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!start || !end) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide start and end times.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= end) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "End time must be later than start time.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ed = await editor
|
||||||
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
const tags = [
|
||||||
|
["d", initialValues?.d || randomId()],
|
||||||
|
["title", title],
|
||||||
|
["location", location || ""],
|
||||||
|
["start", start.toString()],
|
||||||
|
["end", end.toString()],
|
||||||
|
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
||||||
|
...ed.storage.nostr.getEditorTags(),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (await shouldProtect) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h) {
|
||||||
|
tags.push(["h", h])
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||||
|
|
||||||
|
pushToast({message: "Your event has been saved!"})
|
||||||
|
publishThunk({event, relays: [url]})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = initialValues?.content || ""
|
||||||
|
const editor = makeEditor({url, submit, uploading, content})
|
||||||
|
|
||||||
|
let title = $state(initialValues?.title || "")
|
||||||
|
let location = $state(initialValues?.location || "")
|
||||||
|
let start: number | undefined = $state(initialValues?.start)
|
||||||
|
let end: number | undefined = $state(initialValues?.end)
|
||||||
|
let endDirty = Boolean(initialValues?.end)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!endDirty && start) {
|
||||||
|
end = start + HOUR
|
||||||
|
} else if (end) {
|
||||||
|
endDirty = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal tag="form" novalidate onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalBody>
|
||||||
|
{@render header()}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Title*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={title} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Summary</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div
|
||||||
|
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
||||||
|
<div class="input-editor flex-grow overflow-hidden">
|
||||||
|
<EditorContent {editor} />
|
||||||
|
</div>
|
||||||
|
<Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}>
|
||||||
|
{#if $uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={GallerySend} />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Start*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<DateTimeInput bind:value={start} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
End*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<DateTimeInput bind:value={end} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Location (optional)</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={MapPoint} />
|
||||||
|
<input bind:value={location} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
||||||
|
<Spinner loading={$uploading}>Save Event</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {fromPairs} from "@welshman/lib"
|
import {
|
||||||
|
fromPairs,
|
||||||
|
formatTimestamp,
|
||||||
|
formatTimestampAsDate,
|
||||||
|
formatTimestampAsTime,
|
||||||
|
} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
@@ -12,13 +17,20 @@
|
|||||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
const start = $derived(parseInt(meta.start))
|
const start = $derived(parseInt(meta.start))
|
||||||
const end = $derived(parseInt(meta.end))
|
const end = $derived(parseInt(meta.end))
|
||||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
|
||||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
|
||||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="text-xl">{meta.title || meta.name}</p>
|
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<p class="text-xl">{meta.title || meta.name}</p>
|
||||||
<Icon icon="clock-circle" size={4} />
|
{#if !isNaN(start) && !isNaN(end)}
|
||||||
{formatTimestampAsTime(start)} — {isSingleDay ? formatTimestampAsTime(end) : formatTimestamp(end)}
|
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||||
|
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||||
|
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<Icon icon={ClockCircle} size={4} />
|
||||||
|
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||||
|
{formatTimestampAsTime(start)} — {isSingleDay
|
||||||
|
? formatTimestampAsTime(end)
|
||||||
|
: formatTimestamp(end)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue, getAddress} from "@welshman/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import {makeCalendarPath} from "@app/routes"
|
import RoomLink from "@app/components/RoomLink.svelte"
|
||||||
|
import {makeCalendarPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -12,15 +14,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {url, event}: Props = $props()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const h = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
<Link
|
||||||
<div class="flex items-center justify-between gap-2">
|
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||||
<CalendarEventHeader {event} />
|
href={makeCalendarPath(url, getAddress(event))}>
|
||||||
</div>
|
<CalendarEventHeader {event} />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
Posted by <ProfileLink pubkey={event.pubkey} />
|
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||||
|
{#if h}
|
||||||
|
in <RoomLink {url} {h} />
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<CalendarEventActions showActivity {url} {event} />
|
<CalendarEventActions showActivity {url} {event} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {fromPairs} from "@welshman/lib"
|
import {fromPairs} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
|
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event}: Props = $props()
|
const {event, url}: Props = $props()
|
||||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<div class="flex min-w-0 flex-col gap-1 text-sm opacity-75">
|
||||||
Posted by <ProfileLink pubkey={event.pubkey} />
|
|
||||||
</span>
|
|
||||||
{#if meta.location}
|
|
||||||
<span>•</span>
|
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<Icon icon="map-point" size={4} />
|
<Icon icon={UserCircle} size={4} />
|
||||||
{meta.location}
|
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{#if meta.location}
|
||||||
|
<span class="flex items-start gap-1">
|
||||||
|
<Icon icon={MapPoint} class="mt-[2px]" size={4} />
|
||||||
|
<span class="break-words">{meta.location}</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {hash} from "@welshman/lib"
|
|
||||||
import {now} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {
|
|
||||||
thunks,
|
|
||||||
pubkey,
|
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
formatTimestampAsDate,
|
|
||||||
formatTimestampAsTime,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {isMobile} from "@lib/html"
|
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Content from "@app/components/Content.svelte"
|
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
|
||||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
|
||||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
|
||||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
|
||||||
import {colors} from "@app/state"
|
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
url: any
|
|
||||||
room: any
|
|
||||||
event: TrustedEvent
|
|
||||||
replyTo?: any
|
|
||||||
showPubkey?: boolean
|
|
||||||
inert?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url, room, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
|
||||||
const today = formatTimestampAsDate(now())
|
|
||||||
const profile = deriveProfile(event.pubkey)
|
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
|
||||||
|
|
||||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
|
||||||
|
|
||||||
if (reaction) {
|
|
||||||
publishDelete({relays: [url], event: reaction})
|
|
||||||
} else {
|
|
||||||
publishReaction({event, content, relays: [url]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<LongPress
|
|
||||||
data-event={event.id}
|
|
||||||
onLongPress={inert ? null : onLongPress}
|
|
||||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
|
||||||
<div class="flex w-full gap-3 overflow-auto">
|
|
||||||
{#if showPubkey}
|
|
||||||
<Button onclick={openProfile} class="flex items-start">
|
|
||||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<div class="w-8 min-w-8 max-w-8"></div>
|
|
||||||
{/if}
|
|
||||||
<div class="min-w-0 flex-grow pr-1">
|
|
||||||
{#if showPubkey}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
|
||||||
{$profileDisplay}
|
|
||||||
</Button>
|
|
||||||
<span class="text-xs opacity-50">
|
|
||||||
{#if formatTimestampAsDate(event.created_at) === today}
|
|
||||||
Today
|
|
||||||
{:else}
|
|
||||||
{formatTimestampAsDate(event.created_at)}
|
|
||||||
{/if}
|
|
||||||
at {formatTimestampAsTime(event.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="text-sm">
|
|
||||||
<Content {event} relays={[url]} />
|
|
||||||
{#if thunk}
|
|
||||||
<ThunkStatus {thunk} class="mt-2" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row-2 ml-10 mt-1">
|
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
|
||||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
|
||||||
{#if replyTo}
|
|
||||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
|
||||||
<Icon icon="reply" size={4} />
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<ChannelMessageMenuButton {url} {event} />
|
|
||||||
</button>
|
|
||||||
</LongPress>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {noop} from "@welshman/lib"
|
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import {publishReaction} from "@app/commands"
|
|
||||||
|
|
||||||
const {url, room, event} = $props()
|
|
||||||
|
|
||||||
// Tell svelte-check to shut up
|
|
||||||
noop(room)
|
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
|
||||||
<Icon icon="smile-circle" size={4} />
|
|
||||||
</EmojiButton>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
const {url, event, onClick} = $props()
|
|
||||||
|
|
||||||
const report = () => {
|
|
||||||
onClick()
|
|
||||||
pushModal(EventReport, {url, event})
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInfo = () => {
|
|
||||||
onClick()
|
|
||||||
pushModal(EventInfo, {event})
|
|
||||||
}
|
|
||||||
|
|
||||||
const showDelete = () => {
|
|
||||||
onClick()
|
|
||||||
pushModal(ConfirmDelete, {url, event})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
|
||||||
<li>
|
|
||||||
<Button onclick={showInfo}>
|
|
||||||
<Icon size={4} icon="code-2" />
|
|
||||||
Message Details
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{#if event.pubkey === $pubkey}
|
|
||||||
<li>
|
|
||||||
<Button onclick={showDelete} class="text-error">
|
|
||||||
<Icon size={4} icon="trash-bin-2" />
|
|
||||||
Delete Message
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{:else}
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={report}>
|
|
||||||
<Icon size={4} icon="danger" />
|
|
||||||
Report Content
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
|
||||||
import {publishReaction} from "@app/commands"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
const {url, event, reply} = $props()
|
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
|
||||||
history.back()
|
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
|
||||||
}
|
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
|
||||||
|
|
||||||
const sendReply = () => {
|
|
||||||
history.back()
|
|
||||||
reply()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
|
||||||
|
|
||||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-2">
|
|
||||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
|
||||||
<Icon size={4} icon="smile-circle" />
|
|
||||||
Send Reaction
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
|
||||||
<Icon size={4} icon="reply" />
|
|
||||||
Send Reply
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
|
||||||
<Icon size={4} icon="code-2" />
|
|
||||||
Message Details
|
|
||||||
</Button>
|
|
||||||
{#if event.pubkey === $pubkey}
|
|
||||||
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
|
||||||
<Icon size={4} icon="trash-bin-2" />
|
|
||||||
Delete Message
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
|
||||||
|
|
||||||
const {url, room} = $props()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if room === GENERAL}
|
|
||||||
general
|
|
||||||
{:else}
|
|
||||||
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
|
||||||
{/if}
|
|
||||||
@@ -1,51 +1,73 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
|
import {
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
int,
|
||||||
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
ms,
|
||||||
|
partition,
|
||||||
|
spec,
|
||||||
|
nthEq,
|
||||||
|
nthNe,
|
||||||
|
MINUTE,
|
||||||
|
sortBy,
|
||||||
|
remove,
|
||||||
|
enumerate,
|
||||||
|
formatTimestampAsDate,
|
||||||
|
} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||||
|
import {parse, isLink} from "@welshman/content"
|
||||||
|
import {
|
||||||
|
makeEvent,
|
||||||
|
tagsFromIMeta,
|
||||||
|
getTags,
|
||||||
|
DIRECT_MESSAGE,
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
tagPubkey,
|
tagPubkey,
|
||||||
formatTimestampAsDate,
|
sendWrapped,
|
||||||
inboxRelaySelectionsByPubkey,
|
mergeThunks,
|
||||||
load,
|
loadMessagingRelayList,
|
||||||
|
messagingRelayListsByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChannelComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
||||||
import {sendWrapped, prependParent} from "@app/commands"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {prependParent} from "@app/core/commands"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
const {
|
type Props = {
|
||||||
id,
|
pubkeys: string[]
|
||||||
info,
|
|
||||||
}: {
|
|
||||||
id: string
|
|
||||||
info?: Snippet
|
info?: Snippet
|
||||||
} = $props()
|
}
|
||||||
|
|
||||||
const chat = deriveChat(id)
|
const {pubkeys, info}: Props = $props()
|
||||||
const pubkeys = splitChatId(id)
|
|
||||||
|
const chat = deriveChat(pubkeys)
|
||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
const missingRelayLists = $derived(pubkeys.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||||
|
|
||||||
const assertEvent = (e: any) => e as TrustedEvent
|
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
others.length === 1
|
||||||
|
? pushModal(ProfileDetail, {pubkey: others[0]})
|
||||||
|
: pushModal(ChatMembers, {pubkeys: others})
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
parent = event
|
parent = event
|
||||||
@@ -57,13 +79,64 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const onSubmit = async (params: EventContent) => {
|
||||||
// Remove p tags since they result in forking the conversation
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
|
||||||
|
|
||||||
await sendWrapped({
|
// Remove p tags since they result in forking the conversation
|
||||||
pubkeys,
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
|
|
||||||
delay: $userSettingValues.send_delay,
|
// Add our reply quote to content
|
||||||
|
params = prependParent(parent, params)
|
||||||
|
|
||||||
|
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||||
|
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||||
|
const templates: EventTemplate[] = []
|
||||||
|
const buffer = []
|
||||||
|
|
||||||
|
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||||
|
content = content.trim()
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of parse(params)) {
|
||||||
|
const imeta = isLink(p)
|
||||||
|
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (isLink(p) && imeta) {
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
addTemplate(
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
p.value.url.toString(),
|
||||||
|
imeta.slice(1).filter(nthNe(0, "url")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
buffer.push(p.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
|
||||||
|
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||||
|
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||||
|
const thunks = await Promise.all(
|
||||||
|
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||||
|
sendWrapped({
|
||||||
|
event,
|
||||||
|
recipients: pubkeys,
|
||||||
|
delay: $userSettingsValues.send_delay + ms(i),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
timeout: 30_000,
|
||||||
|
children: {
|
||||||
|
component: ThunkToast,
|
||||||
|
props: {thunk: mergeThunks(thunks)},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
clearParent()
|
clearParent()
|
||||||
@@ -72,6 +145,8 @@
|
|||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
@@ -92,7 +167,7 @@
|
|||||||
id,
|
id,
|
||||||
type: "note",
|
type: "note",
|
||||||
value: event,
|
value: event,
|
||||||
showPubkey: created_at - previousCreatedAt > int(15, MINUTE) || previousPubkey !== pubkey,
|
showPubkey: created_at - previousCreatedAt > int(2, MINUTE) || previousPubkey !== pubkey,
|
||||||
})
|
})
|
||||||
|
|
||||||
previousDate = date
|
previousDate = date
|
||||||
@@ -104,8 +179,23 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Don't use loadInboxRelaySelection because we want to force reload
|
for (const pubkey of others) {
|
||||||
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
loadMessagingRelayList(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
if (dynamicPadding && chatCompose) {
|
||||||
|
dynamicPadding.style.minHeight = `${chatCompose.offsetHeight}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(chatCompose!)
|
||||||
|
observer.observe(dynamicPadding!)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(chatCompose!)
|
||||||
|
observer.unobserve(dynamicPadding!)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -113,106 +203,108 @@
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex h-full w-full flex-col">
|
<PageBar>
|
||||||
{#if others.length > 0}
|
{#snippet title()}
|
||||||
<PageBar>
|
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||||
{#snippet title()}
|
{#if others.length === 0}
|
||||||
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
<div class="row-2">
|
||||||
{#if others.length === 1}
|
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||||
{@const pubkey = others[0]}
|
<ProfileName pubkey={$pubkey!} />
|
||||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
</div>
|
||||||
<Button onclick={onClick} class="row-2">
|
{:else if others.length === 1}
|
||||||
<ProfileCircle {pubkey} size={5} />
|
<div class="row-2">
|
||||||
<ProfileName {pubkey} />
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
</Button>
|
<ProfileName pubkey={others[0]} />
|
||||||
{:else}
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
{:else}
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
<div class="flex items-center gap-2">
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
<ProfileName pubkey={others[0]} />
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
and
|
<ProfileName pubkey={others[0]} />
|
||||||
{#if others.length === 2}
|
and
|
||||||
<ProfileName pubkey={others[1]} />
|
{#if others.length === 2}
|
||||||
{:else}
|
<ProfileName pubkey={others[1]} />
|
||||||
{others.length - 1}
|
{:else}
|
||||||
{others.length > 2 ? "others" : "other"}
|
{others.length - 1}
|
||||||
{/if}
|
{others.length > 2 ? "others" : "other"}
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{#if others.length > 2}
|
|
||||||
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
|
||||||
>Show all members</Button>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet action()}
|
|
||||||
<div>
|
|
||||||
{#if remove($pubkey, missingInboxes).length > 0}
|
|
||||||
{@const count = remove($pubkey, missingInboxes).length}
|
|
||||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
|
||||||
<div
|
|
||||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
|
||||||
data-tip="{count} {label} not configured.">
|
|
||||||
<Icon icon="danger" />
|
|
||||||
{count}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</PageBar>
|
|
||||||
{/if}
|
|
||||||
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
|
|
||||||
{#if missingInboxes.includes($pubkey!)}
|
|
||||||
<div class="py-12">
|
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
|
||||||
<p class="row-2 text-lg text-error">
|
|
||||||
<Icon icon="danger" />
|
|
||||||
Your inbox is not configured.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
|
||||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your
|
|
||||||
inbox.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if missingInboxes.length > 0}
|
|
||||||
<div class="py-12">
|
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
|
||||||
<p class="row-2 text-lg text-error">
|
|
||||||
<Icon icon="danger" />
|
|
||||||
{missingInboxes.length}
|
|
||||||
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
|
||||||
sure everyone in this conversation has set up their inbox relays.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet action()}
|
||||||
|
{#if remove($pubkey, missingRelayLists).length > 0}
|
||||||
|
{@const count = remove($pubkey, missingRelayLists).length}
|
||||||
|
{@const label = count > 1 ? "lists are" : "list is"}
|
||||||
|
<div
|
||||||
|
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||||
|
data-tip="{count} messaging {label} not configured.">
|
||||||
|
<Icon icon={Danger} />
|
||||||
|
{count}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
{/snippet}
|
||||||
{#if type === "date"}
|
</PageBar>
|
||||||
<Divider>{value}</Divider>
|
|
||||||
{:else}
|
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||||
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
|
<div bind:this={dynamicPadding}></div>
|
||||||
{/if}
|
{#if missingRelayLists.includes($pubkey!)}
|
||||||
{/each}
|
<div class="py-12">
|
||||||
<p
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
<p class="row-2 text-lg text-error">
|
||||||
<Spinner {loading}>
|
<Icon icon={Danger} />
|
||||||
{#if loading}
|
Your messaging relays are not configured.
|
||||||
Looking for messages...
|
</p>
|
||||||
{:else}
|
<p>
|
||||||
End of message history
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||||
{/if}
|
your <Link class="link" href="/settings/relays">relay settings page</Link> to receive messages.
|
||||||
</Spinner>
|
</p>
|
||||||
{@render info?.()}
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
{:else if missingRelayLists.length > 0}
|
||||||
{#if parent}
|
<div class="py-12">
|
||||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
|
<p class="row-2 text-lg text-error">
|
||||||
|
<Icon icon={Danger} />
|
||||||
|
{missingRelayLists.length} messaging
|
||||||
|
{missingRelayLists.length > 1 ? "lists are" : "list is"} not configured.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
||||||
|
sure everyone in this conversation has set up their messaging relays.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#each elements as { type, id, value, showPubkey } (id)}
|
||||||
|
{#if type === "date"}
|
||||||
|
<Divider>{value}</Divider>
|
||||||
|
{:else}
|
||||||
|
<ChatMessage
|
||||||
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
|
{pubkeys}
|
||||||
|
{showPubkey}
|
||||||
|
{replyTo} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
|
<Spinner {loading}>
|
||||||
|
{#if loading}
|
||||||
|
Looking for messages...
|
||||||
|
{:else}
|
||||||
|
End of message history
|
||||||
|
{/if}
|
||||||
|
</Spinner>
|
||||||
|
{@render info?.()}
|
||||||
|
</p>
|
||||||
|
</PageContent>
|
||||||
|
|
||||||
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||||
|
<div>
|
||||||
|
{#if parent}
|
||||||
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
<ChatCompose bind:this={compose} {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import type {EventContent} from "@welshman/util"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
|
import Plane from "@assets/icons/plane-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
onSubmit: any
|
onSubmit: (event: EventContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onSubmit}: Props = $props()
|
const {onSubmit}: Props = $props()
|
||||||
@@ -16,24 +19,31 @@
|
|||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
export const focus = () => editor.chain().focus().run()
|
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||||
|
|
||||||
const uploadFiles = () => editor.chain().selectFiles().run()
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
const content = editor.getText({blockSeparator: "\n"}).trim()
|
const ed = await editor
|
||||||
const tags = editor.storage.nostr.getEditorTags()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
const tags = ed.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
||||||
onSubmit({content, tags})
|
onSubmit({content, tags})
|
||||||
|
|
||||||
editor.chain().clearContent().run()
|
ed.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({autofocus, submit, uploading, aggressive: true})
|
const editor = makeEditor({
|
||||||
|
autofocus,
|
||||||
|
submit,
|
||||||
|
uploading,
|
||||||
|
aggressive: true,
|
||||||
|
encryptFiles: true,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
@@ -45,7 +55,7 @@
|
|||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="gallery-send" />
|
<Icon icon={GallerySend} />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
@@ -56,6 +66,6 @@
|
|||||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||||
disabled={$uploading}
|
disabled={$uploading}
|
||||||
onclick={submit}>
|
onclick={submit}>
|
||||||
<Icon icon="plain" />
|
<Icon icon={Plane} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {displayProfileByPubkey} from "@welshman/app"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {slide} from "@lib/transition"
|
import {slide} from "@lib/transition"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
verb,
|
verb,
|
||||||
@@ -18,18 +19,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
||||||
transition:slide>
|
transition:slide>
|
||||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
{#key event.id}
|
{#key event.id}
|
||||||
<NoteContent
|
<NoteContentMinimal trimParent {event} />
|
||||||
{event}
|
|
||||||
hideMediaAtDepth={0}
|
|
||||||
minLength={100}
|
|
||||||
maxLength={300}
|
|
||||||
expandMode="disabled" />
|
|
||||||
{/key}
|
{/key}
|
||||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||||
<Icon icon="close-circle" />
|
<Icon icon={CloseCircle} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,36 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {WRAP} from "@welshman/util"
|
|
||||||
import {repository} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {shouldUnwrap} from "@welshman/app"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
const {next} = $props()
|
const {next} = $props()
|
||||||
|
|
||||||
|
const nextUrl = $state.snapshot(next)
|
||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
||||||
const enableChat = async () => {
|
|
||||||
canDecrypt.set(true)
|
|
||||||
|
|
||||||
for (const event of repository.query([{kinds: [WRAP]}])) {
|
|
||||||
ensureUnwrapped(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearModals()
|
|
||||||
goto(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await enableChat()
|
shouldUnwrap.set(true)
|
||||||
|
clearModals()
|
||||||
|
goto(nextUrl)
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -39,31 +37,29 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
<ModalHeader>
|
<ModalBody>
|
||||||
{#snippet title()}
|
<ModalHeader>
|
||||||
<div>Enable Messages</div>
|
<ModalTitle>Enable Messages</ModalTitle>
|
||||||
{/snippet}
|
<ModalSubtitle>Do you want to enable direct messages?</ModalSubtitle>
|
||||||
{#snippet info()}
|
</ModalHeader>
|
||||||
<div>Do you want to enable direct messages?</div>
|
<p>
|
||||||
{/snippet}
|
By default, direct messages are disabled, since loading them requires
|
||||||
</ModalHeader>
|
{PLATFORM_NAME} to download and decrypt a lot of data.
|
||||||
<p>
|
</p>
|
||||||
By default, direct messages are disabled, since loading them requires
|
<p>
|
||||||
{PLATFORM_NAME} to download and decrypt a lot of data.
|
If you'd like to enable them, please make sure your signer is set up to to auto-approve
|
||||||
</p>
|
requests to decrypt data.
|
||||||
<p>
|
</p>
|
||||||
If you'd like to enable them, please make sure your signer is set up to to auto-approve requests
|
</ModalBody>
|
||||||
to decrypt data.
|
|
||||||
</p>
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
<Spinner {loading}>Enable Messages</Spinner>
|
<Spinner {loading}>Enable Messages</Spinner>
|
||||||
<Icon icon="alt-arrow-right" />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</Modal>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {remove} from "@welshman/lib"
|
import {remove, uniq, formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
import {pubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
const {...props}: Props = $props()
|
const {...props}: Props = $props()
|
||||||
|
|
||||||
const others = remove($pubkey!, props.pubkeys)
|
const others = uniq(remove($pubkey!, props.pubkeys))
|
||||||
const active = $page.params.chat === props.id
|
const active = $derived($page.params.chat === props.id)
|
||||||
const path = makeChatPath(props.pubkeys)
|
const path = makeChatPath(props.pubkeys)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
loadInboxRelaySelections(pk)
|
loadMessagingRelayList(pk)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
{#if others.length === 0}
|
{#if others.length === 0}
|
||||||
<ProfileCircle pubkey={$pubkey} size={5} />
|
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||||
Note to self
|
Note to self
|
||||||
{:else if others.length === 1}
|
{:else if others.length === 1}
|
||||||
<ProfileCircle pubkey={others[0]} size={5} />
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
@@ -59,8 +59,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||||
|
<span class="opacity-70">
|
||||||
|
{#if props.messages[0].pubkey === $pubkey}
|
||||||
|
You:
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
{props.messages[0].content}
|
{props.messages[0].content}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="text-xs opacity-70">
|
||||||
|
{formatTimestamp(props.messages[0].created_at)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pubkeys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {pubkeys}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>People in this conversation</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each pubkeys as pubkey (pubkey)}
|
||||||
|
<div class="card2 bg-alt">
|
||||||
|
<Profile {pubkey} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {assoc} from "@welshman/lib"
|
||||||
|
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
||||||
|
import Check from "@assets/icons/check.svg?dataurl"
|
||||||
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
|
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ChatStart from "@app/components/ChatStart.svelte"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {notificationSettings} from "@app/core/state"
|
||||||
|
|
||||||
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
|
|
||||||
|
const markAsRead = () => {
|
||||||
|
setChecked("/chat/*")
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableAlerts = () => notificationSettings.update(assoc("messages", true))
|
||||||
|
|
||||||
|
const disableAlerts = () => notificationSettings.update(assoc("messages", false))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Button class="btn btn-primary" onclick={startChat}>
|
||||||
|
<Icon size={5} icon={ChatSquare} />
|
||||||
|
Start chat
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral" onclick={markAsRead}>
|
||||||
|
<Icon size={5} icon={Check} />
|
||||||
|
Mark all read
|
||||||
|
</Button>
|
||||||
|
{#if $notificationSettings.messages}
|
||||||
|
<Button class="btn btn-neutral" onclick={disableAlerts}>
|
||||||
|
<Icon size={4} icon={BellOff} />
|
||||||
|
Disable alerts
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-neutral" onclick={enableAlerts}>
|
||||||
|
<Icon size={4} icon={Bell} />
|
||||||
|
Enable alerts
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import ChatStart from "@app/components/ChatStart.svelte"
|
|
||||||
import {setChecked} from "@app/notifications"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
|
||||||
|
|
||||||
const markAsRead = () => {
|
|
||||||
setChecked("/chat/*")
|
|
||||||
history.back()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-2">
|
|
||||||
<Button class="btn btn-primary" onclick={startChat}>
|
|
||||||
<Icon size={4} icon="add-circle" />
|
|
||||||
Start chat
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-neutral" onclick={markAsRead}>
|
|
||||||
<Icon size={4} icon="check-circle" />
|
|
||||||
Mark all read
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
@@ -1,55 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {hash} from "@welshman/lib"
|
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {
|
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||||
thunks,
|
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
formatTimestampAsTime,
|
|
||||||
pubkey,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||||
import {colors} from "@app/state"
|
import {colors} from "@app/core/state"
|
||||||
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
import {makeDelete, makeReaction} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo?: any
|
replyTo: (event: TrustedEvent) => void
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, replyTo = undefined, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profile = deriveProfile(event.pubkey)
|
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||||
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
|
|
||||||
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
const reply = () => replyTo(event)
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
|
||||||
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
|
||||||
|
|
||||||
await sendWrapped({template, pubkeys})
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
}
|
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
||||||
|
|
||||||
|
const createReaction = (template: EventContent) =>
|
||||||
|
sendWrapped({event: makeReaction({event, protect: false, ...template}), recipients: pubkeys})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
@@ -64,7 +59,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkStatus {thunk} class="mt-1" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-1" />
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
@@ -72,38 +67,40 @@
|
|||||||
class:chat-start={!isOwn}
|
class:chat-start={!isOwn}
|
||||||
class:flex-row-reverse={!isOwn}
|
class:flex-row-reverse={!isOwn}
|
||||||
class:chat-end={isOwn}>
|
class:chat-end={isOwn}>
|
||||||
<Tippy
|
{#if !isMobile}
|
||||||
bind:popover
|
<Tippy
|
||||||
component={ChatMessageMenu}
|
bind:popover
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
component={ChatMessageMenu}
|
||||||
params={{
|
props={{event, pubkeys, popover, replyTo}}
|
||||||
interactive: true,
|
params={{
|
||||||
trigger: "manual",
|
interactive: true,
|
||||||
onShow() {
|
trigger: "manual",
|
||||||
popoverIsVisible = true
|
onShow() {
|
||||||
},
|
popoverIsVisible = true
|
||||||
onHidden() {
|
},
|
||||||
popoverIsVisible = false
|
onHidden() {
|
||||||
},
|
popoverIsVisible = false
|
||||||
}}>
|
},
|
||||||
<button
|
}}>
|
||||||
type="button"
|
<button
|
||||||
class="opacity-0 transition-all"
|
type="button"
|
||||||
class:group-hover:opacity-100={!isMobile}
|
class="opacity-0 transition-all"
|
||||||
onclick={togglePopover}>
|
class:group-hover:opacity-100={!isMobile}
|
||||||
<Icon icon="menu-dots" size={4} />
|
onclick={togglePopover}>
|
||||||
</button>
|
<Icon icon={MenuDots} size={4} />
|
||||||
</Tippy>
|
</button>
|
||||||
|
</Tippy>
|
||||||
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<LongPress
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||||
onLongPress={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if !isOwn}
|
{#if !isOwn}
|
||||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||||
<Avatar
|
<ProfileCircle
|
||||||
src={$profile?.picture}
|
pubkey={event.pubkey}
|
||||||
class="border border-solid border-base-content"
|
class="border border-solid border-base-content"
|
||||||
size={4} />
|
size={4} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -120,9 +117,9 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content showEntire {event} />
|
<Content showEntire {event} />
|
||||||
</div>
|
</div>
|
||||||
</LongPress>
|
</TapTarget>
|
||||||
<div class="row-2 z-feature -mt-1 ml-4">
|
<div class="row-2 z-feature -mt-4 ml-4">
|
||||||
<ReactionSummary {event} {onReactionClick} noTooltip />
|
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||