mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-07-06 09:40:57 +02:00
message prop graph
This commit is contained in:
Generated
+217
-7
@@ -23,6 +23,7 @@
|
||||
"next": "15.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-d3-tree": "^3.6.6",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
@@ -66,6 +67,31 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bkrem/react-transition-group": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@bkrem/react-transition-group/-/react-transition-group-1.3.5.tgz",
|
||||
"integrity": "sha512-lbBYhC42sxAeFEopxzd9oWdkkV0zirO5E9WyeOBxOrpXsf7m30Aj8vnbayZxFOwD9pvUQ2Pheb1gO79s0Qap3Q==",
|
||||
"dependencies": {
|
||||
"chain-function": "^1.0.0",
|
||||
"dom-helpers": "^3.3.1",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.5.6",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"warning": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clickhouse/client": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.2.tgz",
|
||||
@@ -1430,6 +1456,11 @@
|
||||
"integrity": "sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz",
|
||||
"integrity": "sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA=="
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -2442,6 +2473,11 @@
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
|
||||
},
|
||||
"node_modules/chain-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz",
|
||||
"integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg=="
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2498,6 +2534,14 @@
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -2589,6 +2633,120 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-hierarchy": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
|
||||
"integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ=="
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||
"dependencies": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@@ -2722,6 +2880,14 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@@ -2743,6 +2909,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -4309,8 +4483,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -4731,7 +4904,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -5016,7 +5188,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5299,7 +5470,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -5370,6 +5540,34 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-d3-tree": {
|
||||
"version": "3.6.6",
|
||||
"resolved": "https://registry.npmjs.org/react-d3-tree/-/react-d3-tree-3.6.6.tgz",
|
||||
"integrity": "sha512-E9ByUdeqvlxLlF9BSL7KWQH3ikYHtHO+g1rAPcVgj6mu92tjRUCan2AWxoD4eTSzzAATf8BZtf+CXGSoSd6ioQ==",
|
||||
"dependencies": {
|
||||
"@bkrem/react-transition-group": "^1.3.5",
|
||||
"@types/d3-hierarchy": "^1.1.8",
|
||||
"clone": "^2.1.1",
|
||||
"d3-hierarchy": "^1.1.9",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^1.3.7",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"dequal": "^2.0.2",
|
||||
"uuid": "^8.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16.x || 17.x || 18.x || 19.x",
|
||||
"react-dom": "16.x || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-d3-tree/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
@@ -5384,8 +5582,7 @@
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "5.0.0",
|
||||
@@ -5400,6 +5597,11 @@
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -6440,6 +6642,14 @@
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
||||
"integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"next": "15.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-d3-tree": "^3.6.6",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
|
||||
@@ -269,7 +269,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
|
||||
)}
|
||||
{(startExpanded ? messages : messages.toReversed()).map((msg, i) => (
|
||||
<ChatMessageItem
|
||||
key={msg.ingest_timestamp + (msg.origins?.join(',') ?? '') + i}
|
||||
key={`${msg.ingest_timestamp}-${msg.origin_key_path_array?.length || 0}`}
|
||||
msg={msg}
|
||||
showErrorRow={selectedKey.isAllMessages}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore";
|
||||
import Tree from 'react-d3-tree';
|
||||
|
||||
export interface ChatMessage {
|
||||
ingest_timestamp: string;
|
||||
@@ -15,6 +16,17 @@ export interface ChatMessage {
|
||||
origin_key_path_array: Array<[string, string, string]>; // Array of [origin, pubkey, path] tuples
|
||||
}
|
||||
|
||||
interface PathGroup {
|
||||
path: string;
|
||||
pathSlices: string[]; // Includes origin pubkey as the final hop
|
||||
indices: number[];
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
function formatHex(hex: string): string {
|
||||
return hex.replace(/(.{2})/g, "$1 ").trim();
|
||||
}
|
||||
@@ -24,7 +36,7 @@ function formatLocalTime(utcString: string): string {
|
||||
return utcDate.toLocaleString();
|
||||
}
|
||||
|
||||
export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow?: boolean }) {
|
||||
function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow?: boolean }) {
|
||||
const { config } = useConfig();
|
||||
const knownKeys = useMemo(() => [
|
||||
...(config?.meshcoreKeys?.map((k: any) => k.privateKey) || []),
|
||||
@@ -34,6 +46,7 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
const [parsed, setParsed] = useState<any | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [originsExpanded, setOriginsExpanded] = useState(false);
|
||||
const [showGraph, setShowGraph] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -65,27 +78,107 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
return () => { cancelled = true; };
|
||||
}, [msg.encrypted_message, msg.mac, msg.channel_hash, knownKeysString, knownKeys]);
|
||||
|
||||
const originKeyPathArray = msg.origin_key_path_array && msg.origin_key_path_array.length > 0 ? msg.origin_key_path_array : [];
|
||||
const originKeyPathArray = useMemo(() =>
|
||||
msg.origin_key_path_array && msg.origin_key_path_array.length > 0 ? msg.origin_key_path_array : [],
|
||||
[msg.origin_key_path_array]
|
||||
);
|
||||
const originsCount = originKeyPathArray.length;
|
||||
|
||||
const OriginsBox = () => (
|
||||
// Process data for tree visualization
|
||||
const treeData = useMemo(() => {
|
||||
if (!showGraph || originsCount === 0) return null;
|
||||
|
||||
// Group messages by path similarity
|
||||
const pathGroups: PathGroup[] = [];
|
||||
|
||||
originKeyPathArray.forEach(([origin, pubkey, path], index) => {
|
||||
// Parse path into 2-character slices and include pubkey as final hop
|
||||
const pathSlices = path.match(/.{1,2}/g) || [];
|
||||
const pubkeyPrefix = pubkey.substring(0, 2);
|
||||
const fullPathSlices = [...pathSlices, pubkeyPrefix];
|
||||
|
||||
// Find existing group with same path structure
|
||||
const existingGroup = pathGroups.find(group =>
|
||||
group.pathSlices.length === fullPathSlices.length &&
|
||||
group.pathSlices.every((slice, i) => slice === fullPathSlices[i])
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.indices.push(index);
|
||||
} else {
|
||||
pathGroups.push({
|
||||
path: path + pubkeyPrefix,
|
||||
pathSlices: fullPathSlices,
|
||||
indices: [index]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Build tree structure for react-d3-tree
|
||||
const buildTree = (): TreeNode => {
|
||||
const root: TreeNode = { name: "??", children: [] };
|
||||
|
||||
pathGroups.forEach(group => {
|
||||
let currentNode = root;
|
||||
|
||||
group.pathSlices.forEach((slice, level) => {
|
||||
let child = currentNode.children?.find(c => c.name === slice);
|
||||
|
||||
if (!child) {
|
||||
child = { name: slice, children: [] };
|
||||
if (!currentNode.children) currentNode.children = [];
|
||||
currentNode.children.push(child);
|
||||
}
|
||||
|
||||
currentNode = child;
|
||||
});
|
||||
});
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
return buildTree();
|
||||
}, [showGraph, originKeyPathArray, originsCount]);
|
||||
|
||||
const handleOriginsToggle = useCallback(() => {
|
||||
setOriginsExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleGraphToggle = useCallback(() => {
|
||||
setShowGraph(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const OriginsBox = useCallback(() => (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||||
<button
|
||||
onClick={() => setOriginsExpanded(!originsExpanded)}
|
||||
className="flex items-center gap-1 hover:text-gray-800 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<span>Heard {originsCount > 0 ? `${originsCount} repeat${originsCount !== 1 ? 's' : ''}` : '0 repeats'}</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${originsExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleOriginsToggle}
|
||||
className="flex items-center gap-1 hover:text-gray-800 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span>Heard {originsCount > 0 ? `${originsCount} repeat${originsCount !== 1 ? 's' : ''}` : '0 repeats'}</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${originsExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{originsCount > 0 && (
|
||||
<button
|
||||
onClick={handleGraphToggle}
|
||||
className="px-2 py-1 text-xs rounded bg-gray-200 dark:bg-neutral-700 hover:bg-gray-300 dark:hover:bg-neutral-600 transition-colors"
|
||||
>
|
||||
{showGraph ? 'Hide Graph' : 'Show Graph'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{originsExpanded && originsCount > 0 && (
|
||||
<div className="mt-1 p-2 bg-gray-100 dark:bg-neutral-700 rounded text-xs break-all text-gray-800 dark:text-gray-200">
|
||||
{/* List view (original) */}
|
||||
{originKeyPathArray.map(([origin, pubkey, path], index: number) => {
|
||||
// Parse path into 2-character slices
|
||||
const pathSlices = path.match(/.{1,2}/g) || [];
|
||||
@@ -104,7 +197,54 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
), [originsExpanded, originsCount, showGraph, originKeyPathArray, handleOriginsToggle, handleGraphToggle]);
|
||||
|
||||
const GraphView = useCallback(() => (
|
||||
showGraph && originsCount > 0 && (
|
||||
<div key="graph-container" className="mt-2 p-3 bg-gray-100 dark:bg-neutral-700 rounded text-xs text-gray-800 dark:text-gray-200">
|
||||
<div key="graph-title" className="mb-2 font-medium text-gray-700 dark:text-gray-300">Message Propagation Tree</div>
|
||||
<div key="graph-content" className="w-full h-80">
|
||||
{treeData && (
|
||||
<Tree
|
||||
key={`tree-${msg.ingest_timestamp}-${originsCount}`}
|
||||
data={treeData}
|
||||
orientation="vertical"
|
||||
pathFunc="step"
|
||||
translate={{ x: 200, y: 50 }}
|
||||
separation={{ siblings: 1.2, nonSiblings: 1.5 }}
|
||||
nodeSize={{ x: 60, y: 60 }}
|
||||
renderCustomNodeElement={({ nodeDatum, toggleNode }) => {
|
||||
const isRoot = nodeDatum.name === "??";
|
||||
// Check if this node represents an origin pubkey (final 2-char hex from pubkey)
|
||||
const isOriginPubkey = originKeyPathArray.some(([origin, pubkey, path]) => {
|
||||
const pubkeyPrefix = pubkey.substring(0, 2);
|
||||
return nodeDatum.name === pubkeyPrefix;
|
||||
});
|
||||
|
||||
return (
|
||||
<g key={`node-${nodeDatum.name}`}>
|
||||
<circle
|
||||
r={15}
|
||||
fill={isRoot ? "#3b82f6" : "#6b7280"}
|
||||
stroke={isOriginPubkey && !isRoot ? "#10b981" : "none"}
|
||||
strokeWidth={isOriginPubkey && !isRoot ? 2 : 0}
|
||||
/>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
y="5"
|
||||
style={{ fontSize: "12px", fill: "white", fontWeight: "bold", stroke: "none" }}
|
||||
>
|
||||
{nodeDatum.name}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
), [showGraph, originsCount, treeData, msg.ingest_timestamp]);
|
||||
|
||||
if (parsed) {
|
||||
return (
|
||||
@@ -120,6 +260,7 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
<span>{parsed.text}</span>
|
||||
</div>
|
||||
<OriginsBox />
|
||||
<GraphView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -133,6 +274,7 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channel_hash}</span>
|
||||
</div>
|
||||
<OriginsBox />
|
||||
<GraphView />
|
||||
</div>
|
||||
);
|
||||
}else{
|
||||
@@ -148,6 +290,19 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
</div>
|
||||
<div className="w-full h-5 bg-gray-200 dark:bg-neutral-800 rounded animate-pulse my-2" />
|
||||
<OriginsBox />
|
||||
<GraphView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(ChatMessageItem, (prevProps, nextProps) => {
|
||||
// Only re-render if these key properties change
|
||||
return (
|
||||
prevProps.msg.ingest_timestamp === nextProps.msg.ingest_timestamp &&
|
||||
prevProps.msg.encrypted_message === nextProps.msg.encrypted_message &&
|
||||
prevProps.msg.mac === nextProps.msg.mac &&
|
||||
prevProps.msg.channel_hash === nextProps.msg.channel_hash &&
|
||||
prevProps.msg.origin_key_path_array?.length === nextProps.msg.origin_key_path_array?.length &&
|
||||
prevProps.showErrorRow === nextProps.showErrorRow
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user