Merge pull request #90 from MutinyWallet/activity-design-update

Activity design update
This commit is contained in:
Paul Miller
2023-05-11 17:57:15 -05:00
committed by GitHub
42 changed files with 706 additions and 552 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "mws",
"version": "0.2.0",
"version": "0.3.0",
"license": "MIT",
"scripts": {
"dev": "solid-start dev",
@@ -10,7 +10,7 @@
},
"type": "module",
"devDependencies": {
"@types/node": "^18.16.6",
"@types/node": "^18.16.8",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"autoprefixer": "^10.4.14",
@@ -30,13 +30,13 @@
"workbox-window": "^6.5.4"
},
"dependencies": {
"@kobalte/core": "^0.8.2",
"@kobalte/core": "^0.9.6",
"@kobalte/tailwindcss": "^0.5.0",
"@modular-forms/solid": "^0.13.2",
"@mutinywallet/mutiny-wasm": "^0.3.0",
"@mutinywallet/mutiny-wasm": "^0.3.2",
"@mutinywallet/waila-wasm": "^0.1.5",
"@solid-primitives/upload": "^0.0.111",
"@solidjs/meta": "^0.28.4",
"@solidjs/meta": "^0.28.5",
"@solidjs/router": "^0.8.2",
"@thisbeyond/solid-select": "^0.14.0",
"class-variance-authority": "^0.4.0",
@@ -45,7 +45,7 @@
"solid-js": "^1.7.5",
"solid-qr-code": "^0.0.8",
"solid-start": "^0.2.26",
"undici": "^5.22.0"
"undici": "^5.22.1"
},
"engines": {
"node": ">=16.8"

238
pnpm-lock.yaml generated
View File

@@ -2,8 +2,8 @@ lockfileVersion: '6.0'
dependencies:
'@kobalte/core':
specifier: ^0.8.2
version: 0.8.2(solid-js@1.7.5)
specifier: ^0.9.6
version: 0.9.6(solid-js@1.7.5)
'@kobalte/tailwindcss':
specifier: ^0.5.0
version: 0.5.0(tailwindcss@3.3.2)
@@ -11,8 +11,8 @@ dependencies:
specifier: ^0.13.2
version: 0.13.2(solid-js@1.7.5)
'@mutinywallet/mutiny-wasm':
specifier: ^0.3.0
version: 0.3.0
specifier: ^0.3.2
version: 0.3.2
'@mutinywallet/waila-wasm':
specifier: ^0.1.5
version: 0.1.5
@@ -20,8 +20,8 @@ dependencies:
specifier: ^0.0.111
version: 0.0.111(solid-js@1.7.5)
'@solidjs/meta':
specifier: ^0.28.4
version: 0.28.4(solid-js@1.7.5)
specifier: ^0.28.5
version: 0.28.5(solid-js@1.7.5)
'@solidjs/router':
specifier: ^0.8.2
version: 0.8.2(solid-js@1.7.5)
@@ -45,15 +45,15 @@ dependencies:
version: 0.0.8(qr.js@0.0.0)(solid-js@1.7.5)
solid-start:
specifier: ^0.2.26
version: 0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
version: 0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
undici:
specifier: ^5.22.0
version: 5.22.0
specifier: ^5.22.1
version: 5.22.1
devDependencies:
'@types/node':
specifier: ^18.16.6
version: 18.16.6
specifier: ^18.16.8
version: 18.16.8
'@typescript-eslint/eslint-plugin':
specifier: ^5.59.5
version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@4.9.5)
@@ -86,7 +86,7 @@ devDependencies:
version: 8.4.23
solid-start-node:
specifier: ^0.2.26
version: 0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5)
version: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5)
tailwindcss:
specifier: ^3.3.2
version: 3.3.2
@@ -95,7 +95,7 @@ devDependencies:
version: 4.9.5
vite:
specifier: ^4.3.5
version: 4.3.5(@types/node@18.16.6)
version: 4.3.5(@types/node@18.16.8)
vite-plugin-pwa:
specifier: ^0.14.7
version: 0.14.7(vite@4.3.5)(workbox-build@6.5.4)(workbox-window@6.5.4)
@@ -1467,6 +1467,40 @@ packages:
'@floating-ui/core': 1.2.6
dev: false
/@formatjs/ecma402-abstract@1.15.0:
resolution: {integrity: sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==}
dependencies:
'@formatjs/intl-localematcher': 0.2.32
tslib: 2.5.0
dev: false
/@formatjs/fast-memoize@2.0.1:
resolution: {integrity: sha512-M2GgV+qJn5WJQAYewz7q2Cdl6fobQa69S1AzSM2y0P68ZDbK5cWrJIcPCO395Of1ksftGZoOt4LYCO/j9BKBSA==}
dependencies:
tslib: 2.5.0
dev: false
/@formatjs/icu-messageformat-parser@2.4.0:
resolution: {integrity: sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
'@formatjs/icu-skeleton-parser': 1.4.0
tslib: 2.5.0
dev: false
/@formatjs/icu-skeleton-parser@1.4.0:
resolution: {integrity: sha512-Qq347VM616rVLkvN6QsKJELazRyNlbCiN47LdH0Mc5U7E2xV0vatiVhGqd3KFgbc055BvtnUXR7XX60dCGFuWg==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
tslib: 2.5.0
dev: false
/@formatjs/intl-localematcher@0.2.32:
resolution: {integrity: sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==}
dependencies:
tslib: 2.5.0
dev: false
/@hapi/hoek@9.3.0:
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -1501,14 +1535,15 @@ packages:
'@swc/helpers': 0.4.14
dev: false
/@internationalized/number@3.2.0:
resolution: {integrity: sha512-GUXkhXSX1Ee2RURnzl+47uvbOxnlMnvP9Er+QePTjDjOPWuunmLKlEkYkEcLiiJp7y4l9QxGDLOlVr8m69LS5w==}
/@internationalized/message@3.1.0:
resolution: {integrity: sha512-Oo5m70FcBdADf7G8NkUffVSfuCdeAYVfsvNjZDi9ELpjvkc4YNJVTHt/NyTI9K7FgAVoELxiP9YmN0sJ+HNHYQ==}
dependencies:
'@swc/helpers': 0.4.14
intl-messageformat: 10.3.5
dev: false
/@internationalized/string@3.1.0:
resolution: {integrity: sha512-TJQKiyUb+wyAfKF59UNeZ/kELMnkxyecnyPCnBI1ma4NaXReJW+7Cc2mObXAqraIBJUVv7rgI46RLKrLgi35ng==}
/@internationalized/number@3.2.0:
resolution: {integrity: sha512-GUXkhXSX1Ee2RURnzl+47uvbOxnlMnvP9Er+QePTjDjOPWuunmLKlEkYkEcLiiJp7y4l9QxGDLOlVr8m69LS5w==}
dependencies:
'@swc/helpers': 0.4.14
dev: false
@@ -1547,16 +1582,16 @@ packages:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
/@kobalte/core@0.8.2(solid-js@1.7.5):
resolution: {integrity: sha512-EoBYKpYa3+Csr5Zh7l3aY3yAg7fk1O3ZM9lGyD1mdQ1FutTuwTkyj8z1CvSSj1Klb+rBL+X1N662Occ8Bmsi2w==}
/@kobalte/core@0.9.6(solid-js@1.7.5):
resolution: {integrity: sha512-nuo3+ncZHC2Fl531DdliLE/kRcmdMf2FflSTVqM0FqqgilbzIbdJCFXJddkZj4KtML9F4rHRiPq5reSXMMrFLg==}
peerDependencies:
solid-js: ^1.6.15
solid-js: ^1.7.3
dependencies:
'@floating-ui/dom': 1.2.7
'@internationalized/date': 3.2.0
'@internationalized/message': 3.1.0
'@internationalized/number': 3.2.0
'@internationalized/string': 3.1.0
'@kobalte/utils': 0.6.1(solid-js@1.7.5)
'@kobalte/utils': 0.7.2(solid-js@1.7.5)
solid-js: 1.7.5
dev: false
@@ -1568,17 +1603,17 @@ packages:
tailwindcss: 3.3.2
dev: false
/@kobalte/utils@0.6.1(solid-js@1.7.5):
resolution: {integrity: sha512-YvBqe9t9j0iYFUHfKXSMLQKM3s5+nL72RvT9b75W+IOxUpSpN4rdaI8C2j97k3LsEt7qY4ktJdt8lPM1rr8JXw==}
/@kobalte/utils@0.7.2(solid-js@1.7.5):
resolution: {integrity: sha512-ZdINbHemz+jnixJ63VFi9wUEHEMAsP7iDGEADciKdSKrK4bDuccDw5th1O+5/PykfHqFwSI++JhhUpOd+iZ5jg==}
peerDependencies:
solid-js: ^1.6.12
solid-js: ^1.7.3
dependencies:
'@solid-primitives/event-listener': 2.2.11(solid-js@1.7.5)
'@solid-primitives/keyed': 1.2.0(solid-js@1.7.5)
'@solid-primitives/media': 2.2.1(solid-js@1.7.5)
'@solid-primitives/props': 3.1.5(solid-js@1.7.5)
'@solid-primitives/refs': 1.0.3(solid-js@1.7.5)
'@solid-primitives/utils': 5.5.2(solid-js@1.7.5)
'@solid-primitives/utils': 6.1.1(solid-js@1.7.5)
solid-js: 1.7.5
dev: false
@@ -1590,8 +1625,8 @@ packages:
solid-js: 1.7.5
dev: false
/@mutinywallet/mutiny-wasm@0.3.0:
resolution: {integrity: sha512-K+u2u/XMX1269U8af3T/ZvS+SzzrQcVYrdMi420dWCa14gke0vPWbGp+01zN7SCqBL4jp929emHTUZ4YBEpkzQ==}
/@mutinywallet/mutiny-wasm@0.3.2:
resolution: {integrity: sha512-m0VyEmVJ6Gl3YiTYYZLegeHFFVW21S2khtFljRyKKtcm0T8FZwJi0w2gNBaLQTakl5mpXwBgjTQwLqFnKSuhuQ==}
dev: false
/@mutinywallet/waila-wasm@0.1.5:
@@ -1644,7 +1679,7 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/plugin-commonjs@24.1.0(rollup@3.21.5):
/@rollup/plugin-commonjs@24.1.0(rollup@3.21.6):
resolution: {integrity: sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
@@ -1653,15 +1688,15 @@ packages:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@rollup/pluginutils': 5.0.2(rollup@3.21.6)
commondir: 1.0.1
estree-walker: 2.0.2
glob: 8.1.0
is-reference: 1.2.1
magic-string: 0.27.0
rollup: 3.21.5
rollup: 3.21.6
/@rollup/plugin-json@6.0.0(rollup@3.21.5):
/@rollup/plugin-json@6.0.0(rollup@3.21.6):
resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==}
engines: {node: '>=14.0.0'}
peerDependencies:
@@ -1670,8 +1705,8 @@ packages:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
rollup: 3.21.5
'@rollup/pluginutils': 5.0.2(rollup@3.21.6)
rollup: 3.21.6
/@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1):
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
@@ -1688,7 +1723,7 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/plugin-node-resolve@15.0.2(rollup@3.21.5):
/@rollup/plugin-node-resolve@15.0.2(rollup@3.21.6):
resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==}
engines: {node: '>=14.0.0'}
peerDependencies:
@@ -1697,13 +1732,13 @@ packages:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@rollup/pluginutils': 5.0.2(rollup@3.21.6)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-builtin-module: 3.2.1
is-module: 1.0.0
resolve: 1.22.2
rollup: 3.21.5
rollup: 3.21.6
/@rollup/plugin-replace@2.4.2(rollup@2.79.1):
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
@@ -1715,7 +1750,7 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/plugin-replace@5.0.2(rollup@3.21.5):
/@rollup/plugin-replace@5.0.2(rollup@3.21.6):
resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==}
engines: {node: '>=14.0.0'}
peerDependencies:
@@ -1724,9 +1759,9 @@ packages:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@rollup/pluginutils': 5.0.2(rollup@3.21.6)
magic-string: 0.27.0
rollup: 3.21.5
rollup: 3.21.6
dev: true
/@rollup/pluginutils@3.1.0(rollup@2.79.1):
@@ -1741,7 +1776,7 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/pluginutils@5.0.2(rollup@3.21.5):
/@rollup/pluginutils@5.0.2(rollup@3.21.6):
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
@@ -1753,7 +1788,7 @@ packages:
'@types/estree': 1.0.1
estree-walker: 2.0.2
picomatch: 2.3.1
rollup: 3.21.5
rollup: 3.21.6
/@scure/base@1.1.1:
resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
@@ -1859,14 +1894,6 @@ packages:
solid-js: 1.7.5
dev: false
/@solid-primitives/utils@5.5.2(solid-js@1.7.5):
resolution: {integrity: sha512-L52ig3eHKU6CqbPCKJIb4lweBuINHBOERcE1duApyKozEN8+zCqEKwD1Qo9ljKeEzJTBGWClxNpwEiNTUWTGvg==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.7.5
dev: false
/@solid-primitives/utils@6.1.1(solid-js@1.7.5):
resolution: {integrity: sha512-wxxUdxja126jTROs9Ro8Z5ExbHs9rv2Tl744S3Qmzki/gTcTXW8D1TvTArQcjqkCvSw8OIQ2EO2NI8sR28Trxg==}
peerDependencies:
@@ -1875,8 +1902,8 @@ packages:
solid-js: 1.7.5
dev: false
/@solidjs/meta@0.28.4(solid-js@1.7.5):
resolution: {integrity: sha512-1USElsQuGVcJnmZ6CxPfUVmKvCsVdBQoGrUyMxLtFw36Ytt90dPs/qLyXLvPR/ZPD16/qauWqg6APEkbrDOLcA==}
/@solidjs/meta@0.28.5(solid-js@1.7.5):
resolution: {integrity: sha512-52luJR6hVNMA1K8Od5OD0d8WVz/svqZG4is8lrDimiUGxdia3DzuLF+pK56dnEzbNt9cA42qVFL134U9LkC9Gg==}
peerDependencies:
solid-js: '>=1.4.0'
dependencies:
@@ -1955,8 +1982,8 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/node@18.16.6:
resolution: {integrity: sha512-N7KINmeB8IN3vRR8dhgHEp+YpWvGFcpDoh5XZ8jB5a00AdFKCKEyyGTOPTddUf4JqU1ZKTVxkOxakDvchNVI2Q==}
/@types/node@18.16.8:
resolution: {integrity: sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA==}
/@types/offscreencanvas@2019.7.0:
resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==}
@@ -1965,7 +1992,7 @@ packages:
/@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 18.16.6
'@types/node': 18.16.8
dev: true
/@types/resolve@1.20.2:
@@ -2343,7 +2370,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001486
electron-to-chromium: 1.4.387
electron-to-chromium: 1.4.392
node-releases: 2.0.10
update-browserslist-db: 1.0.11(browserslist@4.21.5)
@@ -2626,8 +2653,8 @@ packages:
jake: 10.8.5
dev: true
/electron-to-chromium@1.4.387:
resolution: {integrity: sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==}
/electron-to-chromium@1.4.392:
resolution: {integrity: sha512-TXQOMW9tnhIms3jGy/lJctLjICOgyueZFJ1KUtm6DTQ+QpxX3p7ZBwB6syuZ9KBuT5S4XX7bgY1ECPgfxKUdOg==}
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3572,6 +3599,15 @@ packages:
side-channel: 1.0.4
dev: true
/intl-messageformat@10.3.5:
resolution: {integrity: sha512-6kPkftF8Jg3XJCkGKa5OD+nYQ+qcSxF4ZkuDdXZ6KGG0VXn+iblJqRFyDdm9VvKcMyC0Km2+JlVQffFM52D0YA==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
'@formatjs/fast-memoize': 2.0.1
'@formatjs/icu-messageformat-parser': 2.4.0
tslib: 2.5.0
dev: false
/is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
@@ -3767,7 +3803,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 18.16.6
'@types/node': 18.16.8
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@@ -4397,10 +4433,10 @@ packages:
jest-worker: 26.6.2
rollup: 2.79.1
serialize-javascript: 4.0.0
terser: 5.17.2
terser: 5.17.3
dev: true
/rollup-plugin-visualizer@5.9.0(rollup@3.21.5):
/rollup-plugin-visualizer@5.9.0(rollup@3.21.6):
resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==}
engines: {node: '>=14'}
hasBin: true
@@ -4412,17 +4448,17 @@ packages:
dependencies:
open: 8.4.2
picomatch: 2.3.1
rollup: 3.21.5
rollup: 3.21.6
source-map: 0.7.4
yargs: 17.7.2
/rollup-route-manifest@1.0.0(rollup@3.21.5):
/rollup-route-manifest@1.0.0(rollup@3.21.6):
resolution: {integrity: sha512-3CmcMmCLAzJDUXiO3z6386/Pt8/k9xTZv8gIHyXI8hYGoAInnYdOsFXiGGzQRMy6TXR1jUZme2qbdwjH2nFMjg==}
engines: {node: '>=8'}
peerDependencies:
rollup: '>=2.0.0'
dependencies:
rollup: 3.21.5
rollup: 3.21.6
route-sort: 1.0.0
/rollup@2.79.1:
@@ -4433,8 +4469,8 @@ packages:
fsevents: 2.3.2
dev: true
/rollup@3.21.5:
resolution: {integrity: sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==}
/rollup@3.21.6:
resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
@@ -4560,28 +4596,28 @@ packages:
'@babel/types': 7.21.5
solid-js: 1.7.5
/solid-start-node@0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5):
/solid-start-node@0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5):
resolution: {integrity: sha512-8vciTGoQV+lIlCUSVHJPazlaoKDRfBowDkPeBr/EZdmtbcMOKoJYf/APPQWFspylF+nhzunMf0+zJP90VtMEYg==}
peerDependencies:
solid-start: '*'
undici: ^5.8.0
vite: '*'
dependencies:
'@rollup/plugin-commonjs': 24.1.0(rollup@3.21.5)
'@rollup/plugin-json': 6.0.0(rollup@3.21.5)
'@rollup/plugin-node-resolve': 15.0.2(rollup@3.21.5)
'@rollup/plugin-commonjs': 24.1.0(rollup@3.21.6)
'@rollup/plugin-json': 6.0.0(rollup@3.21.6)
'@rollup/plugin-node-resolve': 15.0.2(rollup@3.21.6)
compression: 1.7.4
polka: 1.0.0-next.22
rollup: 3.21.5
rollup: 3.21.6
sirv: 2.0.3
solid-start: 0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
terser: 5.17.2
undici: 5.22.0
vite: 4.3.5(@types/node@18.16.6)
solid-start: 0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
terser: 5.17.3
undici: 5.22.1
vite: 4.3.5(@types/node@18.16.8)
transitivePeerDependencies:
- supports-color
/solid-start@0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5):
/solid-start@0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5):
resolution: {integrity: sha512-kne2HZlnSMzsirdnvNs1CsDqBl0L0uvKKt1t4de1CH7JIngyqoMcER97jTE0Ejr84KknANaKAdvJAzZcL7Ueng==}
hasBin: true
peerDependencies:
@@ -4621,7 +4657,7 @@ packages:
'@babel/preset-env': 7.21.5(@babel/core@7.21.8)
'@babel/preset-typescript': 7.21.5(@babel/core@7.21.8)
'@babel/template': 7.20.7
'@solidjs/meta': 0.28.4(solid-js@1.7.5)
'@solidjs/meta': 0.28.5(solid-js@1.7.5)
'@solidjs/router': 0.8.2(solid-js@1.7.5)
'@types/cookie': 0.5.1
chokidar: 3.5.3
@@ -4637,18 +4673,18 @@ packages:
get-port: 6.1.2
parse-multipart-data: 1.5.0
picocolors: 1.0.0
rollup: 3.21.5
rollup-plugin-visualizer: 5.9.0(rollup@3.21.5)
rollup-route-manifest: 1.0.0(rollup@3.21.5)
rollup: 3.21.6
rollup-plugin-visualizer: 5.9.0(rollup@3.21.6)
rollup-route-manifest: 1.0.0(rollup@3.21.6)
sade: 1.8.1
set-cookie-parser: 2.6.0
sirv: 2.0.3
solid-js: 1.7.5
solid-start-node: 0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5)
terser: 5.17.2
undici: 5.22.0
vite: 4.3.5(@types/node@18.16.6)
vite-plugin-inspect: 0.7.26(rollup@3.21.5)(vite@4.3.5)
solid-start-node: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5)
terser: 5.17.3
undici: 5.22.1
vite: 4.3.5(@types/node@18.16.8)
vite-plugin-inspect: 0.7.26(rollup@3.21.6)(vite@4.3.5)
vite-plugin-solid: 2.7.0(solid-js@1.7.5)(vite@4.3.5)
wait-on: 6.0.1(debug@4.3.4)
transitivePeerDependencies:
@@ -4850,8 +4886,8 @@ packages:
unique-string: 2.0.0
dev: true
/terser@5.17.2:
resolution: {integrity: sha512-1D1aGbOF1Mnayq5PvfMc0amAR1y5Z1nrZaGCvI5xsdEfZEVte8okonk02OiaK5fw5hG1GWuuVsakOnpZW8y25A==}
/terser@5.17.3:
resolution: {integrity: sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==}
engines: {node: '>=10'}
hasBin: true
dependencies:
@@ -4969,8 +5005,8 @@ packages:
which-boxed-primitive: 1.0.2
dev: true
/undici@5.22.0:
resolution: {integrity: sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==}
/undici@5.22.1:
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
engines: {node: '>=14.0'}
dependencies:
busboy: 1.6.0
@@ -5044,19 +5080,19 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
/vite-plugin-inspect@0.7.26(rollup@3.21.5)(vite@4.3.5):
/vite-plugin-inspect@0.7.26(rollup@3.21.6)(vite@4.3.5):
resolution: {integrity: sha512-gRjBay+OxLr/Dr+HXlfJVXZH0cqhE5hkkBvo2du2cA1LGUBnV8Aym89AdPrURkSpTk3Rvw9dNWM2VLIuw6RKJg==}
engines: {node: '>=14'}
peerDependencies:
vite: ^3.1.0 || ^4.0.0
dependencies:
'@antfu/utils': 0.7.2
'@rollup/pluginutils': 5.0.2(rollup@3.21.5)
'@rollup/pluginutils': 5.0.2(rollup@3.21.6)
debug: 4.3.4
fs-extra: 11.1.1
picocolors: 1.0.0
sirv: 2.0.3
vite: 4.3.5(@types/node@18.16.6)
vite: 4.3.5(@types/node@18.16.8)
transitivePeerDependencies:
- rollup
- supports-color
@@ -5068,12 +5104,12 @@ packages:
workbox-build: ^6.5.4
workbox-window: ^6.5.4
dependencies:
'@rollup/plugin-replace': 5.0.2(rollup@3.21.5)
'@rollup/plugin-replace': 5.0.2(rollup@3.21.6)
debug: 4.3.4
fast-glob: 3.2.12
pretty-bytes: 6.1.0
rollup: 3.21.5
vite: 4.3.5(@types/node@18.16.6)
rollup: 3.21.6
vite: 4.3.5(@types/node@18.16.8)
workbox-build: 6.5.4
workbox-window: 6.5.4
transitivePeerDependencies:
@@ -5093,7 +5129,7 @@ packages:
merge-anything: 5.1.6
solid-js: 1.7.5
solid-refresh: 0.5.2(solid-js@1.7.5)
vite: 4.3.5(@types/node@18.16.6)
vite: 4.3.5(@types/node@18.16.8)
vitefu: 0.2.4(vite@4.3.5)
transitivePeerDependencies:
- supports-color
@@ -5103,10 +5139,10 @@ packages:
peerDependencies:
vite: ^2 || ^3 || ^4
dependencies:
vite: 4.3.5(@types/node@18.16.6)
vite: 4.3.5(@types/node@18.16.8)
dev: true
/vite@4.3.5(@types/node@18.16.6):
/vite@4.3.5(@types/node@18.16.8):
resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -5131,10 +5167,10 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 18.16.6
'@types/node': 18.16.8
esbuild: 0.17.18
postcss: 8.4.23
rollup: 3.21.5
rollup: 3.21.6
optionalDependencies:
fsevents: 2.3.2
@@ -5146,7 +5182,7 @@ packages:
vite:
optional: true
dependencies:
vite: 4.3.5(@types/node@18.16.6)
vite: 4.3.5(@types/node@18.16.8)
/wait-on@6.0.1(debug@4.3.4):
resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==}

