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/setup-node@v3
|
||||
with:
|
||||
node-version: 17
|
||||
node-version: 19
|
||||
- name: Get npm cache directory
|
||||
id: npm-cache-dir
|
||||
run: |
|
||||
|
||||
31
README.md
31
README.md
@@ -8,4 +8,35 @@ A nostr client like TweetDeck powered by SolidJS.
|
||||
|
||||
## LICENSE
|
||||
|
||||
Copyright (C) 2023 Shusui Moyatani
|
||||
|
||||
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/router": "^0.6.0",
|
||||
"@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",
|
||||
"heroicons": "^2.0.15",
|
||||
"nostr-tools": "^1.3.2",
|
||||
@@ -1348,20 +1350,86 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "4.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
|
||||
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w==",
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||
"integrity": "sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query": {
|
||||
"version": "4.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
|
||||
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
|
||||
"node_modules/@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==",
|
||||
"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": {
|
||||
"type": "github",
|
||||
@@ -4884,8 +4952,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -5423,6 +5490,18 @@
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -6905,6 +6984,18 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -8028,6 +8119,15 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -9338,16 +9438,50 @@
|
||||
}
|
||||
},
|
||||
"@tanstack/query-core": {
|
||||
"version": "4.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
|
||||
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w=="
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||
"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": {
|
||||
"version": "4.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
|
||||
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz",
|
||||
"integrity": "sha512-pwP5vhfcDkwToxRFCXmIr9xFVOGPjanmgrZ5mUge5JE4xAy90lRV4KF36H6QOu0sZ4qwKgh9JcLrVtIcJP1E1g==",
|
||||
"requires": {
|
||||
"@tanstack/query-core": "4.24.6"
|
||||
"@tanstack/query-core": "4.24.10"
|
||||
}
|
||||
},
|
||||
"@thisbeyond/solid-dnd": {
|
||||
@@ -11877,8 +12011,7 @@
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -13367,6 +13509,15 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -14217,6 +14368,13 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
"@solidjs/meta": "^0.28.2",
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"@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",
|
||||
"heroicons": "^2.0.15",
|
||||
"nostr-tools": "^1.3.2",
|
||||
|
||||
17
src/App.tsx
17
src/App.tsx
@@ -1,19 +1,26 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { Routes, Route } from '@solidjs/router';
|
||||
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 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 = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/recovery" element={<AccountRecovery />} />
|
||||
<Route path="/*" element={<NotFound />} />
|
||||
<Route path="/" element={() => <Home />} />
|
||||
<Route path="/*" element={() => <NotFound />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,8 @@ const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => {
|
||||
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';
|
||||
|
||||
const currentDate = (): number => Math.floor(Date.now() / 1000);
|
||||
|
||||
// NIP-20: Command Result
|
||||
const waitCommandResult = (pub: Pub): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -48,11 +50,10 @@ const useCommands = () => {
|
||||
const preSignedEvent: NostrEvent = {
|
||||
kind: 1,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
created_at: currentDate(),
|
||||
tags: [],
|
||||
content,
|
||||
};
|
||||
// TODO define window.nostr
|
||||
return publishEvent(relayUrls, preSignedEvent);
|
||||
},
|
||||
// NIP-25
|
||||
@@ -73,14 +74,40 @@ const useCommands = () => {
|
||||
const preSignedEvent: NostrEvent = {
|
||||
kind: 7,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
created_at: currentDate(),
|
||||
tags: [
|
||||
['e', eventId],
|
||||
['p', notifyPubkey],
|
||||
],
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { SimplePool } from 'nostr-tools';
|
||||
|
||||
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[]) =>
|
||||
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 [events, setEvents] = createSignal<NostrEvent[]>([]);
|
||||
|
||||
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);
|
||||
let pushed = false;
|
||||
let eose = false;
|
||||
const storedEvents: NostrEvent[] = [];
|
||||
|
||||
sub.on('event', (event: NostrEvent) => {
|
||||
if (!eose) {
|
||||
pushed = true;
|
||||
storedEvents.push(event);
|
||||
} else {
|
||||
setEvents((prevEvents) => sortEvents([event, ...prevEvents]));
|
||||
@@ -43,7 +48,10 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps) => {
|
||||
clearInterval(intervalId);
|
||||
return;
|
||||
}
|
||||
if (pushed) {
|
||||
pushed = false;
|
||||
setEvents(sortEvents(storedEvents));
|
||||
}
|
||||
}, 100);
|
||||
|
||||
onCleanup(() => {
|
||||
|
||||
@@ -18,10 +18,6 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
||||
const pubkey = () => props.event.pubkey;
|
||||
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 { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
|
||||
|
||||
@@ -33,7 +29,7 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
||||
</div>
|
||||
<div>{profile()?.display_name} Reposted</div>
|
||||
</div>
|
||||
<Show when={event() != null}>
|
||||
<Show when={event() != null} fallback={'loading'}>
|
||||
<TextNote event={event()} />
|
||||
</Show>
|
||||
</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';
|
||||
|
||||
type NotePostFormProps = {
|
||||
@@ -8,11 +8,18 @@ type NotePostFormProps = {
|
||||
const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
const [text, setText] = createSignal<string>('');
|
||||
|
||||
const handleChangeText: JSX.EventHandler<HTMLTextAreaElement, Event> = (ev) => {
|
||||
setText(ev.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
|
||||
ev.preventDefault();
|
||||
props.onPost({ content: text() });
|
||||
// TODO 投稿完了したらなんかする
|
||||
};
|
||||
|
||||
const submitDisabled = createMemo(() => text().trim().length === 0);
|
||||
|
||||
return (
|
||||
<div class="p-1">
|
||||
<form class="grid w-64 gap-1" onSubmit={handleSubmit}>
|
||||
@@ -21,13 +28,16 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
class="rounded border-none"
|
||||
rows={4}
|
||||
placeholder="いまどうしてる?"
|
||||
onChange={(ev) => {
|
||||
setText(ev.target.value);
|
||||
}}
|
||||
onInput={handleChangeText}
|
||||
value={text()}
|
||||
/>
|
||||
<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 />
|
||||
</button>
|
||||
</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 type { Component } from 'solid-js';
|
||||
import {
|
||||
Show,
|
||||
For,
|
||||
createSignal,
|
||||
createMemo,
|
||||
onMount,
|
||||
onCleanup,
|
||||
type JSX,
|
||||
Component,
|
||||
} from 'solid-js';
|
||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||
|
||||
import HeartOutlined from 'heroicons/24/outline/heart.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 useConfig from '@/clients/useConfig';
|
||||
import useCommands from '@/clients/useCommands';
|
||||
import useDatePulser from '@/hooks/useDatePulser';
|
||||
import { formatRelative } from '@/utils/formatDate';
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
@@ -20,21 +31,37 @@ export type TextNoteProps = {
|
||||
const TextNote: Component<TextNoteProps> = (props) => {
|
||||
const currentDate = useDatePulser();
|
||||
const [config] = useConfig();
|
||||
const commands = useCommands();
|
||||
const { profile: author } = useProfile(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey: props.event.pubkey,
|
||||
}));
|
||||
|
||||
const replyingToPubKeys = createMemo(() =>
|
||||
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
||||
);
|
||||
// TODO 日付をいい感じにフォーマットする関数を作る
|
||||
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 (
|
||||
<div class="textnote">
|
||||
<ColumnItem>
|
||||
<div class="author-icon max-w-10 max-h-10 shrink-0">
|
||||
<Show when={author()?.picture} fallback={<div class="h-10 w-10" />}>
|
||||
<div class="author-icon h-10 w-10 shrink-0">
|
||||
<Show when={author()?.picture}>
|
||||
<img
|
||||
src={author()?.picture}
|
||||
alt="icon"
|
||||
@@ -47,7 +74,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<div class="author flex min-w-0 truncate">
|
||||
{/* 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>
|
||||
</Show>
|
||||
<div class="author-username truncate text-zinc-600">
|
||||
@@ -71,15 +98,21 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
<div class="content whitespace-pre-wrap break-all">
|
||||
<TextNoteContentDisplay event={props.event} />
|
||||
<TextNoteContentDisplay event={props.event} embedding={true} />
|
||||
</div>
|
||||
<div class="flex justify-evenly">
|
||||
<div class="flex justify-end gap-16">
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<ChatBubbleLeft />
|
||||
</button>
|
||||
<button class="h-4 w-4 text-zinc-400" onClick={handleRepost}>
|
||||
<ArrowPathRoundedSquare />
|
||||
</button>
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<button class="h-4 w-4 text-zinc-400" onClick={handleReaction}>
|
||||
<HeartOutlined />
|
||||
</button>
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ColumnItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 DeprecatedRepost from '@/components/DeprecatedRepost';
|
||||
@@ -8,15 +8,15 @@ export type TimelineProps = {
|
||||
events: NostrEvent[];
|
||||
};
|
||||
|
||||
export const Timeline: Component<TimelineProps> = (props) => {
|
||||
const Timeline: Component<TimelineProps> = (props) => {
|
||||
return (
|
||||
<For each={props.events}>
|
||||
{(event) => (
|
||||
<Switch fallback={<div>unknown event</div>}>
|
||||
<Match when={event.kind === 1}>
|
||||
<Match when={event.kind === Kind.Text}>
|
||||
<TextNote event={event} />
|
||||
</Match>
|
||||
<Match when={event.kind === 6}>
|
||||
<Match when={(event.kind as number) === 6}>
|
||||
<DeprecatedRepost event={event} />
|
||||
</Match>
|
||||
</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) => {
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,7 @@ import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
|
||||
|
||||
export type TextNoteContentDisplayProps = {
|
||||
event: NostrEvent;
|
||||
embedding: boolean;
|
||||
};
|
||||
|
||||
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
@@ -19,7 +20,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (item.type === 'MentionedUser') {
|
||||
return <MentionedUserDisplay mentionedUser={item} />;
|
||||
}
|
||||
if (item.type === 'MentionedEvent') {
|
||||
if (item.type === 'MentionedEvent' && props.embedding) {
|
||||
return <MentionedEventDisplay mentionedEvent={item} />;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
type Primitives = number | string | null;
|
||||
type Serializable = Record<string, Primitives | Array<Primitives>>;
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
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];
|
||||
|
||||
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) => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
@@ -44,11 +52,12 @@ const useMessageChannel = <T extends Serializable>(propsProvider: () => UseMessa
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout'));
|
||||
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) {
|
||||
const requestId = Math.random().toString();
|
||||
const messageStr = JSON.stringify({ message, requestId });
|
||||
const response = listen(requestId, timeout);
|
||||
const response = listen(requestId, timeoutMs);
|
||||
channel().postMessage(messageStr);
|
||||
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 SideBar from '@/components/SideBar';
|
||||
import Timeline from '@/components/Timeline';
|
||||
import Notification from '@/components/Notification';
|
||||
import TextNote from '@/components/TextNote';
|
||||
import useCommands from '@/clients/useCommands';
|
||||
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(() => ({
|
||||
relayUrls: ['wss://relay.nostr.band/'],
|
||||
filters: [
|
||||
@@ -108,6 +132,12 @@ const Home: Component = () => {
|
||||
/>
|
||||
<Timeline events={followingsPosts()} />
|
||||
</Column>
|
||||
<Column name="通知" width="medium">
|
||||
<Notification events={notifications()} />
|
||||
</Column>
|
||||
<Column name="ローカル" width="medium">
|
||||
<Timeline events={localTimeline()} />
|
||||
</Column>
|
||||
<Column name="自分の投稿" width="medium">
|
||||
<Timeline events={myPosts()} />
|
||||
</Column>
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = {
|
||||
colors: {
|
||||
// a color for primary actions like a submit button.
|
||||
primary: colors.rose['300'],
|
||||
'primary-disabled': colors.rose['200'],
|
||||
'sidebar-bg': colors.rose['100'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["src/*"],
|
||||
"*": ["types/*"]
|
||||
},
|
||||
"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