diff --git a/package-lock.json b/package-lock.json index ce721b4a..2f4289da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^2.2.8", "@hookform/resolvers": "^3.3.4", + "@nethesis/chat-island": "^0.1.20", "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", @@ -2524,6 +2525,22 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@nethesis/chat-island": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@nethesis/chat-island/-/chat-island-0.1.20.tgz", + "integrity": "sha512-KHsqAz7mVxSVwl4DKRWKYEEfHmPN+ch/KZgvP/1nC/mVCreTo0t2Tjfu9onYIUWmj7OmNJvKIKhbHrNtys86eg==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@xmpp/client": "^0.14.0", + "use-sync-external-store": "^1.2.0", + "zustand": "^4.5.7" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + } + }, "node_modules/@nethesis/nethesis-brands-svg-icons": { "version": "6.2.1", "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#dbcf431acf0e504145486c9ac70f14946279e2e7", @@ -4456,6 +4473,401 @@ "node": ">=10.0.0" } }, + "node_modules/@xmpp/base64": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/base64/-/base64-0.14.0.tgz", + "integrity": "sha512-tz2LuzLMtjGSVVeuXDnDw19H+uOrqhrRFcQKJ3THV0HVjerDewT8WKXdrtBVSQ7n4Ue0k+awYcIE7qDD9rwMMQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/client": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/client/-/client-0.14.0.tgz", + "integrity": "sha512-Y6k77iifYOGuWncOLRr6dVmbsQvcsIFjYI9pXhkSzQDDNnd3hVJGgo/Xx+W/0/o1+kg/tPm/NwX5Yq77NTd9xA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/client-core": "^0.14.0", + "@xmpp/iq": "^0.14.0", + "@xmpp/middleware": "^0.14.0", + "@xmpp/reconnect": "^0.14.0", + "@xmpp/resolve": "^0.14.0", + "@xmpp/resource-binding": "^0.14.0", + "@xmpp/sasl": "^0.14.0", + "@xmpp/sasl-anonymous": "^0.14.0", + "@xmpp/sasl-ht-sha-256-none": "^0.14.0", + "@xmpp/sasl-plain": "^0.14.0", + "@xmpp/sasl-scram-sha-1": "^0.14.0", + "@xmpp/sasl2": "^0.14.0", + "@xmpp/starttls": "^0.14.0", + "@xmpp/stream-features": "^0.14.0", + "@xmpp/stream-management": "^0.14.0", + "@xmpp/tcp": "^0.14.0", + "@xmpp/tls": "^0.14.0", + "@xmpp/websocket": "^0.14.0", + "saslmechanisms": "^0.1.1" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/client-core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/client-core/-/client-core-0.14.0.tgz", + "integrity": "sha512-fW0C6vn4y8Jp1g8uBmCETuOmEDXpjFc/mfY8P0J88nhK2AdPOB5m0ntz0yyAXcPd16smnHVckiOsk9qBjF9dDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/connection": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/jid": "^0.14.0", + "@xmpp/sasl": "^0.14.0", + "@xmpp/xml": "^0.14.0", + "saslmechanisms": "^0.1.1" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/connection": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/connection/-/connection-0.14.0.tgz", + "integrity": "sha512-VRnukXwiXWCsRVQVISUiAxHHgRWH37nxdUaz50o0+cUAYyqtlHIJ2zy+ZHMlxkEWtl+XciNLxwkJKbypJFQheQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/error": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/jid": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/connection-tcp": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/connection-tcp/-/connection-tcp-0.14.0.tgz", + "integrity": "sha512-gvYyUghuCHW0qalEYaZvbHeTnEoXc+hCCWP/qvoVGjjSQNwkYRUsy1/8RP/sA6RlQ2sR0nhpkJkJE6K/4rsFIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/connection": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/error": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/error/-/error-0.14.0.tgz", + "integrity": "sha512-b4W/MwAZl8basfmYhcemK3De92xBOiSrIz0VY2cnN2ss1VMsa4d32GuaJGBIpa4ulN/2DFb3vPCeTn5jXvHAJw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/events": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/events/-/events-0.14.0.tgz", + "integrity": "sha512-6mKRIEi69sYr8H9SrkJKaXobOgCq/sU6/pKTQS5cuQZCy0qLuyx9uttkOyAYmtUbVD+HSrXYS72KhMpdQQh88Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "events": "^3.3.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/id": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/id/-/id-0.14.0.tgz", + "integrity": "sha512-0n8OFYPWkBYDi5fHGiJT6SLg9ncOflTmINzjHzt5A3NxEMVrYmqxbRB44u3NLMk1gBNqGtEkc2wl0WHjS0tHWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/iq": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/iq/-/iq-0.14.0.tgz", + "integrity": "sha512-0r3QVKR4XAvkZ4shQwPBkSM21sSfj26Cg8+AYBkd0BKJTde7mRQCISBNidt1xiXA5VPH1+6Qx7fmtPzk3uel+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/events": "^0.14.0", + "@xmpp/id": "^0.14.0", + "@xmpp/middleware": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/jid": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/jid/-/jid-0.14.0.tgz", + "integrity": "sha512-ggiNgjblkeHPbHJ7JLOMIqxc1qQQt0gV+LhXI16afib2wLr120YgvRChY04gjkvQ+IF2sGz7mzPvcAukTmNGHA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/middleware": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/middleware/-/middleware-0.14.0.tgz", + "integrity": "sha512-UGm7ed5NEMapE/z0jqY5daGSBqto/iciDjtyncoXHZdOnR4iFYFF9gSO/65Z1HsL2pbHQpcK0WS+Kn+FGOxPTw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/error": "^0.14.0", + "@xmpp/jid": "^0.14.0", + "@xmpp/xml": "^0.14.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/reconnect": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/reconnect/-/reconnect-0.14.0.tgz", + "integrity": "sha512-XaaT3KrFLf1ZfYZCIx11zauT8bIVXRz9FMaGj4mofq1tC8SiKvIIqJXSgrouhkibb9vPeM9XSiGkH47B/1hc6g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/events": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/resolve": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/resolve/-/resolve-0.14.0.tgz", + "integrity": "sha512-S4Rhupb2zS4EtT4EFUJLPl2pPSFPfrjp/1bxPil+zBt9sNcbrvIwSNaYeQAAfLD/yYb8/DhaxC/blvdHyM1Xzw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/events": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/resource-binding": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/resource-binding/-/resource-binding-0.14.0.tgz", + "integrity": "sha512-b8EyHUOpkAS25b6cpOQ8fv7cCLfOpJq4dkgs6Td0MdODMmcfWEh38siZKaP0OoNmYWZCxBSANdsBmeidozJxqA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl/-/sasl-0.14.0.tgz", + "integrity": "sha512-C6mWvtRhJCCjv0Q6uuc3FUuDdgUhnX7lCF9afmNT5umy1XB0l1iRmBJYTriSHNBwgErM3i3mydafeRjpuQfP5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/base64": "^0.14.0", + "@xmpp/error": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl-anonymous": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl-anonymous/-/sasl-anonymous-0.14.0.tgz", + "integrity": "sha512-lZ8jaYuKBpfQ2T5Bv+7yxII9OduV1a+mlNkKtG94WchrVzR8fU6VpbTrez88KajA42ngcVCtiO3i6tPepYyK8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "sasl-anonymous": "^0.1.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl-ht-sha-256-none": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl-ht-sha-256-none/-/sasl-ht-sha-256-none-0.14.0.tgz", + "integrity": "sha512-LmMSzX35bI/V2lV26k/hYPm8Pn7InsBEX72b86+ugIBQpzD8/jmB1F/Mfm5vrbWkMLl2KWe61gE+L/p0mvLZdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl-plain": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl-plain/-/sasl-plain-0.14.0.tgz", + "integrity": "sha512-TGQX6gsCi6LP2lPNkkd+HoZ1ynCesRMQrtJrWbDAa6oMyCwie9xuBcG/5lDJ5fn03G7xxRh1YSQp+x8yFK+q4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "sasl-plain": "^0.1.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl-scram-sha-1": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl-scram-sha-1/-/sasl-scram-sha-1-0.14.0.tgz", + "integrity": "sha512-0ZrsMDx9Haou2yTCzDCwvMsYPvHCxF2bkpyBuiPoYAOWRsIQH1TssZK3d88N7wSvvojter905DNwLPizdaF3og==", + "dev": true, + "license": "ISC", + "dependencies": { + "sasl-scram-sha-1": "^1.3.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/sasl2": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/sasl2/-/sasl2-0.14.0.tgz", + "integrity": "sha512-OcDIDr9xwiOO58yqr6hj0eh9CfKOvwBbNDNhC0wByA07cwOsoojLb1dv3WR7pKk1cETv6wHWQ1o/leRoDB/Yzg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/base64": "^0.14.0", + "@xmpp/error": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/jid": "^0.14.0", + "@xmpp/sasl": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@xmpp/starttls": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/starttls/-/starttls-0.14.0.tgz", + "integrity": "sha512-n21Oy5pyD6Cipo96SAVI1ASx/5qo/z3W7J1TWZSG3/gGgrTaCB+VSV0xI+FqRexVJysg3lw3cBidYQL+Lp/pHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/events": "^0.14.0", + "@xmpp/tls": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/stream-features": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/stream-features/-/stream-features-0.14.0.tgz", + "integrity": "sha512-XoogP53qv1lzq/TNnzydT0KmmDKsJ7vpbV1c33fcuBDY0LCmPWA2U5th+Atn3JjEMlyznpVDFco9BF1NgFTHzg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/stream-management": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/stream-management/-/stream-management-0.14.0.tgz", + "integrity": "sha512-i7qLB8KXzLtm2TZXgtVlBprI4Vm8lykJ7Gth2TFavBTky+PQFNr2BH4lGxxvZ7/WkYbhx2jsmDT9JjI7sUCRdw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/error": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/time": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/tcp": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/tcp/-/tcp-0.14.0.tgz", + "integrity": "sha512-vJb5Y60ub+YClK01l+653+qpIe6vedw/82szuB4IU50eTDTL8voer6eGK7AMJOJzoZzRofbLh8Ow5txAyZ8vXw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/connection-tcp": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/time": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/time/-/time-0.14.0.tgz", + "integrity": "sha512-KlOLwZXXYrRiIZ+Lg0Rg+iDxIBLfCgVQOkIaYG2nieLVtbrHItI5gv2eI2I6m0cPsX8LajGkCpvQTqUCNZclag==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/tls": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/tls/-/tls-0.14.0.tgz", + "integrity": "sha512-nY/nHlHYgs3i2+xwt/5Ff3J4aCdZADdTOH+NwdfgqBIK9kWDJPW43nnIrbrZgmUL3YOjW5sb1lUI20bFUJACww==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/connection": "^0.14.0", + "@xmpp/connection-tcp": "^0.14.0", + "@xmpp/events": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/websocket": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/websocket/-/websocket-0.14.0.tgz", + "integrity": "sha512-mRjpRHtOujrPIT7I/X2xvn8w6X/7J8gGPfGIKNGLJHBQu1OlT5noUd++mslCAcGdCOf5TGwtJvE7a8CPtSnTFg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/connection": "^0.14.0", + "@xmpp/events": "^0.14.0", + "@xmpp/xml": "^0.14.0" + }, + "engines": { + "node": ">= 20.10" + } + }, + "node_modules/@xmpp/xml": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xmpp/xml/-/xml-0.14.0.tgz", + "integrity": "sha512-1rrj3SaIM51wtJUn1l2n4FK2e1QLfEmma9bsD1utaaosA8elpCyWi12QDPfaYoTyWSXNvQ7mGzvFevUJUs1mcQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@xmpp/events": "^0.14.0", + "ltx": "^3.1.2" + }, + "engines": { + "node": ">= 20.10" + } + }, "node_modules/abbrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", @@ -9894,6 +10306,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true, + "license": "MIT" + }, "node_modules/koffi": { "version": "2.16.3", "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.3.tgz", @@ -10039,6 +10458,16 @@ "yallist": "^3.0.2" } }, + "node_modules/ltx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/ltx/-/ltx-3.1.2.tgz", + "integrity": "sha512-tFSKojN92FqNK6eRTmKK/ROUTUYVWKAxgohz523TPhF1G3nR3DXQS/I7/705rEPrDSloKDgMdRlh0qgMFQoVYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.4.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -12526,6 +12955,40 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "node_modules/sasl-anonymous": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-anonymous/-/sasl-anonymous-0.1.0.tgz", + "integrity": "sha512-x+0sdsV0Gie2EexxAUsx6ZoB+X6OCthlNBvAQncQxreEWQJByAPntj0EAgTlJc2kZicoc+yFzeR6cl8VfsQGfA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/sasl-plain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sasl-plain/-/sasl-plain-0.1.0.tgz", + "integrity": "sha512-X8mCSfR8y0NryTu0tuVyr4IS2jBunBgyG+3a0gEEkd0nlHGiyqJhlc4EIkzmSwaa7F8S4yo+LS6Cu5qxRkJrmg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/sasl-scram-sha-1": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sasl-scram-sha-1/-/sasl-scram-sha-1-1.4.0.tgz", + "integrity": "sha512-kdP8uAFkak8flnmKTldWnQ98IuC15ASdKxNHWwbqexRj7szHs7y+JEtigLkPoq9gnp/SAxxU4O2huL8OLqSr9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/saslmechanisms": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/saslmechanisms/-/saslmechanisms-0.1.1.tgz", + "integrity": "sha512-pVlvK5ysevz8MzybRnDIa2YMxn0OJ7b9lDiWhMoaKPoJ7YkAg/7YtNjUgaYzElkwHxsw8dBMhaEn7UP6zxEwPg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", diff --git a/package.json b/package.json index 64cbc5e1..7e72b91e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^2.2.8", "@hookform/resolvers": "^3.3.4", + "@nethesis/chat-island": "^0.1.20", "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 5caee65b..406d1ef7 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -695,6 +695,19 @@ "Z-A label": "Z-A", "EXT label": "Extension" }, + "Chat": { + "No longer active": "No longer active", + "Messages": "Messages", + "Chat": "Chat", + "New chat": "New chat", + "No conversations": "No conversations", + "You": "You", + "Delete": "Delete", + "Leave": "Leave", + "Delete confirm": "Delete the conversation with \"{{name}}\"? Your copy of the messages is removed.", + "Delete group confirm": "Delete the group \"{{name}}\" for everyone? Its messages are removed for all members.", + "Leave confirm": "Leave the group \"{{name}}\"? You stop receiving its messages and your copy is removed." + }, "LastCalls": { "Last calls": "Last calls", "No calls": "No calls", @@ -988,7 +1001,8 @@ }, "CommandBar": { "Placeholder": "Search contact or dial number...", - "Call": "Call" + "Call": "Call", + "Chat hint": "Chat (Ctrl/⌘+Enter)" }, "Errors": { "browser_permissions": "Browser permissions error", diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index 6cd3bb1b..61f7e7e6 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -695,6 +695,19 @@ "Z-A label": "Z-A", "EXT label": "Interno" }, + "Chat": { + "No longer active": "Non più attivo", + "Messages": "Messaggi", + "Chat": "Chat", + "New chat": "Nuova chat", + "No conversations": "Nessuna conversazione", + "You": "Tu", + "Delete": "Elimina", + "Leave": "Abbandona", + "Delete confirm": "Eliminare la conversazione con \"{{name}}\"? La tua copia dei messaggi viene rimossa.", + "Delete group confirm": "Eliminare il gruppo \"{{name}}\" per tutti? I suoi messaggi vengono rimossi per tutti i membri.", + "Leave confirm": "Abbandonare il gruppo \"{{name}}\"? Non riceverai più i suoi messaggi e la tua copia viene rimossa." + }, "LastCalls": { "Last calls": "Ultime chiamate", "No calls": "Nessuna chiamata", @@ -988,7 +1001,8 @@ }, "CommandBar": { "Placeholder": "Cerca contatto o componi numero...", - "Call": "Chiama" + "Call": "Chiama", + "Chat hint": "Chat (Ctrl/⌘+Invio)" }, "Errors": { "browser_permissions": "Errore nei permessi del browser", diff --git a/src/main/classes/controllers/AppController.ts b/src/main/classes/controllers/AppController.ts index 83756f98..76f11a21 100644 --- a/src/main/classes/controllers/AppController.ts +++ b/src/main/classes/controllers/AppController.ts @@ -1,6 +1,7 @@ import { LoginController } from './LoginController' import { NethLinkController } from './NethLinkController' import { PhoneIslandController } from './PhoneIslandController' +import { ChatIslandController } from './ChatIslandController' import { TrayController } from './TrayController' import { Log } from '@shared/utils/logger' import { DevToolsController } from './DevToolsController' @@ -32,6 +33,11 @@ export class AppController { Log.warning('unable to correctly close the PhoneIslandController:', e) } } + try { + await ChatIslandController.instance?.safeQuit() + } catch (e) { + Log.warning('unable to correctly close the ChatIslandController:', e) + } if (NethLinkController.instance) { try { await NethLinkController.instance.safeQuit() diff --git a/src/main/classes/controllers/ChatIslandController.ts b/src/main/classes/controllers/ChatIslandController.ts new file mode 100644 index 00000000..74b9ee8e --- /dev/null +++ b/src/main/classes/controllers/ChatIslandController.ts @@ -0,0 +1,75 @@ +import { ChatIslandWindow } from '../windows' +import { Log } from '@shared/utils/logger' +import { screen } from 'electron' + +export class ChatIslandController { + static instance: ChatIslandController | undefined + window: ChatIslandWindow + private wantFocus = false + // Distance from the work area's bottom-right corner, kept after a drag. + private anchor = { right: 0, bottom: 0 } + + constructor() { + ChatIslandController.instance = this + this.window = new ChatIslandWindow() + } + + // Sized to the island, grown from its bottom-right corner; hidden when there is nothing to show. + resize(size: { w: number; h: number }) { + try { + const window = this.window.getWindow() + if (!window) return + if (size.w < 8 || size.h < 8) { + window.hide() + return + } + const { x, y, width, height } = this.workArea() + const w = Math.min(Math.ceil(size.w), width) + const h = Math.min(Math.ceil(size.h), height) + window.setBounds({ + x: Math.max(x, x + width - this.anchor.right - w), + y: Math.max(y, y + height - this.anchor.bottom - h), + width: w, + height: h, + }) + if (this.wantFocus) { + this.wantFocus = false + window.show() + window.focus() + } else if (!window.isVisible()) window.showInactive() + } catch (e) { + Log.warning('error during resizing ChatIslandWindow:', e) + } + } + + private workArea() { + const window = this.window.getWindow() + return window?.isVisible() + ? screen.getDisplayMatching(window.getBounds()).workArea + : screen.getPrimaryDisplay().workArea + } + + // After a drag: the new corner is where the island grows from. + keepPosition() { + const window = this.window.getWindow() + if (!window) return + const b = window.getBounds() + const { x, y, width, height } = this.workArea() + this.anchor = { + right: Math.max(0, x + width - b.x - b.width), + bottom: Math.max(0, y + height - b.y - b.height), + } + } + + // Focus once the island has sized the window for what it opens. + focus() { + const window = this.window.getWindow() + if (window?.isVisible()) window.focus() + this.wantFocus = true + } + + async safeQuit() { + ChatIslandController.instance = undefined + await this.window.quit(true) + } +} diff --git a/src/main/classes/controllers/index.ts b/src/main/classes/controllers/index.ts index fc8760ba..10e6d278 100644 --- a/src/main/classes/controllers/index.ts +++ b/src/main/classes/controllers/index.ts @@ -4,3 +4,4 @@ export * from './PhoneIslandController' export * from './TrayController' export * from './DevToolsController' export * from './CommandBarController' +export * from './ChatIslandController' diff --git a/src/main/classes/windows/ChatIslandWindow.ts b/src/main/classes/windows/ChatIslandWindow.ts new file mode 100644 index 00000000..ae8d6ca4 --- /dev/null +++ b/src/main/classes/windows/ChatIslandWindow.ts @@ -0,0 +1,42 @@ +import { PAGES } from '@shared/types' +import { BaseWindow } from './BaseWindow' + +// Transparent, always on top: the chat stays in view with NethLink closed. +export class ChatIslandWindow extends BaseWindow { + constructor() { + super(PAGES.CHATISLAND, { + width: 440, + height: 600, + show: false, + focusable: true, + acceptFirstMouse: true, + fullscreenable: false, + autoHideMenuBar: true, + closable: false, + alwaysOnTop: true, + minimizable: false, + maximizable: false, + movable: false, + resizable: false, + skipTaskbar: true, + roundedCorners: false, + parent: undefined, + transparent: true, + hiddenInMissionControl: true, + hasShadow: false, + fullscreen: false, + enableLargerThanScreen: false, + frame: false, + thickFrame: false, + trafficLightPosition: { x: 0, y: 0 }, + webPreferences: { + nodeIntegration: true, + backgroundThrottling: false, + }, + }) + + this.addOnBuildListener(() => { + this.getWindow()?.setAlwaysOnTop(true, 'screen-saver') + }) + } +} diff --git a/src/main/classes/windows/index.ts b/src/main/classes/windows/index.ts index 7cc92b28..2c435f92 100644 --- a/src/main/classes/windows/index.ts +++ b/src/main/classes/windows/index.ts @@ -4,3 +4,4 @@ export * from './NethLinkWindow' export * from './PhoneIslandWindow' export * from './DevToolsWindow' export * from './CommandBarWindow' +export * from './ChatIslandWindow' diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index dffaeaa3..ad5bf294 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -2,6 +2,7 @@ import { AccountController, DevToolsController } from '@/classes/controllers' import { LoginController } from '@/classes/controllers/LoginController' import { PhoneIslandController } from '@/classes/controllers/PhoneIslandController' import { CommandBarController } from '@/classes/controllers/CommandBarController' +import { ChatIslandController } from '@/classes/controllers/ChatIslandController' import { IPC_EVENTS } from '@shared/constants' import { Account, OnDraggingWindow, PAGES } from '@shared/types' import { @@ -240,6 +241,9 @@ export function registerIpcEvents() { const cursorPosition = screen.getCursorScreenPoint() const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y + if (window.title === PAGES.CHATISLAND) { + ChatIslandController.instance?.keepPosition() + } if (window.title === PAGES.PHONEISLAND) { const bounds = window.getBounds() Log.info( @@ -279,6 +283,19 @@ export function registerIpcEvents() { }, false, ) + } else if (window.title === PAGES.CHATISLAND) { + // Kept inside the screen under the cursor, like the phone island. + const { width, height } = window.getBounds() + const wa = screen.getDisplayNearestPoint(cursorPosition).workArea + window.setBounds( + { + x: Math.min(Math.max(newX, wa.x), wa.x + wa.width - width), + y: Math.min(Math.max(newY, wa.y), wa.y + wa.height - height), + width, + height, + }, + false, + ) } else { const [w, h] = window.getContentSize() window.setBounds( @@ -349,6 +366,48 @@ export function registerIpcEvents() { clipboard.writeText(text) }) + // Chat island window: size, and island events between it and NethLink. + ipcMain.on(IPC_EVENTS.CHAT_ISLAND_RESIZE, (_, size) => { + ChatIslandController.instance?.resize(size) + }) + ipcMain.on(IPC_EVENTS.CHAT_TO_ISLAND, (_, name, detail) => { + try { + ChatIslandController.instance?.window.emit( + IPC_EVENTS.CHAT_TO_ISLAND, + name, + detail, + ) + if (name === 'chat-island-open' || name === 'chat-island-new') { + ChatIslandController.instance?.focus() + } + } catch (e) { + Log.warning('chat island unreachable:', e) + } + }) + ipcMain.on(IPC_EVENTS.CHAT_FROM_ISLAND, (_, name, detail) => { + try { + NethLinkController.instance?.window.emit( + IPC_EVENTS.CHAT_FROM_ISLAND, + name, + detail, + ) + } catch (e) { + Log.warning('nethlink unreachable:', e) + } + // The command bar only needs to know whether the chat is there. + if (name === 'chat-island-status') { + try { + CommandBarController.instance?.window.emit( + IPC_EVENTS.CHAT_FROM_ISLAND, + name, + detail, + ) + } catch (e) { + Log.warning('command bar unreachable:', e) + } + } + }) + ipcMain.on(IPC_EVENTS.PHONE_ISLAND_RESIZE, (_, size) => { PhoneIslandController.instance.resize(size) }) diff --git a/src/main/main.ts b/src/main/main.ts index d8827f12..a60d2f86 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,6 +21,7 @@ import { import { AccountController } from './classes/controllers' import { PhoneIslandController } from './classes/controllers/PhoneIslandController' import { CommandBarController } from './classes/controllers/CommandBarController' +import { ChatIslandController } from './classes/controllers/ChatIslandController' import { Account, AuthAppData, @@ -135,6 +136,7 @@ function startup() { } await PhoneIslandController.instance.logout() + await ChatIslandController.instance?.safeQuit() NethLinkController.instance.logout() AccountController.instance.logout() await delay(1000) @@ -759,6 +761,16 @@ async function attachProtocolListeners() { } break } + case 'chat': { + // The chat opens in its own window, not in NethLink. + ipcMain.emit( + IPC_EVENTS.CHAT_TO_ISLAND, + undefined, + to ? 'chat-island-open' : 'chat-island-new', + to ? { username: to } : undefined, + ) + return new Promise((resolve) => resolve) + } default: Log.warning('Unknown nethlink action:', action) } @@ -960,6 +972,7 @@ async function createNethLink(show: boolean = true) { if (show) NethLinkController.instance.show() await delay(1000) new PhoneIslandController() + new ChatIslandController() checkForUpdate() const account = store.get('account') as Account if (account) { diff --git a/src/renderer/public/locales/en/translations.json b/src/renderer/public/locales/en/translations.json index fbc96a82..de902266 100644 --- a/src/renderer/public/locales/en/translations.json +++ b/src/renderer/public/locales/en/translations.json @@ -695,6 +695,19 @@ "Z-A label": "Z-A", "EXT label": "Extension" }, + "Chat": { + "No longer active": "No longer active", + "Messages": "Messages", + "Chat": "Chat", + "New chat": "New chat", + "No conversations": "No conversations", + "You": "You", + "Delete": "Delete", + "Leave": "Leave", + "Delete confirm": "Delete the conversation with \"{{name}}\"? Your copy of the messages is removed.", + "Delete group confirm": "Delete the group \"{{name}}\" for everyone? Its messages are removed for all members.", + "Leave confirm": "Leave the group \"{{name}}\"? You stop receiving its messages and your copy is removed." + }, "LastCalls": { "Last calls": "Last calls", "No calls": "No calls", @@ -989,7 +1002,8 @@ }, "CommandBar": { "Placeholder": "Search contact or dial number...", - "Call": "Call" + "Call": "Call", + "Chat hint": "Chat (Ctrl/⌘+Enter)" }, "Errors": { "browser_permissions": "Browser permissions error", diff --git a/src/renderer/public/locales/it/translations.json b/src/renderer/public/locales/it/translations.json index da90b8f8..0b9e7fa0 100644 --- a/src/renderer/public/locales/it/translations.json +++ b/src/renderer/public/locales/it/translations.json @@ -695,6 +695,19 @@ "Z-A label": "Z-A", "EXT label": "Interno" }, + "Chat": { + "No longer active": "Non più attivo", + "Messages": "Messaggi", + "Chat": "Chat", + "New chat": "Nuova chat", + "No conversations": "Nessuna conversazione", + "You": "Tu", + "Delete": "Elimina", + "Leave": "Abbandona", + "Delete confirm": "Eliminare la conversazione con \"{{name}}\"? La tua copia dei messaggi viene rimossa.", + "Delete group confirm": "Eliminare il gruppo \"{{name}}\" per tutti? I suoi messaggi vengono rimossi per tutti i membri.", + "Leave confirm": "Abbandonare il gruppo \"{{name}}\"? Non riceverai più i suoi messaggi e la tua copia viene rimossa." + }, "LastCalls": { "Last calls": "Ultime chiamate", "No calls": "Nessuna chiamata", @@ -989,7 +1002,8 @@ }, "CommandBar": { "Placeholder": "Cerca contatto o componi numero...", - "Call": "Chiama" + "Call": "Chiama", + "Chat hint": "Chat (Ctrl/⌘+Invio)" }, "Errors": { "browser_permissions": "Errore nei permessi del browser", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 5140e346..81d1018d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -14,6 +14,7 @@ import { AvailableThemes, PAGES } from '@shared/types' import { delay } from '@shared/utils/utils' import i18next from 'i18next' import { DevToolsPage } from './pages/DevToolsPage' +import { ChatIslandPage } from './pages/ChatIslandPage' import { parseThemeToClassName } from './utils' import { useRegisterStoreHook, useSharedState } from '@renderer/store' import { PageContext, usePageCtx } from './contexts/pageContext' @@ -143,6 +144,10 @@ const RequestStateComponent = () => { path: PAGES.COMMANDBAR, element: , }, + { + path: PAGES.CHATISLAND, + element: , + }, ], }, ]) @@ -162,6 +167,9 @@ const Layout = ({ theme, page }: { theme?: AvailableThemes; page?: PAGES }) => { if (page === PAGES.PHONEISLAND) { await import('@nethesis/phone-island/dist/index.css') } + if (page === PAGES.CHATISLAND) { + await import('@nethesis/chat-island/dist/index.css') + } setIsCSSLoaded(true) } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx index 9756fde3..c3a351d0 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx @@ -5,6 +5,7 @@ import { faInfoCircle as InfoMenuIcon, faStar as FavouriteMenuIcon, faSquareParking as ParkedCallMenuIcon, + faCommentDots as ChatMenuIcon, } from '@fortawesome/free-solid-svg-icons' import { useNethlinkData, useSharedState } from '@renderer/store' import { ParkingType } from '@shared/types' @@ -27,6 +28,8 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { ) const [, setPhonebookSearchModule] = useNethlinkData('phonebookSearchModule') const [missedCalls] = useNethlinkData('missedCalls') + const [chatStatus] = useNethlinkData('chatStatus') + const [chatUnread] = useNethlinkData('chatUnread') const [notifications] = useSharedState('notifications') const [lastMenu, setLastMenu] = useState( MENU_ELEMENT.FAVOURITES, @@ -99,6 +102,16 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { onClick={() => handleSidebarMenuSelection(MENU_ELEMENT.LAST_CALLS)} isSelected={selectedSidebarMenu === MENU_ELEMENT.LAST_CALLS} /> + {/* CHAT: shown once the chat window is connecting */} + {chatStatus && ( + 0} + onClick={() => handleSidebarMenuSelection(MENU_ELEMENT.CHAT)} + isSelected={selectedSidebarMenu === MENU_ELEMENT.CHAT} + /> + )} {/* PARKED CALLS */} {hasPermission(PERMISSION.PARKINGS) && ( { + const language = i18next.languages?.[0] || 'en' + return `${formatDistanceToNowStrict(ts, { addSuffix: true, locale: shortLocale(language) })} (${format(ts, 'd MMM yyyy HH:mm', { locale: language === 'it' ? it : enGB })})` +} + +// Leaving a group I do not own; deleting a chat, or a group I own for everyone. +const leaves = (c: ChatConversation) => c.kind === 'group' && !c.owner + +function DeleteDialog({ + conversation, + close, +}: { + conversation: ChatConversation + close: () => void +}) { + const [theme] = useSharedState('theme') + const cancelRef = createRef() + const name = truncate(conversation.name, 30) + const confirm = () => { + window.electron.send(IPC_EVENTS.CHAT_TO_ISLAND, 'chat-island-delete', { + username: conversation.peer, + }) + close() + } + return ( + + +
+
+
+

