mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
update
This commit is contained in:
5
.github/ISSUE_TEMPLATE/----.md
vendored
5
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -16,6 +16,5 @@ assignees: syusui-s
|
|||||||
**あなたが検討した他の手段について説明してください**
|
**あなたが検討した他の手段について説明してください**
|
||||||
あらゆる他の解決策や検討した機能について、明確かつ簡潔に説明してください。
|
あらゆる他の解決策や検討した機能について、明確かつ簡潔に説明してください。
|
||||||
|
|
||||||
|
**追加の情報**
|
||||||
**Additional context**
|
要望に関する他の情報やスクリーンショットがあればこちらに。
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 17
|
node-version: 19
|
||||||
- name: Get npm cache directory
|
- name: Get npm cache directory
|
||||||
id: npm-cache-dir
|
id: npm-cache-dir
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -8,4 +8,35 @@ A nostr client like TweetDeck powered by SolidJS.
|
|||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
Copyright (C) 2023 Shusui Moyatani
|
||||||
|
|
||||||
AGPL-3.0-or-later
|
AGPL-3.0-or-later
|
||||||
|
|
||||||
|
### English
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
### 日本語
|
||||||
|
|
||||||
|
このプログラムは自由ソフトウェアです。あなたはこれを、Free Software Foundationによって発行された
|
||||||
|
GNUアフェロー一般公衆利用許諾書(バージョン3か、それ以降のいずれかのバージョン)が定める条件の下で再頒布または改変することができます。
|
||||||
|
|
||||||
|
このプログラムは有用であることを願って頒布されますが、 *全くの無保証* です。
|
||||||
|
*商業可能性* や *特定目的への適合性* に対する保証は言外に示されたものも含め、全く存在しません。
|
||||||
|
詳しくはGNUアフェロー一般公衆利用許諾書をご覧ください。
|
||||||
|
|
||||||
|
あなたはこのプログラムと共にGNUアフェロー一般公衆利用許諾書のコピーを一部受け取っているはずです。
|
||||||
|
もし受け取っていなければ <http://www.gnu.org/licenses/> をご覧ください。
|
||||||
|
|
||||||
|
参考日本語訳: <https://gpl.mhatta.org/agpl.ja.html>
|
||||||
|
|||||||
198
package-lock.json
generated
198
package-lock.json
generated
@@ -12,7 +12,9 @@
|
|||||||
"@solidjs/meta": "^0.28.2",
|
"@solidjs/meta": "^0.28.2",
|
||||||
"@solidjs/router": "^0.6.0",
|
"@solidjs/router": "^0.6.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tanstack/solid-query": "^4.24.6",
|
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
||||||
|
"@tanstack/react-query-persist-client": "^4.24.10",
|
||||||
|
"@tanstack/solid-query": "^4.24.10",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.3",
|
"@thisbeyond/solid-dnd": "^0.7.3",
|
||||||
"heroicons": "^2.0.15",
|
"heroicons": "^2.0.15",
|
||||||
"nostr-tools": "^1.3.2",
|
"nostr-tools": "^1.3.2",
|
||||||
@@ -1348,20 +1350,86 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "4.24.6",
|
"version": "4.24.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||||
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w==",
|
"integrity": "sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/solid-query": {
|
"node_modules/@tanstack/query-persist-client-core": {
|
||||||
"version": "4.24.6",
|
"version": "4.24.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.24.10.tgz",
|
||||||
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
|
"integrity": "sha512-ObE7k4/TN1EgYMrTCkR43UIvviCtT27QcbE14CgdqeOVRSJ+oiIgXlfir69bcgBUW5Ba7S0ezY2SNV6IfSRNrw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "4.24.6"
|
"@tanstack/query-core": "4.24.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-sync-storage-persister": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-bS/vUHmzlnj7FiUZaPEUsEPitiaX2bzPJ9DuqQf7HCNMgqV/pwie90q90XIpGHUw8cF/6e3RnHF346OkH/XehQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-persist-client-core": "4.24.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "4.24.10",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-persist-client": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-Ta8PQua5aJK5F1w1ckX1xFnA4ohNpoeLUvApxtpMb3DKfs1XmyeFaddwyhP7La/EdjTtiInBJ2TmEAjG7EqhCw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-persist-client-core": "4.24.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "4.24.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/solid-query": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-pwP5vhfcDkwToxRFCXmIr9xFVOGPjanmgrZ5mUge5JE4xAy90lRV4KF36H6QOu0sZ4qwKgh9JcLrVtIcJP1E1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "4.24.10"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4884,8 +4952,7 @@
|
|||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
@@ -5423,6 +5490,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@@ -6905,6 +6984,18 @@
|
|||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -8028,6 +8119,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peer": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -9338,16 +9438,50 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@tanstack/query-core": {
|
"@tanstack/query-core": {
|
||||||
"version": "4.24.6",
|
"version": "4.24.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||||
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w=="
|
"integrity": "sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ=="
|
||||||
|
},
|
||||||
|
"@tanstack/query-persist-client-core": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-ObE7k4/TN1EgYMrTCkR43UIvviCtT27QcbE14CgdqeOVRSJ+oiIgXlfir69bcgBUW5Ba7S0ezY2SNV6IfSRNrw==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/query-core": "4.24.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/query-sync-storage-persister": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-bS/vUHmzlnj7FiUZaPEUsEPitiaX2bzPJ9DuqQf7HCNMgqV/pwie90q90XIpGHUw8cF/6e3RnHF346OkH/XehQ==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/query-persist-client-core": "4.24.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/react-query": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/query-core": "4.24.10",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/react-query-persist-client": {
|
||||||
|
"version": "4.24.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.24.10.tgz",
|
||||||
|
"integrity": "sha512-Ta8PQua5aJK5F1w1ckX1xFnA4ohNpoeLUvApxtpMb3DKfs1XmyeFaddwyhP7La/EdjTtiInBJ2TmEAjG7EqhCw==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/query-persist-client-core": "4.24.10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@tanstack/solid-query": {
|
"@tanstack/solid-query": {
|
||||||
"version": "4.24.6",
|
"version": "4.24.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz",
|
||||||
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
|
"integrity": "sha512-pwP5vhfcDkwToxRFCXmIr9xFVOGPjanmgrZ5mUge5JE4xAy90lRV4KF36H6QOu0sZ4qwKgh9JcLrVtIcJP1E1g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@tanstack/query-core": "4.24.6"
|
"@tanstack/query-core": "4.24.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@thisbeyond/solid-dnd": {
|
"@thisbeyond/solid-dnd": {
|
||||||
@@ -11877,8 +12011,7 @@
|
|||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
@@ -12286,6 +12419,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lru-cache": {
|
"lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@@ -13367,6 +13509,15 @@
|
|||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-cache": {
|
"read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -14217,6 +14368,13 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -50,7 +50,9 @@
|
|||||||
"@solidjs/meta": "^0.28.2",
|
"@solidjs/meta": "^0.28.2",
|
||||||
"@solidjs/router": "^0.6.0",
|
"@solidjs/router": "^0.6.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tanstack/solid-query": "^4.24.6",
|
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
||||||
|
"@tanstack/react-query-persist-client": "^4.24.10",
|
||||||
|
"@tanstack/solid-query": "^4.24.10",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.3",
|
"@thisbeyond/solid-dnd": "^0.7.3",
|
||||||
"heroicons": "^2.0.15",
|
"heroicons": "^2.0.15",
|
||||||
"nostr-tools": "^1.3.2",
|
"nostr-tools": "^1.3.2",
|
||||||
|
|||||||
17
src/App.tsx
17
src/App.tsx
@@ -1,19 +1,26 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { Routes, Route } from '@solidjs/router';
|
import { Routes, Route } from '@solidjs/router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
|
||||||
|
// import { persistQueryClient } from '@tanstack/solid-query-persist-client';
|
||||||
|
// import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
||||||
|
|
||||||
import Home from '@/pages/Home';
|
import Home from '@/pages/Home';
|
||||||
import NotFound from '@/pages/NotFound';
|
import NotFound from '@/pages/NotFound';
|
||||||
import AccountRecovery from '@/pages/AccountRecovery';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient({});
|
||||||
|
|
||||||
|
// const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage });
|
||||||
|
|
||||||
|
// persistQueryClient({
|
||||||
|
// queryClient,
|
||||||
|
// persister: localStoragePersister,
|
||||||
|
// });
|
||||||
|
|
||||||
const App: Component = () => (
|
const App: Component = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={() => <Home />} />
|
||||||
<Route path="/recovery" element={<AccountRecovery />} />
|
<Route path="/*" element={() => <NotFound />} />
|
||||||
<Route path="/*" element={<NotFound />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => {
|
|||||||
return getEvents({ pool: pool(), relayUrls, filters, options, signal });
|
return getEvents({ pool: pool(), relayUrls, filters, options, signal });
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes in ms
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
cacheTime: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { Pub } from 'nostr-tools/relay';
|
|||||||
|
|
||||||
import usePool from '@/clients/usePool';
|
import usePool from '@/clients/usePool';
|
||||||
|
|
||||||
|
const currentDate = (): number => Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// NIP-20: Command Result
|
// NIP-20: Command Result
|
||||||
const waitCommandResult = (pub: Pub): Promise<void> => {
|
const waitCommandResult = (pub: Pub): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -48,11 +50,10 @@ const useCommands = () => {
|
|||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: NostrEvent = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: currentDate(),
|
||||||
tags: [],
|
tags: [],
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
// TODO define window.nostr
|
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
},
|
},
|
||||||
// NIP-25
|
// NIP-25
|
||||||
@@ -73,14 +74,40 @@ const useCommands = () => {
|
|||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: NostrEvent = {
|
||||||
kind: 7,
|
kind: 7,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: currentDate(),
|
||||||
tags: [
|
tags: [
|
||||||
['e', eventId],
|
['e', eventId],
|
||||||
['p', notifyPubkey],
|
['p', notifyPubkey],
|
||||||
],
|
],
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
// TODO define window.nostr
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
|
},
|
||||||
|
// NIP-18
|
||||||
|
publishDeprecatedRepost({
|
||||||
|
relayUrls,
|
||||||
|
pubkey,
|
||||||
|
eventId,
|
||||||
|
notifyPubkey,
|
||||||
|
}: {
|
||||||
|
relayUrls: string[];
|
||||||
|
pubkey: string;
|
||||||
|
eventId: string;
|
||||||
|
notifyPubkey: string;
|
||||||
|
}): Promise<Promise<void>[]> {
|
||||||
|
const preSignedEvent: NostrEvent = {
|
||||||
|
kind: 6,
|
||||||
|
pubkey,
|
||||||
|
created_at: currentDate(),
|
||||||
|
tags: [
|
||||||
|
['e', eventId],
|
||||||
|
['p', notifyPubkey],
|
||||||
|
],
|
||||||
|
// Some clients includes some contents here.
|
||||||
|
// Damus includes an original event. Iris includes #[0] as a mention.
|
||||||
|
// We just follow the specification.
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSignal } from 'solid-js';
|
import { createSignal } from 'solid-js';
|
||||||
import { SimplePool } from 'nostr-tools/pool';
|
import { SimplePool } from 'nostr-tools';
|
||||||
|
|
||||||
const [pool] = createSignal<SimplePool>(new SimplePool());
|
const [pool] = createSignal<SimplePool>(new SimplePool());
|
||||||
|
|
||||||
|
|||||||
15
src/clients/usePubkey.ts
Normal file
15
src/clients/usePubkey.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createSignal, onMount, type Accessor } from 'solid-js';
|
||||||
|
|
||||||
|
const usePubkey = (): Accessor<string | undefined> => {
|
||||||
|
const [pubkey, setPubkey] = createSignal<string | undefined>(undefined);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (window.nostr != null) {
|
||||||
|
window.nostr.getPublicKey().then((pubkey) => setPubkey(pubkey));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pubkey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePubkey;
|
||||||
@@ -13,19 +13,24 @@ export type UseSubscriptionProps = {
|
|||||||
const sortEvents = (events: NostrEvent[]) =>
|
const sortEvents = (events: NostrEvent[]) =>
|
||||||
Array.from(events).sort((a, b) => b.created_at - a.created_at);
|
Array.from(events).sort((a, b) => b.created_at - a.created_at);
|
||||||
|
|
||||||
const useSubscription = (propsProvider: () => UseSubscriptionProps) => {
|
const useSubscription = (propsProvider: () => UseSubscriptionProps | undefined) => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const { relayUrls, filters, options } = propsProvider();
|
const props = propsProvider();
|
||||||
|
if (props == null) return;
|
||||||
|
|
||||||
|
const { relayUrls, filters, options } = props;
|
||||||
|
|
||||||
const sub = pool().sub(relayUrls, filters, options);
|
const sub = pool().sub(relayUrls, filters, options);
|
||||||
|
let pushed = false;
|
||||||
let eose = false;
|
let eose = false;
|
||||||
const storedEvents: NostrEvent[] = [];
|
const storedEvents: NostrEvent[] = [];
|
||||||
|
|
||||||
sub.on('event', (event: NostrEvent) => {
|
sub.on('event', (event: NostrEvent) => {
|
||||||
if (!eose) {
|
if (!eose) {
|
||||||
|
pushed = true;
|
||||||
storedEvents.push(event);
|
storedEvents.push(event);
|
||||||
} else {
|
} else {
|
||||||
setEvents((prevEvents) => sortEvents([event, ...prevEvents]));
|
setEvents((prevEvents) => sortEvents([event, ...prevEvents]));
|
||||||
@@ -43,7 +48,10 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps) => {
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEvents(sortEvents(storedEvents));
|
if (pushed) {
|
||||||
|
pushed = false;
|
||||||
|
setEvents(sortEvents(storedEvents));
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
|||||||
const pubkey = () => props.event.pubkey;
|
const pubkey = () => props.event.pubkey;
|
||||||
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
|
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
|
||||||
|
|
||||||
if (eventId() == null) {
|
|
||||||
return 'event not found';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { profile } = useProfile(() => ({ relayUrls: config().relayUrls, pubkey: pubkey() }));
|
const { profile } = useProfile(() => ({ relayUrls: config().relayUrls, pubkey: pubkey() }));
|
||||||
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
|
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
|
||||||
|
|
||||||
@@ -33,7 +29,7 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>{profile()?.display_name} Reposted</div>
|
<div>{profile()?.display_name} Reposted</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={event() != null}>
|
<Show when={event() != null} fallback={'loading'}>
|
||||||
<TextNote event={event()} />
|
<TextNote event={event()} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, type Component, type JSX } from 'solid-js';
|
import { createSignal, createMemo, type Component, type JSX } from 'solid-js';
|
||||||
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
||||||
|
|
||||||
type NotePostFormProps = {
|
type NotePostFormProps = {
|
||||||
@@ -8,11 +8,18 @@ type NotePostFormProps = {
|
|||||||
const NotePostForm: Component<NotePostFormProps> = (props) => {
|
const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||||
const [text, setText] = createSignal<string>('');
|
const [text, setText] = createSignal<string>('');
|
||||||
|
|
||||||
|
const handleChangeText: JSX.EventHandler<HTMLTextAreaElement, Event> = (ev) => {
|
||||||
|
setText(ev.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
|
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
props.onPost({ content: text() });
|
props.onPost({ content: text() });
|
||||||
|
// TODO 投稿完了したらなんかする
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitDisabled = createMemo(() => text().trim().length === 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<form class="grid w-64 gap-1" onSubmit={handleSubmit}>
|
<form class="grid w-64 gap-1" onSubmit={handleSubmit}>
|
||||||
@@ -21,13 +28,16 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
class="rounded border-none"
|
class="rounded border-none"
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="いまどうしてる?"
|
placeholder="いまどうしてる?"
|
||||||
onChange={(ev) => {
|
onInput={handleChangeText}
|
||||||
setText(ev.target.value);
|
|
||||||
}}
|
|
||||||
value={text()}
|
value={text()}
|
||||||
/>
|
/>
|
||||||
<div class="grid justify-end">
|
<div class="grid justify-end">
|
||||||
<button class="h-8 w-8 rounded bg-primary p-2 font-bold text-white" type="submit">
|
<button
|
||||||
|
class="h-8 w-8 rounded bg-primary p-2 font-bold text-white"
|
||||||
|
classList={{ 'bg-primary-disabled': submitDisabled(), 'bg-primary': !submitDisabled() }}
|
||||||
|
type="submit"
|
||||||
|
disabled={submitDisabled()}
|
||||||
|
>
|
||||||
<PaperAirplane />
|
<PaperAirplane />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
src/components/Notification.tsx
Normal file
28
src/components/Notification.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { For, Switch, Match, type Component } from 'solid-js';
|
||||||
|
import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
|
import TextNote from '@/components/TextNote';
|
||||||
|
import Reaction from '@/components/notification/Reaction';
|
||||||
|
|
||||||
|
export type TimelineProps = {
|
||||||
|
events: NostrEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Timeline: Component<TimelineProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<For each={props.events}>
|
||||||
|
{(event) => (
|
||||||
|
<Switch fallback={<div>unknown event</div>}>
|
||||||
|
<Match when={event.kind === Kind.Text}>
|
||||||
|
<TextNote event={event} />
|
||||||
|
</Match>
|
||||||
|
<Match when={event.kind === Kind.Reaction}>
|
||||||
|
<Reaction event={event} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Timeline;
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
import { Show, For, createSignal, createMemo, onMount, onCleanup } from 'solid-js';
|
import {
|
||||||
import type { Component } from 'solid-js';
|
Show,
|
||||||
|
For,
|
||||||
|
createSignal,
|
||||||
|
createMemo,
|
||||||
|
onMount,
|
||||||
|
onCleanup,
|
||||||
|
type JSX,
|
||||||
|
Component,
|
||||||
|
} from 'solid-js';
|
||||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
||||||
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||||
|
import ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg';
|
||||||
|
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg';
|
||||||
|
|
||||||
import useProfile from '@/clients/useProfile';
|
import useProfile from '@/clients/useProfile';
|
||||||
import useConfig from '@/clients/useConfig';
|
import useConfig from '@/clients/useConfig';
|
||||||
|
import useCommands from '@/clients/useCommands';
|
||||||
import useDatePulser from '@/hooks/useDatePulser';
|
import useDatePulser from '@/hooks/useDatePulser';
|
||||||
import { formatRelative } from '@/utils/formatDate';
|
import { formatRelative } from '@/utils/formatDate';
|
||||||
import ColumnItem from '@/components/ColumnItem';
|
import ColumnItem from '@/components/ColumnItem';
|
||||||
@@ -20,21 +31,37 @@ export type TextNoteProps = {
|
|||||||
const TextNote: Component<TextNoteProps> = (props) => {
|
const TextNote: Component<TextNoteProps> = (props) => {
|
||||||
const currentDate = useDatePulser();
|
const currentDate = useDatePulser();
|
||||||
const [config] = useConfig();
|
const [config] = useConfig();
|
||||||
|
const commands = useCommands();
|
||||||
const { profile: author } = useProfile(() => ({
|
const { profile: author } = useProfile(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
pubkey: props.event.pubkey,
|
pubkey: props.event.pubkey,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const replyingToPubKeys = createMemo(() =>
|
const replyingToPubKeys = createMemo(() =>
|
||||||
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
||||||
);
|
);
|
||||||
// TODO 日付をいい感じにフォーマットする関数を作る
|
// TODO 日付をいい感じにフォーマットする関数を作る
|
||||||
const createdAt = () => formatRelative(new Date(props.event.created_at * 1000), currentDate());
|
const createdAt = () => formatRelative(new Date(props.event.created_at * 1000), currentDate());
|
||||||
|
|
||||||
|
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
commands.publishDeprecatedRepost({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
commands.publishReaction({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
pubkey: pubkeyHex,
|
||||||
|
eventId: props.event.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="textnote">
|
<div class="textnote">
|
||||||
<ColumnItem>
|
<ColumnItem>
|
||||||
<div class="author-icon max-w-10 max-h-10 shrink-0">
|
<div class="author-icon h-10 w-10 shrink-0">
|
||||||
<Show when={author()?.picture} fallback={<div class="h-10 w-10" />}>
|
<Show when={author()?.picture}>
|
||||||
<img
|
<img
|
||||||
src={author()?.picture}
|
src={author()?.picture}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
@@ -47,7 +74,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
|||||||
<div class="flex justify-between gap-1 text-xs">
|
<div class="flex justify-between gap-1 text-xs">
|
||||||
<div class="author flex min-w-0 truncate">
|
<div class="author flex min-w-0 truncate">
|
||||||
{/* TODO link to author */}
|
{/* TODO link to author */}
|
||||||
<Show when={author()?.display_name}>
|
<Show when={author()?.display_name != null && author()?.display_name.length > 0}>
|
||||||
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
|
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="author-username truncate text-zinc-600">
|
<div class="author-username truncate text-zinc-600">
|
||||||
@@ -71,15 +98,21 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="content whitespace-pre-wrap break-all">
|
<div class="content whitespace-pre-wrap break-all">
|
||||||
<TextNoteContentDisplay event={props.event} />
|
<TextNoteContentDisplay event={props.event} embedding={true} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-evenly">
|
<div class="flex justify-end gap-16">
|
||||||
<button class="h-4 w-4 text-zinc-400">
|
<button class="h-4 w-4 text-zinc-400">
|
||||||
|
<ChatBubbleLeft />
|
||||||
|
</button>
|
||||||
|
<button class="h-4 w-4 text-zinc-400" onClick={handleRepost}>
|
||||||
<ArrowPathRoundedSquare />
|
<ArrowPathRoundedSquare />
|
||||||
</button>
|
</button>
|
||||||
<button class="h-4 w-4 text-zinc-400">
|
<button class="h-4 w-4 text-zinc-400" onClick={handleReaction}>
|
||||||
<HeartOutlined />
|
<HeartOutlined />
|
||||||
</button>
|
</button>
|
||||||
|
<button class="h-4 w-4 text-zinc-400">
|
||||||
|
<EllipsisHorizontal />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ColumnItem>
|
</ColumnItem>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Switch, Match, type Component } from 'solid-js';
|
import { For, Switch, Match, type Component } from 'solid-js';
|
||||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
import TextNote from '@/components/TextNote';
|
import TextNote from '@/components/TextNote';
|
||||||
import DeprecatedRepost from '@/components/DeprecatedRepost';
|
import DeprecatedRepost from '@/components/DeprecatedRepost';
|
||||||
@@ -8,15 +8,15 @@ export type TimelineProps = {
|
|||||||
events: NostrEvent[];
|
events: NostrEvent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Timeline: Component<TimelineProps> = (props) => {
|
const Timeline: Component<TimelineProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<For each={props.events}>
|
<For each={props.events}>
|
||||||
{(event) => (
|
{(event) => (
|
||||||
<Switch fallback={<div>unknown event</div>}>
|
<Switch fallback={<div>unknown event</div>}>
|
||||||
<Match when={event.kind === 1}>
|
<Match when={event.kind === Kind.Text}>
|
||||||
<TextNote event={event} />
|
<TextNote event={event} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={event.kind === 6}>
|
<Match when={(event.kind as number) === 6}>
|
||||||
<DeprecatedRepost event={event} />
|
<DeprecatedRepost event={event} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
50
src/components/notification/Reaction.tsx
Normal file
50
src/components/notification/Reaction.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Switch, Match, type Component, Show } from 'solid-js';
|
||||||
|
import { type Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
import HeartSolid from 'heroicons/24/solid/heart.svg';
|
||||||
|
|
||||||
|
import useConfig from '@/clients/useConfig';
|
||||||
|
import useProfile from '@/clients/useProfile';
|
||||||
|
import useEvent from '@/clients/useEvent';
|
||||||
|
import TextNote from '../TextNote';
|
||||||
|
|
||||||
|
type ReactionProps = {
|
||||||
|
event: NostrEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Reaction: Component<ReactionProps> = (props) => {
|
||||||
|
const [config] = useConfig();
|
||||||
|
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
|
||||||
|
|
||||||
|
const { profile } = useProfile(() => ({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
pubkey: props.event.pubkey,
|
||||||
|
}));
|
||||||
|
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="flex gap-1 text-sm">
|
||||||
|
<div>
|
||||||
|
<Switch fallback={props.event.content}>
|
||||||
|
<Match when={props.event.content === '+'}>
|
||||||
|
<span class="inline-block h-4 w-4 text-rose-400">
|
||||||
|
<HeartSolid />
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-bold">{profile()?.display_name}</span>
|
||||||
|
{' reacted'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Show when={event() != null} fallback={'loading'}>
|
||||||
|
<TextNote event={event()} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Reaction;
|
||||||
@@ -5,7 +5,7 @@ export type MentionedEventDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
|
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
|
||||||
return <span class="text-blue-500 underline">@{props.mentionedEvent.eventId}</span>;
|
return <span class="text-blue-500 underline">#{props.mentionedEvent.eventId}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MentionedEventDisplay;
|
export default MentionedEventDisplay;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
|||||||
|
|
||||||
export type TextNoteContentDisplayProps = {
|
export type TextNoteContentDisplayProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
embedding: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||||
@@ -19,7 +20,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
|||||||
if (item.type === 'MentionedUser') {
|
if (item.type === 'MentionedUser') {
|
||||||
return <MentionedUserDisplay mentionedUser={item} />;
|
return <MentionedUserDisplay mentionedUser={item} />;
|
||||||
}
|
}
|
||||||
if (item.type === 'MentionedEvent') {
|
if (item.type === 'MentionedEvent' && props.embedding) {
|
||||||
return <MentionedEventDisplay mentionedEvent={item} />;
|
return <MentionedEventDisplay mentionedEvent={item} />;
|
||||||
}
|
}
|
||||||
if (item.type === 'HashTag') {
|
if (item.type === 'HashTag') {
|
||||||
|
|||||||
57
src/core/event.ts
Normal file
57
src/core/event.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
|
type EventMarker = 'reply' | 'root' | 'mention';
|
||||||
|
type TaggedEvent = {
|
||||||
|
id: string;
|
||||||
|
relayUrl?: string;
|
||||||
|
marker: EventMarker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventWrapper = (event: NostrEvent) => {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* "replyingTo"
|
||||||
|
*/
|
||||||
|
taggedUsers(): string[] {
|
||||||
|
const pubkeys = new Set<string>();
|
||||||
|
event.tags.forEach(([tagName, pubkey]) => {
|
||||||
|
if (tagName === 'p') {
|
||||||
|
pubkeys.add(pubkey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(pubkeys);
|
||||||
|
},
|
||||||
|
taggedEvents(): TaggedEvent[] {
|
||||||
|
const events = event.tags.filter(([tagName]) => tagName === 'e');
|
||||||
|
|
||||||
|
const positionToMarker = (index: number): EventMarker => {
|
||||||
|
// One "e" tag
|
||||||
|
if (events.length === 1) return 'reply';
|
||||||
|
// Two "e" tags or many "e" tags : first tag is root
|
||||||
|
if (index === 0) return 'root';
|
||||||
|
// Two "e" tags
|
||||||
|
if (events.length === 2) return 'reply';
|
||||||
|
// Many "e" tags
|
||||||
|
// Last one is reply.
|
||||||
|
if (index === events.length - 1) return 'reply';
|
||||||
|
// other ones are mentions.
|
||||||
|
return 'mention';
|
||||||
|
};
|
||||||
|
|
||||||
|
return events.map(([, eventId, relayUrl, marker], index) => ({
|
||||||
|
id: eventId,
|
||||||
|
relayUrl,
|
||||||
|
marker: (marker as EventMarker) ?? positionToMarker(index),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
replyingToEvent(): TaggedEvent | undefined {
|
||||||
|
return this.taggedEvents().find(({ marker }) => marker === 'reply');
|
||||||
|
},
|
||||||
|
rootEvent(): TaggedEvent | undefined {
|
||||||
|
return this.taggedEvents().find(({ marker }) => marker === 'root');
|
||||||
|
},
|
||||||
|
mentionedEvents(): TaggedEvent[] {
|
||||||
|
return this.taggedEvents().filter(({ marker }) => marker === 'mention');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -13,10 +13,18 @@ export type MessageChannelRequest<T> = {
|
|||||||
message: T;
|
message: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Primitives = number | string | null;
|
// https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||||
type Serializable = Record<string, Primitives | Array<Primitives>>;
|
type Clonable =
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| bigint
|
||||||
|
| Date
|
||||||
|
| Array<Clonable>
|
||||||
|
| Record<string, Clonable>;
|
||||||
|
|
||||||
const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessageChannelProps) => {
|
const useMessageChannel = <T extends Clonable>(propsProvider: () => UseMessageChannelProps) => {
|
||||||
const channel = () => channels()[propsProvider().id];
|
const channel = () => channels()[propsProvider().id];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -29,7 +37,7 @@ const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessa
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const listen = async (requestId: string, timeout = 1000): Promise<T> => {
|
const listen = async (requestId: string, timeoutMs = 1000): Promise<T> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const listener = (event: MessageEvent) => {
|
const listener = (event: MessageEvent) => {
|
||||||
if (event.origin !== window.location.origin) return;
|
if (event.origin !== window.location.origin) return;
|
||||||
@@ -44,11 +52,12 @@ const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessa
|
|||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error('Timeout'));
|
|
||||||
channel().port2.removeEventListener('message', listener);
|
channel().port2.removeEventListener('message', listener);
|
||||||
}, timeout);
|
reject(new Error('TimeoutError'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
window.addEventListener('message', listener, false);
|
channel().port2.addEventListener('message', listener, false);
|
||||||
|
channel().port2.start();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +65,7 @@ const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessa
|
|||||||
async requst(message: T) {
|
async requst(message: T) {
|
||||||
const requestId = Math.random().toString();
|
const requestId = Math.random().toString();
|
||||||
const messageStr = JSON.stringify({ message, requestId });
|
const messageStr = JSON.stringify({ message, requestId });
|
||||||
const response = listen(requestId, timeout);
|
const response = listen(requestId, timeoutMs);
|
||||||
channel().postMessage(messageStr);
|
channel().postMessage(messageStr);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import type { Component } from 'solid-js';
|
|
||||||
import { getEventHash, relayInit } from 'nostr-tools';
|
|
||||||
|
|
||||||
const relays = [
|
|
||||||
'wss://brb.io',
|
|
||||||
'wss://nostr.h3z.jp',
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://relay.snort.social',
|
|
||||||
'wss://relay.nostr.wirednet.jp',
|
|
||||||
'wss://relay-jp.nostr.wirednet.jp',
|
|
||||||
'wss://nos.lol',
|
|
||||||
'wss://eden.nostr.land',
|
|
||||||
'wss://nostr-pub.wellorder.net',
|
|
||||||
'wss://nostr.bitcoiner.social',
|
|
||||||
'wss://offchain.pub',
|
|
||||||
'wss://relay.current.fyi',
|
|
||||||
'wss://nostr.relayer.se',
|
|
||||||
'wss://relay.realsearch.cc',
|
|
||||||
'wss://jiggytom.ddns.net',
|
|
||||||
// 'wss://nostr.fly.dev',
|
|
||||||
// 'wss://nostr-relay.untethr.me',
|
|
||||||
];
|
|
||||||
|
|
||||||
/*
|
|
||||||
*{
|
|
||||||
"event": {
|
|
||||||
"kind": 0,
|
|
||||||
"content": "{\"name\":\"syusui_s\",\"about\":\"多分復活\",\"picture\":\"https://i.gyazo.com/883119a7763e594d30c5706a62969d52.jpg\",\"display_name\":\"しゅうすい\",\"nip05\":\"_@syusui-s.github.io\"}",
|
|
||||||
"tags": [],
|
|
||||||
"created_at": 1676255623,
|
|
||||||
"pubkey": "96203d66276e3214ea93b6c78a577c3c9a7279f9ee7e51b22f3b8c17643a819c",
|
|
||||||
"id": "8776d66d9de4c59abddb0eb83214247edd68b5bc61fa3657b134cf892f8f7610"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const recover = async () => {
|
|
||||||
const event = {
|
|
||||||
kind: 0,
|
|
||||||
content:
|
|
||||||
'{"name":"syusui_s","about":"","display_name":"しゅうすい","picture":"https://i.gyazo.com/883119a7763e594d30c5706a62969d52.jpg","nip05":"_@syusui-s.github.io"}',
|
|
||||||
tags: [],
|
|
||||||
created_at: Math.floor(new Date() / 1000),
|
|
||||||
pubkey: '96203d66276e3214ea93b6c78a577c3c9a7279f9ee7e51b22f3b8c17643a819c',
|
|
||||||
};
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
|
||||||
|
|
||||||
console.log(signedEvent);
|
|
||||||
|
|
||||||
for (const url of relays) {
|
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
const relay = relayInit(url);
|
|
||||||
await relay.connect();
|
|
||||||
|
|
||||||
const pub = relay.publish(signedEvent);
|
|
||||||
|
|
||||||
pub.on('ok', () => {
|
|
||||||
console.log(`${url} has accepted our event`);
|
|
||||||
});
|
|
||||||
pub.on('seen', () => {
|
|
||||||
console.log(`we saw the event on ${url}`);
|
|
||||||
});
|
|
||||||
pub.on('failed', (reason) => {
|
|
||||||
console.log(`failed to publish to ${url}: ${reason}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('done');
|
|
||||||
};
|
|
||||||
|
|
||||||
const AccountRecovery: Component = () => {
|
|
||||||
const handleClick = () => {
|
|
||||||
recover();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={handleClick}>回復</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AccountRecovery;
|
|
||||||
@@ -5,6 +5,7 @@ import Column from '@/components/Column';
|
|||||||
import NotePostForm from '@/components/NotePostForm';
|
import NotePostForm from '@/components/NotePostForm';
|
||||||
import SideBar from '@/components/SideBar';
|
import SideBar from '@/components/SideBar';
|
||||||
import Timeline from '@/components/Timeline';
|
import Timeline from '@/components/Timeline';
|
||||||
|
import Notification from '@/components/Notification';
|
||||||
import TextNote from '@/components/TextNote';
|
import TextNote from '@/components/TextNote';
|
||||||
import useCommands from '@/clients/useCommands';
|
import useCommands from '@/clients/useCommands';
|
||||||
import useConfig from '@/clients/useConfig';
|
import useConfig from '@/clients/useConfig';
|
||||||
@@ -62,6 +63,29 @@ const Home: Component = () => {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { events: notifications } = useSubscription(() => ({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
kinds: [1, 6, 7],
|
||||||
|
'#p': [pubkeyHex],
|
||||||
|
limit: 25,
|
||||||
|
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { events: localTimeline } = useSubscription(() => ({
|
||||||
|
relayUrls: ['wss://relay-jp.nostr.wirednet.jp', 'wss://nostr.h3z.jp/'],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
kinds: [1, 6],
|
||||||
|
limit: 25,
|
||||||
|
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
const { events: searchPosts } = useSubscription(() => ({
|
const { events: searchPosts } = useSubscription(() => ({
|
||||||
relayUrls: ['wss://relay.nostr.band/'],
|
relayUrls: ['wss://relay.nostr.band/'],
|
||||||
filters: [
|
filters: [
|
||||||
@@ -108,6 +132,12 @@ const Home: Component = () => {
|
|||||||
/>
|
/>
|
||||||
<Timeline events={followingsPosts()} />
|
<Timeline events={followingsPosts()} />
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column name="通知" width="medium">
|
||||||
|
<Notification events={notifications()} />
|
||||||
|
</Column>
|
||||||
|
<Column name="ローカル" width="medium">
|
||||||
|
<Timeline events={localTimeline()} />
|
||||||
|
</Column>
|
||||||
<Column name="自分の投稿" width="medium">
|
<Column name="自分の投稿" width="medium">
|
||||||
<Timeline events={myPosts()} />
|
<Timeline events={myPosts()} />
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module.exports = {
|
|||||||
colors: {
|
colors: {
|
||||||
// a color for primary actions like a submit button.
|
// a color for primary actions like a submit button.
|
||||||
primary: colors.rose['300'],
|
primary: colors.rose['300'],
|
||||||
|
'primary-disabled': colors.rose['200'],
|
||||||
'sidebar-bg': colors.rose['100'],
|
'sidebar-bg': colors.rose['100'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"],
|
||||||
|
"*": ["types/*"]
|
||||||
},
|
},
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
|
|||||||
27
types/global.d.ts
vendored
Normal file
27
types/global.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// The original code was published under the public domain license (CC0-1.0).
|
||||||
|
// https://gist.github.com/syusui-s/cd5482ddfc83792b54a756759acbda55
|
||||||
|
import { type Event as NostrEvent } from 'nostr-tools/event';
|
||||||
|
|
||||||
|
type NostrAPI = {
|
||||||
|
/** returns a public key as hex */
|
||||||
|
getPublicKey(): Promise<string>;
|
||||||
|
/** takes an event object, adds `id`, `pubkey` and `sig` and returns it */
|
||||||
|
signEvent(event: Event): Promise<NostrEvent>;
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
|
||||||
|
/** returns a basic map of relay urls to relay policies */
|
||||||
|
getRelays?(): Promise<{ [url: string]: { read: boolean; write: boolean } }>;
|
||||||
|
|
||||||
|
/** NIP-04: Encrypted Direct Messages */
|
||||||
|
nip04: {
|
||||||
|
/** returns ciphertext and iv as specified in nip-04 */
|
||||||
|
encrypt(pubkey: string, plaintext: string): Promise<string>;
|
||||||
|
/** takes ciphertext and iv as specified in nip-04 */
|
||||||
|
decrypt(pubkey: string, ciphertext: string): Promise<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
nostr?: NostrAPI;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user