View File

@@ -0,0 +1,3 @@
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 921 B

View File

@@ -0,0 +1,4 @@
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#fff"/>
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3c-.53043 0-1.03914.21071-1.41421.58579C3.21071 3.96086 3 4.46957 3 5v14c0 .5304.21071 1.0391.58579 1.4142C3.96086 20.7893 4.46957 21 5 21h14c.5304 0 1.0391-.2107 1.4142-.5858S21 19.5304 21 19V5.5L18.5 3H17v6c0 .26522-.1054.51957-.2929.70711C16.5196 9.89464 16.2652 10 16 10H8c-.26522 0-.51957-.10536-.70711-.29289C7.10536 9.51957 7 9.26522 7 9V3H5Zm7 1v5h3V4h-3Zm-5 8h10c.2652 0 .5196.1054.7071.2929S18 12.7348 18 13v6H6v-6c0-.2652.10536-.5196.29289-.7071C6.48043 12.1054 6.73478 12 7 12Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20c-.55 0-1.021-.196-1.413-.588C4.195 19.02 3.99934 18.5493 4 18v-3h2v3h12v-3h2v3c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-4V7.85l-2.6 2.6L7 9l5-5 5 5-1.4 1.45-2.6-2.6V16h-2Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -1,15 +1,13 @@
import send from '~/assets/icons/send.svg';
import receive from '~/assets/icons/receive.svg';
import { ButtonLink, Card, LoadingSpinner, NiceP, SmallAmount, SmallHeader, VStack } from './layout';
import { For, Match, ParentComponent, Show, Suspense, Switch, createMemo, createResource, createSignal } from 'solid-js';
import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from './layout';
import { For, Match, ParentComponent, Show, Switch, createMemo, createResource, createSignal } from 'solid-js';
import { useMegaStore } from '~/state/megaStore';
import { MutinyInvoice } from '@mutinywallet/mutiny-wasm';
import { prettyPrintTime } from '~/utils/prettyPrintTime';
import { JsonModal } from '~/components/JsonModal';
import mempoolTxUrl from '~/utils/mempoolTxUrl';
import wave from "~/assets/wave.gif"
import utxoIcon from '~/assets/icons/coin.svg';
import { getRedshifted } from '~/utils/fakeLabels';
import { ActivityItem } from './ActivityItem';
import { MutinyTagItem } from '~/utils/tags';
export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
@@ -27,7 +25,8 @@ export type OnChainTx = {
height: number
time: number
}
}
},
labels: string[]
}
export type UtxoItem = {
@@ -38,15 +37,16 @@ export type UtxoItem = {
}
keychain: string
is_spent: boolean,
redshifted?: boolean
redshifted?: boolean,
}
const SubtleText: ParentComponent = (props) => {
return <h3 class='text-xs text-gray-500 uppercase'>{props.children}</h3>
}
function OnChainItem(props: { item: OnChainTx }) {
const isReceive = createMemo(() => props.item.received > 0);
function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[] }) {
const [store, actions] = useMegaStore();
const isReceive = () => props.item.received > props.item.sent
const [open, setOpen] = createSignal(false)
@@ -57,26 +57,21 @@ function OnChainItem(props: { item: OnChainTx }) {
Mempool Link
</a>
</JsonModal>
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
<div class="flex items-center">
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
</div>
<div class={CENTER_COLUMN}>
<h2 class={MISSING_LABEL}>Unknown</h2>
{isReceive() ? <SmallAmount amount={props.item.received} /> : <SmallAmount amount={props.item.sent} />}
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader>
<span class="text-neutral-500">On-chain</span>&nbsp;{isReceive() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
</SmallHeader>
<SubtleText>{props.item.confirmation_time?.Confirmed ? prettyPrintTime(props.item.confirmation_time?.Confirmed?.time) : "Unconfirmed"}</SubtleText>
</div>
</div>
{/* {JSON.stringify(props.labels)} */}
<ActivityItem
kind={"onchain"}
labels={props.labels}
amount={isReceive() ? props.item.received : props.item.sent}
date={props.item.confirmation_time?.Confirmed?.time}
positive={isReceive()}
onClick={() => setOpen(!open())}
/>
</>
)
}
function InvoiceItem(props: { item: MutinyInvoice }) {
function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) {
const [store, actions] = useMegaStore();
const isSend = createMemo(() => props.item.is_send);
const [open, setOpen] = createSignal(false)
@@ -84,21 +79,7 @@ function InvoiceItem(props: { item: MutinyInvoice }) {
return (
<>
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
<div class="flex items-center">
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
</div>
<div class={CENTER_COLUMN}>
<h2 class={MISSING_LABEL}>Unknown</h2>
<SmallAmount amount={props.item.amount_sats || 0} />
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader>
<span class="text-neutral-500">Lightning</span>&nbsp;{!isSend() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
</SmallHeader>
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
</div>
</div >
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
</>
)
}
@@ -135,122 +116,45 @@ function Utxo(props: { item: UtxoItem }) {
)
}
export function Activity() {
const [state, _] = useMegaStore();
const getTransactions = async () => {
console.log("Getting onchain txs");
const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[];
return txs.reverse();
}
const getInvoices = async () => {
console.log("Getting invoices");
const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[];
return invoices.filter((inv) => inv.paid).reverse();
}
const getUtXos = async () => {
console.log("Getting utxos");
const utxos = await state.mutiny_wallet?.list_utxos() as UtxoItem[];
return utxos;
}
const [transactions, { refetch: _refetchTransactions }] = createResource(getTransactions);
const [invoices, { refetch: _refetchInvoices }] = createResource(getInvoices);
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
return (
<VStack>
<Suspense>
<Card title="On-chain">
<Switch>
<Match when={transactions.loading}>
<LoadingSpinner wide />
</Match>
<Match when={transactions.state === "ready" && transactions().length === 0}>
<code>No transactions (empty state)</code>
</Match>
<Match when={transactions.state === "ready" && transactions().length >= 0}>
<For each={transactions()}>
{(tx) =>
<OnChainItem item={tx} />
}
</For>
</Match>
</Switch>
</Card>
<Card title="Lightning">
<Switch>
<Match when={invoices.loading}>
<LoadingSpinner wide />
</Match>
<Match when={invoices.state === "ready" && invoices().length === 0}>
<code>No invoices (empty state)</code>
</Match>
<Match when={invoices.state === "ready" && invoices().length >= 0}>
<For each={invoices()}>
{(invoice) =>
<InvoiceItem item={invoice} />
}
</For>
</Match>
</Switch>
</Card>
<Card title="UTXOs">
<Switch>
<Match when={utxos.loading}>
<LoadingSpinner wide />
</Match>
<Match when={utxos.state === "ready" && utxos().length === 0}>
<code>No utxos (empty state)</code>
</Match>
<Match when={utxos.state === "ready" && utxos().length >= 0}>
<For each={utxos()}>
{(utxo) =>
<Utxo item={utxo} />
}
</For>
</Match>
</Switch>
<ButtonLink href="/redshift" layout="small" class="flex items-center gap-2 self-center hover:text-m-red">Redshift <img src={wave} class="h-4" alt="redshift"></img></ButtonLink>
</Card>
</Suspense>
</VStack>
)
}
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number }
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number, labels: MutinyTagItem[] }
function sortByTime(a: ActivityItem, b: ActivityItem) {
return b.time - a.time;
}
export function CombinedActivity(props: { limit?: number }) {
const [state, _] = useMegaStore();
const [state, actions] = useMegaStore();
const getAllActivity = async () => {
console.log("Getting all activity");
const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[];
const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[];
const tags = await actions.listTags();
const activity: ActivityItem[] = [];
let activity: ActivityItem[] = [];
txs.forEach((tx) => {
activity.push({ type: "onchain", item: tx, time: tx.confirmation_time?.Confirmed?.time || Date.now() })
})
for (let i = 0; i < txs.length; i++) {
activity.push({ type: "onchain", item: txs[i], time: txs[i].confirmation_time?.Confirmed?.time || Date.now(), labels: [] })
}
invoices.forEach((invoice) => {
activity.push({ type: "lightning", item: invoice, time: Number(invoice.expire) })
})
for (let i = 0; i < invoices.length; i++) {
if (invoices[i].paid) {
activity.push({ type: "lightning", item: invoices[i], time: Number(invoices[i].expire), labels: [] })
}
}
if (props.limit) {
return activity.sort(sortByTime).slice(0, props.limit);
activity = activity.sort(sortByTime).slice(0, props.limit);
} else {
return activity.sort(sortByTime);
activity.sort(sortByTime);
}
for (let i = 0; i < activity.length; i++) {
// filter the tags to only include the ones that have an id matching one of the labels
activity[i].labels = tags.filter((tag) => activity[i].item.labels.includes(tag.id));
}
return activity;
}
const [activity] = createResource(getAllActivity);
@@ -268,10 +172,12 @@ export function CombinedActivity(props: { limit?: number }) {
{(activityItem) =>
<Switch>
<Match when={activityItem.type === "onchain"}>
<OnChainItem item={activityItem.item as OnChainTx} />
{/* FIXME */}
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.labels} />
</Match>
<Match when={activityItem.type === "lightning"}>
<InvoiceItem item={activityItem.item as MutinyInvoice} />
{/* FIXME */}
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.labels} />
</Match>
</Switch>
}

