From c9c55f56c88693c54a67f8ca9d2f6cd9a1f10c80 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Sun, 20 Sep 2026 09:29:07 +0800 Subject: [PATCH] feat: add forward msg --- ui/package.json | 4 +- ui/pnpm-lock.yaml | 346 +++++++++++++++++++++++++- ui/src/app/chats.tsx | 1 - ui/src/app/messages.tsx | 171 +++++++++---- ui/src/app/page.tsx | 3 + ui/src/app/stores.ts | 5 + ui/src/components/forward-dialog.tsx | 100 ++++++++ ui/src/components/ui/dialog.tsx | 154 ++++++++++++ ui/src/components/ui/react-select.tsx | 251 +++++++++++++++++++ ui/src/lib/utils.ts | 16 +- 10 files changed, 990 insertions(+), 61 deletions(-) create mode 100644 ui/src/app/stores.ts create mode 100644 ui/src/components/forward-dialog.tsx create mode 100644 ui/src/components/ui/dialog.tsx create mode 100644 ui/src/components/ui/react-select.tsx diff --git a/ui/package.json b/ui/package.json index 5b9ec13..0afc22d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,9 +21,11 @@ "nanoid": "^6.0.1", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-select": "^5.10.2", "shadcn": "^4.21.0", "tailwindcss": "^4.3.3", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zustand": "^5.0.15" }, "devDependencies": { "@types/node": "^24.13.3", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 66831cf..32256ce 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -41,15 +41,21 @@ importers: react-dom: specifier: ^19.2.8 version: 19.3.0(react@19.3.0) + react-select: + specifier: ^5.10.2 + version: 5.10.2(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) shadcn: specifier: ^4.21.0 - version: 4.21.0(typescript@6.0.3) + version: 4.21.0(babel-plugin-macros@3.1.0)(typescript@6.0.3) tailwindcss: specifier: ^4.3.3 version: 4.3.3 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + zustand: + specifier: ^5.0.15 + version: 5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)) devDependencies: '@types/node': specifier: ^24.13.3 @@ -242,6 +248,47 @@ packages: '@dotenvx/primitives@0.8.0': resolution: {integrity: sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@floating-ui/core@1.8.0': resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} @@ -625,11 +672,19 @@ packages: '@types/node@24.13.4': resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: '@types/react': ^19.3.0 + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} @@ -698,6 +753,10 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -803,6 +862,9 @@ packages: resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -818,6 +880,10 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cosmiconfig@9.0.2: resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} @@ -895,6 +961,9 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -954,6 +1023,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1023,6 +1096,9 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -1097,6 +1173,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.13.8: resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==} engines: {node: '>=16.9.0'} @@ -1139,6 +1218,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -1422,6 +1505,10 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1441,6 +1528,9 @@ packages: resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -1607,9 +1697,16 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1653,6 +1750,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.8: resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} @@ -1677,6 +1777,21 @@ packages: peerDependencies: react: ^19.3.0 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.3.0: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} @@ -1696,6 +1811,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1797,6 +1917,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -1837,6 +1961,13 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + systeminformation@5.33.10: resolution: {integrity: sha512-/NXbMVASt2UbSVgmWto4aBTIAeuYY7zOELpZybmNxqpsrbo5uYHpuEPf5nkyrPng0uG5YOb+Yv6s0FyKY4mDQg==} engines: {node: '>=10.0.0'} @@ -1912,6 +2043,15 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.7.0: resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} peerDependencies: @@ -1991,6 +2131,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + yocto-spinner@1.2.2: resolution: {integrity: sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==} engines: {node: '>=18.19'} @@ -2007,6 +2151,24 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@5.0.15: + resolution: {integrity: sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@babel/code-frame@7.29.7': @@ -2241,6 +2403,70 @@ snapshots: '@dotenvx/primitives@0.8.0': {} + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.29.7 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.3.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.3.0 + optionalDependencies: + '@types/react': 19.3.0 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.3.0)': + dependencies: + react: 19.3.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@floating-ui/core@1.8.0': dependencies: '@floating-ui/utils': 0.2.12 @@ -2512,10 +2738,16 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/parse-json@4.0.2': {} + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 + '@types/react-transition-group@4.4.12(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + '@types/react@19.3.0': dependencies: csstype: 3.2.3 @@ -2561,6 +2793,12 @@ snapshots: atomically@1.7.0: {} + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.7 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + balanced-match@4.0.4: {} baseline-browser-mapping@2.11.23: {} @@ -2658,6 +2896,8 @@ snapshots: content-type@2.1.0: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.2.2: {} @@ -2669,6 +2909,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + cosmiconfig@9.0.2(typescript@6.0.3): dependencies: env-paths: 2.2.1 @@ -2698,7 +2946,9 @@ snapshots: dependencies: ms: 2.1.3 - dedent@1.7.2: {} + dedent@1.7.2(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 deepmerge@4.3.1: {} @@ -2719,6 +2969,11 @@ snapshots: diff@8.0.4: {} + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + dot-prop@6.0.1: dependencies: is-obj: 2.0.0 @@ -2767,6 +3022,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + esprima@4.0.1: {} etag@1.8.1: {} @@ -2884,6 +3141,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-root@1.1.0: {} + find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -2950,6 +3209,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hono@4.13.8: {} http-errors@2.0.1: @@ -2983,6 +3246,10 @@ snapshots: is-arrayish@0.2.1: {} + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -3175,6 +3442,10 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3191,6 +3462,8 @@ snapshots: media-typer@1.1.1: {} + memoize-one@6.0.0: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -3345,8 +3618,12 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3383,6 +3660,12 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 @@ -3409,6 +3692,34 @@ snapshots: react: 19.3.0 scheduler: 0.28.0 + react-is@16.13.1: {} + + react-select@5.10.2(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@babel/runtime': 7.29.7 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0) + '@floating-ui/dom': 1.8.0 + '@types/react-transition-group': 4.4.12(@types/react@19.3.0) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react-transition-group: 4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.3.0)(react@19.3.0) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-transition-group@4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@babel/runtime': 7.29.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react@19.3.0: {} recast@0.23.21: @@ -3425,6 +3736,13 @@ snapshots: resolve-from@4.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -3504,7 +3822,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.21.0(typescript@6.0.3): + shadcn@4.21.0(babel-plugin-macros@3.1.0)(typescript@6.0.3): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.8 @@ -3517,7 +3835,7 @@ snapshots: cn: 0.2.6 commander: 14.0.3 cosmiconfig: 9.0.2(typescript@6.0.3) - dedent: 1.7.2 + dedent: 1.7.2(babel-plugin-macros@3.1.0) deepmerge: 4.3.1 diff: 8.0.4 execa: 9.6.1 @@ -3594,6 +3912,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.5.7: {} + source-map@0.6.1: {} statuses@2.0.2: {} @@ -3626,6 +3946,10 @@ snapshots: strip-final-newline@4.0.0: {} + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + systeminformation@5.33.10: {} tailwindcss@4.3.3: {} @@ -3684,6 +4008,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + use-isomorphic-layout-effect@1.2.1(@types/react@19.3.0)(react@19.3.0): + dependencies: + react: 19.3.0 + optionalDependencies: + '@types/react': 19.3.0 + use-sync-external-store@1.7.0(react@19.3.0): dependencies: react: 19.3.0 @@ -3723,6 +4053,8 @@ snapshots: yallist@3.1.1: {} + yaml@1.10.3: {} + yocto-spinner@1.2.2: dependencies: yoctocolors: 2.2.0 @@ -3734,3 +4066,9 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zustand@5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)): + optionalDependencies: + '@types/react': 19.3.0 + react: 19.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) diff --git a/ui/src/app/chats.tsx b/ui/src/app/chats.tsx index 87145c3..d91ac96 100644 --- a/ui/src/app/chats.tsx +++ b/ui/src/app/chats.tsx @@ -34,7 +34,6 @@ export default function ChatsSection({ selected, onSelect }: Props) { useEffect(() => { // Refetch chat list when returning to main screen if (!selected) { - console.log("test"); refetch(); } }, [selected]); diff --git a/ui/src/app/messages.tsx b/ui/src/app/messages.tsx index 2457676..8800a97 100644 --- a/ui/src/app/messages.tsx +++ b/ui/src/app/messages.tsx @@ -7,7 +7,9 @@ import { CheckCheckIcon, CheckIcon, ChevronDown, + CornerUpRightIcon, Loader2Icon, + MousePointer2Icon, PaperclipIcon, PhoneCallIcon, SendIcon, @@ -32,9 +34,11 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; +import { forwardDlg } from "./stores"; dayjs.extend(relativeTime); @@ -53,6 +57,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { const { data: chats } = useChats(); const { data: contacts } = useContacts(); const [search, setSearch] = useState(""); + const [selectedIds, setSelectedIds] = useState | null>(null); const { data: messages, @@ -83,7 +88,10 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { }); useEffect(() => { - if (chatId) msgContainerRef.current.scrollTop = 0; + if (chatId) { + msgContainerRef.current.scrollTop = 0; + setSelectedIds(null); + } }, [chatId]); useMessageEvent((data) => { @@ -163,11 +171,23 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { }); }, [messages, contacts, search]); - async function onDeleteMsg(id: string) { + function onSelectMsg(id: string) { + setSelectedIds((s) => { + const set = new Set(s || []); + if (!set.has(id)) { + set.add(id); + } else { + set.delete(id); + } + return !set.size ? null : set; + }); + } + + async function onDeleteMsg(id: string | string[]) { if (!window.confirm("Delete this message?")) return; try { await client.exec( - `return WPP.chat.deleteMessage('${chatId}', '${id}', true, true);`, + `return WPP.chat.deleteMessage('${chatId}', ${JSON.stringify(id)}, true, true);`, ); refetch(); } catch (err) { @@ -192,63 +212,82 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { className={`flex min-w-0 h-full flex-1 flex-col bg-muted ${chatId ? "flex" : "hidden md:flex"}`} >
-
- + {selectedIds?.size ? ( +
+ +

{`${selectedIds.size} selected`}

+
- + + +
+ ) : ( +
+ -
-

- {chat?.formattedTitle || chat?.name} -

+ - {/*

+

+

+ {chat?.formattedTitle || chat?.name} +

+ + {/*

{selected.online ? "Online now" : "Active recently"}

*/} -
+
- -
- {/*
- - - -
*/} + +
+ )}
{/*
@@ -261,9 +300,20 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { id={item.id.replace(/^(?:false_|true_)/, "")} key={`${item.timestamp}-${index}`} className={cn( - "mb-3 flex", + "flex px-4 sm:px-8 py-1.5", item.fromMe ? "justify-end" : "justify-start", + selectedIds != null && "cursor-pointer hover:bg-[#26855c]/10", + selectedIds?.has(item.id) && "bg-[#26855c]/30!", )} + role={selectedIds != null ? "button" : undefined} + onClick={ + selectedIds != null + ? (e) => { + if (e.target !== e.currentTarget) return; + onSelectMsg(item.id); + } + : undefined + } >
} > + onSelectMsg(item.id)}> + + Select + + + forwardDlg.onOpen({ messageIds: [item.id] }) + } + > + + Forward + + onDeleteMsg(item.id)}> Delete diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 7f74329..9db58e1 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -3,6 +3,7 @@ import DevicesSection from "./devices"; import { useLocalStorage } from "@/hooks/useLocalStorage"; import { useHashRouter } from "@/hooks/useHashRouter"; import MessagesSection from "./messages"; +import ForwardDialog from "@/components/forward-dialog"; export default function Page() { const router = useHashRouter(); @@ -75,6 +76,8 @@ export default function Page() { {chatId ? : null} {/*
*/} + + ); } diff --git a/ui/src/app/stores.ts b/ui/src/app/stores.ts new file mode 100644 index 0000000..8c31608 --- /dev/null +++ b/ui/src/app/stores.ts @@ -0,0 +1,5 @@ +import { createDisclosure } from "@/lib/utils"; + +export const forwardDlg = createDisclosure<{ + messageIds: string[]; +}>(); diff --git a/ui/src/components/forward-dialog.tsx b/ui/src/components/forward-dialog.tsx new file mode 100644 index 0000000..06f14d7 --- /dev/null +++ b/ui/src/components/forward-dialog.tsx @@ -0,0 +1,100 @@ +import { forwardDlg } from "@/app/stores"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "./ui/button"; +import { useChats } from "@/hooks/useChats"; +import Select, { type Option } from "@/components/ui/react-select"; +import { useEffect, useMemo, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { useWAClient } from "@/app/context"; +import { Loader2Icon } from "lucide-react"; + +export default function ForwardDialog() { + const { data, isOpen } = forwardDlg.useStore(); + const { client } = useWAClient(); + const { data: chats } = useChats(); + + const [selectedChat, setSelectedChat] = useState([]); + const fwd = useMutation({ + mutationFn: async (payload: { chatIds: string[]; msgIds: string[] }) => { + const { chatIds, msgIds } = payload; + for (const chatId of chatIds) { + await client.exec( + `return WPP.chat.forwardMessages('${chatId}', ${JSON.stringify(msgIds)});`, + ); + } + }, + onSuccess() { + alert("Message forwarded!"); + forwardDlg.onClose(); + }, + onError(err) { + alert(err.message); + }, + }); + + useEffect(() => { + if (isOpen) setSelectedChat([]); + }, [isOpen]); + + const chatOptions = useMemo(() => { + return (chats || []).map((chat) => ({ + value: chat.id, + label: chat.formattedTitle || chat.name, + })); + }, [chats]); + + function onSubmit() { + if ( + fwd.isPending || + !data?.messageIds?.length || + !window.confirm("Send message?") + ) + return; + + fwd.mutate({ + chatIds: selectedChat.map((i) => i.value), + msgIds: data!.messageIds, + }); + } + + return ( + + + + Forward Messages + + {data?.messageIds.length} messages + + + +
+