Compare commits
581 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1110ffa8af
|
|||
| 84cfeb1604 | |||
| 4c37bfc128 | |||
| 548af2bf9d | |||
| 692146fe00 | |||
| 40134b4365 | |||
| 9a547077c1 | |||
| 0d71cc18ad | |||
| d10554ab6c | |||
| 2656c30832 | |||
| 39b6dfb47e | |||
| 5ca5420ce2 | |||
| 4703ed80a7 | |||
| f7e407e030 | |||
| e547e26d99 | |||
| f6044a9eea | |||
| 26bd50c948 | |||
| 6e0af0ba10 | |||
| 44b7ae2054 | |||
| 9cc21fc860 | |||
| c9526b7aa6 | |||
| 055b7af1a3 | |||
| de0b1dbda2 | |||
| 7605af84b5 | |||
| d45eadef35 | |||
| 9cae934062 | |||
| 7fb5cdf6c0 | |||
| 49bbe62d2a | |||
| 705accd309 | |||
| 3375ccc4fa | |||
| a9fecc3047 | |||
| 31b3ad9825 | |||
| 2bde3a9217 | |||
| 6050116314 | |||
| a2cac142c0 | |||
| 8a20e5845e | |||
| 641e2564fb | |||
| 50810033c0 | |||
| fab4e231b6 | |||
| 42ff49a803 | |||
| 8c878cbc4c | |||
| face4268bf | |||
| fed6c47835 | |||
| 8e9fb308f9 | |||
| 89b48db92d | |||
| 9581cc994d | |||
| 34e32bc930 | |||
| dfcef0ba95 | |||
| 3c11ba53ce | |||
| 9759787c95 | |||
| eef428ce4f | |||
| d69647e071 | |||
| c22f5e90a3 | |||
| f2fe02032e | |||
| da2bdad18d | |||
| c7cc8df5ba | |||
| 92df446d72 | |||
| 7ea2af6172 | |||
| 184eea6e68 | |||
| 82372d1bf5 | |||
| 39f59eb798 | |||
| 639deec1a2 | |||
| 18780002bb | |||
| 722180bb9a | |||
| 366a584934 | |||
| 9ee09c3b59 | |||
| e8caf3a7f4 | |||
| b4ff6ee614 | |||
| ed652db3d3 | |||
| 3d01c29148 | |||
| 1c1bb599ed | |||
| 25e6c77d9b | |||
| 5aae81c47d | |||
| 6b2fd4cec1 | |||
| d486af6704 | |||
| 323f920848 | |||
| c58c200acb | |||
| c3786bf849 | |||
| a0e882db64 | |||
| eedf734dae | |||
| cfa06797b7 | |||
| 824279742c | |||
| 2cdbadd09d | |||
| 1ea70c8427 | |||
| 09876c06d0 | |||
| 7a063f8aa0 | |||
| b8ac026a3d | |||
| c18853c957 | |||
| 0a09dbfe1c | |||
| 78e840734a | |||
| 1ccb300dd1 | |||
| 049a32db41 | |||
| d82add1080 | |||
|
9d42715d76
|
|||
|
14fd06c052
|
|||
|
e2ab3a41b4
|
|||
|
6d7c2af504
|
|||
|
f5fbd1d3c1
|
|||
|
c4333280dd
|
|||
|
6b6a98b71f
|
|||
| 3e5029a4ad | |||
| 05b2cb6376 | |||
| c4af40e64f | |||
| afc42d1952 | |||
| 579303f741 | |||
| 104f490e86 | |||
| a07b78e47f | |||
| 4e447ddbed | |||
| f6b59b3f5d | |||
| e40d5b3e83 | |||
| 4bf8a68c9c | |||
| 0a9ac9cb0d | |||
| 9c3b052de2 | |||
| 59cde41764 | |||
| 9502fc30ba | |||
| 6b8cf51720 | |||
| f72b297d77 | |||
| dd78272a5e | |||
| 65be56ba7c | |||
| 8e361a9586 | |||
| 9bbeffe320 | |||
| 3e5d7581ba | |||
| 06445de197 | |||
| e537c7cef4 | |||
| 01c239c0eb | |||
| bec92249f9 | |||
| aa0b9bde8f | |||
| 4e4e8ed460 | |||
| 3605edad8b | |||
| 04c207a11a | |||
| 9d208284c6 | |||
| 213a26cd01 | |||
| b74bde5cc4 | |||
| 0e0c53145f | |||
| 76aa1d3450 | |||
| fa0d5e7d03 | |||
| 8446db7cbc | |||
| 8679c9f293 | |||
| d541153e4c | |||
| 53fc1b6945 | |||
| ff60f8a2db | |||
| bfd78c01ca | |||
| a2af030367 | |||
| 3c2e8b728f | |||
| 8269ca59cd | |||
| 0f9d55d4f9 | |||
| 4109649dc2 | |||
| 466dfcb7d7 | |||
| 27905d24b4 | |||
| 7a5aef94a8 | |||
| 9e4f0122f5 | |||
| a80ddc08ec | |||
| 6daa4f7e13 | |||
| 9686f82e8f | |||
| a1e6be214e | |||
| 87860a7151 | |||
| ad75d8546c | |||
| 878b1caa95 | |||
| 1fcbba5041 | |||
| 20299615ba | |||
| 972a183ed8 | |||
| f976f23854 | |||
| cf13e1ca61 | |||
| 309cbaccce | |||
| d8640e2a1e | |||
| bc330ab5de | |||
| 41c76d9de0 | |||
| eaaf802157 | |||
| bf59b5850c | |||
| 2585a375ab | |||
| 29fa4ecf2e | |||
| 518fdffce9 | |||
| 289e051202 | |||
| 2a6c4d0b61 | |||
| 386a28455a | |||
| b2e555284b | |||
| cc385d3c3f | |||
| 2895c374c0 | |||
| 6863e74c0f | |||
| 280d889f25 | |||
| d1d830fca1 | |||
| d2bb013db7 | |||
| c437a05ec0 | |||
| a2fdb61013 | |||
| 4dd800e6b9 | |||
| 34c0728f21 | |||
| ec604664d8 | |||
| 7710839261 | |||
| 4389cc2128 | |||
| 76508dbbfd | |||
| bbccc27a26 | |||
| 9969e70b5f | |||
| 692d29942b | |||
| 139df33cb7 | |||
| a324523b85 | |||
| 1cf898e0b2 | |||
| 502ceee6d4 | |||
| 4f628ec733 | |||
| 29915159db | |||
| 2b102671e5 | |||
| e70f270c5c | |||
| 4ed79ff3c3 | |||
| 17331301da | |||
| 45904e1bf2 | |||
| 7ae66b8490 | |||
| bf43842590 | |||
| 7c98489904 | |||
| 0277303da7 | |||
| 63fee80c53 | |||
| 2aaedd077e | |||
| 06eb9d4a0e | |||
| 3b76fcb743 | |||
| edb23e4e70 | |||
| 82fba88cc4 | |||
| 439f9974c5 | |||
| cf243e39c9 | |||
| 76f6ed0f86 | |||
| ef035b6300 | |||
| 769f03943c | |||
| 6cdf2dca53 | |||
| 05dee129b5 | |||
| 7bed47c919 | |||
| 7f5707294c | |||
| 7a6a6dffbc | |||
| 472f81b311 | |||
| 7744787c51 | |||
| 5d90b497f0 | |||
| a06be64894 | |||
| 0f9e87cb37 | |||
| 1fabd4c0fe | |||
| f90a485b5e | |||
| a18304f4a3 | |||
| 5e3afd0b16 | |||
| b1c7ef9bd9 | |||
| 7ecb9aad62 | |||
| d0daa9fafa | |||
| 76f3cd4edc | |||
| 04917cfbe4 | |||
| 641b255a71 | |||
| 9f2eafc3cb | |||
| e62ee11b06 | |||
| bd94b76d1e | |||
| 5fa138d050 | |||
| deb75df54e | |||
| 3365c72832 | |||
| ca2960cc73 | |||
| ce5855fe3d | |||
| f56b35972d | |||
| 24c2be02bb | |||
| 5395c45df2 | |||
| 1150a144bc | |||
| 89acde1b90 | |||
| ed98fd06e6 | |||
| 35c581066a | |||
| d6c72403a3 | |||
| 768ab3e9e4 | |||
| 66d731ad0a | |||
| 0f86a41c4a | |||
| 957ac1dc03 | |||
| a58ca2918a | |||
| b86bac2e42 | |||
| 8a2e87718b | |||
| 88b3c6fe8d | |||
| 94d448e8d4 | |||
| dda94cc1c1 | |||
| 7f3cc8b7a1 | |||
| 077d1aa1fd | |||
| 7297db946d | |||
| cc59e149d5 | |||
| aca7dde889 | |||
| b2584476ac | |||
| 88fc8e41f7 | |||
| a955b7beb8 | |||
| 36c0307ebd | |||
| c0377d630b | |||
| 7390808630 | |||
| 7444656043 | |||
| 481280f006 | |||
| aaf587c3a9 | |||
| 641049f6b4 | |||
| 433d186f67 | |||
| d157d72ca1 | |||
| 3a459c83df | |||
| 945604afce | |||
| 3945f20ae4 | |||
| f3449ecaed | |||
| 19857c12b7 | |||
| 61612121f4 | |||
| 9dac31d713 | |||
| 9540016eee | |||
| 22fe9f3dfd | |||
| c469e07ff7 | |||
| 201e4420d1 | |||
| e7a948d362 | |||
| 375d454b16 | |||
| 476f52562a | |||
| f591ad2dff | |||
| dacade299d | |||
| cdacbcfdca | |||
| 41e036cff2 | |||
| eb901a4d84 | |||
| 9f15688699 | |||
| 6d055be3cd | |||
| 01c6e3e9ab | |||
| 4377cf28ef | |||
| 6f67c159ff | |||
| 7a85ae29ca | |||
| fafe3b4b3e | |||
| 69c7acea76 | |||
| 22d635d850 | |||
| fc9b9f2940 | |||
| 622a436589 | |||
| 9398877415 | |||
| 129d3ff101 | |||
| bb4fd75576 | |||
| 8586eed635 | |||
| 440e37c1d3 | |||
| 49283f2bb2 | |||
| 305ee03b0e | |||
| a88f5db10b | |||
| d39a3da3b7 | |||
| 40459e247e | |||
| fff4549933 | |||
| c4dfae9ede | |||
| bfda0d1b74 | |||
| 01b8e43a6e | |||
| aa4ecc2139 | |||
| 617dee3e6b | |||
| 510432bb98 | |||
| c4a9f2fdb2 | |||
| b1e0a62109 | |||
| 1fc5ceff3b | |||
| 16edc3fe13 | |||
| 6a88ca2777 | |||
| e3ccf95780 | |||
| 9bac83352b | |||
| 0803594553 | |||
| 8dad8e6703 | |||
| e30d38e69f | |||
| c13f29e98c | |||
| 5b901656f3 | |||
| 36acdf420e | |||
| 76a6dbc406 | |||
| 1b1d4bd6d1 | |||
| 14586b616c | |||
| 7baf7e66dc | |||
| 4263b9690f | |||
| 7f6a702412 | |||
| 6f35de65f9 | |||
| 65f3651896 | |||
| 94ce604b9d | |||
| b934d66f64 | |||
| 20b6627799 | |||
| 3e15f15a57 | |||
| 5c87b8e610 | |||
| 42234b1cf3 | |||
| 54ba64535d | |||
| 9cf53a9e93 | |||
| 3569da5687 | |||
| f1f3abfb98 | |||
| dec07df2c1 | |||
| 53734ea483 | |||
| b18a0c573e | |||
| f6f7d13f12 | |||
| 6ee0be40e9 | |||
| a64f898df7 | |||
| dd29e87146 | |||
| c71b0ee916 | |||
| 8e92e28faf | |||
| 5657512370 | |||
| 882f6e2534 | |||
| 2f60888fb1 | |||
| ba6792640d | |||
| 984c7b6932 | |||
| 0bbc2c6348 | |||
| c44c0d0863 | |||
| 50d55572be | |||
| caffa0398b | |||
| 92bbc9766d | |||
| 699f77d9e1 | |||
| 4c0166bd31 | |||
| 35b67dc08d | |||
| 1f5f1e28a4 | |||
| f30f93f65c | |||
| 7255481705 | |||
| 16fa701509 | |||
| 2c6999e15c | |||
| 981d500c25 | |||
| d02fc9142d | |||
| db59f74970 | |||
| bf3ca4a186 | |||
| 53c2b3a48d | |||
| 23a8d6fb6b | |||
| 042b7da315 | |||
| e62ba5826b | |||
| 1d11bb40b5 | |||
| 0338297bfe | |||
| 59cf8056bd | |||
| d34d417fcc | |||
| b665a40a11 | |||
| 5caa4a6e97 | |||
| c5d8e4a4a1 | |||
| 8b600a9774 | |||
| 286ae68fd6 | |||
| 6ab893a617 | |||
| 9bfb59c4cc | |||
| dcb94635ea | |||
| c464a26151 | |||
| 9104ddb051 | |||
| 1432087edf | |||
| ae2f7255a7 | |||
| d5b944170f | |||
| 9fb1cc5b57 | |||
| 2e512317e7 | |||
| f9eb669132 | |||
| 066b3cdde8 | |||
| 7f313dcbd4 | |||
| 1dabd88355 | |||
| 4f33641244 | |||
| 006a6ef16f | |||
| 7467a9d5b1 | |||
| 9f01cab2be | |||
| 502917012c | |||
| 916f7d789e | |||
| 21eda288c4 | |||
| 25e022d933 | |||
| e642913944 | |||
| 967785392f | |||
| 9e6fbeefcd | |||
| de58e52199 | |||
| 53e9269da6 | |||
| 85930df8e3 | |||
| cf3a9a576d | |||
| e397fc069b | |||
| 2529797dfb | |||
| bd2193251f | |||
| 1a2ac976a3 | |||
| d4faacb99f | |||
| a73271e3d4 | |||
| 624a7b4e88 | |||
| 5b9803d234 | |||
| 3098d4b4fa | |||
| 0178478199 | |||
| d489bcc586 | |||
| 453d540255 | |||
| 5ded564bdc | |||
| 3908192fe2 | |||
| 92020e551b | |||
| ccd52a09d8 | |||
| ced3c76996 | |||
| 29bba15230 | |||
| fb179ac1d4 | |||
| 7900865c02 | |||
| 0350809e82 | |||
| cddb88b890 | |||
| 14ba33674b | |||
| c0f4e3fe03 | |||
| dae2e8ef56 | |||
| b2d2fbee0d | |||
| cebd1f48ca | |||
| 55bbe8f855 | |||
| 39dce64131 | |||
| b556257edd | |||
| cdc4a7b7a4 | |||
| ef5a3030a6 | |||
| f0b8dcc5e9 | |||
| 72b60573de | |||
| 6e6c1eb7b6 | |||
| 07dfa3b1fb | |||
| 88306d00a3 | |||
| 616de2eebc | |||
| 709aab549b | |||
| 15ab9f7135 | |||
| d4aa8a5602 | |||
| a9b4cfd424 | |||
| 2b99f94d13 | |||
| 66e204eb91 | |||
| 7040235605 | |||
| f9d21ef901 | |||
| a08d0a5a19 | |||
| ff20cc4767 | |||
| aacb336002 | |||
| b40c595a7c | |||
| 80063af19a | |||
| df3b94a1fc | |||
| 06a66a3709 | |||
| 1463ce5e3a | |||
| 480921db20 | |||
| f0de8721c7 | |||
| d11cd76e6a | |||
| 815f4d4a96 | |||
| 4fecf72963 | |||
| 593d0e2abe | |||
| 2f8aa29e92 | |||
| e3c04465fc | |||
| 54d40f7ffd | |||
| 2053033b25 | |||
| 45801f3e6c | |||
| 2d44f2744b | |||
| 04e408bfea | |||
| b3c87bdc07 | |||
| b5dd90b36a | |||
| 6fa9149939 | |||
| 1e9e4a7f3a | |||
| c8e236b6d5 | |||
| e8d0f1db8d | |||
| 99b5dc94cb | |||
| e34351ca37 | |||
| 1a33d639ed | |||
| 5c1043b4e5 | |||
| 23b5763a6b | |||
| dd65209a20 | |||
| f0d07c3663 | |||
| b3119fa41e | |||
| 7ec8da6c73 | |||
| 9e659c49b5 | |||
| c72666b352 | |||
| 1854e10486 | |||
| 58e2fb40ef | |||
| af7ea7024f | |||
| 0263c11a94 | |||
| 6d43754e71 | |||
| 4da23390f8 | |||
| c74993366b | |||
| ad0e1f28b7 | |||
| 61051ee853 | |||
| dc7826c4e5 | |||
| 4eee715bcd | |||
| 08bea16be0 | |||
| 8f04b12a90 | |||
| 9cfed9f3aa | |||
| 123ca3b802 | |||
| 5e7b1f4ff3 | |||
| 12594e35c1 | |||
| ab92f7b561 | |||
| 11b9062865 | |||
| 5c5b55bf67 | |||
| dd6c082a8e | |||
| 2a4ee6c48c | |||
| fa520d48d3 | |||
| 160b293359 | |||
| 7d17b9b476 | |||
| d04f1c6867 | |||
| 5c87dd5bbb | |||
| 12febf9671 | |||
| 4033ad66ba | |||
| 2c0296cce3 | |||
| 080aaf2d1b | |||
| 0e55b08b6c | |||
| ff70cb7ebf | |||
| fe82134a75 | |||
| 60a0c21272 | |||
| 8242ca27d2 | |||
| c7baa153af | |||
| ff654c4e11 | |||
| deaf5f042a | |||
| 4f56ff3dfb | |||
| fd59407171 | |||
| 9b759247ee | |||
| cd7998b69d | |||
| bd4c29604f | |||
| bf1175f22c | |||
| 064888f78d | |||
| fc640b85ed | |||
| d5766253cf | |||
| 571ed39d52 | |||
| 16d81ed40f | |||
| 1135c19fea | |||
| 77331644cb | |||
| 8d14fdffb5 | |||
| 0c95071de7 | |||
| da78a217a3 | |||
| f53b824122 | |||
| 45ab394b09 | |||
| 47e7505573 | |||
| 0f1390f412 | |||
| 6bf5293701 | |||
| 3d6909bf62 | |||
| ecd8b64b8b | |||
| 0c627ae0a0 | |||
| 16c86c1d1c |
@@ -1,32 +0,0 @@
|
|||||||
name: Run Test Suite
|
|
||||||
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
- "ci"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run_tests:
|
|
||||||
runs-on: macos-12
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- xcode: "14.2"
|
|
||||||
ios: "16.2"
|
|
||||||
|
|
||||||
name: Test iOS (${{ matrix.ios }})
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
- name: Select Xcode
|
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
|
||||||
with:
|
|
||||||
xcode-version: ${{ matrix.xcode }}
|
|
||||||
- name: Run Tests
|
|
||||||
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=${{ matrix.ios }}' | xcpretty && exit ${PIPESTATUS[0]}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
xcuserdata
|
xcuserdata
|
||||||
/.direnv
|
/.direnv
|
||||||
damus/TestingPrivate.swift
|
damus/TestingPrivate.swift
|
||||||
|
damus.xcodeproj/xcshareddata/xcbaselines
|
||||||
.DS_Store
|
.DS_Store
|
||||||
TODO.bak
|
TODO.bak
|
||||||
tags
|
tags
|
||||||
|
build-git-hash.txt
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ Ben Weeks <ben.weeks@knowall.ai> <ben.weeks@outlook.com>
|
|||||||
Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github.com>
|
Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github.com>
|
||||||
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
||||||
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
||||||
|
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
|
||||||
|
Transifex <transifex@transifex.com> <43880903+transifex-integration[bot]@users.noreply.github.com>
|
||||||
|
|||||||
+276
@@ -1,3 +1,279 @@
|
|||||||
|
## [1.6-25] - 2023-10-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Tap to dismiss keyboard on user status view (ericholguin)
|
||||||
|
- Add setting that allows users to optionally disable the new profile action sheet feature (Daniel D’Aquino)
|
||||||
|
- Add follow button to profile action sheet (Daniel D’Aquino)
|
||||||
|
- Added reaction counters to nostrdb (William Casarin)
|
||||||
|
- Record when profile is last fetched in nostrdb (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Automatically load extra regional Japanese relays during account creation if user's region is set to Japan. (Daniel D’Aquino)
|
||||||
|
- Updated customize zap view (ericholguin)
|
||||||
|
- Users are now notified when you quote repost them (William Casarin)
|
||||||
|
- Save bandwidth by only fetching new profiles after a certain amount of time (William Casarin)
|
||||||
|
- Zap button on profile action sheet now zaps with a single click, while a long press brings custom zap view (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Use white font color in qrcode view (ericholguin)
|
||||||
|
- Fixed an issue where zapping would silently fail on default settings if the user does not have a lightning wallet preinstalled on their device. (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-25]: https://github.com/damus-io/damus/releases/tag/v1.6-25
|
||||||
|
## [1.6-24] - 2023-10-22 - AppStore Rejection Cope
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Improve discoverability of profile zaps with zappability badges and profile action sheets (Daniel D’Aquino)
|
||||||
|
- Add suggested hashtags to universe view (Daniel D’Aquino)
|
||||||
|
- Suggest first post during onboarding (Daniel D’Aquino)
|
||||||
|
- Add expiry date for images in cache to be auto-deleted after a preset time to save space on storage (Daniel D’Aquino)
|
||||||
|
- Add QR scan nsec logins. (Jericho Hasselbush)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved status view design (ericholguin)
|
||||||
|
- Improve clear cache functionality (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Reduce size of event menu hitbox (William Casarin)
|
||||||
|
- Do not show DMs from muted users (Daniel D’Aquino)
|
||||||
|
- Add more spacing between display name and username, and prefix username with `@` character (Daniel D’Aquino)
|
||||||
|
- Broadcast quoted notes when posting a note with quotes (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-24]: https://github.com/damus-io/damus/releases/tag/v1.6-24
|
||||||
|
|
||||||
|
## [1.6-23] - 2023-10-06 - Appstore Release
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added merch store button to sidebar menu (Daniel D’Aquino)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Damus icon now opens sidebar (Daniel D’Aquino)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Stop tab buttons from causing the root view to scroll to the top unless user is coming from another tab or already at the root view (Daniel D’Aquino)
|
||||||
|
- Fix profiles not updating (William Casarin)
|
||||||
|
- Fix issue where relays with trailing slashes cannot be removed (#1531) (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-23]: https://github.com/damus-io/damus/releases/tag/v1.6-23
|
||||||
|
|
||||||
|
## [1.6-20] - 2023-10-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improve UX around clearing cache (Daniel D’Aquino)
|
||||||
|
- Show muted thread replies at the bottom of the thread view (#1522) (Daniel D’Aquino)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix situations where the note composer cursor gets stuck in one place after tagging a user (Daniel D’Aquino)
|
||||||
|
- Fix some note composer issues, such as when copying/pasting larger text, and make the post composer more robust. (Daniel D’Aquino)
|
||||||
|
- Apply filters to hashtag search timeline view (Daniel D’Aquino)
|
||||||
|
- Hide quoted or reposted notes from people whom the user has muted. (#1216) (Daniel D’Aquino)
|
||||||
|
- Fix profile not updating (William Casarin)
|
||||||
|
- Fix small graphical toolbar bug when scrolling profiles (Daniel D’Aquino)
|
||||||
|
- Fix localization issues and export strings for translation (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-20]: https://github.com/damus-io/damus/releases/tag/v1.6-20
|
||||||
|
|
||||||
|
## [1.6-18] - 2023-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add followed hashtags to your following list (Daniel D’Aquino)
|
||||||
|
- Add "Do not show #nsfw tagged posts" setting (Daniel D’Aquino)
|
||||||
|
- Hold tap to preview status URL (Jericho Hasselbush)
|
||||||
|
- Finnish translations (etrikaj)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Switch to nostrdb for @'s and user search (William Casarin)
|
||||||
|
- Use nostrdb for profiles (William Casarin)
|
||||||
|
- Updated relay view (ericholguin)
|
||||||
|
- Increase size of the hitbox on note ellipsis button (Daniel D’Aquino)
|
||||||
|
- Make carousel tab dots tappable (Bryan Montz)
|
||||||
|
- Move the "Follow you" badge into the profile header (Grimless)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix text composer wrapping issue when mentioning npub (Daniel D’Aquino)
|
||||||
|
- Make blurred videos viewable by allowing blur to disappear once tapped (Daniel D’Aquino)
|
||||||
|
- Fix parsing issue with NIP-47 compliant NWC urls without double-slashes (Daniel D’Aquino)
|
||||||
|
- Fix padding of username next to pfp on some views (William Casarin)
|
||||||
|
- Fixes issue where username with multiple emojis would place cursor in strange position. (Jericho Hasselbush)
|
||||||
|
- Fixed audio in video playing twice (Bryan Montz)
|
||||||
|
- Fix crash when long pressing custom reactions (William Casarin)
|
||||||
|
- Fix random crashom due to old profile database (William Casarin)
|
||||||
|
|
||||||
|
[1.6-18]: https://github.com/damus-io/damus/releases/tag/v1.6-18
|
||||||
|
|
||||||
|
## [1.6-17] - 2023-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add support for status URLs (William Casarin)
|
||||||
|
- Click music statuses to display in spotify (William Casarin)
|
||||||
|
- Add settings for disabling user statuses (William Casarin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- clear statuses if they only contain whitespace (William Casarin)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix long status lines (William Casarin)
|
||||||
|
- Fix status events not expiring locally (William Casarin)
|
||||||
|
|
||||||
|
[1.6-17]: https://github.com/damus-io/damus/releases/tag/v1.6-17
|
||||||
|
|
||||||
|
## [1.6-16] - 2023-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added live music statuses (William Casarin)
|
||||||
|
- Added generic user statuses (William Casarin)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Avoid notification for zaps from muted profiles (tappu75e@duck.com)
|
||||||
|
- Fix text editing issues on characters added right after mention link (Daniel D’Aquino)
|
||||||
|
- Mute hellthreads everywhere (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-16]: https://github.com/damus-io/damus/releases/tag/v1.6-16
|
||||||
|
|
||||||
|
## [1.6-13] - 2023-08-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix bug where it would sometimes show -1 in replies (tappu75e@duck.com)
|
||||||
|
- Fix images and links occasionally appearing with escaped slashes (Daniel D‘Aquino)
|
||||||
|
- Fixed nostrscript not working on smaller phones (William Casarin)
|
||||||
|
- Fix zaps sometimes not appearing (William Casarin)
|
||||||
|
- Fixed issue where reposts would sometimes repost the wrong thing (William Casarin)
|
||||||
|
- Fixed issue where sometimes there would be empty entries on your profile (William Casarin)
|
||||||
|
|
||||||
|
[1.6-13]: https://github.com/damus-io/damus/releases/tag/v1.6-13
|
||||||
|
|
||||||
|
|
||||||
|
## [1.6-11]: "Bugfix Sunday" - 2023-08-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add close button to custom reactions (Suhail Saqan)
|
||||||
|
- Add ability to change order of custom reactions (Suhail Saqan)
|
||||||
|
- Adjustable font size (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Show renotes in Notes timeline (William Casarin)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure the person you're replying to is the first entry in the reply description (William Casarin)
|
||||||
|
- Don't cutoff text in notifications (William Casarin)
|
||||||
|
- Fix wikipedia url detection with parenthesis (William Casarin)
|
||||||
|
- Fixed old notifications always appearing on first start (William Casarin)
|
||||||
|
- Fix issue with slashes on relay urls causing relay connection problems (William Casarin)
|
||||||
|
- Fix rare crash triggered by local notifications (William Casarin)
|
||||||
|
- Fix crash when long-pressing reactions (William Casarin)
|
||||||
|
- Fixed nostr reporting decoding (William Casarin)
|
||||||
|
- Dismiss qr screen on scan (Suhail Saqan)
|
||||||
|
- Show QRCameraView regardless of same user (Suhail Saqan)
|
||||||
|
- Fix wiggle when long press reactions (Suhail Saqan)
|
||||||
|
- Fix reaction button breaking scrolling (Suhail Saqan)
|
||||||
|
- Fix crash when muting threads (Bryan Montz)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-11]: https://github.com/damus-io/damus/releases/tag/v1.6-11
|
||||||
|
|
||||||
|
## [1.6-8]: "nostrdb prep" 2023-08-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Suggested Users to Follow (Joel Klabo)
|
||||||
|
- Add support for multiple reactions (Suhail Saqan)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved memory usage and performance when processing events (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed disappearing text on iOS17 (cr0bar)
|
||||||
|
- Fix UTF support for hashtags (Daniel D‘Aquino)
|
||||||
|
- Fix compilation error on test target in UserSearchCacheTests (Daniel D‘Aquino)
|
||||||
|
- Fix nav crashing and buggyness (William Casarin)
|
||||||
|
- Allow relay logs to be opened in dev mode even if relay (Daniel D'Aquino)
|
||||||
|
- endless connection attempt loop after user removes relay (Bryan Montz)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-8]: https://github.com/damus-io/damus/releases/tag/v1.6-8
|
||||||
|
|
||||||
|
## 1.6 (7): "Less bad" - 2023-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Show nostr address username and support abbreviated _ usernames (William Casarin)
|
||||||
|
- Re-add nip05 badges to profiles (William Casarin)
|
||||||
|
- Add space when tagging users in posts if needed (William Casarin)
|
||||||
|
- Added padding under word count on longform account (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Don't spam lnurls when validating zaps (William Casarin)
|
||||||
|
- Eliminate nostr address validation bandwidth on startup (William Casarin)
|
||||||
|
- Allow user to login to deleted profile (William Casarin)
|
||||||
|
- Fix issue where typing cc@bob would produce brokenb ccnostr:bob mention (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-7]: https://github.com/damus-io/damus/releases/tag/v1.6-7
|
||||||
|
|
||||||
|
## [1.6-6] - 2023-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New markdown renderer (William Casarin)
|
||||||
|
- Added feedback when user adds a relay that is already on the list (Daniel D'Aquino)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Hide nsec when logging in (cr0bar)
|
||||||
|
- Remove nip05 on events (William Casarin)
|
||||||
|
- Rename NIP05 to "nostr address" (William Casarin)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed issue where hashtags were leaking in DMs (William Casarin)
|
||||||
|
- Fix issue with emojis next to hashtags and urls (William Casarin)
|
||||||
|
- relay detail view is not immediately available after adding new relay (Bryan Montz)
|
||||||
|
- Fix nostr:nostr:... bugs (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.6-6]: https://github.com/damus-io/damus/releases/tag/v1.6-6
|
||||||
|
|
||||||
## [1.6-4] - 2023-07-13
|
## [1.6-4] - 2023-07-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?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>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.damus</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?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>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.usernotifications.service</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// NostrEventInfoFromPushNotification.swift
|
||||||
|
// DamusNotificationService
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-11-13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// The representation of a JSON-encoded Nostr Event used by the push notification server
|
||||||
|
/// Needs to match with https://gitlab.com/soapbox-pub/strfry-policies/-/raw/433459d8084d1f2d6500fdf916f22caa3b4d7be5/src/types.ts
|
||||||
|
struct NostrEventInfoFromPushNotification: Codable {
|
||||||
|
let id: String // Hex-encoded
|
||||||
|
let sig: String // Hex-encoded
|
||||||
|
let kind: NostrKind
|
||||||
|
let tags: [[String]]
|
||||||
|
let pubkey: String // Hex-encoded
|
||||||
|
let content: String
|
||||||
|
let created_at: Int
|
||||||
|
|
||||||
|
static func from(dictionary: [AnyHashable: Any]) -> NostrEventInfoFromPushNotification? {
|
||||||
|
guard let id = dictionary["id"] as? String,
|
||||||
|
let sig = dictionary["sig"] as? String,
|
||||||
|
let kind_int = dictionary["kind"] as? UInt32,
|
||||||
|
let kind = NostrKind(rawValue: kind_int),
|
||||||
|
let tags = dictionary["tags"] as? [[String]],
|
||||||
|
let pubkey = dictionary["pubkey"] as? String,
|
||||||
|
let content = dictionary["content"] as? String,
|
||||||
|
let created_at = dictionary["created_at"] as? Int else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NostrEventInfoFromPushNotification(id: id, sig: sig, kind: kind, tags: tags, pubkey: pubkey, content: content, created_at: created_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reactionEmoji() -> String? {
|
||||||
|
guard self.kind == NostrKind.like else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self.content {
|
||||||
|
case "", "+":
|
||||||
|
return "❤️"
|
||||||
|
case "-":
|
||||||
|
return "👎"
|
||||||
|
default:
|
||||||
|
return self.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// NotificationFormatter.swift
|
||||||
|
// DamusNotificationService
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-11-13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
struct NotificationFormatter {
|
||||||
|
static var shared = NotificationFormatter()
|
||||||
|
|
||||||
|
// TODO: These is a very generic notification formatter. Once we integrate NostrDB into the extension, we should reuse various functions present in `HomeModel.swift`
|
||||||
|
func formatMessage(event: NostrEventInfoFromPushNotification) -> UNNotificationContent? {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
if let event_json_data = try? JSONEncoder().encode(event), // Must be encoded, as the notification completion handler requires this object to conform to `NSSecureCoding`
|
||||||
|
let event_json_string = String(data: event_json_data, encoding: .utf8) {
|
||||||
|
content.userInfo = [
|
||||||
|
"nostr_event_info": event_json_string
|
||||||
|
]
|
||||||
|
}
|
||||||
|
switch event.kind {
|
||||||
|
case .text:
|
||||||
|
content.title = NSLocalizedString("Someone posted a note", comment: "Title label for push notification where someone posted a note")
|
||||||
|
content.body = event.content
|
||||||
|
break
|
||||||
|
case .dm:
|
||||||
|
content.title = NSLocalizedString("New message", comment: "Title label for push notifications where a direct message was sent to the user")
|
||||||
|
content.body = NSLocalizedString("(Contents are encrypted)", comment: "Label on push notification indicating that the contents of the message are encrypted")
|
||||||
|
break
|
||||||
|
case .like:
|
||||||
|
guard let reactionEmoji = event.reactionEmoji() else {
|
||||||
|
content.title = NSLocalizedString("Someone reacted to your note", comment: "Generic title label for push notifications where someone reacted to the user's post")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
content.title = NSLocalizedString("New note reaction", comment: "Title label for push notifications where someone reacted to the user's post with a specific emoji")
|
||||||
|
content.body = String(format: NSLocalizedString("Someone reacted to your note with %@", comment: "Body label for push notifications where someone reacted to the user's post with a specific emoji"), reactionEmoji)
|
||||||
|
break
|
||||||
|
case .zap:
|
||||||
|
content.title = NSLocalizedString("Someone zapped you ⚡️", comment: "Title label for a push notification where someone zapped the user")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// NotificationService.swift
|
||||||
|
// DamusNotificationService
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-11-10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UserNotifications
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
|
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
|
var bestAttemptContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
|
self.contentHandler = contentHandler
|
||||||
|
|
||||||
|
let ndb: Ndb? = try? Ndb(owns_db_file: false)
|
||||||
|
|
||||||
|
// Modify the notification content here...
|
||||||
|
guard let nostrEventInfoDictionary = request.content.userInfo["nostr_event"] as? [AnyHashable: Any],
|
||||||
|
let nostrEventInfo = NostrEventInfoFromPushNotification.from(dictionary: nostrEventInfoDictionary) else {
|
||||||
|
contentHandler(request.content)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log that we got a push notification
|
||||||
|
if let pubkey = Pubkey(hex: nostrEventInfo.pubkey),
|
||||||
|
let txn = ndb?.lookup_profile(pubkey) {
|
||||||
|
Log.debug("Got push notification from %s (%s)", for: .push_notifications, (txn.unsafeUnownedValue?.profile?.display_name ?? "Unknown"), nostrEventInfo.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let improvedContent = NotificationFormatter.shared.formatMessage(event: nostrEventInfo) {
|
||||||
|
contentHandler(improvedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func serviceExtensionTimeWillExpire() {
|
||||||
|
// Called just before the extension will be terminated by the system.
|
||||||
|
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||||
|
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
||||||
|
contentHandler(bestAttemptContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,5 +4,10 @@ all: nostrscript/primal.wasm
|
|||||||
nostrscript/%.wasm: nostrscript/%.ts nostrscript/nostr.ts Makefile
|
nostrscript/%.wasm: nostrscript/%.ts nostrscript/nostr.ts Makefile
|
||||||
asc $< --runtime stub --outFile $@ --optimize
|
asc $< --runtime stub --outFile $@ --optimize
|
||||||
|
|
||||||
|
tags:
|
||||||
|
find damus-c -name '*.c' -or -name '*.h' | xargs ctags
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm nostrscript/*.wasm
|
rm nostrscript/*.wasm
|
||||||
|
|
||||||
|
.PHONY: tags
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"identifier" : "64C21A2D",
|
||||||
|
"nonRenewingSubscriptions" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"products" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"settings" : {
|
||||||
|
"_applicationInternalID" : "1628663131",
|
||||||
|
"_developerTeamID" : "XK7H4JAB3D",
|
||||||
|
"_failTransactionsEnabled" : false,
|
||||||
|
"_lastSynchronizedDate" : 704848066.26849198,
|
||||||
|
"_locale" : "en_US",
|
||||||
|
"_storefront" : "USA",
|
||||||
|
"_storeKitErrors" : [
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Load Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Purchase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Verification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "App Store Sync"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Subscription Status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "App Transaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Manage Subscriptions Sheet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Refund Request Sheet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current" : null,
|
||||||
|
"enabled" : false,
|
||||||
|
"name" : "Offer Code Redeem Sheet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"subscriptionGroups" : [
|
||||||
|
{
|
||||||
|
"id" : "21283177",
|
||||||
|
"localizations" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"name" : "Purple",
|
||||||
|
"subscriptions" : [
|
||||||
|
{
|
||||||
|
"adHocOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"codeOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"displayPrice" : "6.99",
|
||||||
|
"familyShareable" : false,
|
||||||
|
"groupNumber" : 1,
|
||||||
|
"internalID" : "6446591615",
|
||||||
|
"introductoryOffer" : null,
|
||||||
|
"localizations" : [
|
||||||
|
{
|
||||||
|
"description" : "Support damus development with Damus Purple!",
|
||||||
|
"displayName" : "Damus Purple",
|
||||||
|
"locale" : "en_CA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"productID" : "purple",
|
||||||
|
"recurringSubscriptionPeriod" : "P1M",
|
||||||
|
"referenceName" : "Purple",
|
||||||
|
"subscriptionGroupID" : "21283177",
|
||||||
|
"type" : "RecurringSubscription"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"adHocOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"codeOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"displayPrice" : "69.99",
|
||||||
|
"familyShareable" : false,
|
||||||
|
"groupNumber" : 2,
|
||||||
|
"internalID" : "6448764101",
|
||||||
|
"introductoryOffer" : null,
|
||||||
|
"localizations" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"productID" : "purpleyearly",
|
||||||
|
"recurringSubscriptionPeriod" : "P1Y",
|
||||||
|
"referenceName" : "Purple Yearly",
|
||||||
|
"subscriptionGroupID" : "21283177",
|
||||||
|
"type" : "RecurringSubscription"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : {
|
||||||
|
"major" : 3,
|
||||||
|
"minor" : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,12 +16,14 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
- [NIP-08: Mentions][nip08]
|
- [NIP-08: Mentions][nip08]
|
||||||
- [NIP-10: Reply conventions][nip10]
|
- [NIP-10: Reply conventions][nip10]
|
||||||
- [NIP-12: Generic tag queries (hashtags)][nip12]
|
- [NIP-12: Generic tag queries (hashtags)][nip12]
|
||||||
|
- [NIP-42: Authentication of clients to relays][nip42]
|
||||||
|
|
||||||
[nips]: https://github.com/nostr-protocol/nips
|
[nips]: https://github.com/nostr-protocol/nips
|
||||||
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||||
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
|
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
|
||||||
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
|
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
|
||||||
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
|
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
|
||||||
|
[nip42]: https://github.com/nostr-protocol/nips/blob/master/42.md
|
||||||
|
|
||||||
## Getting Started on Damus
|
## Getting Started on Damus
|
||||||
|
|
||||||
|
|||||||
+256
-24
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
#ifndef PROTOVERSE_CURSOR_H
|
#ifndef JB55_CURSOR_H
|
||||||
#define PROTOVERSE_CURSOR_H
|
#define JB55_CURSOR_H
|
||||||
|
|
||||||
#include "typedefs.h"
|
#include "typedefs.h"
|
||||||
#include "varint.h"
|
#include "varint.h"
|
||||||
@@ -96,6 +96,16 @@ static inline void copy_cursor(struct cursor *src, struct cursor *dest)
|
|||||||
dest->end = src->end;
|
dest->end = src->end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int cursor_skip(struct cursor *cursor, int n)
|
||||||
|
{
|
||||||
|
if (cursor->p + n >= cursor->end)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
cursor->p += n;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static inline int pull_byte(struct cursor *cursor, u8 *c)
|
static inline int pull_byte(struct cursor *cursor, u8 *c)
|
||||||
{
|
{
|
||||||
if (unlikely(cursor->p >= cursor->end))
|
if (unlikely(cursor->p >= cursor->end))
|
||||||
@@ -107,6 +117,36 @@ static inline int pull_byte(struct cursor *cursor, u8 *c)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int parse_byte(struct cursor *cursor, u8 *c)
|
||||||
|
{
|
||||||
|
if (unlikely(cursor->p >= cursor->end))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
*c = *cursor->p;
|
||||||
|
//cursor->p++;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int parse_char(struct cursor *cur, char c) {
|
||||||
|
if (cur->p >= cur->end)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (*cur->p == c) {
|
||||||
|
cur->p++;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int peek_char(struct cursor *cur, int ind) {
|
||||||
|
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return *(cur->p + ind);
|
||||||
|
}
|
||||||
|
|
||||||
static inline int cursor_pull_c_str(struct cursor *cursor, const char **str)
|
static inline int cursor_pull_c_str(struct cursor *cursor, const char **str)
|
||||||
{
|
{
|
||||||
*str = (const char*)cursor->p;
|
*str = (const char*)cursor->p;
|
||||||
@@ -303,6 +343,10 @@ static inline int cursor_pull_int(struct cursor *cursor, int *i)
|
|||||||
return cursor_pull(cursor, (u8*)i, sizeof(*i));
|
return cursor_pull(cursor, (u8*)i, sizeof(*i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int cursor_push_u32(struct cursor *cursor, uint32_t i) {
|
||||||
|
return cursor_push(cursor, (unsigned char*)&i, sizeof(i));
|
||||||
|
}
|
||||||
|
|
||||||
static inline int cursor_push_u16(struct cursor *cursor, u16 i)
|
static inline int cursor_push_u16(struct cursor *cursor, u16 i)
|
||||||
{
|
{
|
||||||
return cursor_push(cursor, (u8*)&i, sizeof(i));
|
return cursor_push(cursor, (u8*)&i, sizeof(i));
|
||||||
@@ -325,6 +369,20 @@ static inline int push_sized_str(struct cursor *cursor, const char *str, int len
|
|||||||
return cursor_push(cursor, (u8*)str, len);
|
return cursor_push(cursor, (u8*)str, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int cursor_push_lowercase(struct cursor *cur, const char *str, int len)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (unlikely(cur->p + len >= cur->end))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
for (i = 0; i < len; i++)
|
||||||
|
cur->p[i] = tolower(str[i]);
|
||||||
|
|
||||||
|
cur->p += len;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static inline int cursor_push_str(struct cursor *cursor, const char *str)
|
static inline int cursor_push_str(struct cursor *cursor, const char *str)
|
||||||
{
|
{
|
||||||
return cursor_push(cursor, (u8*)str, (int)strlen(str));
|
return cursor_push(cursor, (u8*)str, (int)strlen(str));
|
||||||
@@ -427,32 +485,195 @@ static inline int parse_str(struct cursor *cur, const char *str) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int is_whitespace(char c) {
|
static inline int is_whitespace(int c) {
|
||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int is_boundary(char c) {
|
|
||||||
return is_whitespace(c) || ispunct(c);
|
static inline int next_char_is_whitespace(unsigned char *curChar, unsigned char *endChar) {
|
||||||
|
unsigned char * next = curChar + 1;
|
||||||
|
if(next > endChar) return 0;
|
||||||
|
else if(next == endChar) return 1;
|
||||||
|
return is_whitespace(*next);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int is_invalid_url_ending(char c) {
|
static int char_disallowed_at_end_url(char c){
|
||||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
return c == '.' || c == ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int is_final_url_char(unsigned char *curChar, unsigned char *endChar){
|
||||||
|
if(is_whitespace(*curChar)){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if(next_char_is_whitespace(curChar, endChar)) {
|
||||||
|
// next char is whitespace so this char could be the final char in the url
|
||||||
|
return char_disallowed_at_end_url(*curChar);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
// next char isn't whitespace so it can't be a final char
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int is_underscore(int c) {
|
||||||
|
return c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int is_utf8_byte(u8 c) {
|
||||||
|
return c & 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int parse_utf8_char(struct cursor *cursor, unsigned int *code_point, unsigned int *utf8_length)
|
||||||
|
{
|
||||||
|
u8 first_byte;
|
||||||
|
if (!parse_byte(cursor, &first_byte))
|
||||||
|
return 0; // Not enough data
|
||||||
|
|
||||||
|
// Determine the number of bytes in this UTF-8 character
|
||||||
|
int remaining_bytes = 0;
|
||||||
|
if (first_byte < 0x80) {
|
||||||
|
*code_point = first_byte;
|
||||||
|
return 1;
|
||||||
|
} else if ((first_byte & 0xE0) == 0xC0) {
|
||||||
|
remaining_bytes = 1;
|
||||||
|
*utf8_length = remaining_bytes + 1;
|
||||||
|
*code_point = first_byte & 0x1F;
|
||||||
|
} else if ((first_byte & 0xF0) == 0xE0) {
|
||||||
|
remaining_bytes = 2;
|
||||||
|
*utf8_length = remaining_bytes + 1;
|
||||||
|
*code_point = first_byte & 0x0F;
|
||||||
|
} else if ((first_byte & 0xF8) == 0xF0) {
|
||||||
|
remaining_bytes = 3;
|
||||||
|
*utf8_length = remaining_bytes + 1;
|
||||||
|
*code_point = first_byte & 0x07;
|
||||||
|
} else {
|
||||||
|
remaining_bytes = 0;
|
||||||
|
*utf8_length = 1; // Assume 1 byte length for unrecognized UTF-8 characters
|
||||||
|
// TODO: We need to gracefully handle unrecognized UTF-8 characters
|
||||||
|
printf("Invalid UTF-8 byte: %x\n", *code_point);
|
||||||
|
*code_point = ((first_byte & 0xF0) << 6); // Prevent testing as punctuation
|
||||||
|
return 0; // Invalid first byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek at remaining bytes
|
||||||
|
for (int i = 0; i < remaining_bytes; ++i) {
|
||||||
|
signed char next_byte;
|
||||||
|
if ((next_byte = peek_char(cursor, i+1)) == -1) {
|
||||||
|
*utf8_length = 1;
|
||||||
|
return 0; // Not enough data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging lines
|
||||||
|
//printf("Cursor: %s\n", cursor->p);
|
||||||
|
//printf("Codepoint: %x\n", *code_point);
|
||||||
|
//printf("Codepoint <<6: %x\n", ((*code_point << 6) | (next_byte & 0x3F)));
|
||||||
|
//printf("Remaining bytes: %x\n", remaining_bytes);
|
||||||
|
//printf("First byte: %x\n", first_byte);
|
||||||
|
//printf("Next byte: %x\n", next_byte);
|
||||||
|
//printf("Bitwise AND result: %x\n", (next_byte & 0xC0));
|
||||||
|
|
||||||
|
if ((next_byte & 0xC0) != 0x80) {
|
||||||
|
*utf8_length = 1;
|
||||||
|
return 0; // Invalid byte in sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
*code_point = (*code_point << 6) | (next_byte & 0x3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given Unicode code point is a punctuation character
|
||||||
|
*
|
||||||
|
* @param codepoint The Unicode code point to check. @return true if the
|
||||||
|
* code point is a punctuation character, false otherwise.
|
||||||
|
*/
|
||||||
|
static inline int is_punctuation(unsigned int codepoint) {
|
||||||
|
|
||||||
|
// Check for underscore (underscore is not treated as punctuation)
|
||||||
|
if (is_underscore(codepoint))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Check for ASCII punctuation
|
||||||
|
if (ispunct(codepoint))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
// Check for Unicode punctuation exceptions (punctuation allowed in hashtags)
|
||||||
|
if (codepoint == 0x301C || codepoint == 0xFF5E) // Japanese Wave Dash / Tilde
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Check for Unicode punctuation
|
||||||
|
// NOTE: We may need to adjust the codepoint ranges in the future,
|
||||||
|
// to include/exclude certain types of Unicode characters in hashtags.
|
||||||
|
// Unicode Blocks Reference: https://www.compart.com/en/unicode/block
|
||||||
|
return (
|
||||||
|
// Latin-1 Supplement No-Break Space (NBSP): U+00A0
|
||||||
|
(codepoint == 0x00A0) ||
|
||||||
|
|
||||||
|
// Latin-1 Supplement Punctuation: U+00A1 to U+00BF
|
||||||
|
(codepoint >= 0x00A1 && codepoint <= 0x00BF) ||
|
||||||
|
|
||||||
|
// General Punctuation: U+2000 to U+206F
|
||||||
|
(codepoint >= 0x2000 && codepoint <= 0x206F) ||
|
||||||
|
|
||||||
|
// Currency Symbols: U+20A0 to U+20CF
|
||||||
|
(codepoint >= 0x20A0 && codepoint <= 0x20CF) ||
|
||||||
|
|
||||||
|
// Supplemental Punctuation: U+2E00 to U+2E7F
|
||||||
|
(codepoint >= 0x2E00 && codepoint <= 0x2E7F) ||
|
||||||
|
|
||||||
|
// CJK Symbols and Punctuation: U+3000 to U+303F
|
||||||
|
(codepoint >= 0x3000 && codepoint <= 0x303F) ||
|
||||||
|
|
||||||
|
// Ideographic Description Characters: U+2FF0 to U+2FFF
|
||||||
|
(codepoint >= 0x2FF0 && codepoint <= 0x2FFF)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int is_right_boundary(int c) {
|
||||||
|
return is_whitespace(c) || is_punctuation(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int is_left_boundary(char c) {
|
||||||
|
return is_right_boundary(c) || is_utf8_byte(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int is_alphanumeric(char c) {
|
static inline int is_alphanumeric(char c) {
|
||||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int consume_until_boundary(struct cursor *cur) {
|
static inline int consume_until_boundary(struct cursor *cur) {
|
||||||
char c;
|
unsigned int c;
|
||||||
|
unsigned int char_length = 1;
|
||||||
|
unsigned int *utf8_char_length = &char_length;
|
||||||
|
|
||||||
while (cur->p < cur->end) {
|
while (cur->p < cur->end) {
|
||||||
c = *cur->p;
|
c = *cur->p;
|
||||||
|
|
||||||
if (is_boundary(c))
|
*utf8_char_length = 1;
|
||||||
|
|
||||||
|
if (is_whitespace(c))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
cur->p++;
|
// Need to check for UTF-8 characters, which can be multiple bytes long
|
||||||
|
if (is_utf8_byte(c)) {
|
||||||
|
if (!parse_utf8_char(cur, &c, utf8_char_length)) {
|
||||||
|
if (!is_right_boundary(c)){
|
||||||
|
// TODO: We should work towards handling all UTF-8 characters.
|
||||||
|
printf("Invalid UTF-8 code point: %x\n", c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_right_boundary(c))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
// Need to use a variable character byte length for UTF-8 (2-4 bytes)
|
||||||
|
if (cur->p + *utf8_char_length <= cur->end)
|
||||||
|
cur->p += *utf8_char_length;
|
||||||
|
else
|
||||||
|
cur->p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
@@ -475,6 +696,23 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
|||||||
return or_end;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int consume_until_end_url(struct cursor *cur, int or_end) {
|
||||||
|
char c;
|
||||||
|
int consumedAtLeastOne = 0;
|
||||||
|
|
||||||
|
while (cur->p < cur->end) {
|
||||||
|
c = *cur->p;
|
||||||
|
|
||||||
|
if (is_final_url_char(cur->p, cur->end))
|
||||||
|
return consumedAtLeastOne;
|
||||||
|
|
||||||
|
cur->p++;
|
||||||
|
consumedAtLeastOne = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return or_end;
|
||||||
|
}
|
||||||
|
|
||||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||||
char c;
|
char c;
|
||||||
int consumedAtLeastOne = 0;
|
int consumedAtLeastOne = 0;
|
||||||
@@ -492,23 +730,17 @@ static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end)
|
|||||||
return or_end;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int parse_char(struct cursor *cur, char c) {
|
|
||||||
if (cur->p >= cur->end)
|
static inline int cursor_memset(struct cursor *cursor, unsigned char c, int n)
|
||||||
|
{
|
||||||
|
if (cursor->p + n >= cursor->end)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (*cur->p == c) {
|
memset(cursor->p, c, n);
|
||||||
cur->p++;
|
cursor->p += n;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int peek_char(struct cursor *cur, int ind) {
|
|
||||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
return *(cur->p + ind);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -8,4 +8,6 @@
|
|||||||
#include "nostr_bech32.h"
|
#include "nostr_bech32.h"
|
||||||
#include "wasm.h"
|
#include "wasm.h"
|
||||||
#include "nostrscript.h"
|
#include "nostrscript.h"
|
||||||
|
#include "nostrdb.h"
|
||||||
|
#include "lmdb.h"
|
||||||
|
|
||||||
|
|||||||
+115
-6
@@ -104,8 +104,74 @@ static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8
|
|||||||
return add_block(blocks, b);
|
return add_block(blocks, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int consume_url_fragment(struct cursor *cur)
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
if ((c = peek_char(cur, 0)) < 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (c != '#' && c != '?') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur->p++;
|
||||||
|
|
||||||
|
return consume_until_end_url(cur, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int consume_url_path(struct cursor *cur)
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
|
||||||
|
if ((c = peek_char(cur, 0)) < 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (c != '/') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cur->p < cur->end) {
|
||||||
|
c = *cur->p;
|
||||||
|
|
||||||
|
if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur->p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int consume_url_host(struct cursor *cur)
|
||||||
|
{
|
||||||
|
char c;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while (cur->p < cur->end) {
|
||||||
|
c = *cur->p;
|
||||||
|
// TODO: handle IDNs
|
||||||
|
if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
cur->p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// this means the end of the URL hostname is the end of the buffer and we finished
|
||||||
|
return count != 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int parse_url(struct cursor *cur, struct note_block *block) {
|
static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||||
u8 *start = cur->p;
|
u8 *start = cur->p;
|
||||||
|
u8 *host;
|
||||||
|
int host_len;
|
||||||
|
struct cursor path_cur;
|
||||||
|
|
||||||
if (!parse_str(cur, "http"))
|
if (!parse_str(cur, "http"))
|
||||||
return 0;
|
return 0;
|
||||||
@@ -122,13 +188,56 @@ static int parse_url(struct cursor *cur, struct note_block *block) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!consume_until_whitespace(cur, 1)) {
|
// make sure to save the hostname. We will use this to detect damus.io links
|
||||||
cur->p = start;
|
host = cur->p;
|
||||||
return 0;
|
|
||||||
|
if (!consume_url_host(cur)) {
|
||||||
|
cur->p = start;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip any unwanted characters
|
// get the length of the host string
|
||||||
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
|
host_len = (int)(cur->p - host);
|
||||||
|
|
||||||
|
// save the current parse state so that we can continue from here when
|
||||||
|
// parsing the bech32 in the damus.io link if we have it
|
||||||
|
copy_cursor(cur, &path_cur);
|
||||||
|
|
||||||
|
// skip leading /
|
||||||
|
cursor_skip(&path_cur, 1);
|
||||||
|
|
||||||
|
if (!consume_url_path(cur)) {
|
||||||
|
cur->p = start;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consume_url_fragment(cur)) {
|
||||||
|
cur->p = start;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// smart parens
|
||||||
|
if (start - 1 >= 0 &&
|
||||||
|
start < cur->end &&
|
||||||
|
*(start - 1) == '(' &&
|
||||||
|
(cur->p - 1) < cur->end &&
|
||||||
|
*(cur->p - 1) == ')')
|
||||||
|
{
|
||||||
|
cur->p--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the bech32 string pos in case we hit a damus.io link
|
||||||
|
block->block.str.start = (const char *)path_cur.p;
|
||||||
|
|
||||||
|
// if we have a damus link, make it a mention
|
||||||
|
if (host_len == 8
|
||||||
|
&& !strncmp((const char *)host, "damus.io", 8)
|
||||||
|
&& parse_nostr_bech32(&path_cur, &block->block.mention_bech32.bech32))
|
||||||
|
{
|
||||||
|
block->block.str.end = (const char *)path_cur.p;
|
||||||
|
block->type = BLOCK_MENTION_BECH32;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
block->type = BLOCK_URL;
|
block->type = BLOCK_URL;
|
||||||
block->block.str.start = (const char *)start;
|
block->block.str.start = (const char *)start;
|
||||||
@@ -231,7 +340,7 @@ int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre_mention = cur.p;
|
pre_mention = cur.p;
|
||||||
if (cp == -1 || is_boundary(cp) || c == '#') {
|
if (cp == -1 || is_left_boundary(cp) || c == '#') {
|
||||||
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -39,15 +39,6 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize)
|
|||||||
return slen == 0 && bufsize == 0;
|
return slen == 0 && bufsize == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char hexchar(unsigned int val)
|
|
||||||
{
|
|
||||||
if (val < 10)
|
|
||||||
return '0' + val;
|
|
||||||
if (val < 16)
|
|
||||||
return 'a' + val - 10;
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize)
|
bool hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize)
|
||||||
{
|
{
|
||||||
size_t i;
|
size_t i;
|
||||||
|
|||||||
@@ -70,4 +70,15 @@ static inline size_t hex_data_size(size_t strlen)
|
|||||||
{
|
{
|
||||||
return strlen / 2;
|
return strlen / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline char hexchar(unsigned int val)
|
||||||
|
{
|
||||||
|
if (val < 10)
|
||||||
|
return '0' + val;
|
||||||
|
if (val < 16)
|
||||||
|
return 'a' + val - 10;
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif /* CCAN_HEX_H */
|
#endif /* CCAN_HEX_H */
|
||||||
|
|||||||
+2
-1
@@ -7100,7 +7100,8 @@ int wasm_interp_init(struct wasm_interp *interp, struct module *module)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
memory_pages_size = 64 * 256 * WASM_PAGE_SIZE; /* 1 gb virt */
|
// keep total memory size small for now, iOS doesn't like like mallocs
|
||||||
|
memory_pages_size = 8 * WASM_PAGE_SIZE;
|
||||||
|
|
||||||
memsize =
|
memsize =
|
||||||
stack_size +
|
stack_size +
|
||||||
|
|||||||
+1391
-84
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,32 @@
|
|||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-markdown-ui",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/damus-io/swift-markdown-ui",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "76bb7971da7fbf429de1c84f1244adf657242fee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-snapshot-testing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5b356adceabff6ca027f6574aac79e9fee145d26",
|
||||||
|
"version" : "1.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-syntax.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
|
||||||
|
"version" : "509.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 2
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1500"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D79C4C132AFEB061003A41B4"
|
||||||
|
BuildableName = "DamusNotificationService.appex"
|
||||||
|
BlueprintName = "DamusNotificationService"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<RemoteRunnable
|
||||||
|
runnableDebuggingMode = "1"
|
||||||
|
BundleIdentifier = "com.jb55.damus2"
|
||||||
|
RemotePath = "/Users/danielnogueira/Library/Developer/CoreSimulator/Devices/99E60B35-CE5D-4B45-AC35-00818C0AF3CB/data/Containers/Bundle/Application/5A083DD0-FDE2-43D7-9172-2F97FAD18F20/damus.app">
|
||||||
|
</RemoteRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
codeCoverageEnabled = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
@@ -70,6 +71,9 @@
|
|||||||
ReferencedContainer = "container:damus.xcodeproj">
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../Purple.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFF",
|
||||||
|
"green" : "0xFF",
|
||||||
|
"red" : "0xFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x00",
|
||||||
|
"green" : "0x00",
|
||||||
|
"red" : "0x00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE3",
|
||||||
|
"green" : "0xD7",
|
||||||
|
"red" : "0xF7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x20",
|
||||||
|
"green" : "0x13",
|
||||||
|
"red" : "0x61"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x63",
|
||||||
|
"green" : "0x11",
|
||||||
|
"red" : "0xF5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x6E",
|
||||||
|
"green" : "0x20",
|
||||||
|
"red" : "0xF8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xEE",
|
||||||
|
"green" : "0xE8",
|
||||||
|
"red" : "0xF7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x35",
|
||||||
|
"green" : "0x04",
|
||||||
|
"red" : "0x8B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x3D",
|
||||||
|
"green" : "0x07",
|
||||||
|
"red" : "0x9C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x44",
|
||||||
|
"green" : "0x06",
|
||||||
|
"red" : "0xB2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2D",
|
||||||
|
"green" : "0x05",
|
||||||
|
"red" : "0x75"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xD8",
|
||||||
|
"green" : "0xC2",
|
||||||
|
"red" : "0xFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFA",
|
||||||
|
"green" : "0xFA",
|
||||||
|
"red" : "0xF9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x24",
|
||||||
|
"green" : "0x22",
|
||||||
|
"red" : "0x20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE3",
|
||||||
|
"green" : "0xE1",
|
||||||
|
"red" : "0xDD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2A",
|
||||||
|
"green" : "0x26",
|
||||||
|
"red" : "0x23"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x59",
|
||||||
|
"green" : "0x53",
|
||||||
|
"red" : "0x4A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x85",
|
||||||
|
"green" : "0x7A",
|
||||||
|
"red" : "0x6A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE4",
|
||||||
|
"green" : "0xF1",
|
||||||
|
"red" : "0xD6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x38",
|
||||||
|
"green" : "0x5C",
|
||||||
|
"red" : "0x12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x5A",
|
||||||
|
"green" : "0xAB",
|
||||||
|
"red" : "0x04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x64",
|
||||||
|
"green" : "0xBF",
|
||||||
|
"red" : "0x03"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF0",
|
||||||
|
"green" : "0xF7",
|
||||||
|
"red" : "0xE8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1F",
|
||||||
|
"green" : "0x33",
|
||||||
|
"red" : "0x0A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x34",
|
||||||
|
"green" : "0x64",
|
||||||
|
"red" : "0x02"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x3F",
|
||||||
|
"green" : "0x79",
|
||||||
|
"red" : "0x02"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1F",
|
||||||
|
"green" : "0x3C",
|
||||||
|
"red" : "0x01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE4",
|
||||||
|
"green" : "0xFF",
|
||||||
|
"red" : "0xAD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xD1",
|
||||||
|
"green" : "0xEE",
|
||||||
|
"red" : "0xFE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x12",
|
||||||
|
"green" : "0x43",
|
||||||
|
"red" : "0x5C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1C",
|
||||||
|
"green" : "0xAD",
|
||||||
|
"red" : "0xF9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2C",
|
||||||
|
"green" : "0xB5",
|
||||||
|
"red" : "0xFC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE1",
|
||||||
|
"green" : "0xF4",
|
||||||
|
"red" : "0xFE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x0A",
|
||||||
|
"green" : "0x25",
|
||||||
|
"red" : "0x33"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x06",
|
||||||
|
"green" : "0x85",
|
||||||
|
"red" : "0xC6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x03",
|
||||||
|
"green" : "0x93",
|
||||||
|
"red" : "0xDD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x04",
|
||||||
|
"green" : "0x6A",
|
||||||
|
"red" : "0x9F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xCC",
|
||||||
|
"green" : "0xF5",
|
||||||
|
"red" : "0xFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Damus dark-gray.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Damus dark.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "special-features.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "special-features.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "special-features.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "stars-bg.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "shadow-2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
+21
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "shadow.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 511 KiB |
@@ -11,6 +11,7 @@ import SwiftUI
|
|||||||
class DamusColors {
|
class DamusColors {
|
||||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||||
static let adaptableBlack = Color("DamusAdaptableBlack")
|
static let adaptableBlack = Color("DamusAdaptableBlack")
|
||||||
|
static let adaptableWhite = Color("DamusAdaptableWhite")
|
||||||
static let white = Color("DamusWhite")
|
static let white = Color("DamusWhite")
|
||||||
static let black = Color("DamusBlack")
|
static let black = Color("DamusBlack")
|
||||||
static let brown = Color("DamusBrown")
|
static let brown = Color("DamusBrown")
|
||||||
@@ -22,5 +23,26 @@ class DamusColors {
|
|||||||
static let purple = Color("DamusPurple")
|
static let purple = Color("DamusPurple")
|
||||||
static let deepPurple = Color("DamusDeepPurple")
|
static let deepPurple = Color("DamusDeepPurple")
|
||||||
static let blue = Color("DamusBlue")
|
static let blue = Color("DamusBlue")
|
||||||
|
static let success = Color("DamusSuccessPrimary")
|
||||||
|
static let successSecondary = Color("DamusSuccessSecondary")
|
||||||
|
static let successTertiary = Color("DamusSuccessTertiary")
|
||||||
|
static let successQuaternary = Color("DamusSuccessQuaternary")
|
||||||
|
static let successBorder = Color("DamusSuccessBorder")
|
||||||
|
static let warning = Color("DamusWarningPrimary")
|
||||||
|
static let warningSecondary = Color("DamusWarningSecondary")
|
||||||
|
static let warningTertiary = Color("DamusWarningTertiary")
|
||||||
|
static let warningQuaternary = Color("DamusWarningQuaternary")
|
||||||
|
static let warningBorder = Color("DamusWarningBorder")
|
||||||
|
static let danger = Color("DamusDangerPrimary")
|
||||||
|
static let dangerSecondary = Color("DamusDangerSecondary")
|
||||||
|
static let dangerTertiary = Color("DamusDangerTertiary")
|
||||||
|
static let dangerQuaternary = Color("DamusDangerQuaternary")
|
||||||
|
static let dangerBorder = Color("DamusDangerBorder")
|
||||||
|
static let neutral1 = Color("DamusNeutral1")
|
||||||
|
static let neutral3 = Color("DamusNeutral3")
|
||||||
|
static let neutral6 = Color("DamusNeutral6")
|
||||||
|
static let pink = Color(red: 211/255.0, green: 76/255.0, blue: 217/255.0)
|
||||||
|
static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0)
|
||||||
|
static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ import SwiftUI
|
|||||||
struct EndBlock: View {
|
struct EndBlock: View {
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
|
|
||||||
init () {
|
init(height: Float = 10) {
|
||||||
self.height = 10.0
|
|
||||||
}
|
|
||||||
|
|
||||||
init (height: Float) {
|
|
||||||
self.height = CGFloat(height)
|
self.height = CGFloat(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct GradientButtonStyle: ButtonStyle {
|
|||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(PinkGradient)
|
.fill(PinkGradient)
|
||||||
}
|
}
|
||||||
.scaleEffect(configuration.isPressed ? 0.8 : 1)
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// DamusLightGradient.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by eric on 9/8/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x2d, b: 0xc3)
|
||||||
|
fileprivate let damus_grad_c2 = hex_col(r: 0x33, g: 0xc5, b: 0xbc)
|
||||||
|
fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2]
|
||||||
|
|
||||||
|
struct DamusLightGradient: View {
|
||||||
|
var body: some View {
|
||||||
|
DamusLightGradient.gradient
|
||||||
|
.opacity(0.5)
|
||||||
|
.edgesIgnoringSafeArea([.top,.bottom])
|
||||||
|
}
|
||||||
|
|
||||||
|
static var gradient: LinearGradient {
|
||||||
|
LinearGradient(colors: damus_grad, startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DamusLightGradient_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
DamusLightGradient()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// GrayGradient.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by klabo on 7/20/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let GrayGradient = LinearGradient(gradient:
|
||||||
|
Gradient(colors: [Color(#colorLiteral(red: 0.9764705882, green: 0.9803921569, blue: 0.9803921569, alpha: 1))]),
|
||||||
|
startPoint: .leading,
|
||||||
|
endPoint: .trailing)
|
||||||
|
|
||||||
|
struct GrayGradientView: View {
|
||||||
|
var body: some View {
|
||||||
|
GrayGradient
|
||||||
|
.edgesIgnoringSafeArea([.top, .bottom])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GrayGradient_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
GrayGradientView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ enum ImageShape {
|
|||||||
struct ImageCarousel: View {
|
struct ImageCarousel: View {
|
||||||
var urls: [MediaUrl]
|
var urls: [MediaUrl]
|
||||||
|
|
||||||
let evid: String
|
let evid: NoteId
|
||||||
|
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ struct ImageCarousel: View {
|
|||||||
@State private var selectedIndex = 0
|
@State private var selectedIndex = 0
|
||||||
@State private var video_size: CGSize? = nil
|
@State private var video_size: CGSize? = nil
|
||||||
|
|
||||||
init(state: DamusState, evid: String, urls: [MediaUrl]) {
|
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
||||||
_open_sheet = State(initialValue: false)
|
_open_sheet = State(initialValue: false)
|
||||||
_current_url = State(initialValue: nil)
|
_current_url = State(initialValue: nil)
|
||||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||||
@@ -105,17 +105,13 @@ struct ImageCarousel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
|
if self.image_fill == nil, let size = state.video.size_for_url(url) {
|
||||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||||
self.image_fill = fill
|
self.image_fill = fill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func video_model(_ url: URL) -> VideoPlayerModel {
|
|
||||||
return state.events.get_video_player_model(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||||
Group {
|
Group {
|
||||||
switch url {
|
switch url {
|
||||||
@@ -125,7 +121,7 @@ struct ImageCarousel: View {
|
|||||||
open_sheet = true
|
open_sheet = true
|
||||||
}
|
}
|
||||||
case .video(let url):
|
case .video(let url):
|
||||||
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
|
DamusVideoPlayer(url: url, video_size: $video_size, controller: state.video)
|
||||||
.onChange(of: video_size) { size in
|
.onChange(of: video_size) { size in
|
||||||
guard let size else { return }
|
guard let size else { return }
|
||||||
|
|
||||||
@@ -194,7 +190,7 @@ struct ImageCarousel: View {
|
|||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||||
.fullScreenCover(isPresented: $open_sheet) {
|
.fullScreenCover(isPresented: $open_sheet) {
|
||||||
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
|
ImageView(video_controller: state.video, urls: urls, settings: state.settings)
|
||||||
}
|
}
|
||||||
.frame(height: height)
|
.frame(height: height)
|
||||||
.onChange(of: selectedIndex) { value in
|
.onChange(of: selectedIndex) { value in
|
||||||
@@ -289,7 +285,7 @@ public struct ImageFill {
|
|||||||
struct ImageCarousel_Previews: PreviewProvider {
|
struct ImageCarousel_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
|
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct InvoiceView: View {
|
struct InvoiceView: View {
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
let our_pubkey: String
|
let our_pubkey: Pubkey
|
||||||
let invoice: Invoice
|
let invoice: Invoice
|
||||||
@State var showing_select_wallet: Bool = false
|
@State var showing_select_wallet: Bool = false
|
||||||
@State var copied = false
|
@State var copied = false
|
||||||
@@ -39,7 +39,12 @@ struct InvoiceView: View {
|
|||||||
if settings.show_wallet_selector {
|
if settings.show_wallet_selector {
|
||||||
present_sheet(.select_wallet(invoice: invoice.string))
|
present_sheet(.select_wallet(invoice: invoice.string))
|
||||||
} else {
|
} else {
|
||||||
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
do {
|
||||||
|
try open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
present_sheet(.select_wallet(invoice: invoice.string))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||||
@@ -82,21 +87,26 @@ struct InvoiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
enum OpenWalletError: Error {
|
||||||
|
case no_wallet_to_open
|
||||||
|
case store_link_invalid
|
||||||
|
case system_cannot_open_store_link
|
||||||
|
}
|
||||||
|
|
||||||
|
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
} else {
|
} else {
|
||||||
guard let store_link = wallet.appStoreLink else {
|
guard let store_link = wallet.appStoreLink else {
|
||||||
// TODO: do something here if we don't have an appstore link
|
throw OpenWalletError.no_wallet_to_open
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: store_link) else {
|
guard let url = URL(string: store_link) else {
|
||||||
return
|
throw OpenWalletError.store_link_invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UIApplication.shared.canOpenURL(url) else {
|
guard UIApplication.shared.canOpenURL(url) else {
|
||||||
return
|
throw OpenWalletError.system_cannot_open_store_link
|
||||||
}
|
}
|
||||||
|
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
@@ -108,12 +118,12 @@ let test_invoice = Invoice(description: .description("this is a description"), a
|
|||||||
|
|
||||||
struct InvoiceView_Previews: PreviewProvider {
|
struct InvoiceView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoiceView(our_pubkey: "", invoice: test_invoice, settings: test_damus_state().settings)
|
InvoiceView(our_pubkey: .empty, invoice: test_invoice, settings: test_damus_state.settings)
|
||||||
.frame(width: 300, height: 200)
|
.frame(width: 300, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func present_sheet(_ sheet: Sheets) {
|
func present_sheet(_ sheet: Sheets) {
|
||||||
notify(.present_sheet, sheet)
|
notify(.present_sheet(sheet))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InvoicesView: View {
|
struct InvoicesView: View {
|
||||||
let our_pubkey: String
|
let our_pubkey: Pubkey
|
||||||
var invoices: [Invoice]
|
var invoices: [Invoice]
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ struct InvoicesView: View {
|
|||||||
|
|
||||||
struct InvoicesView_Previews: PreviewProvider {
|
struct InvoicesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state().settings)
|
InvoicesView(our_pubkey: test_note.pubkey, invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state.settings)
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ import SwiftUI
|
|||||||
|
|
||||||
struct NIP05Badge: View {
|
struct NIP05Badge: View {
|
||||||
let nip05: NIP05
|
let nip05: NIP05
|
||||||
let pubkey: String
|
let pubkey: Pubkey
|
||||||
let contacts: Contacts
|
let contacts: Contacts
|
||||||
let show_domain: Bool
|
let show_domain: Bool
|
||||||
let clickable: Bool
|
let profiles: Profiles
|
||||||
|
|
||||||
@Environment(\.openURL) var openURL
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
init (nip05: NIP05, pubkey: String, contacts: Contacts, show_domain: Bool, clickable: Bool) {
|
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
|
||||||
self.nip05 = nip05
|
self.nip05 = nip05
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.contacts = contacts
|
self.contacts = contacts
|
||||||
self.show_domain = show_domain
|
self.show_domain = show_domain
|
||||||
self.clickable = clickable
|
self.profiles = profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
var nip05_color: Bool {
|
var nip05_color: Bool {
|
||||||
@@ -44,23 +44,35 @@ struct NIP05Badge: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var username_matches_nip05: Bool {
|
||||||
|
guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.lowercased() == nip05.username.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
var nip05_string: String {
|
||||||
|
if nip05.username == "_" || username_matches_nip05 {
|
||||||
|
return nip05.host
|
||||||
|
} else {
|
||||||
|
return "\(nip05.username)@\(nip05.host)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Seal
|
Seal
|
||||||
|
|
||||||
if show_domain {
|
if show_domain {
|
||||||
if clickable {
|
Text(nip05_string)
|
||||||
Text(nip05.host)
|
.nip05_colorized(gradient: nip05_color)
|
||||||
.nip05_colorized(gradient: nip05_color)
|
.onTapGesture {
|
||||||
.onTapGesture {
|
if let nip5url = nip05.siteUrl {
|
||||||
if let nip5url = nip05.siteUrl {
|
openURL(nip5url)
|
||||||
openURL(nip5url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Text(nip05.host)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,17 +90,21 @@ extension View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
|
func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool {
|
||||||
return contacts.is_friend_or_self(pubkey) ? true : false
|
return contacts.is_friend_or_self(pubkey) ? true : false
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NIP05Badge_Previews: PreviewProvider {
|
struct NIP05Badge_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let test_state = test_damus_state()
|
let test_state = test_damus_state
|
||||||
VStack {
|
VStack {
|
||||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, clickable: false)
|
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||||
|
|
||||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: "sdkfjsdf"), show_domain: true, clickable: false)
|
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||||
|
|
||||||
|
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||||
|
|
||||||
|
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// NeutralButtonStyle.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by eric on 9/1/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum NeutralButtonShape {
|
||||||
|
case rounded, capsule, circle
|
||||||
|
|
||||||
|
var style: NeutralButtonStyle {
|
||||||
|
switch self {
|
||||||
|
case .rounded:
|
||||||
|
return NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 12)
|
||||||
|
case .capsule:
|
||||||
|
return NeutralButtonStyle(padding: EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15), cornerRadius: 20)
|
||||||
|
case .circle:
|
||||||
|
return NeutralButtonStyle(padding: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), cornerRadius: 9999)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NeutralButtonStyle: ButtonStyle {
|
||||||
|
let padding: EdgeInsets
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
let scaleEffect: CGFloat
|
||||||
|
|
||||||
|
init(padding: EdgeInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0), cornerRadius: CGFloat = 15, scaleEffect: CGFloat = 0.95) {
|
||||||
|
self.padding = padding
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
|
self.scaleEffect = scaleEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(padding)
|
||||||
|
.background(DamusColors.neutral1)
|
||||||
|
.cornerRadius(cornerRadius)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||||
|
)
|
||||||
|
.scaleEffect(configuration.isPressed ? scaleEffect : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
print("dynamic size")
|
||||||
|
}) {
|
||||||
|
Text(verbatim: "Dynamic Size")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
print("infinite width")
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(verbatim: "Infinite Width")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Button("Rounded Button", action: {})
|
||||||
|
.buttonStyle(NeutralButtonShape.rounded.style)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Button("Capsule Button", action: {})
|
||||||
|
.buttonStyle(NeutralButtonShape.capsule.style)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Button(action: {}, label: {Image("messages")})
|
||||||
|
.buttonStyle(NeutralButtonShape.circle.style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// ZapButton.swift
|
// NoteZapButton.swift
|
||||||
// damus
|
// damus
|
||||||
//
|
//
|
||||||
// Created by William Casarin on 2023-01-17.
|
// Created by William Casarin on 2023-01-17.
|
||||||
@@ -18,6 +18,19 @@ enum ZappingError {
|
|||||||
case bad_lnurl
|
case bad_lnurl
|
||||||
case canceled
|
case canceled
|
||||||
case send_failed
|
case send_failed
|
||||||
|
|
||||||
|
func humanReadableMessage() -> String {
|
||||||
|
switch self {
|
||||||
|
case .fetching_invoice:
|
||||||
|
return NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
|
||||||
|
case .bad_lnurl:
|
||||||
|
return NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
|
||||||
|
case .canceled:
|
||||||
|
return NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
|
||||||
|
case .send_failed:
|
||||||
|
return NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ZappingEvent {
|
struct ZappingEvent {
|
||||||
@@ -26,7 +39,7 @@ struct ZappingEvent {
|
|||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ZapButton: View {
|
struct NoteZapButton: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
let lnurl: String
|
let lnurl: String
|
||||||
@@ -141,10 +154,10 @@ struct ZapButton: View {
|
|||||||
|
|
||||||
struct ZapButton_Previews: PreviewProvider {
|
struct ZapButton_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||||
let zaps = ZapsDataModel([.pending(pending_zap)])
|
let zaps = ZapsDataModel([.pending(pending_zap)])
|
||||||
|
|
||||||
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
|
NoteZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,90 +196,74 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||||
damus_state.add_zap(zap: .pending(pending_zap))
|
damus_state.add_zap(zap: .pending(pending_zap))
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else {
|
||||||
if mpayreq == nil {
|
|
||||||
mpayreq = await fetch_static_payreq(lnurl)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let payreq = mpayreq else {
|
|
||||||
// TODO: show error
|
// TODO: show error
|
||||||
DispatchQueue.main.async {
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
notify(.zapping(ev))
|
||||||
notify(.zapping, ev)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
|
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
|
||||||
DispatchQueue.main.async {
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
notify(.zapping(ev))
|
||||||
notify(.zapping, ev)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
switch pending_zap_state {
|
||||||
|
case .nwc(let nwc_state):
|
||||||
switch pending_zap_state {
|
// don't both continuing, user has canceled
|
||||||
case .nwc(let nwc_state):
|
if case .cancel_fetching_invoice = nwc_state.state {
|
||||||
// don't both continuing, user has canceled
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
if case .cancel_fetching_invoice = nwc_state.state {
|
let typ = ZappingEventType.failed(.canceled)
|
||||||
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
let typ = ZappingEventType.failed(.canceled)
|
notify(.zapping(ev))
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
return
|
||||||
notify(.zapping, ev)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var flusher: OnFlush? = nil
|
|
||||||
|
|
||||||
// donations are only enabled on one-tap zaps and off appstore
|
|
||||||
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
|
|
||||||
flusher = .once({ pe in
|
|
||||||
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
|
||||||
Task { @MainActor in
|
|
||||||
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
|
||||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
|
||||||
|
|
||||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
|
||||||
|
|
||||||
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
|
||||||
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
|
||||||
|
|
||||||
let typ = ZappingEventType.failed(.send_failed)
|
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
|
||||||
notify(.zapping, ev)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
|
|
||||||
|
|
||||||
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
|
|
||||||
// we don't need to trigger a ZapsDataModel update here
|
|
||||||
}
|
|
||||||
|
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
|
|
||||||
notify(.zapping, ev)
|
|
||||||
|
|
||||||
case .external(let pending_ext):
|
|
||||||
pending_ext.state = .done
|
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
|
||||||
notify(.zapping, ev)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var flusher: OnFlush? = nil
|
||||||
|
|
||||||
|
// donations are only enabled on one-tap zaps and off appstore
|
||||||
|
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
|
||||||
|
flusher = .once({ pe in
|
||||||
|
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
||||||
|
Task { @MainActor in
|
||||||
|
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||||
|
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||||
|
|
||||||
|
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
||||||
|
|
||||||
|
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
||||||
|
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
||||||
|
|
||||||
|
let typ = ZappingEventType.failed(.send_failed)
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
|
notify(.zapping(ev))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
|
||||||
|
|
||||||
|
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
|
||||||
|
// we don't need to trigger a ZapsDataModel update here
|
||||||
|
}
|
||||||
|
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
|
||||||
|
notify(.zapping(ev))
|
||||||
|
|
||||||
|
case .external(let pending_ext):
|
||||||
|
pending_ext.state = .done
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
|
||||||
|
notify(.zapping(ev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,14 +9,13 @@ import SwiftUI
|
|||||||
|
|
||||||
struct Reposted: View {
|
struct Reposted: View {
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let pubkey: String
|
let pubkey: Pubkey
|
||||||
let profile: Profile?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Image("repost")
|
Image("repost")
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
|
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
@@ -26,7 +25,7 @@ struct Reposted: View {
|
|||||||
|
|
||||||
struct Reposted_Previews: PreviewProvider {
|
struct Reposted_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let test_state = test_damus_state()
|
let test_state = test_damus_state
|
||||||
Reposted(damus: test_state, pubkey: test_state.pubkey, profile: make_test_profile())
|
Reposted(damus: test_state, pubkey: test_state.pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,22 +25,11 @@ struct SearchHeaderView: View {
|
|||||||
|
|
||||||
var Icon: some View {
|
var Icon: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
|
||||||
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
|
||||||
.frame(width: 54, height: 54)
|
|
||||||
|
|
||||||
switch described {
|
switch described {
|
||||||
case .hashtag:
|
case .hashtag:
|
||||||
Text(verbatim: "#")
|
SingleCharacterAvatar(character: "#")
|
||||||
.font(.largeTitle.bold())
|
case .unknown:
|
||||||
.foregroundStyle(PinkGradient)
|
SystemIconAvatar(system_name: "magnifyingglass")
|
||||||
.mask(Text(verbatim: "#")
|
|
||||||
.font(.largeTitle.bold()))
|
|
||||||
|
|
||||||
case .unknown:
|
|
||||||
Image(systemName: "magnifyingglass")
|
|
||||||
.font(.title.bold())
|
|
||||||
.foregroundStyle(PinkGradient)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,32 +38,6 @@ struct SearchHeaderView: View {
|
|||||||
Text(described.description)
|
Text(described.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow(_ hashtag: String) {
|
|
||||||
is_following = false
|
|
||||||
handle_unfollow(state: state, unfollow: .t(hashtag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func follow(_ hashtag: String) {
|
|
||||||
is_following = true
|
|
||||||
handle_follow(state: state, follow: .t(hashtag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FollowButton(_ ht: String) -> some View {
|
|
||||||
return Button(action: { follow(ht) }) {
|
|
||||||
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
|
||||||
.font(.footnote.bold())
|
|
||||||
}
|
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnfollowButton(_ ht: String) -> some View {
|
|
||||||
return Button(action: { unfollow(ht) }) {
|
|
||||||
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
|
||||||
.font(.footnote.bold())
|
|
||||||
}
|
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 30) {
|
HStack(alignment: .center, spacing: 30) {
|
||||||
Icon
|
Icon
|
||||||
@@ -86,44 +49,128 @@ struct SearchHeaderView: View {
|
|||||||
|
|
||||||
if state.is_privkey_user, case .hashtag(let ht) = described {
|
if state.is_privkey_user, case .hashtag(let ht) = described {
|
||||||
if is_following {
|
if is_following {
|
||||||
UnfollowButton(ht)
|
HashtagUnfollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||||
} else {
|
} else {
|
||||||
FollowButton(ht)
|
HashtagFollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.followed)) { notif in
|
.onReceive(handle_notify(.followed)) { ref in
|
||||||
let ref = notif.object as! ReferencedId
|
|
||||||
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||||
self.is_following = true
|
self.is_following = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
.onReceive(handle_notify(.unfollowed)) { ref in
|
||||||
let ref = notif.object as! ReferencedId
|
|
||||||
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||||
self.is_following = false
|
self.is_following = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool {
|
struct SystemIconAvatar: View {
|
||||||
guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht
|
let system_name: String
|
||||||
else { return false }
|
|
||||||
|
var body: some View {
|
||||||
|
NonImageAvatar {
|
||||||
|
Image(systemName: system_name)
|
||||||
|
.font(.title.bold())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SingleCharacterAvatar: View {
|
||||||
|
let character: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NonImageAvatar {
|
||||||
|
Text(verbatim: character)
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
.mask(Text(verbatim: character)
|
||||||
|
.font(.largeTitle.bold()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NonImageAvatar<Content: View>: View {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(@ViewBuilder content: () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(DamusColors.lightBackgroundPink)
|
||||||
|
.frame(width: 54, height: 54)
|
||||||
|
|
||||||
|
content
|
||||||
|
.foregroundStyle(PinkGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HashtagUnfollowButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let hashtag: String
|
||||||
|
@Binding var is_following: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
return Button(action: { unfollow(hashtag) }) {
|
||||||
|
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow(_ hashtag: String) {
|
||||||
|
is_following = false
|
||||||
|
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HashtagFollowButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let hashtag: String
|
||||||
|
@Binding var is_following: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
return Button(action: { follow(hashtag) }) {
|
||||||
|
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func follow(_ hashtag: String) {
|
||||||
|
is_following = true
|
||||||
|
handle_follow(state: damus_state, follow: .hashtag(hashtag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
|
||||||
|
guard case .hashtag(let follow_ht) = ref,
|
||||||
|
case .hashtag(let search_ht) = desc,
|
||||||
|
follow_ht == search_ht
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
|
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
|
||||||
guard let contacts else { return false }
|
guard let contacts else { return false }
|
||||||
return is_already_following(contacts: contacts, follow: .t(hashtag))
|
return is_already_following(contacts: contacts, follow: .hashtag(hashtag))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct SearchHeaderView_Previews: PreviewProvider {
|
struct SearchHeaderView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
SearchHeaderView(state: test_damus_state(), described: .hashtag("damus"))
|
SearchHeaderView(state: test_damus_state, described: .hashtag("damus"))
|
||||||
|
|
||||||
SearchHeaderView(state: test_damus_state(), described: .unknown)
|
SearchHeaderView(state: test_damus_state, described: .unknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,19 @@ import SwiftUI
|
|||||||
struct SelectableText: View {
|
struct SelectableText: View {
|
||||||
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
|
let textAlignment: NSTextAlignment
|
||||||
|
|
||||||
@State private var selectedTextHeight: CGFloat = .zero
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
@State private var selectedTextWidth: CGFloat = .zero
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
|
init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
||||||
|
self.attributedString = attributedString
|
||||||
|
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
TextViewRepresentable(
|
TextViewRepresentable(
|
||||||
@@ -24,11 +31,16 @@ struct SelectableText: View {
|
|||||||
textColor: UIColor.label,
|
textColor: UIColor.label,
|
||||||
font: eventviewsize_to_uifont(size),
|
font: eventviewsize_to_uifont(size),
|
||||||
fixedWidth: selectedTextWidth,
|
fixedWidth: selectedTextWidth,
|
||||||
|
textAlignment: self.textAlignment,
|
||||||
height: $selectedTextHeight
|
height: $selectedTextHeight
|
||||||
)
|
)
|
||||||
.padding([.leading, .trailing], -1.0)
|
.padding([.leading, .trailing], -1.0)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.selectedTextWidth = geo.size.width
|
if geo.size.width == .zero {
|
||||||
|
self.selectedTextHeight = 1000.0
|
||||||
|
} else {
|
||||||
|
self.selectedTextWidth = geo.size.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: geo.size) { newSize in
|
.onChange(of: geo.size) { newSize in
|
||||||
self.selectedTextWidth = newSize.width
|
self.selectedTextWidth = newSize.width
|
||||||
@@ -44,6 +56,7 @@ struct SelectableText: View {
|
|||||||
let textColor: UIColor
|
let textColor: UIColor
|
||||||
let font: UIFont
|
let font: UIFont
|
||||||
let fixedWidth: CGFloat
|
let fixedWidth: CGFloat
|
||||||
|
let textAlignment: NSTextAlignment
|
||||||
|
|
||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
@@ -57,12 +70,14 @@ struct SelectableText: View {
|
|||||||
view.textContainerInset = .zero
|
view.textContainerInset = .zero
|
||||||
view.textContainerInset.left = 1.0
|
view.textContainerInset.left = 1.0
|
||||||
view.textContainerInset.right = 1.0
|
view.textContainerInset.right = 1.0
|
||||||
|
view.textAlignment = textAlignment
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||||
let mutableAttributedString = createNSAttributedString()
|
let mutableAttributedString = createNSAttributedString()
|
||||||
uiView.attributedText = mutableAttributedString
|
uiView.attributedText = mutableAttributedString
|
||||||
|
uiView.textAlignment = self.textAlignment
|
||||||
|
|
||||||
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// MusicController.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-08-21.
|
||||||
|
//
|
||||||
|
import SwiftUI
|
||||||
|
import MediaPlayer
|
||||||
|
|
||||||
|
enum MusicState {
|
||||||
|
case playback_state(MPMusicPlaybackState)
|
||||||
|
case song(MPMediaItem?)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MusicController {
|
||||||
|
let player: MPMusicPlayerController
|
||||||
|
|
||||||
|
let onChange: (MusicState) -> ()
|
||||||
|
|
||||||
|
init(onChange: @escaping (MusicState) -> ()) {
|
||||||
|
player = .systemMusicPlayer
|
||||||
|
|
||||||
|
player.beginGeneratingPlaybackNotifications()
|
||||||
|
|
||||||
|
self.onChange = onChange
|
||||||
|
|
||||||
|
print("Playback State: \(player.playbackState)")
|
||||||
|
print("Now Playing Item: \(player.nowPlayingItem?.title ?? "None")")
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.songChanged(notification:)), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: player)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.playbackStatusChanged(notification:)), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: player)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("deinit musiccontroller")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func songChanged(notification: Notification) {
|
||||||
|
onChange(.song(player.nowPlayingItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func playbackStatusChanged(notification: Notification) {
|
||||||
|
onChange(.playback_state(player.playbackState))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
//
|
||||||
|
// UserStatus.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-08-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MediaPlayer
|
||||||
|
|
||||||
|
struct Song {
|
||||||
|
let started_playing: Date
|
||||||
|
let content: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserStatus {
|
||||||
|
let type: UserStatusType
|
||||||
|
let expires_at: Date?
|
||||||
|
var content: String
|
||||||
|
let created_at: UInt32
|
||||||
|
var url: URL?
|
||||||
|
|
||||||
|
func to_note(keypair: FullKeypair) -> NostrEvent? {
|
||||||
|
return make_user_status_note(status: self, keypair: keypair)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(type: UserStatusType, expires_at: Date?, content: String, created_at: UInt32, url: URL? = nil) {
|
||||||
|
self.type = type
|
||||||
|
self.expires_at = expires_at
|
||||||
|
self.content = content
|
||||||
|
self.created_at = created_at
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
func expired() -> Bool {
|
||||||
|
guard let expires_at else { return false }
|
||||||
|
return Date.now >= expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(ev: NostrEvent) {
|
||||||
|
guard let tag = ev.referenced_params.just_one() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = tag.param.string()
|
||||||
|
if str == "general" {
|
||||||
|
self.type = .general
|
||||||
|
} else if str == "music" {
|
||||||
|
self.type = .music
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_char("r") }),
|
||||||
|
tag.count >= 2,
|
||||||
|
let url = URL(string: tag[1].string())
|
||||||
|
{
|
||||||
|
self.url = url
|
||||||
|
} else {
|
||||||
|
self.url = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_str("expiration") }),
|
||||||
|
tag.count == 2,
|
||||||
|
let expires = UInt32(tag[1].string())
|
||||||
|
{
|
||||||
|
self.expires_at = Date(timeIntervalSince1970: TimeInterval(expires))
|
||||||
|
} else {
|
||||||
|
self.expires_at = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content = ev.content
|
||||||
|
self.created_at = ev.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserStatusType: String {
|
||||||
|
case music
|
||||||
|
case general
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserStatusModel: ObservableObject {
|
||||||
|
@Published var general: UserStatus?
|
||||||
|
@Published var music: UserStatus?
|
||||||
|
|
||||||
|
func update_status(_ s: UserStatus) {
|
||||||
|
// whitespace = delete
|
||||||
|
let del = s.content.allSatisfy({ c in c.isWhitespace })
|
||||||
|
|
||||||
|
switch s.type {
|
||||||
|
case .music:
|
||||||
|
if del {
|
||||||
|
self.music = nil
|
||||||
|
} else {
|
||||||
|
self.music = s
|
||||||
|
}
|
||||||
|
case .general:
|
||||||
|
if del {
|
||||||
|
self.general = nil
|
||||||
|
} else {
|
||||||
|
self.general = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func try_expire() {
|
||||||
|
if let general, general.expired() {
|
||||||
|
self.general = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let music, music.expired() {
|
||||||
|
self.music = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _playing_enabled: Bool
|
||||||
|
var playing_enabled: Bool {
|
||||||
|
set {
|
||||||
|
var new_val = newValue
|
||||||
|
|
||||||
|
if newValue {
|
||||||
|
MPMediaLibrary.requestAuthorization { astatus in
|
||||||
|
switch astatus {
|
||||||
|
case .notDetermined: new_val = false
|
||||||
|
case .denied: new_val = false
|
||||||
|
case .restricted: new_val = false
|
||||||
|
case .authorized: new_val = true
|
||||||
|
@unknown default:
|
||||||
|
new_val = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_val != playing_enabled {
|
||||||
|
_playing_enabled = new_val
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
return _playing_enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(playing: UserStatus? = nil, status: UserStatus? = nil) {
|
||||||
|
self.general = status
|
||||||
|
self.music = playing
|
||||||
|
self._playing_enabled = false
|
||||||
|
self.playing_enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
static var current_track: String? {
|
||||||
|
let player = MPMusicPlayerController.systemMusicPlayer
|
||||||
|
guard let nowPlayingItem = player.nowPlayingItem else { return nil }
|
||||||
|
return nowPlayingItem.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_user_status_note(status: UserStatus, keypair: FullKeypair, expiry: Date? = nil) -> NostrEvent?
|
||||||
|
{
|
||||||
|
var tags: [[String]] = [ ["d", status.type.rawValue] ]
|
||||||
|
|
||||||
|
if let expiry {
|
||||||
|
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||||
|
} else if let expiry = status.expires_at {
|
||||||
|
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||||
|
}
|
||||||
|
|
||||||
|
if let url = status.url {
|
||||||
|
tags.append(["r", url.absoluteString])
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = NostrKind.status.rawValue
|
||||||
|
guard let ev = NostrEvent(content: status.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
//
|
||||||
|
// UserStatusSheet.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-08-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum StatusDuration: CustomStringConvertible, CaseIterable {
|
||||||
|
case never
|
||||||
|
case thirty_mins
|
||||||
|
case hour
|
||||||
|
case four_hours
|
||||||
|
case day
|
||||||
|
case week
|
||||||
|
|
||||||
|
var timeInterval: TimeInterval? {
|
||||||
|
switch self {
|
||||||
|
case .never:
|
||||||
|
return nil
|
||||||
|
case .thirty_mins:
|
||||||
|
return 60 * 30
|
||||||
|
case .hour:
|
||||||
|
return 60 * 60
|
||||||
|
case .four_hours:
|
||||||
|
return 60 * 60 * 4
|
||||||
|
case .day:
|
||||||
|
return 60 * 60 * 24
|
||||||
|
case .week:
|
||||||
|
return 60 * 60 * 24 * 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration: Date? {
|
||||||
|
guard let timeInterval else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date.now.addingTimeInterval(timeInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
guard let timeInterval else {
|
||||||
|
return NSLocalizedString("Never", comment: "Profile status duration setting of never expiring.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
formatter.allowedUnits = [.minute, .hour, .day, .weekOfMonth]
|
||||||
|
return formatter.string(from: timeInterval) ?? "\(timeInterval) seconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Fields{
|
||||||
|
case status
|
||||||
|
case link
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserStatusSheet: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let postbox: PostBox
|
||||||
|
let keypair: Keypair
|
||||||
|
|
||||||
|
@State var duration: StatusDuration = .never
|
||||||
|
@State var show_link: Bool = false
|
||||||
|
|
||||||
|
@ObservedObject var status: UserStatusModel
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
var status_binding: Binding<String> {
|
||||||
|
Binding(get: {
|
||||||
|
status.general?.content ?? ""
|
||||||
|
}, set: { v in
|
||||||
|
if let general = status.general {
|
||||||
|
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: general.url)
|
||||||
|
} else {
|
||||||
|
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var url_binding: Binding<String> {
|
||||||
|
Binding(get: {
|
||||||
|
status.general?.url?.absoluteString ?? ""
|
||||||
|
}, set: { v in
|
||||||
|
if let general = status.general {
|
||||||
|
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: general.content, created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
|
||||||
|
} else {
|
||||||
|
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: "", created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
// This is needed to prevent the view from being moved when the keyboard is shown
|
||||||
|
GeometryReader { geometry in
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Button(action: {
|
||||||
|
dismiss()
|
||||||
|
}, label: {
|
||||||
|
Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.")
|
||||||
|
.padding(10)
|
||||||
|
})
|
||||||
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
guard let status = self.status.general,
|
||||||
|
let kp = keypair.to_full(),
|
||||||
|
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postbox.send(ev)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}, label: {
|
||||||
|
Text("Share", comment: "Save button text for saving profile status settings.")
|
||||||
|
})
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
.padding(5)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
ZStack(alignment: .top) {
|
||||||
|
ProfilePicView(pubkey: keypair.pubkey, size: 120.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||||
|
.padding(.top, 30)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
TextField(NSLocalizedString("Staying humble...", comment: "Placeholder as an example of what the user could set as their profile status."), text: status_binding, axis: .vertical)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.lineLimit(3)
|
||||||
|
.frame(width: 175)
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(colorScheme == .light ? .white : DamusColors.neutral3)
|
||||||
|
.cornerRadius(15)
|
||||||
|
.shadow(color: colorScheme == .light ? DamusColors.neutral3 : .clear, radius: 15)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(colorScheme == .light ? .white : DamusColors.neutral3)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.padding(.trailing, 140)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(colorScheme == .light ? .white : DamusColors.neutral3)
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
.padding(.trailing, 120)
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding(.leading, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Image("link")
|
||||||
|
.foregroundColor(DamusColors.neutral3)
|
||||||
|
|
||||||
|
TextField(text: url_binding, label: {
|
||||||
|
Text("Add an external link", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.")
|
||||||
|
})
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Toggle(isOn: $status.playing_enabled, label: {
|
||||||
|
Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.")
|
||||||
|
})
|
||||||
|
.tint(DamusColors.purple)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.")
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
|
||||||
|
ForEach(StatusDuration.allCases, id: \.self) { d in
|
||||||
|
Text(verbatim: d.description)
|
||||||
|
.tag(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
.background(DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all))
|
||||||
|
}
|
||||||
|
.dismissKeyboardOnTap()
|
||||||
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct UserStatusSheet_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// UserStatus.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-08-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import MediaPlayer
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
struct UserStatusView: View {
|
||||||
|
@ObservedObject var status: UserStatusModel
|
||||||
|
|
||||||
|
var show_general: Bool
|
||||||
|
var show_music: Bool
|
||||||
|
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
|
func Status(st: UserStatus, prefix: String = "") -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(verbatim: "\(prefix)\(st.content)")
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.callout.italic())
|
||||||
|
if st.url != nil {
|
||||||
|
Image("link")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
if let url = st.url {
|
||||||
|
openURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextMenu(
|
||||||
|
menuItems: {
|
||||||
|
if let url = st.url {
|
||||||
|
Button(url.absoluteString, action: { openURL(url) }) }
|
||||||
|
}, preview: {
|
||||||
|
if let url = st.url {
|
||||||
|
URLPreview(url: url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
if show_general, let general = status.general {
|
||||||
|
Status(st: general)
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_music, let playing = status.music {
|
||||||
|
Status(st: playing, prefix: "🎵")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct URLPreview: UIViewRepresentable {
|
||||||
|
var url: URL
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
|
return WKWebView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ wkView: WKWebView, context: Context) {
|
||||||
|
let request = URLRequest(url: url)
|
||||||
|
wkView.load(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct UserStatusView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UserStatusView(status: UserStatus(type: .music, expires_at: nil, content: "Track - Artist", created_at: 0, url: URL(string: "spotify:search:abc")), show_general: true, show_music: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -42,7 +42,7 @@ struct TranslateView: View {
|
|||||||
.translate_button_style()
|
.translate_button_style()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated) -> some View {
|
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
|
||||||
return VStack(alignment: .leading) {
|
return VStack(alignment: .leading) {
|
||||||
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
|
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
|
||||||
Text(translatedFromLanguageString)
|
Text(translatedFromLanguageString)
|
||||||
@@ -54,7 +54,7 @@ struct TranslateView: View {
|
|||||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||||
} else {
|
} else {
|
||||||
artifacts.content.text
|
artifacts.content.text
|
||||||
.font(eventviewsize_to_font(self.size))
|
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ struct TranslateView: View {
|
|||||||
guard let note_language = translations_model.note_language else {
|
guard let note_language = translations_model.note_language else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings, note_lang: note_language)
|
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language, purple: damus_state.purple)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.translations_model.state = res
|
self.translations_model.state = res
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ struct TranslateView: View {
|
|||||||
Text("")
|
Text("")
|
||||||
case .translated(let translated):
|
case .translated(let translated):
|
||||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||||
TranslatedView(lang: languageName, artifacts: translated.artifacts)
|
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
|
||||||
case .not_needed:
|
case .not_needed:
|
||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
@@ -120,16 +120,16 @@ extension View {
|
|||||||
|
|
||||||
struct TranslateView_Previews: PreviewProvider {
|
struct TranslateView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let ds = test_damus_state()
|
let ds = test_damus_state
|
||||||
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
TranslateView(damus_state: ds, event: test_note, size: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String, purple: DamusPurple) async -> TranslateStatus {
|
||||||
|
|
||||||
// If the note language is different from our preferred languages, send a translation request.
|
// If the note language is different from our preferred languages, send a translation request.
|
||||||
let translator = Translator(settings)
|
let translator = Translator(settings, purple: purple)
|
||||||
let originalContent = event.get_content(privkey)
|
let originalContent = event.get_content(keypair)
|
||||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||||
|
|
||||||
guard let translated_note else {
|
guard let translated_note else {
|
||||||
@@ -143,7 +143,7 @@ func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, set
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render translated note
|
// Render translated note
|
||||||
let translated_blocks = event.get_blocks(content: translated_note)
|
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
||||||
|
|
||||||
// and cache it
|
// and cache it
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct UserViewRow: View {
|
struct UserViewRow: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let pubkey: String
|
let pubkey: Pubkey
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||||
@@ -20,12 +20,12 @@ struct UserViewRow: View {
|
|||||||
|
|
||||||
struct UserView: View {
|
struct UserView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let pubkey: String
|
let pubkey: Pubkey
|
||||||
let spacer: Bool
|
let spacer: Bool
|
||||||
|
|
||||||
@State var about_text: Text? = nil
|
@State var about_text: Text? = nil
|
||||||
|
|
||||||
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
|
init(damus_state: DamusState, pubkey: Pubkey, spacer: Bool = true) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.spacer = spacer
|
self.spacer = spacer
|
||||||
@@ -37,8 +37,7 @@ struct UserView: View {
|
|||||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
ProfileName(pubkey: pubkey, damus: damus_state, show_nip5_domain: false)
|
||||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
|
|
||||||
if let about_text {
|
if let about_text {
|
||||||
about_text
|
about_text
|
||||||
.lineLimit(3)
|
.lineLimit(3)
|
||||||
@@ -56,6 +55,6 @@ struct UserView: View {
|
|||||||
|
|
||||||
struct UserView_Previews: PreviewProvider {
|
struct UserView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UserView(damus_state: test_damus_state(), pubkey: "pk")
|
UserView(damus_state: test_damus_state, pubkey: test_note.pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,33 +9,57 @@ import SwiftUI
|
|||||||
|
|
||||||
struct WebsiteLink: View {
|
struct WebsiteLink: View {
|
||||||
let url: URL
|
let url: URL
|
||||||
|
let style: StyleVariant
|
||||||
@Environment(\.openURL) var openURL
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
|
init(url: URL, style: StyleVariant? = nil) {
|
||||||
|
self.url = url
|
||||||
|
self.style = style ?? .normal
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image("link")
|
Image("link")
|
||||||
.foregroundColor(.gray)
|
.resizable()
|
||||||
.font(.footnote)
|
.frame(width: 16, height: 16)
|
||||||
|
.foregroundColor(self.style == .accent ? .white : .gray)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.padding([.leading], 10)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
openURL(url)
|
openURL(url)
|
||||||
}, label: {
|
}, label: {
|
||||||
Text(link_text)
|
Text(link_text)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(self.style == .accent ? .white : .accentColor)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
})
|
})
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.padding([.trailing], 10)
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
self.style == .accent ?
|
||||||
|
AnyView(RoundedRectangle(cornerRadius: 50).fill(PinkGradient))
|
||||||
|
: AnyView(Color.clear)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var link_text: String {
|
var link_text: String {
|
||||||
url.host ?? url.absoluteString
|
url.host ?? url.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum StyleVariant {
|
||||||
|
case normal
|
||||||
|
case accent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebsiteLink_Previews: PreviewProvider {
|
struct WebsiteLink_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
WebsiteLink(url: URL(string: "https://jb55.com")!)
|
WebsiteLink(url: URL(string: "https://jb55.com")!)
|
||||||
|
.previewDisplayName("Normal")
|
||||||
|
WebsiteLink(url: URL(string: "https://jb55.com")!, style: .accent)
|
||||||
|
.previewDisplayName("Accent")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// ContentParsing.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-07-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum NoteContent {
|
||||||
|
case note(NostrEvent)
|
||||||
|
case content(String, TagsSequence?)
|
||||||
|
|
||||||
|
init(note: NostrEvent, keypair: Keypair) {
|
||||||
|
if note.known_kind == .dm {
|
||||||
|
self = .content(note.get_content(keypair), note.tags)
|
||||||
|
} else {
|
||||||
|
self = .note(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks {
|
||||||
|
var out: [Block] = []
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i < bs.num_blocks) {
|
||||||
|
let block = bs.blocks[i]
|
||||||
|
|
||||||
|
if let converted = Block(block, tags: tags) {
|
||||||
|
out.append(converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let words = Int(bs.words)
|
||||||
|
blocks_free(&bs)
|
||||||
|
|
||||||
|
return Blocks(words: words, blocks: out)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse_note_content(content: NoteContent) -> Blocks {
|
||||||
|
var bs = note_blocks()
|
||||||
|
bs.num_blocks = 0;
|
||||||
|
|
||||||
|
blocks_init(&bs)
|
||||||
|
|
||||||
|
switch content {
|
||||||
|
case .content(let s, let tags):
|
||||||
|
return s.withCString { cptr in
|
||||||
|
damus_parse_content(&bs, cptr)
|
||||||
|
return parsed_blocks_finish(bs: &bs, tags: tags)
|
||||||
|
}
|
||||||
|
case .note(let note):
|
||||||
|
damus_parse_content(&bs, note.content_raw)
|
||||||
|
return parsed_blocks_finish(bs: &bs, tags: note.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
|
||||||
|
if tags.count == 0 {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// build a set of indices for each event mention
|
||||||
|
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||||
|
|
||||||
|
/// simpler case with no mentions
|
||||||
|
if mention_indices.count == 0 {
|
||||||
|
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
var evrefs: [EventRef] = []
|
||||||
|
var first: Bool = true
|
||||||
|
var first_ref: NoteRef? = nil
|
||||||
|
|
||||||
|
for ref in ev_tags {
|
||||||
|
if first {
|
||||||
|
first_ref = ref
|
||||||
|
evrefs.append(.thread_id(ref))
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
|
||||||
|
evrefs.append(.reply(ref))
|
||||||
|
}
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if let first_ref, count == 1 {
|
||||||
|
let r = first_ref
|
||||||
|
return [.reply_to_root(r)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return evrefs
|
||||||
|
}
|
||||||
|
|
||||||
|
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
|
||||||
|
var mentions: [EventRef] = []
|
||||||
|
var ev_refs: [NoteRef] = []
|
||||||
|
var i: Int = 0
|
||||||
|
|
||||||
|
for tag in tags {
|
||||||
|
if let note_id = NoteRef.from_tag(tag: tag) {
|
||||||
|
if mention_indices.contains(i) {
|
||||||
|
mentions.append(.mention(.noteref(note_id, index: i)))
|
||||||
|
} else {
|
||||||
|
ev_refs.append(note_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||||
|
replies.append(contentsOf: mentions)
|
||||||
|
return replies
|
||||||
|
}
|
||||||
+292
-227
@@ -7,12 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import MediaPlayer
|
||||||
struct TimestampedProfile {
|
|
||||||
let profile: Profile
|
|
||||||
let timestamp: Int64
|
|
||||||
let event: NostrEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ZapSheet {
|
struct ZapSheet {
|
||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
@@ -27,9 +22,12 @@ enum Sheets: Identifiable {
|
|||||||
case post(PostAction)
|
case post(PostAction)
|
||||||
case report(ReportTarget)
|
case report(ReportTarget)
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
|
case profile_action(Pubkey)
|
||||||
case zap(ZapSheet)
|
case zap(ZapSheet)
|
||||||
case select_wallet(SelectWallet)
|
case select_wallet(SelectWallet)
|
||||||
case filter
|
case filter
|
||||||
|
case user_status
|
||||||
|
case onboardingSuggestions
|
||||||
|
|
||||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||||
@@ -42,55 +40,45 @@ enum Sheets: Identifiable {
|
|||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .report: return "report"
|
case .report: return "report"
|
||||||
case .post(let action): return "post-" + (action.ev?.id ?? "")
|
case .user_status: return "user_status"
|
||||||
case .event(let ev): return "event-" + ev.id
|
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
|
||||||
case .zap(let sheet): return "zap-" + sheet.target.id
|
case .event(let ev): return "event-" + ev.id.hex()
|
||||||
|
case .profile_action(let pubkey): return "profile-action-" + pubkey.npub
|
||||||
|
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
|
||||||
case .select_wallet: return "select-wallet"
|
case .select_wallet: return "select-wallet"
|
||||||
case .filter: return "filter"
|
case .filter: return "filter"
|
||||||
}
|
case .onboardingSuggestions: return "onboarding-suggestions"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FilterState : Int {
|
|
||||||
case posts_and_replies = 1
|
|
||||||
case posts = 0
|
|
||||||
|
|
||||||
func filter(ev: NostrEvent) -> Bool {
|
|
||||||
switch self {
|
|
||||||
case .posts:
|
|
||||||
return !ev.is_reply(nil)
|
|
||||||
case .posts_and_replies:
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
|
let appDelegate: AppDelegate?
|
||||||
|
|
||||||
var pubkey: String {
|
var pubkey: Pubkey {
|
||||||
return keypair.pubkey
|
return keypair.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
var privkey: String? {
|
var privkey: Privkey? {
|
||||||
return keypair.privkey
|
return keypair.privkey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
@State var active_sheet: Sheets? = nil
|
@State var active_sheet: Sheets? = nil
|
||||||
@State var damus_state: DamusState? = nil
|
@State var damus_state: DamusState!
|
||||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||||
@State var is_deleted_account: Bool = false
|
@State var muting: Pubkey? = nil
|
||||||
@State var muting: String? = nil
|
|
||||||
@State var confirm_mute: Bool = false
|
@State var confirm_mute: Bool = false
|
||||||
|
@State var hide_bar: Bool = false
|
||||||
@State var user_muted_confirm: Bool = false
|
@State var user_muted_confirm: Bool = false
|
||||||
@State var confirm_overwrite_mutelist: Bool = false
|
@State var confirm_overwrite_mutelist: Bool = false
|
||||||
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||||
@State private var isSideBarOpened = false
|
@State private var isSideBarOpened = false
|
||||||
var home: HomeModel = HomeModel()
|
var home: HomeModel = HomeModel()
|
||||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
|
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||||
let sub_id = UUID().description
|
let sub_id = UUID().description
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@@ -103,6 +91,12 @@ struct ContentView: View {
|
|||||||
.id("what")
|
.id("what")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||||
|
var filters = ContentFilters.defaults(damus_state: damus_state!)
|
||||||
|
filters.append(fstate.filter)
|
||||||
|
return ContentFilters(filters: filters).filter
|
||||||
|
}
|
||||||
|
|
||||||
var PostingTimelineView: some View {
|
var PostingTimelineView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -110,10 +104,10 @@ struct ContentView: View {
|
|||||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||||
mystery
|
mystery
|
||||||
|
|
||||||
contentTimelineView(filter: FilterState.posts.filter)
|
contentTimelineView(filter: content_filter(.posts))
|
||||||
.tag(FilterState.posts)
|
.tag(FilterState.posts)
|
||||||
.id(FilterState.posts)
|
.id(FilterState.posts)
|
||||||
contentTimelineView(filter: FilterState.posts_and_replies.filter)
|
contentTimelineView(filter: content_filter(.posts_and_replies))
|
||||||
.tag(FilterState.posts_and_replies)
|
.tag(FilterState.posts_and_replies)
|
||||||
.id(FilterState.posts_and_replies)
|
.id(FilterState.posts_and_replies)
|
||||||
}
|
}
|
||||||
@@ -140,13 +134,15 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||||
ZStack {
|
TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) {
|
||||||
if let damus = self.damus_state {
|
PullDownSearchView(state: damus_state, on_cancel: {})
|
||||||
TimelineView<AnyView>(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func navIsAtRoot() -> Bool {
|
||||||
|
return navigationCoordinator.isAtRoot()
|
||||||
|
}
|
||||||
|
|
||||||
func popToRoot() {
|
func popToRoot() {
|
||||||
navigationCoordinator.popToRoot()
|
navigationCoordinator.popToRoot()
|
||||||
isSideBarOpened = false
|
isSideBarOpened = false
|
||||||
@@ -190,6 +186,9 @@ struct ContentView: View {
|
|||||||
.shadow(color: DamusColors.purple, radius: 2)
|
.shadow(color: DamusColors.purple, radius: 2)
|
||||||
.opacity(isSideBarOpened ? 0 : 1)
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
|
.onTapGesture {
|
||||||
|
isSideBarOpened.toggle()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
timelineNavItem
|
timelineNavItem
|
||||||
.opacity(isSideBarOpened ? 0 : 1)
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
@@ -202,12 +201,8 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func MaybeReportView(target: ReportTarget) -> some View {
|
func MaybeReportView(target: ReportTarget) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if let damus_state {
|
if let keypair = damus_state.keypair.to_full() {
|
||||||
if let sec = damus_state.keypair.privkey {
|
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
||||||
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@@ -230,9 +225,9 @@ struct ContentView: View {
|
|||||||
navigationCoordinator.push(route: Route.Script(script: model))
|
navigationCoordinator.push(route: Route.Script(script: model))
|
||||||
}
|
}
|
||||||
|
|
||||||
func open_profile(id: String) {
|
func open_profile(pubkey: Pubkey) {
|
||||||
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
|
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
|
||||||
let followers = FollowersModel(damus_state: damus_state!, target: id)
|
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
|
||||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,16 +260,13 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// maybe expand this to other timelines in the future
|
// maybe expand this to other timelines in the future
|
||||||
if selected_timeline == .search {
|
if selected_timeline == .search {
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
//isFilterVisible.toggle()
|
|
||||||
present_sheet(.filter)
|
present_sheet(.filter)
|
||||||
}) {
|
}, label: {
|
||||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
Image("filter")
|
||||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
|
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
//.contentShape(Rectangle())
|
})
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +277,7 @@ struct ContentView: View {
|
|||||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||||
)
|
)
|
||||||
.navigationDestination(for: Route.self) { route in
|
.navigationDestination(for: Route.self) { route in
|
||||||
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
|
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||||
navigationCoordinator.popToRoot()
|
navigationCoordinator.popToRoot()
|
||||||
@@ -293,16 +285,26 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
|
|
||||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
if !hide_bar {
|
||||||
.padding([.bottom], 8)
|
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
.padding([.bottom], 8)
|
||||||
|
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||||
|
} else {
|
||||||
|
Text("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.keyboard)
|
.ignoresSafeArea(.keyboard)
|
||||||
|
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.connect()
|
self.connect()
|
||||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||||
setup_notifications()
|
setup_notifications()
|
||||||
|
if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions {
|
||||||
|
active_sheet = .onboardingSuggestions
|
||||||
|
hasSeenOnboardingSuggestions = true
|
||||||
|
}
|
||||||
|
self.appDelegate?.settings = damus_state?.settings
|
||||||
}
|
}
|
||||||
.sheet(item: $active_sheet) { item in
|
.sheet(item: $active_sheet) { item in
|
||||||
switch item {
|
switch item {
|
||||||
@@ -310,21 +312,24 @@ struct ContentView: View {
|
|||||||
MaybeReportView(target: target)
|
MaybeReportView(target: target)
|
||||||
case .post(let action):
|
case .post(let action):
|
||||||
PostView(action: action, damus_state: damus_state!)
|
PostView(action: action, damus_state: damus_state!)
|
||||||
|
case .user_status:
|
||||||
|
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
case .event:
|
case .event:
|
||||||
EventDetailView()
|
EventDetailView()
|
||||||
|
case .profile_action(let pubkey):
|
||||||
|
ProfileActionSheetView(damus_state: damus_state!, pubkey: pubkey)
|
||||||
case .zap(let zapsheet):
|
case .zap(let zapsheet):
|
||||||
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
||||||
case .select_wallet(let select):
|
case .select_wallet(let select):
|
||||||
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
||||||
case .filter:
|
case .filter:
|
||||||
let timeline = selected_timeline
|
let timeline = selected_timeline
|
||||||
if #available(iOS 16.0, *) {
|
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
.presentationDetents([.height(550)])
|
||||||
.presentationDetents([.height(550)])
|
.presentationDragIndicator(.visible)
|
||||||
.presentationDragIndicator(.visible)
|
case .onboardingSuggestions:
|
||||||
} else {
|
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
||||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
@@ -335,106 +340,101 @@ struct ContentView: View {
|
|||||||
|
|
||||||
switch res {
|
switch res {
|
||||||
case .filter(let filt): self.open_search(filt: filt)
|
case .filter(let filt): self.open_search(filt: filt)
|
||||||
case .profile(let id): self.open_profile(id: id)
|
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||||
case .event(let ev): self.open_event(ev: ev)
|
case .event(let ev): self.open_event(ev: ev)
|
||||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||||
case .script(let data): self.open_script(data)
|
case .script(let data): self.open_script(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.compose)) { notif in
|
.onReceive(handle_notify(.compose)) { action in
|
||||||
let action = notif.object as! PostAction
|
|
||||||
self.active_sheet = .post(action)
|
self.active_sheet = .post(action)
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.display_tabbar)) { display in
|
||||||
|
let show = display
|
||||||
|
self.hide_bar = !show
|
||||||
|
}
|
||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.postbox.try_flushing_events()
|
self.damus_state?.postbox.try_flushing_events()
|
||||||
|
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.deleted_account)) { notif in
|
.onReceive(handle_notify(.report)) { target in
|
||||||
self.is_deleted_account = true
|
|
||||||
}
|
|
||||||
.onReceive(handle_notify(.report)) { notif in
|
|
||||||
let target = notif.object as! ReportTarget
|
|
||||||
self.active_sheet = .report(target)
|
self.active_sheet = .report(target)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.mute)) { notif in
|
.onReceive(handle_notify(.mute)) { pubkey in
|
||||||
let pubkey = notif.object as! String
|
|
||||||
self.muting = pubkey
|
self.muting = pubkey
|
||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.attached_wallet)) { notif in
|
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||||
// update the lightning address on our profile when we attach a
|
// update the lightning address on our profile when we attach a
|
||||||
// wallet with an associated
|
// wallet with an associated
|
||||||
let nwc = notif.object as! WalletConnectURL
|
|
||||||
guard let ds = self.damus_state,
|
guard let ds = self.damus_state,
|
||||||
let lud16 = nwc.lud16,
|
let lud16 = nwc.lud16,
|
||||||
let keypair = ds.keypair.to_full(),
|
let keypair = ds.keypair.to_full()
|
||||||
let profile = ds.profiles.lookup(id: ds.pubkey),
|
|
||||||
lud16 != profile.lud16
|
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||||
|
|
||||||
|
guard let profile = profile_txn.unsafeUnownedValue,
|
||||||
|
lud16 != profile.lud16 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// clear zapper cache for old lud16
|
// clear zapper cache for old lud16
|
||||||
if profile.lud16 != nil {
|
if profile.lud16 != nil {
|
||||||
// TODO: should this be somewhere else, where we process profile events!?
|
// TODO: should this be somewhere else, where we process profile events!?
|
||||||
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.lud16 = lud16
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||||
let ev = make_metadata_event(keypair: keypair, metadata: profile)
|
|
||||||
|
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
.onReceive(handle_notify(.broadcast)) { ev in
|
||||||
let ev = obj.object as! NostrEvent
|
guard let ds = self.damus_state else { return }
|
||||||
guard let ds = self.damus_state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
|
|
||||||
ds.postbox.send(profile.event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollow)) { notif in
|
.onReceive(handle_notify(.unfollow)) { target in
|
||||||
guard let state = self.damus_state else { return }
|
guard let state = self.damus_state else { return }
|
||||||
_ = handle_unfollow_notif(state: state, notif: notif)
|
_ = handle_unfollow(state: state, unfollow: target.follow_ref)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
.onReceive(handle_notify(.unfollowed)) { unfollow in
|
||||||
let unfollow = notif.object as! ReferencedId
|
|
||||||
home.resubscribe(.unfollowing(unfollow))
|
home.resubscribe(.unfollowing(unfollow))
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.follow)) { notif in
|
.onReceive(handle_notify(.follow)) { target in
|
||||||
guard let state = self.damus_state else { return }
|
guard let state = self.damus_state else { return }
|
||||||
guard handle_follow_notif(state: state, notif: notif) else { return }
|
handle_follow_notif(state: state, target: target)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.followed)) { notif in
|
.onReceive(handle_notify(.followed)) { _ in
|
||||||
home.resubscribe(.following)
|
home.resubscribe(.following)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.post)) { notif in
|
.onReceive(handle_notify(.post)) { post in
|
||||||
guard let state = self.damus_state,
|
guard let state = self.damus_state,
|
||||||
let keypair = state.keypair.to_full() else {
|
let keypair = state.keypair.to_full() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, notif: notif) {
|
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
|
||||||
self.active_sheet = nil
|
self.active_sheet = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
.onReceive(handle_notify(.new_mutes)) { _ in
|
||||||
home.filter_events()
|
home.filter_events()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.mute_thread)) { notif in
|
.onReceive(handle_notify(.mute_thread)) { _ in
|
||||||
home.filter_events()
|
home.filter_events()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
.onReceive(handle_notify(.unmute_thread)) { _ in
|
||||||
home.filter_events()
|
home.filter_events()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.present_sheet)) { notif in
|
.onReceive(handle_notify(.present_sheet)) { sheet in
|
||||||
let sheet = notif.object as! Sheets
|
|
||||||
self.active_sheet = sheet
|
self.active_sheet = sheet
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.zapping)) { notif in
|
.onReceive(handle_notify(.zapping)) { zap_ev in
|
||||||
let zap_ev = notif.object as! ZappingEvent
|
|
||||||
|
|
||||||
guard !zap_ev.is_custom else {
|
guard !zap_ev.is_custom else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -447,12 +447,20 @@ struct ContentView: View {
|
|||||||
present_sheet(.select_wallet(invoice: inv))
|
present_sheet(.select_wallet(invoice: inv))
|
||||||
} else {
|
} else {
|
||||||
let wallet = damus_state!.settings.default_wallet.model
|
let wallet = damus_state!.settings.default_wallet.model
|
||||||
open_with_wallet(wallet: wallet, invoice: inv)
|
do {
|
||||||
|
try open_with_wallet(wallet: wallet, invoice: inv)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
present_sheet(.select_wallet(invoice: inv))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .sent_from_nwc:
|
case .sent_from_nwc:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||||
|
damus_state.pool.disconnect()
|
||||||
|
}
|
||||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||||
switch phase {
|
switch phase {
|
||||||
case .background:
|
case .background:
|
||||||
@@ -469,57 +477,49 @@ struct ContentView: View {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.local_notification)) { notif in
|
.onReceive(handle_notify(.local_notification)) { local in
|
||||||
|
guard let damus_state else { return }
|
||||||
|
|
||||||
guard let local = notif.object as? LossyLocalNotification,
|
switch local.mention {
|
||||||
let damus_state else {
|
case .pubkey(let pubkey):
|
||||||
return
|
open_profile(pubkey: pubkey)
|
||||||
|
|
||||||
|
case .note(let noteId):
|
||||||
|
guard let target = damus_state.events.lookup(noteId) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch local.type {
|
||||||
|
case .dm:
|
||||||
|
selected_timeline = .dms
|
||||||
|
damus_state.dms.set_active_dm(target.pubkey)
|
||||||
|
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||||
|
case .like, .zap, .mention, .repost:
|
||||||
|
open_event(ev: target)
|
||||||
|
case .profile_zap:
|
||||||
|
// Handled separately above.
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if local.type == .profile_zap {
|
|
||||||
open_profile(id: local.event_id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch local.type {
|
|
||||||
case .dm:
|
|
||||||
selected_timeline = .dms
|
|
||||||
damus_state.dms.set_active_dm(target.pubkey)
|
|
||||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
|
||||||
case .like: fallthrough
|
|
||||||
case .zap: fallthrough
|
|
||||||
case .mention: fallthrough
|
|
||||||
case .repost:
|
|
||||||
open_event(ev: target)
|
|
||||||
case .profile_zap:
|
|
||||||
// Handled separately above.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
|
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||||
let hide = notif.object as! Bool
|
|
||||||
home.filter_events()
|
home.filter_events()
|
||||||
|
|
||||||
guard let damus_state,
|
guard let ds = damus_state else { return }
|
||||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||||
let keypair = damus_state.keypair.to_full()
|
|
||||||
|
guard let profile = profile_txn.unsafeUnownedValue,
|
||||||
|
let keypair = ds.keypair.to_full()
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.reactions = !hide
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||||
let profile_ev = make_metadata_event(keypair: keypair, metadata: profile)
|
|
||||||
damus_state.postbox.send(profile_ev)
|
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
}
|
ds.postbox.send(profile_ev)
|
||||||
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
|
||||||
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
|
||||||
is_deleted_account = false
|
|
||||||
notify(.logout, ())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||||
@@ -527,11 +527,12 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if let pubkey = self.muting {
|
if let pubkey = self.muting {
|
||||||
let profile = damus_state!.profiles.lookup(id: pubkey)
|
let name = damus_state!.profiles.lookup(id: pubkey).map { profile in
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
|
}.value
|
||||||
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||||
} else {
|
} else {
|
||||||
Text("User has been muted", comment: "Alert message that informs a user was d.")
|
Text("User has been muted", comment: "Alert message that informs a user was muted.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
||||||
@@ -544,7 +545,7 @@ struct ContentView: View {
|
|||||||
guard let ds = damus_state,
|
guard let ds = damus_state,
|
||||||
let keypair = ds.keypair.to_full(),
|
let keypair = ds.keypair.to_full(),
|
||||||
let pubkey = muting,
|
let pubkey = muting,
|
||||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey)
|
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -571,24 +572,25 @@ struct ContentView: View {
|
|||||||
if ds.contacts.mutelist == nil {
|
if ds.contacts.mutelist == nil {
|
||||||
confirm_overwrite_mutelist = true
|
confirm_overwrite_mutelist = true
|
||||||
} else {
|
} else {
|
||||||
guard let keypair = ds.keypair.to_full() else {
|
guard let keypair = ds.keypair.to_full(),
|
||||||
return
|
let pubkey = muting
|
||||||
}
|
else {
|
||||||
guard let pubkey = muting else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
|
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state?.contacts.set_mutelist(ev)
|
damus_state?.contacts.set_mutelist(ev)
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if let pubkey = muting {
|
if let pubkey = muting {
|
||||||
let profile = damus_state?.profiles.lookup(id: pubkey)
|
let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
|
}).value ?? "unknown"
|
||||||
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||||
} else {
|
} else {
|
||||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||||
@@ -598,12 +600,13 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func switch_timeline(_ timeline: Timeline) {
|
func switch_timeline(_ timeline: Timeline) {
|
||||||
self.isSideBarOpened = false
|
self.isSideBarOpened = false
|
||||||
|
let navWasAtRoot = self.navIsAtRoot()
|
||||||
self.popToRoot()
|
self.popToRoot()
|
||||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
|
||||||
|
|
||||||
if timeline == self.selected_timeline {
|
notify(.switched_timeline(timeline))
|
||||||
NotificationCenter.default.post(name: .scroll_to_top, object: nil)
|
|
||||||
|
if timeline == self.selected_timeline && navWasAtRoot {
|
||||||
|
notify(.scroll_to_top)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +614,23 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func connect() {
|
func connect() {
|
||||||
let pool = RelayPool()
|
// nostrdb
|
||||||
|
var mndb = Ndb()
|
||||||
|
if mndb == nil {
|
||||||
|
// try recovery
|
||||||
|
print("DB ISSUE! RECOVERING")
|
||||||
|
mndb = Ndb.safemode()
|
||||||
|
|
||||||
|
// out of space or something?? maybe we need a in-memory fallback
|
||||||
|
if mndb == nil {
|
||||||
|
notify(.logout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ndb = mndb else { return }
|
||||||
|
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||||
let model_cache = RelayModelCache()
|
let model_cache = RelayModelCache()
|
||||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||||
@@ -636,13 +655,12 @@ struct ContentView: View {
|
|||||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_search_cache = UserSearchCache()
|
|
||||||
self.damus_state = DamusState(pool: pool,
|
self.damus_state = DamusState(pool: pool,
|
||||||
keypair: keypair,
|
keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
contacts: Contacts(our_pubkey: pubkey),
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
profiles: Profiles(user_search_cache: user_search_cache),
|
profiles: Profiles(ndb: ndb),
|
||||||
dms: home.dms,
|
dms: home.dms,
|
||||||
previews: PreviewCache(),
|
previews: PreviewCache(),
|
||||||
zaps: Zaps(our_pubkey: pubkey),
|
zaps: Zaps(our_pubkey: pubkey),
|
||||||
@@ -651,7 +669,7 @@ struct ContentView: View {
|
|||||||
relay_filters: relay_filters,
|
relay_filters: relay_filters,
|
||||||
relay_model_cache: model_cache,
|
relay_model_cache: model_cache,
|
||||||
drafts: Drafts(),
|
drafts: Drafts(),
|
||||||
events: EventCache(),
|
events: EventCache(ndb: ndb),
|
||||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
postbox: PostBox(pool: pool),
|
postbox: PostBox(pool: pool),
|
||||||
bootstrap_relays: bootstrap_relays,
|
bootstrap_relays: bootstrap_relays,
|
||||||
@@ -659,23 +677,57 @@ struct ContentView: View {
|
|||||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings),
|
||||||
nav: self.navigationCoordinator,
|
nav: self.navigationCoordinator,
|
||||||
user_search_cache: user_search_cache
|
music: MusicController(onChange: music_changed),
|
||||||
|
video: VideoController(),
|
||||||
|
ndb: ndb
|
||||||
)
|
)
|
||||||
|
|
||||||
home.damus_state = self.damus_state!
|
home.damus_state = self.damus_state!
|
||||||
|
|
||||||
|
if let damus_state, damus_state.settings.enable_experimental_purple_api {
|
||||||
|
// Assign delegate so that we can send receipts to the Purple API server as soon as we get updates from user's purchases
|
||||||
|
StoreObserver.standard.delegate = damus_state.purple
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||||
|
}
|
||||||
|
|
||||||
pool.connect()
|
pool.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func music_changed(_ state: MusicState) {
|
||||||
|
guard let damus_state else { return }
|
||||||
|
switch state {
|
||||||
|
case .playback_state:
|
||||||
|
break
|
||||||
|
case .song(let song):
|
||||||
|
guard let song, let kp = damus_state.keypair.to_full() else { return }
|
||||||
|
|
||||||
|
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
|
||||||
|
|
||||||
|
let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")"
|
||||||
|
let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||||
|
let url = encodedDesc.flatMap { enc in
|
||||||
|
URL(string: "spotify:search:\(enc)")
|
||||||
|
}
|
||||||
|
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url)
|
||||||
|
|
||||||
|
pdata.status.music = music
|
||||||
|
|
||||||
|
guard let ev = music.to_note(keypair: kp) else { return }
|
||||||
|
damus_state.postbox.send(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView(keypair: Keypair(pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", privkey: nil))
|
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_since_time(last_event: NostrEvent?) -> Int64? {
|
func get_since_time(last_event: NostrEvent?) -> UInt32? {
|
||||||
if let last_event = last_event {
|
if let last_event = last_event {
|
||||||
return last_event.created_at - 60 * 10
|
return last_event.created_at - 60 * 10
|
||||||
}
|
}
|
||||||
@@ -695,7 +747,7 @@ extension UINavigationController: UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct LastNotification {
|
struct LastNotification {
|
||||||
let id: String
|
let id: NoteId
|
||||||
let created_at: Int64
|
let created_at: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,27 +757,30 @@ func get_last_event(_ timeline: Timeline) -> LastNotification? {
|
|||||||
let last_created = UserDefaults.standard.string(forKey: "last_\(str)_time")
|
let last_created = UserDefaults.standard.string(forKey: "last_\(str)_time")
|
||||||
.flatMap { Int64($0) }
|
.flatMap { Int64($0) }
|
||||||
|
|
||||||
return last.flatMap { id in
|
guard let last,
|
||||||
last_created.map { created in
|
let note_id = NoteId(hex: last),
|
||||||
return LastNotification(id: id, created_at: created)
|
let last_created
|
||||||
}
|
else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return LastNotification(id: note_id, created_at: last_created)
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
|
func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
|
||||||
let str = timeline.rawValue
|
let str = timeline.rawValue
|
||||||
UserDefaults.standard.set(ev.id, forKey: "last_\(str)")
|
UserDefaults.standard.set(ev.id.hex(), forKey: "last_\(str)")
|
||||||
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
||||||
}
|
}
|
||||||
|
|
||||||
func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
||||||
|
|
||||||
return filters.map { filter in
|
return filters.map { filter in
|
||||||
let kinds = filter.kinds ?? []
|
let kinds = filter.kinds ?? []
|
||||||
let initial: Int64? = nil
|
let initial: UInt32? = nil
|
||||||
let earliest = kinds.reduce(initial) { earliest, kind in
|
let earliest = kinds.reduce(initial) { earliest, kind in
|
||||||
let last = last_of_kind[kind.rawValue]
|
let last = last_of_kind[kind.rawValue]
|
||||||
let since: Int64? = get_since_time(last_event: last)
|
let since: UInt32? = get_since_time(last_event: last)
|
||||||
|
|
||||||
if earliest == nil {
|
if earliest == nil {
|
||||||
if since == nil {
|
if since == nil {
|
||||||
@@ -753,7 +808,6 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
|
|||||||
|
|
||||||
|
|
||||||
func setup_notifications() {
|
func setup_notifications() {
|
||||||
|
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
@@ -772,27 +826,31 @@ struct FindEvent {
|
|||||||
let type: FindEventType
|
let type: FindEventType
|
||||||
let find_from: [String]?
|
let find_from: [String]?
|
||||||
|
|
||||||
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
|
static func profile(pubkey: Pubkey, find_from: [String]? = nil) -> FindEvent {
|
||||||
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
|
static func event(evid: NoteId, find_from: [String]? = nil) -> FindEvent {
|
||||||
return FindEvent(type: .event(evid), find_from: find_from)
|
return FindEvent(type: .event(evid), find_from: find_from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FindEventType {
|
enum FindEventType {
|
||||||
case profile(String)
|
case profile(Pubkey)
|
||||||
case event(String)
|
case event(NoteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FoundEvent {
|
enum FoundEvent {
|
||||||
case profile(Profile, NostrEvent)
|
case profile(Pubkey)
|
||||||
case invalid_profile(NostrEvent)
|
case invalid_profile(NostrEvent)
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
||||||
|
return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
|
||||||
|
|
||||||
var filter: NostrFilter? = nil
|
var filter: NostrFilter? = nil
|
||||||
let find_from = query_.find_from
|
let find_from = query_.find_from
|
||||||
@@ -800,8 +858,10 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
|||||||
|
|
||||||
switch query {
|
switch query {
|
||||||
case .profile(let pubkey):
|
case .profile(let pubkey):
|
||||||
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
|
if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue,
|
||||||
callback(.profile(profile.profile, profile.event))
|
record.profile != nil
|
||||||
|
{
|
||||||
|
callback(.profile(pubkey))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
||||||
@@ -815,7 +875,6 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
|||||||
filter = NostrFilter(ids: [evid], limit: 1)
|
filter = NostrFilter(ids: [evid], limit: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let subid = UUID().description
|
|
||||||
var attempts: Int = 0
|
var attempts: Int = 0
|
||||||
var has_event = false
|
var has_event = false
|
||||||
guard let filter else { return }
|
guard let filter else { return }
|
||||||
@@ -839,14 +898,11 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
|||||||
switch query {
|
switch query {
|
||||||
case .profile:
|
case .profile:
|
||||||
if ev.known_kind == .metadata {
|
if ev.known_kind == .metadata {
|
||||||
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
|
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
|
||||||
guard let profile else {
|
callback(.invalid_profile(ev))
|
||||||
callback(.invalid_profile(ev))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback(.profile(profile, ev))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
callback(.profile(ev.pubkey))
|
||||||
}
|
}
|
||||||
case .event:
|
case .event:
|
||||||
callback(.event(ev))
|
callback(.event(ev))
|
||||||
@@ -859,7 +915,9 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
|
|||||||
}
|
}
|
||||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||||
}
|
}
|
||||||
case .notice(_):
|
case .notice:
|
||||||
|
break
|
||||||
|
case .auth:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,7 +941,7 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -895,32 +953,23 @@ func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(.unfollowed, unfollow)
|
notify(.unfollowed(unfollow))
|
||||||
|
|
||||||
state.contacts.event = ev
|
state.contacts.event = ev
|
||||||
|
|
||||||
if unfollow.key == "p" {
|
switch unfollow {
|
||||||
state.contacts.remove_friend(unfollow.ref_id)
|
case .pubkey(let pk):
|
||||||
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
|
state.contacts.remove_friend(pk)
|
||||||
|
case .hashtag:
|
||||||
|
// nothing to handle here really
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_unfollow_notif(state: DamusState, notif: Notification) -> ReferencedId? {
|
|
||||||
let target = notif.object as! FollowTarget
|
|
||||||
let pk = target.pubkey
|
|
||||||
|
|
||||||
let ref = ReferencedId.p(pk)
|
|
||||||
if handle_unfollow(state: state, unfollow: ref) {
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -930,41 +979,51 @@ func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(.followed, follow)
|
notify(.followed(follow))
|
||||||
|
|
||||||
state.contacts.event = ev
|
state.contacts.event = ev
|
||||||
if follow.key == "p" {
|
switch follow {
|
||||||
state.contacts.add_friend_pubkey(follow.ref_id)
|
case .pubkey(let pubkey):
|
||||||
|
state.contacts.add_friend_pubkey(pubkey)
|
||||||
|
case .hashtag:
|
||||||
|
// nothing to do
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_follow_notif(state: DamusState, notif: Notification) -> Bool {
|
func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
|
||||||
let fnotify = notif.object as! FollowTarget
|
switch target {
|
||||||
switch fnotify {
|
|
||||||
case .pubkey(let pk):
|
case .pubkey(let pk):
|
||||||
state.contacts.add_friend_pubkey(pk)
|
state.contacts.add_friend_pubkey(pk)
|
||||||
case .contact(let ev):
|
case .contact(let ev):
|
||||||
state.contacts.add_friend_contact(ev)
|
state.contacts.add_friend_contact(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handle_follow(state: state, follow: .p(fnotify.pubkey))
|
return handle_follow(state: state, follow: target.follow_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
|
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool {
|
||||||
let post_res = notif.object as! NostrPostResult
|
switch post {
|
||||||
switch post_res {
|
|
||||||
case .post(let post):
|
case .post(let post):
|
||||||
//let post = tup.0
|
//let post = tup.0
|
||||||
//let to_relays = tup.1
|
//let to_relays = tup.1
|
||||||
print("post \(post.content)")
|
print("post \(post.content)")
|
||||||
let new_ev = post_to_event(post: post, privkey: keypair.privkey, pubkey: keypair.pubkey)
|
guard let new_ev = post_to_event(post: post, keypair: keypair) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
postbox.send(new_ev)
|
postbox.send(new_ev)
|
||||||
for eref in new_ev.referenced_ids.prefix(3) {
|
for eref in new_ev.referenced_ids.prefix(3) {
|
||||||
// also broadcast at most 3 referenced events
|
// also broadcast at most 3 referenced events
|
||||||
if let ev = events.lookup(eref.ref_id) {
|
if let ev = events.lookup(eref) {
|
||||||
|
postbox.send(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for qref in new_ev.referenced_quote_ids.prefix(3) {
|
||||||
|
// also broadcast at most 3 referenced quoted events
|
||||||
|
if let ev = events.lookup(qref.note_id) {
|
||||||
postbox.send(ev)
|
postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -977,7 +1036,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
|
|
||||||
|
|
||||||
enum OpenResult {
|
enum OpenResult {
|
||||||
case profile(String)
|
case profile(Pubkey)
|
||||||
case filter(NostrFilter)
|
case filter(NostrFilter)
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
case wallet_connect(WalletConnectURL)
|
case wallet_connect(WalletConnectURL)
|
||||||
@@ -997,13 +1056,19 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
|
|
||||||
switch link {
|
switch link {
|
||||||
case .ref(let ref):
|
case .ref(let ref):
|
||||||
if ref.key == "p" {
|
switch ref {
|
||||||
result(.profile(ref.ref_id))
|
case .pubkey(let pk):
|
||||||
} else if ref.key == "e" {
|
result(.profile(pk))
|
||||||
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
|
case .event(let noteid):
|
||||||
|
find_event(state: state, query: .event(evid: noteid)) { res in
|
||||||
guard let res, case .event(let ev) = res else { return }
|
guard let res, case .event(let ev) = res else { return }
|
||||||
result(.event(ev))
|
result(.event(ev))
|
||||||
}
|
}
|
||||||
|
case .hashtag(let ht):
|
||||||
|
result(.filter(.filter_hashtag([ht.string()])))
|
||||||
|
case .param, .quote:
|
||||||
|
// doesn't really make sense here
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case .filter(let filt):
|
case .filter(let filt):
|
||||||
result(.filter(filt))
|
result(.filter(filt))
|
||||||
|
|||||||
+8
-2
@@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict/>
|
||||||
|
</array>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -67,8 +71,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
<string>Damus needs access to your camera in order to upload photos and scan QR codes.</string>
|
||||||
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
|
<string>Damus needs access to your media library for playback statuses</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
<string>Damus needs access to your microphone for creating video recording posts</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -28,19 +28,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) {
|
||||||
self.our_like = nil
|
|
||||||
self.our_boost = nil
|
|
||||||
self.our_reply = nil
|
|
||||||
self.our_zap = nil
|
|
||||||
self.likes = 0
|
|
||||||
self.boosts = 0
|
|
||||||
self.zaps = 0
|
|
||||||
self.zap_total = 0
|
|
||||||
self.replies = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) {
|
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
@@ -52,7 +40,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.our_reply = our_reply
|
self.our_reply = our_reply
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(damus: DamusState, evid: String) {
|
func update(damus: DamusState, evid: NoteId) {
|
||||||
self.likes = damus.likes.counts[evid] ?? 0
|
self.likes = damus.likes.counts[evid] ?? 0
|
||||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||||
|
|||||||
@@ -7,18 +7,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
fileprivate func get_bookmarks_key(pubkey: String) -> String {
|
fileprivate func get_bookmarks_key(pubkey: Pubkey) -> String {
|
||||||
pk_setting_key(pubkey, key: "bookmarks")
|
pk_setting_key(pubkey, key: "bookmarks")
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_bookmarks(pubkey: String) -> [NostrEvent] {
|
func load_bookmarks(pubkey: Pubkey) -> [NostrEvent] {
|
||||||
let key = get_bookmarks_key(pubkey: pubkey)
|
let key = get_bookmarks_key(pubkey: pubkey)
|
||||||
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
|
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
|
||||||
event_from_json(dat: $0)
|
event_from_json(dat: $0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
func save_bookmarks(pubkey: Pubkey, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||||
let uniq_bookmarks = uniq(value)
|
let uniq_bookmarks = uniq(value)
|
||||||
|
|
||||||
if uniq_bookmarks != current_value {
|
if uniq_bookmarks != current_value {
|
||||||
@@ -32,7 +32,7 @@ func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEv
|
|||||||
|
|
||||||
class BookmarksManager: ObservableObject {
|
class BookmarksManager: ObservableObject {
|
||||||
|
|
||||||
private let pubkey: String
|
private let pubkey: Pubkey
|
||||||
|
|
||||||
private var _bookmarks: [NostrEvent]
|
private var _bookmarks: [NostrEvent]
|
||||||
var bookmarks: [NostrEvent] {
|
var bookmarks: [NostrEvent] {
|
||||||
@@ -47,7 +47,7 @@ class BookmarksManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(pubkey: String) {
|
init(pubkey: Pubkey) {
|
||||||
self._bookmarks = load_bookmarks(pubkey: pubkey)
|
self._bookmarks = load_bookmarks(pubkey: pubkey)
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//
|
||||||
|
// CameraModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class CameraModel: ObservableObject {
|
||||||
|
private let service = CameraService()
|
||||||
|
|
||||||
|
@Published var showAlertError = false
|
||||||
|
|
||||||
|
@Published var isFlashOn = false
|
||||||
|
|
||||||
|
@Published var willCapturePhoto = false
|
||||||
|
|
||||||
|
@Published var isCameraButtonDisabled = false
|
||||||
|
|
||||||
|
@Published var isPhotoProcessing = false
|
||||||
|
|
||||||
|
@Published var isRecording = false
|
||||||
|
|
||||||
|
@Published var captureMode: CameraMediaType = .image
|
||||||
|
|
||||||
|
@Published public var mediaItems: [MediaItem] = []
|
||||||
|
|
||||||
|
@Published var thumbnail: Thumbnail!
|
||||||
|
|
||||||
|
var alertError: AlertError!
|
||||||
|
|
||||||
|
var session: AVCaptureSession
|
||||||
|
|
||||||
|
private var subscriptions = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.session = service.session
|
||||||
|
|
||||||
|
service.$shouldShowAlertView.sink { [weak self] (val) in
|
||||||
|
self?.alertError = self?.service.alertError
|
||||||
|
self?.showAlertError = val
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$flashMode.sink { [weak self] (mode) in
|
||||||
|
self?.isFlashOn = mode == .on
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$willCapturePhoto.sink { [weak self] (val) in
|
||||||
|
self?.willCapturePhoto = val
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$isCameraButtonDisabled.sink { [weak self] (val) in
|
||||||
|
self?.isCameraButtonDisabled = val
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$isPhotoProcessing.sink { [weak self] (val) in
|
||||||
|
self?.isPhotoProcessing = val
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$isRecording.sink { [weak self] (val) in
|
||||||
|
self?.isRecording = val
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$captureMode.sink { [weak self] (mode) in
|
||||||
|
self?.captureMode = mode
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$mediaItems.sink { [weak self] (mode) in
|
||||||
|
self?.mediaItems = mode
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
|
||||||
|
service.$thumbnail.sink { [weak self] (thumbnail) in
|
||||||
|
guard let pic = thumbnail else { return }
|
||||||
|
self?.thumbnail = pic
|
||||||
|
}
|
||||||
|
.store(in: &self.subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
service.checkForPermissions()
|
||||||
|
service.configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
service.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func capturePhoto() {
|
||||||
|
service.capturePhoto()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRecording() {
|
||||||
|
service.startRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording() {
|
||||||
|
service.stopRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
func flipCamera() {
|
||||||
|
service.changeCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoom(with factor: CGFloat) {
|
||||||
|
service.set(zoom: factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func switchFlash() {
|
||||||
|
service.flashMode = service.flashMode == .on ? .off : .on
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// CameraService+Extensions.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
extension AVCaptureVideoOrientation {
|
||||||
|
init?(deviceOrientation: UIDeviceOrientation) {
|
||||||
|
switch deviceOrientation {
|
||||||
|
case .portrait: self = .portrait
|
||||||
|
case .portraitUpsideDown: self = .portraitUpsideDown
|
||||||
|
case .landscapeLeft: self = .landscapeRight
|
||||||
|
case .landscapeRight: self = .landscapeLeft
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(interfaceOrientation: UIInterfaceOrientation) {
|
||||||
|
switch interfaceOrientation {
|
||||||
|
case .portrait: self = .portrait
|
||||||
|
case .portraitUpsideDown: self = .portraitUpsideDown
|
||||||
|
case .landscapeLeft: self = .landscapeLeft
|
||||||
|
case .landscapeRight: self = .landscapeRight
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
//
|
||||||
|
// CameraService.swift
|
||||||
|
// Campus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import AVFoundation
|
||||||
|
import Photos
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public struct Thumbnail: Identifiable, Equatable {
|
||||||
|
public var id: String
|
||||||
|
public var type: CameraMediaType
|
||||||
|
public var url: URL
|
||||||
|
|
||||||
|
public init(id: String = UUID().uuidString, type: CameraMediaType, url: URL) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
public var thumbnailImage: UIImage? {
|
||||||
|
switch type {
|
||||||
|
case .image:
|
||||||
|
return ImageResizer(targetWidth: 100).resize(at: url)
|
||||||
|
case .video:
|
||||||
|
return generateVideoThumbnail(for: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AlertError {
|
||||||
|
public var title: String = ""
|
||||||
|
public var message: String = ""
|
||||||
|
public var primaryButtonTitle = "Accept"
|
||||||
|
public var secondaryButtonTitle: String?
|
||||||
|
public var primaryAction: (() -> ())?
|
||||||
|
public var secondaryAction: (() -> ())?
|
||||||
|
|
||||||
|
public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) {
|
||||||
|
self.title = title
|
||||||
|
self.message = message
|
||||||
|
self.primaryAction = primaryAction
|
||||||
|
self.primaryButtonTitle = primaryButtonTitle
|
||||||
|
self.secondaryAction = secondaryAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVideoThumbnail(for videoURL: URL) -> UIImage? {
|
||||||
|
let asset = AVAsset(url: videoURL)
|
||||||
|
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
imageGenerator.appliesPreferredTrackTransform = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
let cgImage = try imageGenerator.copyCGImage(at: .zero, actualTime: nil)
|
||||||
|
return UIImage(cgImage: cgImage)
|
||||||
|
} catch {
|
||||||
|
print("Error generating thumbnail: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CameraMediaType {
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct MediaItem {
|
||||||
|
let url: URL
|
||||||
|
let type: CameraMediaType
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CameraService: NSObject, Identifiable {
|
||||||
|
public let session = AVCaptureSession()
|
||||||
|
|
||||||
|
public var isSessionRunning = false
|
||||||
|
public var isConfigured = false
|
||||||
|
var setupResult: SessionSetupResult = .success
|
||||||
|
|
||||||
|
public var alertError: AlertError = AlertError()
|
||||||
|
|
||||||
|
@Published public var flashMode: AVCaptureDevice.FlashMode = .off
|
||||||
|
@Published public var shouldShowAlertView = false
|
||||||
|
@Published public var isPhotoProcessing = false
|
||||||
|
@Published public var captureMode: CameraMediaType = .image
|
||||||
|
@Published public var isRecording: Bool = false
|
||||||
|
|
||||||
|
@Published public var willCapturePhoto = false
|
||||||
|
@Published public var isCameraButtonDisabled = false
|
||||||
|
@Published public var isCameraUnavailable = false
|
||||||
|
@Published public var thumbnail: Thumbnail?
|
||||||
|
@Published public var mediaItems: [MediaItem] = []
|
||||||
|
|
||||||
|
public let sessionQueue = DispatchQueue(label: "io.damus.camera")
|
||||||
|
|
||||||
|
@objc dynamic public var videoDeviceInput: AVCaptureDeviceInput!
|
||||||
|
@objc dynamic public var audioDeviceInput: AVCaptureDeviceInput!
|
||||||
|
|
||||||
|
public let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified)
|
||||||
|
|
||||||
|
public let photoOutput = AVCapturePhotoOutput()
|
||||||
|
|
||||||
|
public let movieOutput = AVCaptureMovieFileOutput()
|
||||||
|
|
||||||
|
var videoCaptureProcessor: VideoCaptureProcessor?
|
||||||
|
var photoCaptureProcessor: PhotoCaptureProcessor?
|
||||||
|
|
||||||
|
public var keyValueObservations = [NSKeyValueObservation]()
|
||||||
|
|
||||||
|
override public init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SessionSetupResult {
|
||||||
|
case success
|
||||||
|
case notAuthorized
|
||||||
|
case configurationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
public func configure() {
|
||||||
|
if !self.isSessionRunning && !self.isConfigured {
|
||||||
|
sessionQueue.async {
|
||||||
|
self.configureSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func checkForPermissions() {
|
||||||
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||||
|
case .authorized:
|
||||||
|
break
|
||||||
|
case .notDetermined:
|
||||||
|
sessionQueue.suspend()
|
||||||
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
|
||||||
|
if !granted {
|
||||||
|
self.setupResult = .notAuthorized
|
||||||
|
}
|
||||||
|
self.sessionQueue.resume()
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
setupResult = .notAuthorized
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
|
||||||
|
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
|
||||||
|
options: [:], completionHandler: nil)
|
||||||
|
|
||||||
|
}, secondaryAction: nil)
|
||||||
|
self.shouldShowAlertView = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSession() {
|
||||||
|
if setupResult != .success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.beginConfiguration()
|
||||||
|
|
||||||
|
session.sessionPreset = .high
|
||||||
|
|
||||||
|
// Add video input.
|
||||||
|
do {
|
||||||
|
var defaultVideoDevice: AVCaptureDevice?
|
||||||
|
|
||||||
|
if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
||||||
|
// If a rear dual camera is not available, default to the rear wide angle camera.
|
||||||
|
defaultVideoDevice = backCameraDevice
|
||||||
|
} else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
|
||||||
|
// If the rear wide angle camera isn't available, default to the front wide angle camera.
|
||||||
|
defaultVideoDevice = frontCameraDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let videoDevice = defaultVideoDevice else {
|
||||||
|
print("Default video device is unavailable.")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||||
|
|
||||||
|
if session.canAddInput(videoDeviceInput) {
|
||||||
|
session.addInput(videoDeviceInput)
|
||||||
|
self.videoDeviceInput = videoDeviceInput
|
||||||
|
} else {
|
||||||
|
print("Couldn't add video device input to the session.")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioDevice = AVCaptureDevice.default(for: .audio)
|
||||||
|
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!)
|
||||||
|
|
||||||
|
if session.canAddInput(audioDeviceInput) {
|
||||||
|
session.addInput(audioDeviceInput)
|
||||||
|
self.audioDeviceInput = audioDeviceInput
|
||||||
|
} else {
|
||||||
|
print("Couldn't add audio device input to the session.")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add video output
|
||||||
|
if session.canAddOutput(movieOutput) {
|
||||||
|
session.addOutput(movieOutput)
|
||||||
|
} else {
|
||||||
|
print("Could not add movie output to the session")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Couldn't create video device input: \(error)")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the photo output.
|
||||||
|
if session.canAddOutput(photoOutput) {
|
||||||
|
session.addOutput(photoOutput)
|
||||||
|
|
||||||
|
photoOutput.maxPhotoQualityPrioritization = .quality
|
||||||
|
|
||||||
|
} else {
|
||||||
|
print("Could not add photo output to the session")
|
||||||
|
setupResult = .configurationFailed
|
||||||
|
session.commitConfiguration()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.commitConfiguration()
|
||||||
|
self.isConfigured = true
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resumeInterruptedSession() {
|
||||||
|
sessionQueue.async {
|
||||||
|
self.session.startRunning()
|
||||||
|
self.isSessionRunning = self.session.isRunning
|
||||||
|
if !self.session.isRunning {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.alertError = AlertError(title: "Camera Error", message: "Unable to resume camera", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
|
||||||
|
self.shouldShowAlertView = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraUnavailable = false
|
||||||
|
self.isCameraButtonDisabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func changeCamera() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionQueue.async {
|
||||||
|
let currentVideoDevice = self.videoDeviceInput.device
|
||||||
|
let currentPosition = currentVideoDevice.position
|
||||||
|
|
||||||
|
let preferredPosition: AVCaptureDevice.Position
|
||||||
|
let preferredDeviceType: AVCaptureDevice.DeviceType
|
||||||
|
|
||||||
|
switch currentPosition {
|
||||||
|
case .unspecified, .front:
|
||||||
|
preferredPosition = .back
|
||||||
|
preferredDeviceType = .builtInWideAngleCamera
|
||||||
|
|
||||||
|
case .back:
|
||||||
|
preferredPosition = .front
|
||||||
|
preferredDeviceType = .builtInWideAngleCamera
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
print("Unknown capture position. Defaulting to back, dual-camera.")
|
||||||
|
preferredPosition = .back
|
||||||
|
preferredDeviceType = .builtInWideAngleCamera
|
||||||
|
}
|
||||||
|
let devices = self.videoDeviceDiscoverySession.devices
|
||||||
|
var newVideoDevice: AVCaptureDevice? = nil
|
||||||
|
|
||||||
|
if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) {
|
||||||
|
newVideoDevice = device
|
||||||
|
} else if let device = devices.first(where: { $0.position == preferredPosition }) {
|
||||||
|
newVideoDevice = device
|
||||||
|
}
|
||||||
|
|
||||||
|
if let videoDevice = newVideoDevice {
|
||||||
|
do {
|
||||||
|
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||||
|
|
||||||
|
self.session.beginConfiguration()
|
||||||
|
|
||||||
|
self.session.removeInput(self.videoDeviceInput)
|
||||||
|
|
||||||
|
if self.session.canAddInput(videoDeviceInput) {
|
||||||
|
NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: currentVideoDevice)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: .AVCaptureDeviceSubjectAreaDidChange, object: videoDeviceInput.device)
|
||||||
|
|
||||||
|
self.session.addInput(videoDeviceInput)
|
||||||
|
self.videoDeviceInput = videoDeviceInput
|
||||||
|
} else {
|
||||||
|
self.session.addInput(self.videoDeviceInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let connection = self.photoOutput.connection(with: .video) {
|
||||||
|
if connection.isVideoStabilizationSupported {
|
||||||
|
connection.preferredVideoStabilizationMode = .auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.photoOutput.maxPhotoQualityPrioritization = .quality
|
||||||
|
|
||||||
|
self.session.commitConfiguration()
|
||||||
|
} catch {
|
||||||
|
print("Error occurred while creating video device input: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func focus(with focusMode: AVCaptureDevice.FocusMode, exposureMode: AVCaptureDevice.ExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) {
|
||||||
|
sessionQueue.async {
|
||||||
|
guard let device = self.videoDeviceInput?.device else { return }
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
|
||||||
|
if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
|
||||||
|
device.focusPointOfInterest = devicePoint
|
||||||
|
device.focusMode = focusMode
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
|
||||||
|
device.exposurePointOfInterest = devicePoint
|
||||||
|
device.exposureMode = exposureMode
|
||||||
|
}
|
||||||
|
|
||||||
|
device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
} catch {
|
||||||
|
print("Could not lock device for configuration: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func focus(at focusPoint: CGPoint) {
|
||||||
|
let device = self.videoDeviceInput.device
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
if device.isFocusPointOfInterestSupported {
|
||||||
|
device.focusPointOfInterest = focusPoint
|
||||||
|
device.exposurePointOfInterest = focusPoint
|
||||||
|
device.exposureMode = .continuousAutoExposure
|
||||||
|
device.focusMode = .continuousAutoFocus
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
print(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func stop(completion: (() -> ())? = nil) {
|
||||||
|
sessionQueue.async {
|
||||||
|
if self.isSessionRunning {
|
||||||
|
if self.setupResult == .success {
|
||||||
|
self.session.stopRunning()
|
||||||
|
self.isSessionRunning = self.session.isRunning
|
||||||
|
print("CAMERA STOPPED")
|
||||||
|
self.removeObservers()
|
||||||
|
|
||||||
|
if !self.session.isRunning {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func start() {
|
||||||
|
sessionQueue.async {
|
||||||
|
if !self.isSessionRunning && self.isConfigured {
|
||||||
|
switch self.setupResult {
|
||||||
|
case .success:
|
||||||
|
self.addObservers()
|
||||||
|
self.session.startRunning()
|
||||||
|
print("CAMERA RUNNING")
|
||||||
|
self.isSessionRunning = self.session.isRunning
|
||||||
|
|
||||||
|
if self.session.isRunning {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = false
|
||||||
|
self.isCameraUnavailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .notAuthorized:
|
||||||
|
print("Application not authorized to use camera")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case .configurationFailed:
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or other application is using it", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
|
||||||
|
self.shouldShowAlertView = true
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func set(zoom: CGFloat) {
|
||||||
|
let factor = zoom < 1 ? 1 : zoom
|
||||||
|
let device = self.videoDeviceInput.device
|
||||||
|
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
device.videoZoomFactor = factor
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
print(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func capturePhoto() {
|
||||||
|
if self.setupResult != .configurationFailed {
|
||||||
|
let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
|
||||||
|
sessionQueue.async {
|
||||||
|
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
|
||||||
|
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
||||||
|
}
|
||||||
|
var photoSettings = AVCapturePhotoSettings()
|
||||||
|
|
||||||
|
// Capture HEIF photos when supported. Enable according to user settings and high-resolution photos.
|
||||||
|
if (self.photoOutput.availablePhotoCodecTypes.contains(.hevc)) {
|
||||||
|
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.videoDeviceInput.device.isFlashAvailable {
|
||||||
|
photoSettings.flashMode = self.flashMode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty {
|
||||||
|
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
|
||||||
|
}
|
||||||
|
|
||||||
|
photoSettings.photoQualityPrioritization = .speed
|
||||||
|
|
||||||
|
if self.photoCaptureProcessor == nil {
|
||||||
|
self.photoCaptureProcessor = PhotoCaptureProcessor(with: photoSettings, photoOutput: self.photoOutput, willCapturePhotoAnimation: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.willCapturePhoto.toggle()
|
||||||
|
self.willCapturePhoto.toggle()
|
||||||
|
}
|
||||||
|
}, completionHandler: { (photoCaptureProcessor) in
|
||||||
|
if let data = photoCaptureProcessor.photoData {
|
||||||
|
let url = self.savePhoto(data: data)
|
||||||
|
if let unwrappedURL = url {
|
||||||
|
self.thumbnail = Thumbnail(type: .image, url: unwrappedURL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Data for photo not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isCameraButtonDisabled = false
|
||||||
|
}, photoProcessingHandler: { animate in
|
||||||
|
self.isPhotoProcessing = animate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.photoCaptureProcessor?.capturePhoto(settings: photoSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startRecording() {
|
||||||
|
if self.setupResult != .configurationFailed {
|
||||||
|
let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
|
||||||
|
self.isCameraButtonDisabled = true
|
||||||
|
|
||||||
|
sessionQueue.async {
|
||||||
|
if let videoOutputConnection = self.movieOutput.connection(with: .video) {
|
||||||
|
videoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
||||||
|
|
||||||
|
var videoSettings = [String: Any]()
|
||||||
|
|
||||||
|
if self.movieOutput.availableVideoCodecTypes.contains(.hevc) == true {
|
||||||
|
videoSettings[AVVideoCodecKey] = AVVideoCodecType.hevc
|
||||||
|
self.movieOutput.setOutputSettings(videoSettings, for: videoOutputConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.videoCaptureProcessor == nil {
|
||||||
|
self.videoCaptureProcessor = VideoCaptureProcessor(movieOutput: self.movieOutput, beginHandler: {
|
||||||
|
self.isRecording = true
|
||||||
|
}, completionHandler: { (videoCaptureProcessor, outputFileURL) in
|
||||||
|
self.isCameraButtonDisabled = false
|
||||||
|
self.captureMode = .image
|
||||||
|
|
||||||
|
self.mediaItems.append(MediaItem(url: outputFileURL, type: .video))
|
||||||
|
self.thumbnail = Thumbnail(type: .video, url: outputFileURL)
|
||||||
|
}, videoProcessingHandler: { animate in
|
||||||
|
self.isPhotoProcessing = animate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoCaptureProcessor?.startCapture(session: self.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording() {
|
||||||
|
if let videoCaptureProcessor = self.videoCaptureProcessor {
|
||||||
|
isRecording = false
|
||||||
|
videoCaptureProcessor.stopCapture()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePhoto(imageType: String = "jpeg", data: Data) -> URL? {
|
||||||
|
guard let uiImage = UIImage(data: data) else {
|
||||||
|
print("Error converting media data to UIImage")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let compressedData = uiImage.jpegData(compressionQuality: 0.8) else {
|
||||||
|
print("Error converting UIImage to JPEG data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let temporaryDirectory = NSTemporaryDirectory()
|
||||||
|
let tempFileName = "\(UUID().uuidString).\(imageType)"
|
||||||
|
let tempFileURL = URL(fileURLWithPath: temporaryDirectory).appendingPathComponent(tempFileName)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try compressedData.write(to: tempFileURL)
|
||||||
|
self.mediaItems.append(MediaItem(url: tempFileURL, type: .image))
|
||||||
|
return tempFileURL
|
||||||
|
} catch {
|
||||||
|
print("Error saving image data to temporary URL: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addObservers() {
|
||||||
|
let systemPressureStateObservation = observe(\.videoDeviceInput.device.systemPressureState, options: .new) { _, change in
|
||||||
|
guard let systemPressureState = change.newValue else { return }
|
||||||
|
self.setRecommendedFrameRateRangeForPressureState(systemPressureState: systemPressureState)
|
||||||
|
}
|
||||||
|
keyValueObservations.append(systemPressureStateObservation)
|
||||||
|
|
||||||
|
// NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(subjectAreaDidChange),
|
||||||
|
name: .AVCaptureDeviceSubjectAreaDidChange,
|
||||||
|
object: videoDeviceInput.device)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(uiRequestedNewFocusArea), name: .init(rawValue: "UserDidRequestNewFocusPoint"), object: nil)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(sessionRuntimeError),
|
||||||
|
name: .AVCaptureSessionRuntimeError,
|
||||||
|
object: session)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(sessionWasInterrupted),
|
||||||
|
name: .AVCaptureSessionWasInterrupted,
|
||||||
|
object: session)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(sessionInterruptionEnded),
|
||||||
|
name: .AVCaptureSessionInterruptionEnded,
|
||||||
|
object: session)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeObservers() {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
|
||||||
|
for keyValueObservation in keyValueObservations {
|
||||||
|
keyValueObservation.invalidate()
|
||||||
|
}
|
||||||
|
keyValueObservations.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func uiRequestedNewFocusArea(notification: NSNotification) {
|
||||||
|
guard let userInfo = notification.userInfo as? [String: Any], let devicePoint = userInfo["devicePoint"] as? CGPoint else { return }
|
||||||
|
self.focus(at: devicePoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func subjectAreaDidChange(notification: NSNotification) {
|
||||||
|
let devicePoint = CGPoint(x: 0.5, y: 0.5)
|
||||||
|
focus(with: .continuousAutoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func sessionRuntimeError(notification: NSNotification) {
|
||||||
|
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
|
||||||
|
|
||||||
|
print("Capture session runtime error: \(error)")
|
||||||
|
|
||||||
|
if error.code == .mediaServicesWereReset {
|
||||||
|
sessionQueue.async {
|
||||||
|
if self.isSessionRunning {
|
||||||
|
self.session.startRunning()
|
||||||
|
self.isSessionRunning = self.session.isRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setRecommendedFrameRateRangeForPressureState(systemPressureState: AVCaptureDevice.SystemPressureState) {
|
||||||
|
let pressureLevel = systemPressureState.level
|
||||||
|
if pressureLevel == .serious || pressureLevel == .critical {
|
||||||
|
do {
|
||||||
|
try self.videoDeviceInput.device.lockForConfiguration()
|
||||||
|
print("WARNING: Reached elevated system pressure level: \(pressureLevel). Throttling frame rate.")
|
||||||
|
self.videoDeviceInput.device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 20)
|
||||||
|
self.videoDeviceInput.device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 15)
|
||||||
|
self.videoDeviceInput.device.unlockForConfiguration()
|
||||||
|
} catch {
|
||||||
|
print("Could not lock device for configuration: \(error)")
|
||||||
|
}
|
||||||
|
} else if pressureLevel == .shutdown {
|
||||||
|
print("Session stopped running due to shutdown system pressure level.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func sessionWasInterrupted(notification: NSNotification) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraUnavailable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
|
||||||
|
let reasonIntegerValue = userInfoValue.integerValue,
|
||||||
|
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
|
||||||
|
print("Capture session was interrupted with reason \(reason)")
|
||||||
|
|
||||||
|
if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
|
||||||
|
print("Session stopped running due to video devies in use by another client.")
|
||||||
|
} else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps {
|
||||||
|
print("Session stopped running due to video devies is not available with multiple foreground apps.")
|
||||||
|
} else if reason == .videoDeviceNotAvailableDueToSystemPressure {
|
||||||
|
print("Session stopped running due to shutdown system pressure level.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func sessionInterruptionEnded(notification: NSNotification) {
|
||||||
|
print("Capture session interruption ended")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isCameraUnavailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// ImageResizer.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum ImageResizingError: Error {
|
||||||
|
case cannotRetrieveFromURL
|
||||||
|
case cannotRetrieveFromData
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ImageResizer {
|
||||||
|
public var targetWidth: CGFloat
|
||||||
|
|
||||||
|
public init(targetWidth: CGFloat) {
|
||||||
|
self.targetWidth = targetWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resize(at url: URL) -> UIImage? {
|
||||||
|
guard let image = UIImage(contentsOfFile: url.path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.resize(image: image)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resize(image: UIImage) -> UIImage {
|
||||||
|
let originalSize = image.size
|
||||||
|
let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||||
|
return renderer.image { (context) in
|
||||||
|
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// PhotoCaptureProcessor.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
class PhotoCaptureProcessor: NSObject {
|
||||||
|
private(set) var requestedPhotoSettings: AVCapturePhotoSettings
|
||||||
|
private(set) var photoOutput: AVCapturePhotoOutput?
|
||||||
|
|
||||||
|
lazy var context = CIContext()
|
||||||
|
var photoData: Data?
|
||||||
|
private var maxPhotoProcessingTime: CMTime?
|
||||||
|
|
||||||
|
private let willCapturePhotoAnimation: () -> Void
|
||||||
|
private let completionHandler: (PhotoCaptureProcessor) -> Void
|
||||||
|
private let photoProcessingHandler: (Bool) -> Void
|
||||||
|
|
||||||
|
init(with requestedPhotoSettings: AVCapturePhotoSettings,
|
||||||
|
photoOutput: AVCapturePhotoOutput?,
|
||||||
|
willCapturePhotoAnimation: @escaping () -> Void,
|
||||||
|
completionHandler: @escaping (PhotoCaptureProcessor) -> Void,
|
||||||
|
photoProcessingHandler: @escaping (Bool) -> Void) {
|
||||||
|
self.requestedPhotoSettings = requestedPhotoSettings
|
||||||
|
self.willCapturePhotoAnimation = willCapturePhotoAnimation
|
||||||
|
self.completionHandler = completionHandler
|
||||||
|
self.photoProcessingHandler = photoProcessingHandler
|
||||||
|
self.photoOutput = photoOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func capturePhoto(settings: AVCapturePhotoSettings) {
|
||||||
|
if let photoOutput = self.photoOutput {
|
||||||
|
photoOutput.capturePhoto(with: settings, delegate: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||||
|
maxPhotoProcessingTime = resolvedSettings.photoProcessingTimeRange.start + resolvedSettings.photoProcessingTimeRange.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.willCapturePhotoAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let maxPhotoProcessingTime = maxPhotoProcessingTime else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.photoProcessingHandler(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let oneSecond = CMTime(seconds: 2, preferredTimescale: 1)
|
||||||
|
if maxPhotoProcessingTime > oneSecond {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.photoProcessingHandler(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.photoProcessingHandler(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
print("Error capturing photo: \(error)")
|
||||||
|
} else {
|
||||||
|
photoData = photo.fileDataRepresentation()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Error capturing photo: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.completionHandler(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// VideoCaptureProcessor.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Suhail Saqan on 8/5/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
class VideoCaptureProcessor: NSObject {
|
||||||
|
private(set) var movieOutput: AVCaptureMovieFileOutput?
|
||||||
|
|
||||||
|
private let beginHandler: () -> Void
|
||||||
|
private let completionHandler: (VideoCaptureProcessor, URL) -> Void
|
||||||
|
private let videoProcessingHandler: (Bool) -> Void
|
||||||
|
private var session: AVCaptureSession?
|
||||||
|
|
||||||
|
init(movieOutput: AVCaptureMovieFileOutput?,
|
||||||
|
beginHandler: @escaping () -> Void,
|
||||||
|
completionHandler: @escaping (VideoCaptureProcessor, URL) -> Void,
|
||||||
|
videoProcessingHandler: @escaping (Bool) -> Void) {
|
||||||
|
self.beginHandler = beginHandler
|
||||||
|
self.completionHandler = completionHandler
|
||||||
|
self.videoProcessingHandler = videoProcessingHandler
|
||||||
|
self.movieOutput = movieOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCapture(session: AVCaptureSession) {
|
||||||
|
if let movieOutput = self.movieOutput, session.isRunning {
|
||||||
|
let outputFileURL = uniqueOutputFileURL()
|
||||||
|
movieOutput.startRecording(to: outputFileURL, recordingDelegate: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCapture() {
|
||||||
|
if let movieOutput = self.movieOutput {
|
||||||
|
if movieOutput.isRecording {
|
||||||
|
movieOutput.stopRecording()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uniqueOutputFileURL() -> URL {
|
||||||
|
let tempDirectory = FileManager.default.temporaryDirectory
|
||||||
|
let fileName = UUID().uuidString + ".mov"
|
||||||
|
return tempDirectory.appendingPathComponent(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VideoCaptureProcessor: AVCaptureFileOutputRecordingDelegate {
|
||||||
|
|
||||||
|
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.beginHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileOutput(_ output: AVCaptureFileOutput, willFinishRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.videoProcessingHandler(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Error capturing video: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.completionHandler(self, outputFileURL)
|
||||||
|
self.videoProcessingHandler(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
-80
@@ -9,21 +9,21 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
class Contacts {
|
class Contacts {
|
||||||
private var friends: Set<String> = Set()
|
private var friends: Set<Pubkey> = Set()
|
||||||
private var friend_of_friends: Set<String> = Set()
|
private var friend_of_friends: Set<Pubkey> = Set()
|
||||||
/// Tracks which friends are friends of a given pubkey.
|
/// Tracks which friends are friends of a given pubkey.
|
||||||
private var pubkey_to_our_friends = [String : Set<String>]()
|
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||||
private var muted: Set<String> = Set()
|
private var muted: Set<Pubkey> = Set()
|
||||||
|
|
||||||
let our_pubkey: String
|
let our_pubkey: Pubkey
|
||||||
var event: NostrEvent?
|
var event: NostrEvent?
|
||||||
var mutelist: NostrEvent?
|
var mutelist: NostrEvent?
|
||||||
|
|
||||||
init(our_pubkey: String) {
|
init(our_pubkey: Pubkey) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_muted(_ pk: String) -> Bool {
|
func is_muted(_ pk: Pubkey) -> Bool {
|
||||||
return muted.contains(pk)
|
return muted.contains(pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,34 +31,34 @@ class Contacts {
|
|||||||
let oldlist = self.mutelist
|
let oldlist = self.mutelist
|
||||||
self.mutelist = ev
|
self.mutelist = ev
|
||||||
|
|
||||||
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
|
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
|
||||||
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
let new = Set(ev.referenced_pubkeys)
|
||||||
let diff = old.symmetricDifference(new)
|
let diff = old.symmetricDifference(new)
|
||||||
|
|
||||||
var new_mutes = Array<String>()
|
var new_mutes = Set<Pubkey>()
|
||||||
var new_unmutes = Array<String>()
|
var new_unmutes = Set<Pubkey>()
|
||||||
|
|
||||||
for d in diff {
|
for d in diff {
|
||||||
if new.contains(d) {
|
if new.contains(d) {
|
||||||
new_mutes.append(d)
|
new_mutes.insert(d)
|
||||||
} else {
|
} else {
|
||||||
new_unmutes.append(d)
|
new_unmutes.insert(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: set local mutelist here
|
// TODO: set local mutelist here
|
||||||
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
|
self.muted = Set(ev.referenced_pubkeys)
|
||||||
|
|
||||||
if new_mutes.count > 0 {
|
if new_mutes.count > 0 {
|
||||||
notify(.new_mutes, new_mutes)
|
notify(.new_mutes(new_mutes))
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_unmutes.count > 0 {
|
if new_unmutes.count > 0 {
|
||||||
notify(.new_unmutes, new_unmutes)
|
notify(.new_unmutes(new_unmutes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_friend(_ pubkey: String) {
|
func remove_friend(_ pubkey: Pubkey) {
|
||||||
friends.remove(pubkey)
|
friends.remove(pubkey)
|
||||||
|
|
||||||
pubkey_to_our_friends.forEach {
|
pubkey_to_our_friends.forEach {
|
||||||
@@ -66,107 +66,105 @@ class Contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_friend_list() -> Set<String> {
|
func get_friend_list() -> Set<Pubkey> {
|
||||||
return friends
|
return friends
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_followed_hashtags() -> Set<String> {
|
func get_followed_hashtags() -> Set<String> {
|
||||||
guard let ev = self.event else { return Set() }
|
guard let ev = self.event else { return Set() }
|
||||||
return ev.tags.reduce(into: Set<String>(), { htags, tag in
|
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
||||||
if tag.count >= 2 && tag[0] == "t" && tag[1] != "" {
|
|
||||||
htags.insert(tag[1])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_friend_pubkey(_ pubkey: String) {
|
func follows(hashtag: Hashtag) -> Bool {
|
||||||
|
guard let ev = self.event else { return false }
|
||||||
|
return ev.referenced_hashtags.first(where: { $0 == hashtag }) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func add_friend_pubkey(_ pubkey: Pubkey) {
|
||||||
friends.insert(pubkey)
|
friends.insert(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_friend_contact(_ contact: NostrEvent) {
|
func add_friend_contact(_ contact: NostrEvent) {
|
||||||
friends.insert(contact.pubkey)
|
friends.insert(contact.pubkey)
|
||||||
for tag in contact.tags {
|
for pk in contact.referenced_pubkeys {
|
||||||
if tag.count >= 2 && tag[0] == "p" {
|
friend_of_friends.insert(pk)
|
||||||
friend_of_friends.insert(tag[1])
|
|
||||||
|
|
||||||
// Exclude themself and us.
|
// Exclude themself and us.
|
||||||
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
|
if contact.pubkey != our_pubkey && contact.pubkey != pk {
|
||||||
if pubkey_to_our_friends[tag[1]] == nil {
|
if pubkey_to_our_friends[pk] == nil {
|
||||||
pubkey_to_our_friends[tag[1]] = Set<String>()
|
pubkey_to_our_friends[pk] = Set<Pubkey>()
|
||||||
}
|
|
||||||
|
|
||||||
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pubkey_to_our_friends[pk]?.insert(contact.pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_friend_of_friend(_ pubkey: String) -> Bool {
|
func is_friend_of_friend(_ pubkey: Pubkey) -> Bool {
|
||||||
return friend_of_friends.contains(pubkey)
|
return friend_of_friends.contains(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_in_friendosphere(_ pubkey: String) -> Bool {
|
func is_in_friendosphere(_ pubkey: Pubkey) -> Bool {
|
||||||
return friends.contains(pubkey) || friend_of_friends.contains(pubkey)
|
return friends.contains(pubkey) || friend_of_friends.contains(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_friend(_ pubkey: String) -> Bool {
|
func is_friend(_ pubkey: Pubkey) -> Bool {
|
||||||
return friends.contains(pubkey)
|
return friends.contains(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_friend_or_self(_ pubkey: String) -> Bool {
|
func is_friend_or_self(_ pubkey: Pubkey) -> Bool {
|
||||||
return pubkey == our_pubkey || is_friend(pubkey)
|
return pubkey == our_pubkey || is_friend(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow_state(_ pubkey: String) -> FollowState {
|
func follow_state(_ pubkey: Pubkey) -> FollowState {
|
||||||
return is_friend(pubkey) ? .follows : .unfollows
|
return is_friend(pubkey) ? .follows : .unfollows
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the list of pubkeys of our friends who follow the given pubkey.
|
/// Gets the list of pubkeys of our friends who follow the given pubkey.
|
||||||
func get_friended_followers(_ pubkey: String) -> [String] {
|
func get_friended_followers(_ pubkey: Pubkey) -> [Pubkey] {
|
||||||
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: ReferencedId) -> NostrEvent? {
|
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||||
guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: keypair.pubkey, follow: follow) else {
|
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.calculate_id()
|
|
||||||
ev.sign(privkey: keypair.privkey)
|
|
||||||
|
|
||||||
box.send(ev)
|
box.send(ev)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: ReferencedId) -> NostrEvent? {
|
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||||
guard let cs = our_contacts else {
|
guard let cs = our_contacts else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let ev = unfollow_reference_event(our_contacts: cs, our_pubkey: keypair.pubkey, unfollow: unfollow)
|
guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else {
|
||||||
ev.calculate_id()
|
return nil
|
||||||
ev.sign(privkey: keypair.privkey)
|
}
|
||||||
|
|
||||||
postbox.send(ev)
|
postbox.send(ev)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow_reference_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: ReferencedId) -> NostrEvent {
|
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||||
let tags = our_contacts.tags.filter { tag in
|
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
|
||||||
if tag.count >= 2 && tag[0] == unfollow.key && tag[1] == unfollow.ref_id {
|
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
ts.append(tag.strings())
|
||||||
}
|
}
|
||||||
|
|
||||||
let kind = NostrKind.contacts.rawValue
|
let kind = NostrKind.contacts.rawValue
|
||||||
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
|
|
||||||
|
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: ReferencedId) -> NostrEvent? {
|
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||||
guard let cs = our_contacts else {
|
guard let cs = our_contacts else {
|
||||||
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
|
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
|
||||||
// we should only create contacts during profile creation
|
// we should only create contacts during profile creation
|
||||||
@@ -174,7 +172,7 @@ func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: Re
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ev = follow_with_existing_contacts(our_pubkey: our_pubkey, our_contacts: cs, follow: follow) else {
|
guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,25 +184,26 @@ func decode_json_relays(_ content: String) -> [String: RelayInfo]? {
|
|||||||
return decode_json(content)
|
return decode_json(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], privkey: String, relay: String) -> NostrEvent? {
|
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
|
||||||
|
return decode_json(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||||
|
|
||||||
relays.removeValue(forKey: relay)
|
relays.removeValue(forKey: relay)
|
||||||
|
|
||||||
print("remove_relay \(relays)")
|
|
||||||
guard let content = encode_json(relays) else {
|
guard let content = encode_json(relays) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||||
new_ev.calculate_id()
|
|
||||||
new_ev.sign(privkey: privkey)
|
|
||||||
return new_ev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
|
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? {
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||||
|
|
||||||
|
|
||||||
guard relays.index(forKey: relay) == nil else {
|
guard relays.index(forKey: relay) == nil else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -215,37 +214,56 @@ func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||||
new_ev.calculate_id()
|
|
||||||
new_ev.sign(privkey: privkey)
|
|
||||||
return new_ev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] {
|
func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? {
|
||||||
guard let relay_info = decode_json_relays(content) else {
|
let tags = relays.compactMap { r -> [String]? in
|
||||||
return make_contact_relays(relays)
|
var tag = ["r", r.url.id]
|
||||||
|
if (r.info.read ?? true) != (r.info.write ?? true) {
|
||||||
|
tag += r.info.read == true ? ["read"] : ["write"]
|
||||||
|
}
|
||||||
|
if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return relay_info
|
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_already_following(contacts: NostrEvent, follow: ReferencedId) -> Bool {
|
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] {
|
||||||
return contacts.references(id: follow.ref_id, key: follow.key)
|
return decode_json_relays(content) ?? make_contact_relays(relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
|
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||||
|
return contacts.references.contains { ref in
|
||||||
|
switch (ref, follow) {
|
||||||
|
case let (.hashtag(ht), .hashtag(follow_ht)):
|
||||||
|
return ht.string() == follow_ht
|
||||||
|
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||||
|
return pk == follow_pk
|
||||||
|
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||||
|
(.event, _), (.quote, _), (.param, _):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
|
||||||
// don't update if we're already following
|
// don't update if we're already following
|
||||||
if is_already_following(contacts: our_contacts, follow: follow) {
|
if is_already_following(contacts: our_contacts, follow: follow) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let kind = NostrKind.contacts.rawValue
|
let kind = NostrKind.contacts.rawValue
|
||||||
var tags = our_contacts.tags
|
|
||||||
tags.append(refid_to_tag(follow))
|
var tags = our_contacts.tags.strings()
|
||||||
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
|
tags.append(follow.tag)
|
||||||
|
|
||||||
|
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
|
func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] {
|
||||||
return relays.reduce(into: [:]) { acc, relay in
|
return relays.reduce(into: [:]) { acc, relay in
|
||||||
acc[relay.url.url.absoluteString] = relay.info
|
acc[relay.url] = relay.info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// ContentFilters.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-09-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||||
|
enum FilterState : Int {
|
||||||
|
case posts_and_replies = 1
|
||||||
|
case posts = 0
|
||||||
|
|
||||||
|
func filter(ev: NostrEvent) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .posts:
|
||||||
|
return ev.known_kind == .boost || !ev.is_reply(.empty)
|
||||||
|
case .posts_and_replies:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple filter to determine whether to show posts with #nsfw tags
|
||||||
|
func nsfw_tag_filter(ev: NostrEvent) -> Bool {
|
||||||
|
return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) {
|
||||||
|
return { ev in
|
||||||
|
guard ev.known_kind == .boost else { return true }
|
||||||
|
guard let inner_ev = ev.get_inner_event(cache: damus_state.events) else { return true }
|
||||||
|
return should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: inner_ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic filter with various tweakable settings
|
||||||
|
struct ContentFilters {
|
||||||
|
var filters: [(NostrEvent) -> Bool]
|
||||||
|
|
||||||
|
func filter(ev: NostrEvent) -> Bool {
|
||||||
|
for filter in filters {
|
||||||
|
if !filter(ev) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ContentFilters {
|
||||||
|
static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] {
|
||||||
|
var filters = Array<(NostrEvent) -> Bool>()
|
||||||
|
if damus_state.settings.hide_nsfw_tagged_content {
|
||||||
|
filters.append(nsfw_tag_filter)
|
||||||
|
}
|
||||||
|
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,18 +12,10 @@ class CreateAccountModel: ObservableObject {
|
|||||||
@Published var real_name: String = ""
|
@Published var real_name: String = ""
|
||||||
@Published var nick_name: String = ""
|
@Published var nick_name: String = ""
|
||||||
@Published var about: String = ""
|
@Published var about: String = ""
|
||||||
@Published var pubkey: String = ""
|
@Published var pubkey: Pubkey = .empty
|
||||||
@Published var privkey: String = ""
|
@Published var privkey: Privkey = .empty
|
||||||
@Published var profile_image: URL? = nil
|
@Published var profile_image: URL? = nil
|
||||||
|
|
||||||
var pubkey_bech32: String {
|
|
||||||
return bech32_pubkey(self.pubkey) ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var privkey_bech32: String {
|
|
||||||
return bech32_privkey(self.privkey) ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var rendered_name: String {
|
var rendered_name: String {
|
||||||
if real_name.isEmpty {
|
if real_name.isEmpty {
|
||||||
return nick_name
|
return nick_name
|
||||||
@@ -35,16 +27,10 @@ class CreateAccountModel: ObservableObject {
|
|||||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(real: String = "", nick: String = "", about: String = "") {
|
||||||
let keypair = generate_new_keypair()
|
let keypair = generate_new_keypair()
|
||||||
self.pubkey = keypair.pubkey
|
self.pubkey = keypair.pubkey
|
||||||
self.privkey = keypair.privkey!
|
self.privkey = keypair.privkey
|
||||||
}
|
|
||||||
|
|
||||||
init(real: String, nick: String, about: String) {
|
|
||||||
let keypair = generate_new_keypair()
|
|
||||||
self.pubkey = keypair.pubkey
|
|
||||||
self.privkey = keypair.privkey!
|
|
||||||
|
|
||||||
self.real_name = real
|
self.real_name = real
|
||||||
self.nick_name = nick
|
self.nick_name = nick
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// DamusCacheManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-10-04.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct DamusCacheManager {
|
||||||
|
static var shared: DamusCacheManager = DamusCacheManager()
|
||||||
|
|
||||||
|
func clear_cache(damus_state: DamusState, completion: (() -> Void)? = nil) {
|
||||||
|
Log.info("Clearing all caches", for: .storage)
|
||||||
|
clear_kingfisher_cache(completion: {
|
||||||
|
clear_cache_folder(completion: {
|
||||||
|
Log.info("All caches cleared", for: .storage)
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear_kingfisher_cache(completion: (() -> Void)? = nil) {
|
||||||
|
Log.info("Clearing Kingfisher cache", for: .storage)
|
||||||
|
KingfisherManager.shared.cache.clearMemoryCache()
|
||||||
|
KingfisherManager.shared.cache.clearDiskCache {
|
||||||
|
Log.info("Kingfisher cache cleared", for: .storage)
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear_cache_folder(completion: (() -> Void)? = nil) {
|
||||||
|
Log.info("Clearing entire cache folder", for: .storage)
|
||||||
|
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileNames = try FileManager.default.contentsOfDirectory(atPath: cacheURL.path)
|
||||||
|
|
||||||
|
for fileName in fileNames {
|
||||||
|
let filePath = cacheURL.appendingPathComponent(fileName)
|
||||||
|
|
||||||
|
// Prevent issues by double-checking if files are in use, and do not delete them if they are.
|
||||||
|
// This is not perfect. There is still a small chance for a race condition if a file is opened between this check and the file removal.
|
||||||
|
let isBusy = (!(access(filePath.path, F_OK) == -1 && errno == ETXTBSY))
|
||||||
|
if isBusy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.removeItem(at: filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("Cache folder cleared successfully.", for: .storage)
|
||||||
|
completion?()
|
||||||
|
} catch {
|
||||||
|
Log.error("Could not clear cache folder", for: .storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,42 @@ struct DamusState {
|
|||||||
let muted_threads: MutedThreadsManager
|
let muted_threads: MutedThreadsManager
|
||||||
let wallet: WalletModel
|
let wallet: WalletModel
|
||||||
let nav: NavigationCoordinator
|
let nav: NavigationCoordinator
|
||||||
let user_search_cache: UserSearchCache
|
let music: MusicController?
|
||||||
|
let video: VideoController
|
||||||
|
let ndb: Ndb
|
||||||
|
var purple: DamusPurple
|
||||||
|
|
||||||
|
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
|
||||||
|
self.pool = pool
|
||||||
|
self.keypair = keypair
|
||||||
|
self.likes = likes
|
||||||
|
self.boosts = boosts
|
||||||
|
self.contacts = contacts
|
||||||
|
self.profiles = profiles
|
||||||
|
self.dms = dms
|
||||||
|
self.previews = previews
|
||||||
|
self.zaps = zaps
|
||||||
|
self.lnurls = lnurls
|
||||||
|
self.settings = settings
|
||||||
|
self.relay_filters = relay_filters
|
||||||
|
self.relay_model_cache = relay_model_cache
|
||||||
|
self.drafts = drafts
|
||||||
|
self.events = events
|
||||||
|
self.bookmarks = bookmarks
|
||||||
|
self.postbox = postbox
|
||||||
|
self.bootstrap_relays = bootstrap_relays
|
||||||
|
self.replies = replies
|
||||||
|
self.muted_threads = muted_threads
|
||||||
|
self.wallet = wallet
|
||||||
|
self.nav = nav
|
||||||
|
self.music = music
|
||||||
|
self.video = video
|
||||||
|
self.ndb = ndb
|
||||||
|
self.purple = purple ?? DamusPurple(
|
||||||
|
environment: settings.purple_api_local_test_mode ? .local_test : .production,
|
||||||
|
keypair: keypair
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add_zap(zap: Zapping) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
@@ -42,15 +77,15 @@ struct DamusState {
|
|||||||
// thread zaps
|
// thread zaps
|
||||||
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
|
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
|
||||||
// [nozaps]: thread zaps are only available outside of the app store
|
// [nozaps]: thread zaps are only available outside of the app store
|
||||||
replies.count_replies(ev)
|
replies.count_replies(ev, keypair: self.keypair)
|
||||||
events.add_replies(ev: ev)
|
events.add_replies(ev: ev, keypair: self.keypair)
|
||||||
}
|
}
|
||||||
|
|
||||||
// associate with events as well
|
// associate with events as well
|
||||||
return stored
|
return stored
|
||||||
}
|
}
|
||||||
|
|
||||||
var pubkey: String {
|
var pubkey: Pubkey {
|
||||||
return keypair.pubkey
|
return keypair.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +94,36 @@ struct DamusState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var empty: DamusState {
|
static var empty: DamusState {
|
||||||
let user_search_cache = UserSearchCache()
|
let empty_pub: Pubkey = .empty
|
||||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(user_search_cache: user_search_cache), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_model_cache: RelayModelCache(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator(), user_search_cache: user_search_cache) }
|
let empty_sec: Privkey = .empty
|
||||||
|
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||||
|
|
||||||
|
return DamusState.init(
|
||||||
|
pool: RelayPool(ndb: .empty),
|
||||||
|
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||||
|
likes: EventCounter(our_pubkey: empty_pub),
|
||||||
|
boosts: EventCounter(our_pubkey: empty_pub),
|
||||||
|
contacts: Contacts(our_pubkey: empty_pub),
|
||||||
|
profiles: Profiles(ndb: .empty),
|
||||||
|
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||||
|
previews: PreviewCache(),
|
||||||
|
zaps: Zaps(our_pubkey: empty_pub),
|
||||||
|
lnurls: LNUrls(),
|
||||||
|
settings: UserSettingsStore(),
|
||||||
|
relay_filters: RelayFilters(our_pubkey: empty_pub),
|
||||||
|
relay_model_cache: RelayModelCache(),
|
||||||
|
drafts: Drafts(),
|
||||||
|
events: EventCache(ndb: .empty),
|
||||||
|
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||||
|
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||||
|
bootstrap_relays: [],
|
||||||
|
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||||
|
muted_threads: MutedThreadsManager(keypair: kp),
|
||||||
|
wallet: WalletModel(settings: UserSettingsStore()),
|
||||||
|
nav: NavigationCoordinator(),
|
||||||
|
music: nil,
|
||||||
|
video: VideoController(),
|
||||||
|
ndb: .empty
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ class DirectMessageModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var draft: String
|
@Published var draft: String = ""
|
||||||
|
|
||||||
let pubkey: String
|
let pubkey: Pubkey
|
||||||
|
|
||||||
var is_request: Bool
|
var is_request = false
|
||||||
var our_pubkey: String
|
var our_pubkey: Pubkey
|
||||||
|
|
||||||
func determine_is_request() -> Bool {
|
func determine_is_request() -> Bool {
|
||||||
for event in events {
|
for event in events {
|
||||||
@@ -31,19 +31,9 @@ class DirectMessageModel: ObservableObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
|
init(events: [NostrEvent] = [], our_pubkey: Pubkey, pubkey: Pubkey) {
|
||||||
self.events = events
|
self.events = events
|
||||||
self.is_request = false
|
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
self.draft = ""
|
|
||||||
self.pubkey = pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
init(our_pubkey: String, pubkey: String) {
|
|
||||||
self.events = []
|
|
||||||
self.is_request = false
|
|
||||||
self.our_pubkey = our_pubkey
|
|
||||||
self.draft = ""
|
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ class DirectMessagesModel: ObservableObject {
|
|||||||
@Published var dms: [DirectMessageModel] = []
|
@Published var dms: [DirectMessageModel] = []
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
@Published var open_dm: Bool = false
|
@Published var open_dm: Bool = false
|
||||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
|
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: .empty, pubkey: .empty)
|
||||||
let our_pubkey: String
|
let our_pubkey: Pubkey
|
||||||
|
|
||||||
init(our_pubkey: String) {
|
init(our_pubkey: Pubkey) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +30,14 @@ class DirectMessagesModel: ObservableObject {
|
|||||||
self.active_model = model
|
self.active_model = model
|
||||||
}
|
}
|
||||||
|
|
||||||
func set_active_dm(_ pubkey: String) {
|
func set_active_dm(_ pubkey: Pubkey) {
|
||||||
for model in self.dms where model.pubkey == pubkey {
|
for model in self.dms where model.pubkey == pubkey {
|
||||||
self.set_active_dm_model(model)
|
self.set_active_dm_model(model)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
func lookup_or_create(_ pubkey: Pubkey) -> DirectMessageModel {
|
||||||
if let dm = lookup(pubkey) {
|
if let dm = lookup(pubkey) {
|
||||||
return dm
|
return dm
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ class DirectMessagesModel: ObservableObject {
|
|||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup(_ pubkey: String) -> DirectMessageModel? {
|
func lookup(_ pubkey: Pubkey) -> DirectMessageModel? {
|
||||||
for dm in dms {
|
for dm in dms {
|
||||||
if pubkey == dm.pubkey {
|
if pubkey == dm.pubkey {
|
||||||
return dm
|
return dm
|
||||||
|
|||||||
@@ -7,19 +7,21 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class DraftArtifacts {
|
class DraftArtifacts: Equatable {
|
||||||
var content: NSMutableAttributedString
|
var content: NSMutableAttributedString
|
||||||
var media: [UploadedMedia]
|
var media: [UploadedMedia]
|
||||||
|
|
||||||
init() {
|
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = []) {
|
||||||
self.content = NSMutableAttributedString(string: "")
|
|
||||||
self.media = []
|
|
||||||
}
|
|
||||||
|
|
||||||
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
|
|
||||||
self.content = content
|
self.content = content
|
||||||
self.media = media
|
self.media = media
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool {
|
||||||
|
return (
|
||||||
|
lhs.media == rhs.media &&
|
||||||
|
lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Drafts: ObservableObject {
|
class Drafts: ObservableObject {
|
||||||
|
|||||||
+22
-29
@@ -7,20 +7,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum EventRef {
|
enum EventRef: Equatable {
|
||||||
case mention(Mention)
|
case mention(Mention<NoteRef>)
|
||||||
case thread_id(ReferencedId)
|
case thread_id(NoteRef)
|
||||||
case reply(ReferencedId)
|
case reply(NoteRef)
|
||||||
case reply_to_root(ReferencedId)
|
case reply_to_root(NoteRef)
|
||||||
|
|
||||||
var is_mention: Mention? {
|
var is_mention: NoteRef? {
|
||||||
if case .mention(let m) = self {
|
if case .mention(let m) = self { return m.ref }
|
||||||
return m
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_direct_reply: ReferencedId? {
|
var is_direct_reply: NoteRef? {
|
||||||
switch self {
|
switch self {
|
||||||
case .mention:
|
case .mention:
|
||||||
return nil
|
return nil
|
||||||
@@ -33,7 +31,7 @@ enum EventRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_thread_id: ReferencedId? {
|
var is_thread_id: NoteRef? {
|
||||||
switch self {
|
switch self {
|
||||||
case .mention:
|
case .mention:
|
||||||
return nil
|
return nil
|
||||||
@@ -46,7 +44,7 @@ enum EventRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_reply: ReferencedId? {
|
var is_reply: NoteRef? {
|
||||||
switch self {
|
switch self {
|
||||||
case .mention:
|
case .mention:
|
||||||
return nil
|
return nil
|
||||||
@@ -64,10 +62,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
|||||||
return blocks.reduce(into: []) { acc, block in
|
return blocks.reduce(into: []) { acc, block in
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
if m.type == type {
|
if m.ref.key == type, let idx = m.index {
|
||||||
if let idx = m.index {
|
acc.insert(idx)
|
||||||
acc.insert(idx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .relay:
|
case .relay:
|
||||||
return
|
return
|
||||||
@@ -83,7 +79,7 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
|
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
|
||||||
if refs.count == 0 {
|
if refs.count == 0 {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -105,16 +101,15 @@ func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
|
|||||||
return evrefs
|
return evrefs
|
||||||
}
|
}
|
||||||
|
|
||||||
func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>) -> [EventRef] {
|
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
|
||||||
var mentions: [EventRef] = []
|
var mentions: [EventRef] = []
|
||||||
var ev_refs: [ReferencedId] = []
|
var ev_refs: [NoteRef] = []
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
|
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
if tag.count >= 2 && tag[0] == "e" {
|
if let ref = NoteRef.from_tag(tag: tag) {
|
||||||
let ref = tag_to_refid(tag)!
|
|
||||||
if mention_indices.contains(i) {
|
if mention_indices.contains(i) {
|
||||||
let mention = Mention(index: i, type: .event, ref: ref)
|
let mention = Mention<NoteRef>(index: i, ref: ref)
|
||||||
mentions.append(.mention(mention))
|
mentions.append(.mention(mention))
|
||||||
} else {
|
} else {
|
||||||
ev_refs.append(ref)
|
ev_refs.append(ref)
|
||||||
@@ -128,27 +123,25 @@ func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>
|
|||||||
return replies
|
return replies
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
|
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
|
||||||
if tags.count == 0 {
|
if tags.count == 0 {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
/// build a set of indices for each event mention
|
/// build a set of indices for each event mention
|
||||||
let mention_indices = build_mention_indices(blocks, type: .event)
|
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||||
|
|
||||||
/// simpler case with no mentions
|
/// simpler case with no mentions
|
||||||
if mention_indices.count == 0 {
|
if mention_indices.count == 0 {
|
||||||
let ev_refs = get_referenced_ids(tags: tags, key: "e")
|
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
||||||
return interp_event_refs_without_mentions(ev_refs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
|
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool {
|
func event_is_reply(_ refs: [EventRef]) -> Bool {
|
||||||
return ev.event_refs(privkey).contains { evref in
|
return refs.contains { evref in
|
||||||
return evref.is_reply != nil
|
return evref.is_reply != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import Foundation
|
|||||||
|
|
||||||
class EventsModel: ObservableObject {
|
class EventsModel: ObservableObject {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
let target: String
|
let target: NoteId
|
||||||
let kind: NostrKind
|
let kind: NostrKind
|
||||||
let sub_id = UUID().uuidString
|
let sub_id = UUID().uuidString
|
||||||
let profiles_id = UUID().uuidString
|
let profiles_id = UUID().uuidString
|
||||||
|
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
|
|
||||||
init(state: DamusState, target: String, kind: NostrKind) {
|
init(state: DamusState, target: NoteId, kind: NostrKind) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.target = target
|
self.target = target
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
@@ -41,11 +41,8 @@ class EventsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handle_event(relay_id: String, ev: NostrEvent) {
|
private func handle_event(relay_id: String, ev: NostrEvent) {
|
||||||
guard ev.kind == kind.rawValue else {
|
guard ev.kind == kind.rawValue,
|
||||||
return
|
ev.referenced_ids.last == target else {
|
||||||
}
|
|
||||||
|
|
||||||
guard last_etag(tags: ev.tags) == target else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +59,15 @@ class EventsModel: ObservableObject {
|
|||||||
switch nev {
|
switch nev {
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
handle_event(relay_id: relay_id, ev: ev)
|
handle_event(relay_id: relay_id, ev: ev)
|
||||||
case .notice(_):
|
case .notice:
|
||||||
break
|
break
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
case .eose(_):
|
case .auth:
|
||||||
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
break
|
||||||
|
case .eose:
|
||||||
|
let txn = NdbTxn(ndb: self.state.ndb)
|
||||||
|
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
enum FollowTarget {
|
enum FollowTarget {
|
||||||
case pubkey(String)
|
case pubkey(Pubkey)
|
||||||
case contact(NostrEvent)
|
case contact(NostrEvent)
|
||||||
|
|
||||||
var pubkey: String {
|
var follow_ref: FollowRef {
|
||||||
|
FollowRef.pubkey(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubkey: Pubkey {
|
||||||
switch self {
|
switch self {
|
||||||
case .pubkey(let pk):
|
case .pubkey(let pk): return pk
|
||||||
return pk
|
case .contact(let ev): return ev.pubkey
|
||||||
case .contact(let ev):
|
|
||||||
return ev.pubkey
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import Foundation
|
|||||||
|
|
||||||
class FollowersModel: ObservableObject {
|
class FollowersModel: ObservableObject {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let target: String
|
let target: Pubkey
|
||||||
|
|
||||||
@Published var contacts: [String]? = nil
|
@Published var contacts: [Pubkey]? = nil
|
||||||
var has_contact: Set<String> = Set()
|
var has_contact: Set<Pubkey> = Set()
|
||||||
|
|
||||||
let sub_id: String = UUID().description
|
let sub_id: String = UUID().description
|
||||||
let profiles_id: String = UUID().description
|
let profiles_id: String = UUID().description
|
||||||
@@ -24,20 +24,19 @@ class FollowersModel: ObservableObject {
|
|||||||
return contacts.count
|
return contacts.count
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState, target: String) {
|
init(damus_state: DamusState, target: Pubkey) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.target = target
|
self.target = target
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_filter() -> NostrFilter {
|
func get_filter() -> NostrFilter {
|
||||||
NostrFilter(kinds: [.contacts],
|
NostrFilter(kinds: [.contacts], pubkeys: [target])
|
||||||
pubkeys: [target])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
let filter = get_filter()
|
let filter = get_filter()
|
||||||
let filters = [filter]
|
let filters = [filter]
|
||||||
print_filters(relay_id: "following", filters: [filters])
|
//print_filters(relay_id: "following", filters: [filters])
|
||||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +53,8 @@ class FollowersModel: ObservableObject {
|
|||||||
has_contact.insert(ev.pubkey)
|
has_contact.insert(ev.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_profiles(relay_id: String) {
|
func load_profiles<Y>(relay_id: String, txn: NdbTxn<Y>) {
|
||||||
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
|
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [], txn: txn)
|
||||||
if authors.isEmpty {
|
if authors.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -78,22 +77,22 @@ class FollowersModel: ObservableObject {
|
|||||||
|
|
||||||
if ev.known_kind == .contacts {
|
if ev.known_kind == .contacts {
|
||||||
handle_contact_event(ev)
|
handle_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
|
||||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
print("followingmodel notice: \(msg)")
|
print("followingmodel notice: \(msg)")
|
||||||
|
|
||||||
case .eose(let sub_id):
|
case .eose(let sub_id):
|
||||||
if sub_id == self.sub_id {
|
if sub_id == self.sub_id {
|
||||||
load_profiles(relay_id: relay_id)
|
let txn = NdbTxn(ndb: self.damus_state.ndb)
|
||||||
|
load_profiles(relay_id: relay_id, txn: txn)
|
||||||
} else if sub_id == self.profiles_id {
|
} else if sub_id == self.profiles_id {
|
||||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
|
case .auth:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user