View File

@@ -0,0 +1,100 @@
import { ParentComponent, createMemo, createResource } from "solid-js";
import { InlineAmount } from "./AmountCard";
import { satsToUsd } from "~/utils/conversions";
import bolt from "~/assets/icons/bolt.svg"
import chain from "~/assets/icons/chain.svg"
import { timeAgo } from "~/utils/prettyPrintTime";
import { MutinyTagItem } from "~/utils/tags";
import { generateGradient } from "~/utils/gradientHash";
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean }> = (props) => {
const amountInUsd = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return satsToUsd(props.price, parsed, true);
}
})
const prettyPrint = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return parsed.toLocaleString();
}
})
return (
<div class="flex flex-col items-end">
<div class="text-base"
classList={{ "text-m-green": props.positive }}
>{props.positive && "+ "}{prettyPrint()}&nbsp;<span class="text-sm">SATS</span>
</div>
<div class="text-sm text-neutral-500">&#8776;&nbsp;{amountInUsd()}&nbsp;<span class="text-sm">USD</span></div>
</div>
)
}
function LabelCircle(props: { name?: string, contact: boolean }) {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(props.name, async (name: string) => {
return generateGradient(name || "?")
})
const text = () => (props.contact && props.name && props.name.length) ? props.name[0] : (props.name && props.name.length) ? "≡" : "?"
const bg = () => (props.name && props.contact) ? gradient() : "gray"
return (
<div class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: bg() }}
>
{text()}
</div>
)
}
// function that takes a list of MutinyTagItems and returns bool if one of those items is of kind Contact
function includesContact(labels: MutinyTagItem[]) {
return labels.some((label) => label.kind === "Contact")
}
// sort the labels so that the contact is always first
function sortLabels(labels: MutinyTagItem[]) {
const contact = labels.find(label => label.kind === "Contact");
return contact ? [contact, ...labels.filter(label => label !== contact)] : labels;
}
// return a string of each label name separated by a comma and a space. if the array is empty return "Unknown"
function labelString(labels: MutinyTagItem[]) {
return labels.length ? labels.map(label => label.name).join(", ") : "Unknown"
}
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: MutinyTagItem[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
const labels = () => sortLabels(props.labels)
return (
<div
onClick={() => props.onClick && props.onClick()}
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
classList={{ "cursor-pointer": !!props.onClick }}
>
<div class="flex gap-2 md:gap-4 items-center">
<div class="">
{props.kind === "lightning" ? <img class="w-[1rem]" src={bolt} alt="lightning" /> : <img class="w-[1rem]" src={chain} alt="onchain" />}
</div>
<div class="">
<LabelCircle name={labels().length ? labels()[0].name : ""} contact={includesContact(labels())} />
</div>
</div>
<div class="flex flex-col">
<span class="text-base font-semibold truncate" classList={{ "text-neutral-500": labels().length === 0 }}>{labelString(labels())}</span>
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
</div>
<div class="">
<ActivityAmount amount={props.amount.toString()} price={30000} positive={props.positive} />
</div>
</div>
)
}