+ {leaves(conversation) ? t('Chat.Leave') : t('Chat.Delete')} +

+

+ {conversation.kind === 'group' + ? conversation.owner + ? t('Chat.Delete group confirm', { name }) + : t('Chat.Leave confirm', { name }) + : t('Chat.Delete confirm', { name })} +

+
+
+ + + + +
+ ) +} + +export function ChatModule() { + const [conversations] = useNethlinkData('chatConversations') + const [operators] = useNethlinkData('operators') + const [toDelete, setToDelete] = useState() + const nameOf = (u: string) => operators?.operators?.[u]?.name || u + + return ( + <> + openChat()} + actionIcon={NewChatIcon} + actionText={t('Chat.New chat')} + /> + + {conversations?.length ? ( + conversations.map((c, idx) => ( +
openChat(c.peer)} + className='group cursor-pointer dark:hover:bg-hoverDark hover:bg-hoverLight hover:shadow-[0px_-1px_0px_0px_#E5E7EB] dark:hover:shadow-[0px_-1px_0px_0px_#374151]' + > +
+
+ {/* Same layout as the last calls */} +
+
+ {c.kind === 'group' && !c.avatar ? ( + + + + ) : ( + + )} +
+
+

+ {c.name} + {c.inactive && ( + + {t('Chat.No longer active')} + + )} +

+

+ {c.last && + (c.last.mine + ? `${t('Chat.You')}: ` + : c.kind === 'group' && c.last.nick + ? `${nameOf(c.last.nick)}: ` + : '')} + {c.last?.body} +

+ {c.last && ( +

+ {when(c.last.ts)} +

+ )} +
+
+ {c.unread > 0 && ( + + {c.unread} + + )} + +
+
+
+
+
+ )) + ) : ( + + )} +
+ {toDelete && ( + setToDelete(undefined)} + /> + )} + + ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx index 8134049a..75ab9b32 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx @@ -7,6 +7,9 @@ import { usePhonebookSearchModule } from './hook/usePhoneBookSearchModule' import { ContactNameAndActions } from '@renderer/components/Modules/NethVoice/BaseModule/ContactNameAndAction' import { useFavouriteModule } from '../Speeddials/hook/useFavouriteModule' import { ClassNames } from '@renderer/utils' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCommentDots } from '@fortawesome/free-solid-svg-icons' +import { openChat } from '@renderer/hooks/useChatBridge' export interface SearchNumberProps { user: SearchData @@ -18,6 +21,7 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const phoneBookModule = usePhonebookSearchModule() const [searchText] = phoneBookModule.searchTextState const [operators] = useNethlinkData('operators') + const [chatStatus] = useNethlinkData('chatStatus') const { isSearchAlsoAFavourite } = useFavouriteModule() const getUsernameFromPhoneNumber = (number: string) => { @@ -127,6 +131,17 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { } /> + {/* An operator: chat with them */} + {username && chatStatus === 'online' && ( + + )} {onClick && ( + )} ) })} diff --git a/src/renderer/src/pages/NethLinkPage.tsx b/src/renderer/src/pages/NethLinkPage.tsx index a9b4d040..8b84f3cb 100644 --- a/src/renderer/src/pages/NethLinkPage.tsx +++ b/src/renderer/src/pages/NethLinkPage.tsx @@ -13,6 +13,7 @@ import { debouncer } from '@shared/utils/utils' import { useAccount } from '@renderer/hooks/useAccount' import { Sidebar } from '@renderer/components/Modules/NethVoice/BaseModule/Sidebar' import { sendNotification } from '@renderer/utils' +import { useChatBridge } from '@renderer/hooks/useChatBridge' export interface NethLinkPageProps { handleRefreshConnection: () => void @@ -38,6 +39,7 @@ export function NethLinkPage({ handleRefreshConnection }: NethLinkPageProps) { const { NethVoiceAPI } = useLoggedNethVoiceAPI() const accountMeInterval = useRef() + useChatBridge(account?.username) useInitialize(() => { Log.info('INITIALIZE NETHLINK FRONTEND') diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index c1007344..713d7cd1 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -180,6 +180,9 @@ export const useNethlinkData = createGlobalStateHook({ }, showAddContactModule: false, showPhonebookSearchModule: false, + chatStatus: undefined, + chatUnread: 0, + chatConversations: [], } as NethLinkPageData).useGlobalState export const useLoginPageData = createGlobalStateHook({ diff --git a/src/shared/constants.ts b/src/shared/constants.ts index d81643d9..c8877a06 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -40,6 +40,7 @@ export enum MENU_ELEMENT { LAST_CALLS, PARKED_CALLS, ABOUT, + CHAT, } export enum IPC_EVENTS { LOGIN = 'LOGIN', @@ -48,6 +49,9 @@ export enum IPC_EVENTS { EMIT_START_CALL = 'EMIT_START_CALL', INITIALIZATION_COMPELTED = 'INITIALIZATION_COMPELTED', PHONE_ISLAND_RESIZE = 'PHONE_ISLAND_RESIZE', + CHAT_ISLAND_RESIZE = 'CHAT_ISLAND_RESIZE', + CHAT_TO_ISLAND = 'CHAT_TO_ISLAND', + CHAT_FROM_ISLAND = 'CHAT_FROM_ISLAND', LOGIN_WINDOW_RESIZE = 'LOGIN_WINDOW_RESIZE', HIDE_LOGIN_WINDOW = 'HIDE_LOGIN_WINDOW', CHANGE_THEME = 'CHANGE_THEME', diff --git a/src/shared/types.ts b/src/shared/types.ts index a216bedf..b555f289 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -21,6 +21,7 @@ export enum PAGES { NETHLINK = 'NethLink', DEVTOOLS = 'devtoolspage', COMMANDBAR = 'commandbarpage', + CHATISLAND = 'chatislandpage', } export type StateType = [T | undefined, (value: T | undefined) => void] @@ -502,6 +503,22 @@ export type NethLinkPageData = { speeddialsModule?: SpeedDialModuleData phonebookSearchModule?: PhonebookSearchModuleData phonebookModule?: PhonebookModuleData + chatStatus?: string + chatUnread?: number + chatConversations?: ChatConversation[] +} + +// One row of the chat-island-conversations event. +export type ChatConversation = { + peer: string + kind: 'chat' | 'group' + name: string + avatar?: string + online: boolean + inactive?: boolean + owner?: boolean + unread: number + last?: { body: string; ts: number; mine: boolean; nick?: string } } export type SpeedDialModuleData = {