View File

@@ -135,7 +135,7 @@ export const AmountEditable: ParentComponent<{ initialAmountSats: string, initia
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return (
<Dialog.Root isOpen={isOpen()}>
<Dialog.Root open={isOpen()}>
<button onClick={() => setIsOpen(true)} class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center">
{/* <Amount amountSats={Number(displayAmount())} showFiat /><span>&#x270F;&#xFE0F;</span> */}
<Show when={displayAmount() !== "0"} fallback={<div class="inline-block font-semibold">Set amount</div>}>

View File

@@ -1,35 +1,42 @@
import logo from '~/assets/icons/mutiny-logo.svg';
import { DefaultMain, MutinyWalletGuard, SafeArea, VStack, Card } from "~/components/layout";
import BalanceBox from "~/components/BalanceBox";
import { DefaultMain, SafeArea, VStack, Card, LoadingSpinner } from "~/components/layout";
import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
import NavBar from "~/components/NavBar";
import ReloadPrompt from "~/components/Reload";
import { A } from 'solid-start';
import { OnboardWarning } from '~/components/OnboardWarning';
import { CombinedActivity } from './Activity';
import userClock from '~/assets/icons/user-clock.svg';
import { useMegaStore } from '~/state/megaStore';
import { Show } from 'solid-js';
export default function App() {
const [state, _actions] = useMegaStore();
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<header class="w-full flex justify-between items-center mt-4 mb-2">
<img src={logo} class="h-10" alt="logo" />
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity"><img src={userClock} alt="Activity" /></A>
</header>
<SafeArea>
<DefaultMain>
<header class="w-full flex justify-between items-center mt-4 mb-2">
<img src={logo} class="h-10" alt="logo" />
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity"><img src={userClock} alt="Activity" /></A>
</header>
<Show when={!state.wallet_loading}>
<OnboardWarning />
<ReloadPrompt />
<BalanceBox />
<Card title="Activity">
<VStack>
</Show>
<BalanceBox loading={state.wallet_loading} />
<Card title="Activity">
<div class="p-1" />
<VStack>
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
<CombinedActivity limit={3} />
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
</VStack>
</Card>
</DefaultMain>
<NavBar activeTab="home" />
</SafeArea>
</MutinyWalletGuard>
</Show>
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
</VStack>
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
</Card>
</DefaultMain>
<NavBar activeTab="home" />
</SafeArea>
);
}

View File

@@ -1,7 +1,8 @@
import { Show, Suspense } from "solid-js";
import { ButtonLink, FancyCard, Indicator } from "~/components/layout";
import { Button, ButtonLink, FancyCard, Indicator } from "~/components/layout";
import { useMegaStore } from "~/state/megaStore";
import { Amount } from "./Amount";
import { useNavigate } from "solid-start";
function prettyPrintAmount(n?: number | bigint): string {
if (!n || n.valueOf() === 0) {
@@ -10,19 +11,38 @@ function prettyPrintAmount(n?: number | bigint): string {
return n.toLocaleString()
}
export default function BalanceBox() {
export function LoadingShimmer() {
return (<div class="flex flex-col gap-2 animate-pulse">
<h1 class="text-4xl font-light">
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]"></div>
</h1>
<h2 class="text-xl font-light text-white/70" >
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]"></div>
</h2>
</div>)
}
export default function BalanceBox(props: { loading?: boolean }) {
const [state, actions] = useMegaStore();
const emptyBalance = () => (state.balance?.confirmed || 0n) === 0n && (state.balance?.lightning || 0n) === 0n
const navigate = useNavigate()
return (
<>
<FancyCard title="Lightning">
<Amount amountSats={state.balance?.lightning || 0} showFiat />
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Amount amountSats={state.balance?.lightning || 0} showFiat />
</Show>
</FancyCard>
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
<div onClick={actions.sync}>
<Amount amountSats={state.balance?.confirmed} showFiat />
</div>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div onClick={actions.sync}>
<Amount amountSats={state.balance?.confirmed} showFiat />
</div>
</Show>
<Suspense>
<Show when={state.balance?.unconfirmed}>
<div class="flex flex-col gap-2">
@@ -37,8 +57,8 @@ export default function BalanceBox() {
</Suspense>
</FancyCard>
<div class="flex gap-2 py-4">
<ButtonLink href="/send" intent="green">Send</ButtonLink>
<ButtonLink href="/receive" intent="blue">Receive</ButtonLink>
<Button onClick={() => navigate("/send")} disabled={emptyBalance() || props.loading} intent="green">Send</Button>
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">Receive</Button>
</div>
</>
)

View File

@@ -1,59 +0,0 @@
import { RadioGroup as Kobalte } from '@kobalte/core';
import { type JSX, Show, splitProps, For } from 'solid-js';
type RadioGroupProps = {
name: string;
label?: string | undefined;
options: { label: string; value: string }[];
value: string | undefined;
error: string;
required?: boolean | undefined;
disabled?: boolean | undefined;
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
};
type Color = "blue" | "green" | "red" | "gray"
export const colorVariants = {
blue: "bg-m-blue",
green: "bg-m-green",
red: "bg-m-red",
gray: "bg-[#898989]",
}
export function ColorRadioGroup(props: RadioGroupProps) {
const [rootProps, inputProps] = splitProps(
props,
['name', 'value', 'required', 'disabled'],
['ref', 'onInput', 'onChange', 'onBlur']
);
return (
<Kobalte.Root
{...rootProps}
validationState={props.error ? 'invalid' : 'valid'}
class="flex flex-col gap-2"
>
<Show when={props.label}>
<Kobalte.Label class="text-sm uppercase font-semibold">
{props.label}
</Kobalte.Label>
</Show>
<div class="flex gap-2">
<For each={props.options}>
{(option) => (
<Kobalte.Item value={option.value} class="ui-checked:bg-neutral-950 rounded outline outline-black/50 ui-checked:outline-white ui-checked:outline-2">
<Kobalte.ItemInput {...inputProps} />
<Kobalte.ItemControl class={`${colorVariants[option.value as Color]} w-8 h-8 rounded`}>
<Kobalte.ItemIndicator />
</Kobalte.ItemControl>
{/* <Kobalte.ItemLabel>{option.label}</Kobalte.ItemLabel> */}
</Kobalte.Item>
)}
</For>
</div>
<Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
</Kobalte.Root>
);
}

View File

@@ -1,24 +1,17 @@
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
import { Match, Switch, createSignal } from 'solid-js';
import { SmallHeader, TinyButton } from '~/components/layout';
import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg";
import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm';
import { ContactFormValues } from './ContactViewer';
const INITIAL: ContactItem = { id: createUniqueId(), kind: "contact", name: "", color: "gray" }
export function ContactEditor(props: { createContact: (contact: ContactItem) => void, list?: boolean }) {
export function ContactEditor(props: { createContact: (contact: ContactFormValues) => void, list?: boolean }) {
const [isOpen, setIsOpen] = createSignal(false);
// What we're all here for in the first place: returning a value
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
// TODO: why do the id and color disappear?
const odd = { id: createUniqueId(), kind: "contact" }
props.createContact({ ...odd, ...c })
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
props.createContact(c)
setIsOpen(false);
}
@@ -26,7 +19,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return (
<Dialog.Root isOpen={isOpen()}>
<Dialog.Root open={isOpen()}>
<Switch>
<Match when={props.list}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2">
@@ -50,7 +43,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
<img src={close} alt="Close" />
</button>
</div>
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} initialValues={INITIAL} />
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} />
</Dialog.Content>
</div>
</Dialog.Portal>

View File

@@ -1,13 +1,10 @@
import { SubmitHandler, createForm, required } from "@modular-forms/solid";
import { ContactItem } from "~/state/contacts";
import { Button, LargeHeader, VStack } from "~/components/layout";
import { TextField } from "~/components/layout/TextField";
import { ColorRadioGroup } from "~/components/ColorRadioGroup";
import { ContactFormValues } from "./ContactViewer";
const colorOptions = [{ label: "blue", value: "blue" }, { label: "green", value: "green" }, { label: "red", value: "red" }, { label: "gray", value: "gray" }]
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, initialValues?: ContactItem, title: string, cta: string }) {
const [_contactForm, { Form, Field }] = createForm<ContactItem>({ initialValues: props.initialValues });
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValues>, initialValues?: ContactFormValues, title: string, cta: string }) {
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ initialValues: props.initialValues });
return (
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
@@ -19,16 +16,11 @@ export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, i
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
)}
</Field>
<Field name="npub" validate={[]}>
{/* <Field name="npub" validate={[]}>
{(field, props) => (
<TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" />
)}
</Field>
<Field name="color">
{(field, props) => (
<ColorRadioGroup options={colorOptions} {...props} value={field.value} error={field.error} label="Color" />
)}
</Field>
</Field> */}
</VStack>
</div>
<Button type="submit" intent="blue" class="w-full flex-none">

View File

@@ -3,16 +3,24 @@ import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg";
import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm';
import { showToast } from './Toaster';
import { Contact } from '@mutinywallet/mutiny-wasm';
export function ContactViewer(props: { contact: ContactItem, gradient: string, saveContact: (contact: ContactItem) => void }) {
export type ContactFormValues = {
name: string,
npub?: string,
}
export function ContactViewer(props: { contact: Contact, gradient: string, saveContact: (contact: Contact) => void }) {
const [isOpen, setIsOpen] = createSignal(false);
const [isEditing, setIsEditing] = createSignal(false);
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
props.saveContact({ ...props.contact, ...c })
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
// FIXME: merge with existing contact if saving (need edit contact method)
// FIXME: npub not valid? other undefineds
const contact = new Contact(c.name, undefined, undefined, undefined)
props.saveContact(contact)
setIsEditing(false)
}
@@ -20,7 +28,7 @@ export function ContactViewer(props: { contact: ContactItem, gradient: string, s
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return (
<Dialog.Root isOpen={isOpen()}>
<Dialog.Root open={isOpen()}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden">
<div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: props.gradient }}

View File

@@ -53,7 +53,7 @@ export function DeleteEverything() {
return (
<>
<Button onClick={confirmReset}>Delete Everything</Button>
<ConfirmDialog loading={confirmLoading()} isOpen={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}>
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}>
This will delete your node's state. This can't be undone!
</ConfirmDialog>
</>

View File

@@ -7,9 +7,9 @@ const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
// TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })`
export const ConfirmDialog: ParentComponent<{ isOpen: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => {
export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => {
return (
<Dialog.Root isOpen={props.isOpen} onOpenChange={props.onCancel}>
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>

View File

@@ -67,7 +67,7 @@ export function ImportExport() {
<Button onClick={uploadFile}>Upload Saved State</Button>
</VStack>
</InnerCard>
<ConfirmDialog loading={confirmLoading()} isOpen={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}>
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}>
Do you want to replace your state with {files()[0].name}?
</ConfirmDialog>
</>

View File

@@ -13,7 +13,7 @@ export function JsonModal(props: { title: string, open: boolean, data?: unknown,
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return (
<Dialog.Root isOpen={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>

View File

@@ -110,7 +110,7 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={value()}
onValueChange={setValue}
onChange={setValue}
validationState={(value() == "" || value().startsWith("mutiny:")) ? "valid" : "invalid"}
class="flex flex-col gap-4"
>
@@ -167,7 +167,7 @@ function ChannelItem(props: { channel: MutinyChannel, network?: string }) {
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
</VStack>
<ConfirmDialog isOpen={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
<ConfirmDialog open={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
<p>Are you sure you want to close this channel?</p>
</ConfirmDialog>
</Collapsible.Content>
@@ -259,7 +259,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={peerPubkey()}
onValueChange={setPeerPubkey}
onChange={setPeerPubkey}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label>
@@ -267,7 +267,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
</TextField.Root>
<TextField.Root
value={amount()}
onValueChange={setAmount}
onChange={setAmount}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label>
@@ -313,7 +313,7 @@ function LnUrlAuth() {
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={value()}
onValueChange={setValue}
onChange={setValue}
validationState={(value() == "" || value().toLowerCase().startsWith("lnurl")) ? "valid" : "invalid"}
class="flex flex-col gap-4"
>
@@ -327,6 +327,32 @@ function LnUrlAuth() {
)
}
function ListTags() {
const [_state, actions] = useMegaStore()
const [tags] = createResource(actions.listTags)
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} Tags
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(tags(), null, 2)}
</pre>
</VStack>
</Collapsible.Content>
</Collapsible.Root>
)
}
export default function KitchenSink() {
@@ -339,6 +365,9 @@ export default function KitchenSink() {
<ChannelsList />
<Hr />
<LnUrlAuth />
<Hr />
<ListTags />
<Hr />
<ImportExport />
</Card>

View File

@@ -7,7 +7,7 @@ export function Logs() {
async function handleSave() {
const logs = await state.mutiny_wallet?.get_logs()
downloadTextFile(logs.join() || "", "mutiny-logs.txt", "text/plain")
downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain")
}
return (

View File

@@ -1,7 +1,10 @@
import { Show, createSignal, onMount } from "solid-js";
import { Button, ButtonLink, SmallHeader, VStack } from "./layout";
import { Button, ButtonLink, SmallHeader } from "./layout";
import { useMegaStore } from "~/state/megaStore";
import { showToast } from "./Toaster";
import save from "~/assets/icons/save.svg"
import close from "~/assets/icons/close.svg";
import restore from "~/assets/icons/upload.svg";
export function OnboardWarning() {
const [state, actions] = useMegaStore();
@@ -18,30 +21,42 @@ export function OnboardWarning() {
return (
<>
{/* TODO: show this once we have a restore flow */}
<Show when={!state.dismissed_restore_prompt && false}>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
<SmallHeader>Welcome!</SmallHeader>
<VStack>
<p class="text-2xl font-light">
Do you want to restore an existing Mutiny Wallet?
</p>
<div class="w-full flex gap-2">
<Button intent="green" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Restore</Button>
<Button onClick={actions.dismissRestorePrompt}>Nope</Button>
<Show when={false}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={restore} alt="backup" class="w-8 h-8" />
</div>
<div class='flex md:flex-row flex-col items-center gap-4'>
<div class="flex flex-col">
<SmallHeader>Welcome!</SmallHeader>
<p class="text-base font-light">
If you've used Mutiny before you can restore from a backup. Otherwise you can skip this and enjoy your new wallet!
</p>
</div>
</VStack>
<Button intent="green" layout="xs" class="self-start md:self-auto" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Restore</Button>
</div>
<button tabindex="-1" onClick={() => { actions.dismissRestorePrompt() }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div>
</Show>
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
<SmallHeader>Secure your funds</SmallHeader>
<p class="text-2xl font-light">
You have money stored in this browser. Let's make sure you have a backup.
</p>
<div class="w-full flex gap-2">
<ButtonLink intent="blue" href="/backup">Backup</ButtonLink>
<Button onClick={() => { setDismissedBackup(true) }}>Nope</Button>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={save} alt="backup" class="w-8 h-8" />
</div>
<div class='flex md:flex-row flex-col items-center gap-4'>
<div class="flex flex-col">
<SmallHeader>Secure your funds</SmallHeader>
<p class="text-base font-light">
You have money stored in this browser. Let's make sure you have a backup.
</p>
</div>
<ButtonLink intent="blue" layout="xs" class="self-start md:self-auto" href="/backup">Backup</ButtonLink>
</div>
<button tabindex="-1" onClick={() => { setDismissedBackup(true) }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div>
</Show>
</>

View File

@@ -25,7 +25,7 @@ const ReloadPrompt: Component = () => {
return (
<Show when={offlineReady() || needRefresh()}>
<Card title="PWA settings">
{/* <Card title="PWA settings">
<div>
<Show
fallback={<span>New content available, click on reload button to update.</span>}
@@ -38,7 +38,7 @@ const ReloadPrompt: Component = () => {
<Button onClick={() => updateServiceWorker(true)}>Reload</Button>
</Show>
<Button onClick={() => close()}>Close</Button>
</Card>
</Card> */}
</Show>
)
}

View File

@@ -35,13 +35,12 @@ export function StringShower(props: { text: string }) {
return (
<>
<JsonModal open={open()} data={props.text} title="Details" setOpen={setOpen} />
<div class="flex gap-2">
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
<pre class="truncate text-neutral-400">{props.text}</pre>
<button class="w-[16rem]" onClick={() => setOpen(true)}>
<button class="w-[2rem]" onClick={() => setOpen(true)}>
<img src={eyeIcon} alt="eye" />
</button>
</div>
</>
)
}

View File

@@ -1,9 +1,12 @@
import { Select, createOptions } from "@thisbeyond/solid-select";
import "~/styles/solid-select.css"
import { For, createUniqueId } from "solid-js";
import { For } from "solid-js";
import { ContactEditor } from "./ContactEditor";
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
import { TinyButton } from "./layout";
import { ContactFormValues } from "./ContactViewer";
import { MutinyTagItem } from "~/utils/tags";
import { Contact } from "@mutinywallet/mutiny-wasm";
import { useMegaStore } from "~/state/megaStore";
// take two arrays, subtract the second from the first, then return the first
function subtract<T>(a: T[], b: T[]) {
@@ -11,12 +14,20 @@ function subtract<T>(a: T[], b: T[]) {
return a.filter(x => !set.has(x));
}
const createValue = (name: string): TextItem => {
return { id: createUniqueId(), name, kind: "text" };
const createLabelValue = (label: string): Partial<MutinyTagItem> => {
return { id: label, name: label, kind: "Label" };
};
export function TagEditor(props: { values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
const onChange = (selected: TagItem[]) => {
export function TagEditor(props: {
values: MutinyTagItem[],
setValues: (values: MutinyTagItem[]) => void,
selectedValues: MutinyTagItem[],
setSelectedValues: (values: MutinyTagItem[]) => void,
placeholder: string
}) {
const [state, actions] = useMegaStore();
const onChange = (selected: MutinyTagItem[]) => {
props.setSelectedValues(selected);
console.log(selected)
@@ -31,12 +42,23 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
key: "name",
disable: (value) => props.selectedValues.includes(value),
filterable: true, // Default
createable: createValue,
createable: createLabelValue,
});
const newContact = async (contact: ContactItem) => {
await addContact(contact)
onChange([...props.selectedValues, contact])
async function createContact(contact: ContactFormValues) {
// FIXME: undefineds
// FIXME: npub not valid? other undefineds
const c = new Contact(contact.name, undefined, undefined, undefined);
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
const contactItem = await state.mutiny_wallet?.get_contact(newContactId ?? "");
const mutinyContactItem: MutinyTagItem = { id: contactItem?.id || "", name: contactItem?.name || "", kind: "Contact", last_used_time: 0n };
if (contactItem) {
// @ts-ignore
// FIXME: make typescript less mad about this
onChange([...props.selectedValues, mutinyContactItem])
} else {
console.error("Failed to create contact")
}
}
return (
@@ -52,13 +74,13 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
<div class="flex gap-2 flex-wrap">
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
{(tag) => (
<TinyButton onClick={() => onChange([...props.selectedValues, tag])}
<TinyButton tag={tag} onClick={() => onChange([...props.selectedValues, tag])}
>
{tag.name}
</TinyButton>
)}
</For>
<ContactEditor createContact={newContact} />
<ContactEditor createContact={createContact} />
</div>
</div >
)

View File

@@ -43,13 +43,10 @@ export function ToastItem(props: { toastId: number, title: string, description:
</p>
</Toast.Description>
</div>
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem] flex-0">
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue flex-0">
<img src={close} alt="Close" />
</Toast.CloseButton>
</div>
{/* <Toast.ProgressTrack class="toast__progress-track">
<Toast.ProgressFill class="toast__progress-fill" />
</Toast.ProgressTrack> */}
</Toast.Root>
)
}

View File

@@ -4,7 +4,7 @@ import { Dynamic } from "solid-js/web";
import { A } from "solid-start";
import { LoadingSpinner } from ".";
const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 disabled:grayscale transition", {
const button = cva("p-3 rounded-xl font-semibold disabled:opacity-50 disabled:grayscale transition", {
variants: {
// TODO: button hover has to work different than buttonlinks (like disabled state)
intent: {
@@ -16,10 +16,10 @@ const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 dis
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button",
},
layout: {
flex: "flex-1",
pad: "px-8",
flex: "flex-1 text-xl",
pad: "px-8 text-xl",
small: "px-4 py-2 w-auto text-lg",
xs: "px-2 py-1 w-auto rounded-lg font-normal text-base"
xs: "px-4 py-2 w-auto rounded-lg text-base"
},
},
defaultVariants: {
@@ -32,7 +32,8 @@ const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 dis
type StyleProps = VariantProps<typeof button>
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, StyleProps {
loading?: boolean
loading?: boolean,
disabled?: boolean,
}
export const Button: ParentComponent<ButtonProps> = props => {
@@ -42,6 +43,7 @@ export const Button: ParentComponent<ButtonProps> = props => {
return (
<button
{...attrs}
disabled={props.disabled || props.loading}
class={button({
class: local.class || "",
intent: local.intent,

View File

@@ -18,7 +18,7 @@ type FullscreenModalProps = {
export function FullscreenModal(props: FullscreenModalProps) {
return (
<Dialog.Root isOpen={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Portal>
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>

View File

@@ -7,7 +7,7 @@ type Choices = { value: string, label: string, caption: string }[]
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
return (
// TODO: rewrite this with CVA, props are bad for tailwind
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)}
<RadioGroup.Root value={props.value} onChange={(e) => props.onValueChange(e)}
class={"grid w-full gap-4"}
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
>

View File

@@ -1,8 +1,11 @@
import { JSX, ParentComponent, Show, Suspense } from "solid-js"
import { JSX, ParentComponent, Show, Suspense, createResource, createSignal } from "solid-js"
import Linkify from "./Linkify"
import { Button, ButtonLink } from "./Button"
import { Separator } from "@kobalte/core"
import { Checkbox as KCheckbox, Separator } from "@kobalte/core"
import { useMegaStore } from "~/state/megaStore"
import check from "~/assets/icons/check.svg"
import { MutinyTagItem } from "~/utils/tags"
import { generateGradient } from "~/utils/gradientHash"
export {
Button,
@@ -118,12 +121,23 @@ export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: stri
}
export const NiceP: ParentComponent = (props) => {
return (<p class="text-2xl font-light">{props.children}</p>)
return (<p class="text-xl font-light">{props.children}</p>)
}
export const TinyButton: ParentComponent<{ onClick: () => void }> = (props) => {
export const TinyButton: ParentComponent<{ onClick: () => void, tag?: MutinyTagItem }> = (props) => {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(props.tag?.name, async (name: string) => {
return generateGradient(name || "?")
})
const bg = () => (props.tag?.name && props.tag?.kind === "Contact") ? gradient() : "rgb(255 255 255 / 0.1)"
console.log("tiny tag", props.tag?.name, gradient())
return (
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}>
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}
style={{ background: bg() }}
>
{props.children}
</button>
)
@@ -134,3 +148,17 @@ export const Indicator: ParentComponent = (props) => {
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
)
}
export function Checkbox(props: { label: string, checked: boolean, onChange: (checked: boolean) => void }) {
return (
<KCheckbox.Root class="inline-flex items-center gap-2" checked={props.checked} onChange={props.onChange}>
<KCheckbox.Input class="" />
<KCheckbox.Control class="flex-0 w-8 h-8 rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red">
<KCheckbox.Indicator>
<img src={check} class="w-8 h-8" alt="check" />
</KCheckbox.Indicator>
</KCheckbox.Control>
<KCheckbox.Label class="flex-1 text-xl font-light">{props.label}</KCheckbox.Label>
</KCheckbox.Root>
)
}

View File

@@ -5,23 +5,41 @@ import { BackLink } from "~/components/layout/BackLink";
import { CombinedActivity } from "~/components/Activity";
import { A } from "solid-start";
import settings from '~/assets/icons/settings.svg';
import { ContactItem, addContact, editContact, listContacts } from "~/state/contacts";
import { Tabs } from "@kobalte/core";
import { gradientsPerContact } from "~/utils/gradientHash";
import { ContactEditor } from "~/components/ContactEditor";
import { ContactViewer } from "~/components/ContactViewer";
import { ContactFormValues, ContactViewer } from "~/components/ContactViewer";
import { useMegaStore } from "~/state/megaStore";
import { Contact } from "@mutinywallet/mutiny-wasm";
import { showToast } from "~/components/Toaster";
function ContactRow() {
const [contacts, { refetch }] = createResource(listContacts)
const [state, actions] = useMegaStore();
const [contacts, { refetch }] = createResource(async () => {
const contacts = state.mutiny_wallet?.get_contacts();
console.log(contacts)
let c: Contact[] = []
if (contacts) {
for (let contact in contacts) {
c.push(contacts[contact])
}
}
return c || []
})
const [gradients] = createResource(contacts, gradientsPerContact);
async function createContact(contact: ContactItem) {
await addContact(contact)
async function createContact(contact: ContactFormValues) {
// FIXME: npub not valid? other undefineds
const c = new Contact(contact.name, undefined, undefined, undefined);
await state.mutiny_wallet?.create_new_contact(c)
refetch();
}
async function saveContact(contact: ContactItem) {
await editContact(contact)
//
async function saveContact(contact: ContactFormValues) {
showToast(new Error("Unimplemented"))
// await editContact(contact)
refetch();
}
@@ -31,7 +49,7 @@ function ContactRow() {
<Show when={contacts() && gradients()}>
<For each={contacts()}>
{(contact) => (
<ContactViewer contact={contact} gradient={gradients()?.get(contact.id)} saveContact={saveContact} />
<ContactViewer contact={contact} gradient={gradients()?.get(contact.name)} saveContact={saveContact} />
)}
</For>
</Show>
@@ -49,6 +67,7 @@ export default function Activity() {
<BackLink />
<LargeHeader action={<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>}>Activity</LargeHeader>
<ContactRow />
<Tabs.Root defaultValue="mutiny">
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
<Tabs.Trigger value="mutiny" class={TAB}>Mutiny</Tabs.Trigger>
@@ -58,7 +77,10 @@ export default function Activity() {
<Tabs.Content value="mutiny">
{/* <MutinyActivity /> */}
<Card title="Activity">
<CombinedActivity />
<div class="p-1" />
<VStack>
<CombinedActivity />
</VStack>
</Card>
</Tabs.Content>
<Tabs.Content value="nostr">

View File

@@ -1,16 +1,39 @@
import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack } from "~/components/layout";
import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack, Checkbox } from "~/components/layout";
import NavBar from "~/components/NavBar";
import { useNavigate } from 'solid-start';
import { SeedWords } from '~/components/SeedWords';
import { useMegaStore } from '~/state/megaStore';
import { Show, createSignal } from 'solid-js';
import { Show, createEffect, createSignal } from 'solid-js';
import { BackLink } from "~/components/layout/BackLink";
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
const [one, setOne] = createSignal(false);
const [two, setTwo] = createSignal(false);
const [three, setThree] = createSignal(false);
createEffect(() => {
if (one() && two() && three()) {
props.setHasCheckedAll(true)
} else {
props.setHasCheckedAll(false)
}
})
return (
<VStack>
<Checkbox checked={one()} onChange={setOne} label="I wrote down the words" />
<Checkbox checked={two()} onChange={setTwo} label="I understand that my funds are my responsibility" />
<Checkbox checked={three()} onChange={setThree} label="I'm not lying just to get this over with" />
</VStack>
)
}
export default function App() {
const [store, actions] = useMegaStore();
const navigate = useNavigate();
const [hasSeenBackup, setHasSeenBackup] = createSignal(false);
const [hasCheckedAll, setHasCheckedAll] = createSignal(false);
function wroteDownTheWords() {
actions.setHasBackedUp()
@@ -23,6 +46,7 @@ export default function App() {
<DefaultMain>
<BackLink />
<LargeHeader>Backup</LargeHeader>
<VStack>
<NiceP>Let's get these funds secured.</NiceP>
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
@@ -32,9 +56,9 @@ export default function App() {
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
<Show when={hasSeenBackup()}>
<NiceP>You are responsible for your funds!</NiceP>
<Quiz setHasCheckedAll={setHasCheckedAll} />
</Show>
<Button disabled={!hasSeenBackup()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
<Button disabled={!hasSeenBackup() || !hasCheckedAll()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
</VStack>
</DefaultMain>
<NavBar activeTab="none" />

View File

@@ -14,10 +14,10 @@ import { StyledRadioGroup } from "~/components/layout/Radio";
import { showToast } from "~/components/Toaster";
import { useNavigate } from "solid-start";
import megacheck from "~/assets/icons/megacheck.png";
import { TagItem, listTags } from "~/state/contacts";
import { AmountCard } from "~/components/AmountCard";
import { ShareCard } from "~/components/ShareCard";
import { BackButton } from "~/components/layout/BackButton";
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
type OnChainTx = {
transaction: {
@@ -43,8 +43,6 @@ type OnChainTx = {
}
}
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
type ReceiveFlavor = "unified" | "lightning" | "onchain"
@@ -52,7 +50,7 @@ type ReceiveState = "edit" | "show" | "paid"
type PaidState = "lightning_paid" | "onchain_paid";
export default function Receive() {
const [state, _] = useMegaStore()
const [state, actions] = useMegaStore()
const navigate = useNavigate();
const [amount, setAmount] = createSignal("")
@@ -62,8 +60,8 @@ export default function Receive() {
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
// The data we get after a payment
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
@@ -86,8 +84,8 @@ export default function Receive() {
})
onMount(() => {
listTags().then((tags) => {
setValues(prev => [...prev, ...tags || []])
actions.listTags().then((tags) => {
setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
});
})
@@ -104,8 +102,7 @@ export default function Receive() {
async function getUnifiedQr(amount: string) {
const bigAmount = BigInt(amount);
try {
// FIXME: actual labels
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, []);
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tagsToIds(selectedValues()));
// Save the raw info so we can watch the address and invoice
setBip21Raw(raw);

View File

@@ -341,7 +341,7 @@ export default function Redshift() {
<Match when={shiftStage() === "choose"}>
<VStack>
<NiceP>Where is this going?</NiceP>
<StyledRadioGroup red value={shiftType()} onValueChange={(newValue) => setShiftType(newValue as ShiftOption)} choices={SHIFT_OPTIONS} />
<StyledRadioGroup accent="red" value={shiftType()} onValueChange={(newValue) => setShiftType(newValue as ShiftOption)} choices={SHIFT_OPTIONS} />
</VStack>
<VStack>
<NiceP>Choose your <span class="inline-block"><img class="h-4" src={wave} alt="sine wave" /></span> UTXO to begin</NiceP>

View File

@@ -17,9 +17,9 @@ import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { BackLink } from "~/components/layout/BackLink";
import { useNavigate } from "solid-start";
import { TagEditor } from "~/components/TagEditor";
import { TagItem, createUniqueId, listTags } from "~/state/contacts";
import { StringShower } from "~/components/ShareCard";
import { AmountCard } from "~/components/AmountCard";
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
type SendSource = "lightning" | "onchain";
@@ -97,23 +97,6 @@ function DestinationShower(props: {
)
}
function SendTags() {
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
onMount(() => {
listTags().then((tags) => {
setValues(prev => [...prev, ...tags || []])
});
})
return (
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
)
}
export default function Send() {
const [state, actions] = useMegaStore();
const navigate = useNavigate()
@@ -136,6 +119,10 @@ export default function Send() {
const [sending, setSending] = createSignal(false);
const [sentDetails, setSentDetails] = createSignal<SentDetails>();
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
function clearAll() {
setDestination(undefined);
setAmountSats(0n);
@@ -158,6 +145,10 @@ export default function Send() {
setDestination(state.scan_result);
actions.setScanResult(undefined);
}
actions.listTags().then((tags) => {
setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
});
})
// Rerun every time the destination changes
@@ -235,17 +226,16 @@ export default function Send() {
sentDetails.destination = bolt11;
// If the invoice has sats use that, otherwise we pass the user-defined amount
if (invoice()?.amount_sats) {
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11);
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tagsToIds(selectedValues()));
sentDetails.amount = invoice()?.amount_sats;
} else {
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats());
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tagsToIds(selectedValues()));
sentDetails.amount = amountSats();
}
} else if (source() === "lightning" && nodePubkey()) {
const nodes = await state.mutiny_wallet?.list_nodes();
const firstNode = nodes[0] as string || ""
const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats());
console.log(payment?.value)
const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats(), tagsToIds(selectedValues()));
// TODO: handle timeouts
if (!payment?.paid) {
@@ -254,9 +244,8 @@ export default function Send() {
sentDetails.amount = amountSats();
}
} else if (source() === "onchain" && address()) {
// FIXME: actual labels
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), []);
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tagsToIds(selectedValues()));
sentDetails.amount = amountSats();
sentDetails.destination = address();
// TODO: figure out if this is necessary, it takes forever
@@ -320,7 +309,7 @@ export default function Send() {
<Card>
<VStack>
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
<SendTags />
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
</VStack>
</Card>
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={fakeFee().toString()} isAmountEditable={!(invoice()?.amount_sats)} />

View File

@@ -1,7 +1,9 @@
import { ActivityItem } from "~/components/ActivityItem";
import { AmountCard } from "~/components/AmountCard";
import NavBar from "~/components/NavBar";
import { OnboardWarning } from "~/components/OnboardWarning";
import { ShareCard } from "~/components/ShareCard";
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
export default function Admin() {
@@ -9,12 +11,16 @@ export default function Admin() {
<SafeArea>
<DefaultMain>
<LargeHeader>Storybook</LargeHeader>
<OnboardWarning />
<VStack>
<AmountCard amountSats={"100000"} fee={"69"} />
<AmountCard amountSats={"100000"} />
<AmountCard amountSats={"100000"} isAmountEditable />
<AmountCard amountSats={"0"} isAmountEditable />
<ShareCard text={SAMPLE} />
<Card title="Activity">
<ActivityItem kind="lightning" labels={["benthecarman"]} amount={100000} date={1683664966} />
<ActivityItem kind="onchain" labels={["tony"]} amount={42000000} positive date={1683664966} />
<ActivityItem kind="onchain" labels={["a fake name thati is too long"]} amount={42000000} date={1683664966} />
<ActivityItem kind="onchain" labels={["a fake name thati is too long"]} amount={42000000} date={1683664966} />
</Card>
</VStack>
</DefaultMain>
<NavBar activeTab="none" />

View File

@@ -1,58 +0,0 @@
export type TagItem = TextItem | ContactItem;
export type TextItem = {
id: string;
kind: "text";
name: string;
}
export type ContactItem = {
id: string;
kind: "contact";
name: string;
npub?: string;
color: Color;
}
export type Color = "blue" | "green" | "red" | "gray"
export const createUniqueId = () => Math.random().toString(36).substr(2, 9);
export async function listContacts(): Promise<ContactItem[]> {
// get contacts from localstorage
const contacts: ContactItem[] = JSON.parse(localStorage.getItem("contacts") || "[]");
return contacts;
}
export async function listTexts(): Promise<TextItem[]> {
// get texts from localstorage
const texts: TextItem[] = JSON.parse(localStorage.getItem("texts") || "[]");
return texts;
}
export async function listTags(): Promise<TagItem[]> {
const contacts = await listContacts();
const texts = await listTexts();
return [...contacts, ...texts];
}
export async function addContact(contact: ContactItem): Promise<void> {
const contacts = await listContacts();
contacts.push(contact);
localStorage.setItem("contacts", JSON.stringify(contacts));
}
export async function editContact(contact: ContactItem): Promise<void> {
const contacts = await listContacts();
const index = contacts.findIndex(c => c.id === contact.id);
contacts[index] = contact;
localStorage.setItem("contacts", JSON.stringify(contacts));
}
export async function addTextTag(text: TextItem): Promise<void> {
const texts = await listTexts();
texts.push(text);
localStorage.setItem("texts", JSON.stringify(texts));
}

View File

@@ -6,6 +6,7 @@ import { createStore } from "solid-js/store";
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
import { MutinyBalance, MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { ParsedParams } from "~/routes/Scanner";
import { MutinyTagItem } from "~/utils/tags";
const MegaStoreContext = createContext<MegaStore>();
@@ -23,7 +24,8 @@ export type MegaStore = [{
last_sync?: number;
price: number
has_backed_up: boolean,
dismissed_restore_prompt: boolean
dismissed_restore_prompt: boolean,
wallet_loading: boolean
}, {
fetchUserStatus(): Promise<UserStatus>;
setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void>;
@@ -33,6 +35,7 @@ export type MegaStore = [{
sync(): Promise<void>;
dismissRestorePrompt(): void;
setHasBackedUp(): void;
listTags(): Promise<MutinyTagItem[]>;
}];
export const Provider: ParentComponent = (props) => {
@@ -49,7 +52,8 @@ export const Provider: ParentComponent = (props) => {
balance: undefined as MutinyBalance | undefined,
last_sync: undefined as number | undefined,
is_syncing: false,
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true"
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true",
wallet_loading: true
});
const actions = {
@@ -81,8 +85,9 @@ export const Provider: ParentComponent = (props) => {
},
async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> {
try {
setState({ wallet_loading: true })
const mutinyWallet = await setupMutinyWallet(settings)
setState({ mutiny_wallet: mutinyWallet })
setState({ mutiny_wallet: mutinyWallet, wallet_loading: false })
} catch (e) {
console.error(e)
}
@@ -126,6 +131,9 @@ export const Provider: ParentComponent = (props) => {
dismissRestorePrompt() {
localStorage.setItem("dismissed_restore_prompt", "true")
setState({ dismissed_restore_prompt: true })
},
async listTags(): Promise<MutinyTagItem[]> {
return state.mutiny_wallet?.get_tag_items() as MutinyTagItem[]
}
};

View File

@@ -1,6 +1,6 @@
import { ContactItem } from "~/state/contacts";
import { Contact } from "@mutinywallet/mutiny-wasm";
async function generateGradientFromHashedString(str: string) {
export async function generateGradient(str: string) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const digestBuffer = await crypto.subtle.digest('SHA-256', data);
@@ -13,11 +13,12 @@ async function generateGradientFromHashedString(str: string) {
return gradient;
}
export async function gradientsPerContact(contacts: ContactItem[]) {
export async function gradientsPerContact(contacts: Contact[]) {
console.log(contacts);
const gradients = new Map();
for (const contact of contacts) {
const gradient = await generateGradientFromHashedString(contact.name);
gradients.set(contact.id, gradient);
const gradient = await generateGradient(contact.name);
gradients.set(contact.name, gradient);
}
return gradients;

View File

@@ -9,4 +9,31 @@ export function prettyPrintTime(ts: number) {
};
return new Date(ts * 1000).toLocaleString('en-US', options);
}
}
export function timeAgo(ts?: number | bigint): string {
if (!ts || ts === 0) return "Pending";
const timestamp = Number(ts) * 1000;
const now = Date.now();
const elapsedMilliseconds = now - timestamp;
const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
const elapsedHours = Math.floor(elapsedMinutes / 60);
const elapsedDays = Math.floor(elapsedHours / 24);
if (elapsedSeconds < 60) {
return "Just now";
} else if (elapsedMinutes < 60) {
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? 's' : ''} ago`;
} else if (elapsedHours < 24) {
return `${elapsedHours} hour${elapsedHours > 1 ? 's' : ''} ago`;
} else if (elapsedDays < 7) {
return `${elapsedDays} day${elapsedDays > 1 ? 's' : ''} ago`;
} else {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${month}/${day}/${year}`;
}
}

27
src/utils/tags.ts Normal file
View File

@@ -0,0 +1,27 @@
import { TagItem } from "@mutinywallet/mutiny-wasm"
export type MutinyTagItem = {
id: string,
kind: "Label" | "Contact"
name: string,
last_used_time: bigint,
npub?: string,
ln_address?: string,
lnurl?: string,
}
export const UNKNOWN_TAG: MutinyTagItem = { id: "Unknown", kind: "Label", name: "Unknown", last_used_time: 0n }
export function tagsToIds(tags: MutinyTagItem[]): string[] {
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id)
}
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
// @ts-ignore
// FIXME: make typescript less mad about this
return tag as MutinyTagItem
}
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
return Number(b.last_used_time - a.last_used_time);
}