diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e89ca1cb2..52114f026d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add support for new format of invitation link: `c=&t=&s=&i=` ([#2310](https://github.com/TryQuiet/quiet/issues/2310)) * Use server for downloading initial community metadata if v2 invitation link is detected ([#2295](https://github.com/TryQuiet/quiet/issues/2295)) +* Adds connection status information to messages panel on desktop when no peers are connected ([#1706](https://github.com/TryQuiet/quiet/ # Refactorings: * Consolidate colors and align theme with MUI standards ([#2445](https://github.com/TryQuiet/quiet/issues/2445)) diff --git a/bs.log b/bs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/package.json b/package.json index c9e44e9828..1f87134098 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "postpublish": "node copy-changelog.js && git add . && git commit -m 'Update packages CHANGELOG.md' && git push", "start:desktop": "lerna run --scope @quiet/desktop start", "lint:all": "lerna run lint", - "distAndRunE2ETests:mac:local": "lerna run --scope @quiet/desktop distMac:local && lerna run --scope e2e-tests test:localBinary --", + "clean": "lerna clean", + "bootstrap": "lerna bootstrap", + "bootstrap:clean": "npm run clean && npm run bootstrap", + "lerna:run:desktop": "lerna run --scope @quiet/desktop", + "distAndRunE2ETests:mac:local": "npm run lerna:run:desktop distMac:local && lerna run --scope e2e-tests test:localBinary --", "e2e:linux:build": "lerna run --scope @quiet/backend webpack:prod && lerna run --scope @quiet/desktop distUbuntu && lerna run --scope e2e-tests linux:copy", "e2e:linux:run": "lerna run --scope e2e-tests test --", "prepare": "husky", diff --git a/packages/.DS_Store b/packages/.DS_Store deleted file mode 100644 index 0d635ae46e..0000000000 Binary files a/packages/.DS_Store and /dev/null differ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 13228c2b77..73dd1dc142 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -12,6 +12,10 @@ * Refactor: Consolidate profile photo validation and match magic byte check to type check +* Refactor: Updates peer connecting events to include updating peers DB and refactors some payload types ([#1706](https://github.com/TryQuiet/quiet/issues/1706)) + +* Refactor: Update Tor initialized status when Tor is connected ([#1706](https://github.com/TryQuiet/quiet/issues/1706)) + [2.0.3-alpha.6] * Fix: filter out invalid peer addresses in peer list. Update peer list in localdb. diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index 4c42ca7702..1b8509c19c 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -45,6 +45,7 @@ import { type SavedOwnerCertificatePayload, type UserProfile, type UserProfilesStoredEvent, + PeersNetworkDataPayload, } from '@quiet/types' import Logger from '../common/logger' import { CONFIG_OPTIONS, QUIET_DIR, SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' @@ -617,19 +618,23 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI await this.libp2pService.createInstance(params) // Libp2p event listeners - this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, async (payload: { peers: string[] }) => { + this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, async (payload: PeersNetworkDataPayload) => { this.serverIoProvider.io.emit(SocketActionTypes.PEER_CONNECTED, payload) - for (const peer of payload.peers) { - const peerStats: NetworkStats = { - peerId: peer, - connectionTime: 0, - lastSeen: DateTime.utc().toSeconds(), - } - await this.localDbService.update(LocalDBKeys.PEERS, { - [peer]: peerStats, - }) - } + const peerStats: { [peerId: string]: NetworkStats } = await payload.peers.reduce( + async (updateObj, peer) => { + return { + ...(await updateObj), + [peer.peer]: { + peerId: peer.peer, + lastSeen: peer.lastSeen, + connectionTime: peer.connectionDuration, + } as NetworkStats, + } + }, + Promise.resolve({} as { [peerId: string]: NetworkStats }) + ) + await this.localDbService.update(LocalDBKeys.PEERS, peerStats) }) this.libp2pService.on(Libp2pEvents.PEER_DISCONNECTED, async (payload: NetworkDataPayload) => { diff --git a/packages/backend/src/nest/libp2p/libp2p.service.ts b/packages/backend/src/nest/libp2p/libp2p.service.ts index dcec245cd4..15b2401d32 100644 --- a/packages/backend/src/nest/libp2p/libp2p.service.ts +++ b/packages/backend/src/nest/libp2p/libp2p.service.ts @@ -5,7 +5,13 @@ import { mplex } from '@libp2p/mplex' import { multiaddr } from '@multiformats/multiaddr' import { Inject, Injectable } from '@nestjs/common' import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common' -import { ConnectionProcessInfo, type NetworkDataPayload, PeerId, SocketActionTypes } from '@quiet/types' +import { + ConnectionProcessInfo, + type NetworkDataPayload, + PeerId, + SocketActionTypes, + PeersNetworkDataPayload, +} from '@quiet/types' import crypto from 'crypto' import { EventEmitter } from 'events' import { Agent } from 'https' @@ -221,24 +227,36 @@ export class Libp2pService extends EventEmitter { }) this.libp2pInstance.addEventListener('peer:connect', async peer => { + this.logger(`Connecting peer: ${JSON.stringify(peer)}`) const remotePeerId = peer.detail.remotePeer.toString() const localPeerId = peerId.toString() this.logger(`${localPeerId} connected to ${remotePeerId}`) + const now = DateTime.utc() const connectedPeer: Libp2pConnectedPeer = { address: peer.detail.remoteAddr.toString(), - connectedAtSeconds: DateTime.utc().valueOf(), + connectedAtSeconds: now.valueOf(), } this.connectedPeers.set(remotePeerId, connectedPeer) this.logger(`${localPeerId} is connected to ${this.connectedPeers.size} peers`) this.logger(`${localPeerId} has ${this.libp2pInstance?.getConnections().length} open connections`) - this.emit(Libp2pEvents.PEER_CONNECTED, { - peers: [remotePeerId], - }) + const payload: PeersNetworkDataPayload = { + peers: [ + { + peer: remotePeerId, + lastSeen: now.toSeconds(), + connectionDuration: 0, + }, + ], + } + + this.logger(`Emitting ${Libp2pEvents.PEER_CONNECTED} event with payload ${JSON.stringify(payload)}`) + this.emit(Libp2pEvents.PEER_CONNECTED, payload) }) this.libp2pInstance.addEventListener('peer:disconnect', async peer => { + this.logger(`Disconnecting peer: ${JSON.stringify(peer)}`) const remotePeerId = peer.detail.remotePeer.toString() const localPeerId = peerId.toString() this.logger(`${localPeerId} disconnected from ${remotePeerId}`) @@ -259,12 +277,14 @@ export class Libp2pService extends EventEmitter { const connectionDuration: number = connectionEndTime - connectionStartTime this.connectedPeers.delete(remotePeerId) - this.logger(`${localPeerId} is connected to ${this.connectedPeers.size} peers`) + this.logger(`${localPeerId} is now connected to ${this.connectedPeers.size} peers`) const peerStat: NetworkDataPayload = { peer: remotePeerId, connectionDuration, lastSeen: connectionEndTime, } + + this.logger(`Emitting ${Libp2pEvents.PEER_DISCONNECTED} event with payload ${JSON.stringify(peerStat)}`) this.emit(Libp2pEvents.PEER_DISCONNECTED, peerStat) }) diff --git a/packages/backend/tsconfig.build.json b/packages/backend/tsconfig.build.json index e8c62532d1..975dfc9768 100644 --- a/packages/backend/tsconfig.build.json +++ b/packages/backend/tsconfig.build.json @@ -4,7 +4,7 @@ "target": "ES2020", "module": "ES2022", "strict": true, - "declaration": true, + "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index dbfe219b55..bc4c77249b 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -2,6 +2,8 @@ # New features: +* Adds connection status information to messages panel when no peers are connected ([#1706](https://github.com/TryQuiet/quiet/issues/1706)) + # Refactorings: # Fixes: diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json index c7404ce111..351ec26707 100644 --- a/packages/desktop/package-lock.json +++ b/packages/desktop/package-lock.json @@ -28,6 +28,7 @@ "@babel/core": "^7.22.5", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-transform-block-scoping": "7.5.5", "@babel/preset-env": "^7.22.5", "@babel/preset-react": "^7.22.5", "@cypress/react18": "2.0.0", @@ -1490,15 +1491,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", - "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz", + "integrity": "sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -9515,6 +9514,30 @@ "@babel/core": "^7.4.0-0" } }, + "node_modules/@storybook/builder-webpack5/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/@storybook/addons": { "version": "6.5.15", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz", @@ -11415,6 +11438,30 @@ "@babel/core": "^7.4.0-0" } }, + "node_modules/@storybook/core-common/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@storybook/core-common/node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@storybook/core-common/node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -13709,6 +13756,30 @@ "@babel/core": "^7.4.0-0" } }, + "node_modules/@storybook/manager-webpack5/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@storybook/manager-webpack5/node_modules/@storybook/addons": { "version": "6.5.15", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz", @@ -47880,12 +47951,13 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", - "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz", + "integrity": "sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" } }, "@babel/plugin-transform-classes": { @@ -53767,6 +53839,21 @@ "semver": "^6.1.2" } }, + "@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.0" + } + }, "@storybook/addons": { "version": "6.5.15", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz", @@ -55152,6 +55239,21 @@ "semver": "^6.1.2" } }, + "@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.0" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -56928,6 +57030,21 @@ "semver": "^6.1.2" } }, + "@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.0" + } + }, "@storybook/addons": { "version": "6.5.15", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 7b648586a6..53eaebd1c3 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -89,7 +89,7 @@ "copyBinariesDarwin": "cp -R ../../3rd-party/tor/$SOURCE_PATH/ ./tor/ && chmod 775 ./tor/arm64/tor ./tor/x64/tor", "copyBinariesWin": "xcopy ..\\..\\3rd-party\\tor\\win32 .\\tor\\", "pullLibs": "wget -N https://zbay-binaries.s3.us-east-2.amazonaws.com/$SOURCE_PATH/libssl.so -P ./ && chmod 775 ./libssl.so", - "distMac:local": "export DEBUG=* SOURCE_PATH=darwin TEST_MODE=true IS_LOCAL=true && npm run copyBinariesDarwin && npm run build:dev:dist && electron-builder --mac -p never -c.mac.type=development -c.mac.identity=null", + "distMac:local": "export DEBUG=* SOURCE_PATH=darwin TEST_MODE=true IS_LOCAL=true && npm run copyBinariesDarwin && ./scripts/shell/clear_electron_builds.sh && npm run build:dev:dist && electron-builder --mac -p never -c.mac.type=development -c.mac.identity=null", "dist": "npm run distMac", "distMac": "export SOURCE_PATH=darwin TEST_MODE=true && npm run copyBinariesDarwin && npm run build:prod && electron-builder --mac", "distUbuntu": "export SOURCE_PATH=linux TEST_MODE=true && npm run setMainEnvs && npm run copyBinaries && npm run pullLibs && npm run build:prod && electron-builder --linux", @@ -151,6 +151,7 @@ "@babel/core": "^7.22.5", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-transform-block-scoping": "7.5.5", "@babel/preset-env": "^7.22.5", "@babel/preset-react": "^7.22.5", "@cypress/react18": "2.0.0", diff --git a/packages/desktop/scripts/shell/clear_electron_builds.sh b/packages/desktop/scripts/shell/clear_electron_builds.sh new file mode 100755 index 0000000000..8ee4b66916 --- /dev/null +++ b/packages/desktop/scripts/shell/clear_electron_builds.sh @@ -0,0 +1,3 @@ +#! /bin/zsh +set -o kshglob +rm -rf dist/*.(zip|blockmap|dmg) || true \ No newline at end of file diff --git a/packages/desktop/src/renderer/components/Channel/Channel.stories.cy.tsx b/packages/desktop/src/renderer/components/Channel/Channel.stories.cy.tsx index b9c82b83be..87b1fad56b 100644 --- a/packages/desktop/src/renderer/components/Channel/Channel.stories.cy.tsx +++ b/packages/desktop/src/renderer/components/Channel/Channel.stories.cy.tsx @@ -12,6 +12,7 @@ import { DisplayableMessage } from '@quiet/types' import ChannelComponent from './ChannelComponent' import { payloadDuplicated, payloadUnregistered } from '../widgets/userLabel/UserLabel.types' +import { DateTime } from 'luxon' const Template: ComponentStory = () => { const [messages, setMessages] = useState<{ @@ -21,6 +22,8 @@ const Template: ComponentStory = () => { } }>(mock_messages()) + const [connectedPeers, setConnectedPeers] = useState(["peer"]) + const onInputEnter = (message: string) => { const _message: DisplayableMessage = { id: '32', @@ -62,7 +65,7 @@ const Template: ComponentStory = () => { privateKey: 'privateKey', }, peerId: { - id: 'id', + id: 'peer', privKey: 'privKey', pubKey: 'pubKey', }, @@ -79,6 +82,10 @@ const Template: ComponentStory = () => { joinTimestamp: null, }} isCommunityInitialized={true} + connectedPeers={connectedPeers} + communityPeerList={connectedPeers} + lastConnectedTime={DateTime.utc().toSeconds()} + allPeersDisconnectedTime={undefined} uploadedFileModal={{ open: false, handleOpen: function (_args?: any): any {}, diff --git a/packages/desktop/src/renderer/components/Channel/Channel.stories.tsx b/packages/desktop/src/renderer/components/Channel/Channel.stories.tsx index 948bfc678a..21df63e2e8 100644 --- a/packages/desktop/src/renderer/components/Channel/Channel.stories.tsx +++ b/packages/desktop/src/renderer/components/Channel/Channel.stories.tsx @@ -11,6 +11,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend' import ChannelComponent, { ChannelComponentProps } from './ChannelComponent' import { UploadFilesPreviewsProps } from './File/UploadingPreview' import { DownloadState } from '@quiet/types' +import { DateTime } from 'luxon' const args: Partial = { user: { @@ -54,6 +55,7 @@ const args: Partial = { pubKey: 'pubKey', }, pendingMessages: {}, + lastConnectedTime: DateTime.utc().toMillis(), channelId: 'general', channelName: 'general', lazyLoading: function (_load: boolean): void {}, diff --git a/packages/desktop/src/renderer/components/Channel/Channel.tsx b/packages/desktop/src/renderer/components/Channel/Channel.tsx index 0b56f29188..f93f3048be 100644 --- a/packages/desktop/src/renderer/components/Channel/Channel.tsx +++ b/packages/desktop/src/renderer/components/Channel/Channel.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useEffect } from 'react' import { shell, ipcRenderer } from 'electron' import { useDispatch, useSelector } from 'react-redux' -import { identity, messages, publicChannels, communities, files, network } from '@quiet/state-manager' +import { identity, messages, publicChannels, communities, files, network, connection } from '@quiet/state-manager' import { FileMetadata, CancelDownload, FileContent, FilePreviewData } from '@quiet/types' import ChannelComponent, { ChannelComponentProps } from './ChannelComponent' @@ -38,6 +38,10 @@ const Channel = () => { const initializedCommunities = useSelector(network.selectors.initializedCommunities) const isCommunityInitialized = Boolean(community && initializedCommunities[community.id]) + const communityPeerList = useSelector(communities.selectors.peerList) + const connectedPeers = useSelector(network.selectors.connectedPeers) + const lastConnectedTime = useSelector(network.selectors.communityLastConnectedAt) + const allPeersDisconnectedTime = useSelector(network.selectors.allPeersDisconnectedAt) const pendingGeneralChannelRecreationSelector = useSelector(publicChannels.selectors.pendingGeneralChannelRecreation) @@ -194,7 +198,7 @@ const Channel = () => { if (!user || !currentChannelId) return null const channelComponentProps: ChannelComponentProps = { - user: user, + user, channelId: currentChannelId, channelName: currentChannelName, messages: { @@ -202,7 +206,7 @@ const Channel = () => { groups: currentChannelDisplayableMessages, }, newestMessage: newestCurrentChannelMessage, - pendingMessages: pendingMessages, + pendingMessages, downloadStatuses: downloadStatusesMapping, lazyLoading: lazyLoading, onInputChange: onInputChange, @@ -210,11 +214,15 @@ const Channel = () => { openUrl: openUrl, handleFileDrop: handleFileDrop, openFilesDialog: openFilesDialog, - isCommunityInitialized: isCommunityInitialized, + isCommunityInitialized, + communityPeerList, + connectedPeers, + lastConnectedTime, + allPeersDisconnectedTime, handleClipboardFiles: handleClipboardFiles, - uploadedFileModal: uploadedFileModal, + uploadedFileModal, openContextMenu: openContextMenu, - pendingGeneralChannelRecreation: pendingGeneralChannelRecreation, + pendingGeneralChannelRecreation, unregisteredUsernameModalHandleOpen, duplicatedUsernameModalHandleOpen, } diff --git a/packages/desktop/src/renderer/components/Channel/ChannelComponent.tsx b/packages/desktop/src/renderer/components/Channel/ChannelComponent.tsx index 752a06f0e1..a573d33539 100644 --- a/packages/desktop/src/renderer/components/Channel/ChannelComponent.tsx +++ b/packages/desktop/src/renderer/components/Channel/ChannelComponent.tsx @@ -26,6 +26,7 @@ import { NewMessagesInfoComponent } from './NewMessagesInfo/NewMessagesInfoCompo import { FileActionsProps } from './File/FileComponent/FileComponent' import { UseModalType } from '../../containers/hooks' import { HandleOpenModalType } from '../widgets/userLabel/UserLabel.types' +import ChannelNetworkStatus from '../widgets/channels/ChannelNetworkStatus' const ChannelMessagesWrapperStyled = styled(Grid)(({ theme }) => ({ position: 'relative', @@ -51,6 +52,10 @@ export interface ChannelComponentProps { openFilesDialog: () => void handleFileDrop: (arg: any) => void isCommunityInitialized: boolean + connectedPeers: string[] | undefined + communityPeerList: string[] | undefined + lastConnectedTime: number + allPeersDisconnectedTime: number | undefined handleClipboardFiles: (arg: ArrayBuffer, ext: string, name: string) => void uploadedFileModal?: UseModalType<{ src: string @@ -84,6 +89,10 @@ export const ChannelComponent: React.FC(0) + const checkForConnectedPeers = (connectedPeers: string[] | undefined) => { + if (connectedPeers && connectedPeers.length > 0) { + return true + } + return false + } + + const checkForCommunityPeers = (peerList: string[] | undefined) => { + console.info(peerList, peerList?.length) + if (peerList && peerList.length > 1) { + return true + } + return false + } + + const [isConnectedToOtherPeers, onConnectedPeersChange] = React.useState( + checkForConnectedPeers(connectedPeers) + ) + + const [communityHasPeers, onCommunityPeerListChanged] = React.useState( + checkForCommunityPeers(communityPeerList) + ) + + useEffect(() => { + onConnectedPeersChange(checkForConnectedPeers(connectedPeers)) + }, [connectedPeers]) + + useEffect(() => { + onCommunityPeerListChanged(checkForCommunityPeers(communityPeerList)) + }, [communityPeerList]) + const updateMathMessagesRendered = () => { // To rerender Channel on each call onMathMessageRendered(mathMessagesRendered + 1) @@ -227,6 +267,10 @@ export const ChannelComponent: React.FC + & DeleteChannelProps = { channelName: 'general', deleteChannel: () => { - console.log('deleting channel') + console.info('deleting channel') }, open: true, // @ts-expect-error diff --git a/packages/desktop/src/renderer/components/Channel/DropZone/DropZoneComponent.tsx b/packages/desktop/src/renderer/components/Channel/DropZone/DropZoneComponent.tsx index 0773af35d9..99847a0ded 100644 --- a/packages/desktop/src/renderer/components/Channel/DropZone/DropZoneComponent.tsx +++ b/packages/desktop/src/renderer/components/Channel/DropZone/DropZoneComponent.tsx @@ -67,7 +67,7 @@ export const DropZoneComponent: React.FC = ({ children, collect: (monitor: DropTargetMonitor) => { const item: any = monitor.getItem() if (item) { - console.log('collect', item.files, item.items) + console.info('collect', item.files, item.items) } return { diff --git a/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.stories.tsx b/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.stories.tsx index 0c33e712ab..4048aaa1c5 100644 --- a/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.stories.tsx +++ b/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.stories.tsx @@ -56,6 +56,7 @@ const args: FileComponentProps = { downloadState: DownloadState.Ready, downloadProgress: undefined, }, + isUnsent: false, } Uploading.args = { @@ -97,13 +98,13 @@ Queued.args = { }, }, cancelDownload: () => { - console.log('cancel download') + console.info('cancel download') }, } Ready.args = { ...args, downloadFile: () => { - console.log('download file') + console.info('download file') }, } Downloading.args = { @@ -119,7 +120,7 @@ Downloading.args = { }, }, cancelDownload: () => { - console.log('cancel download') + console.info('cancel download') }, } Canceling.args = { @@ -151,7 +152,7 @@ Completed.args = { }, }, openContainingFolder: () => { - console.log('show in folder') + console.info('show in folder') }, } Malicious.args = { diff --git a/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.test.tsx b/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.test.tsx index 3fe8c311fc..1edcc86790 100644 --- a/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.test.tsx +++ b/packages/desktop/src/renderer/components/Channel/File/FileComponent/FileComponent.test.tsx @@ -38,13 +38,14 @@ describe('FileComponent', () => { downloadState: DownloadState.Ready, downloadProgress: undefined, }} + isUnsent={false} /> ) expect(result.baseElement).toMatchInlineSnapshot(`
@@ -82,6 +83,113 @@ describe('FileComponent', () => {
+
+
+ +

+ Download file +

+
+
+
+
+ + + `) + }) + + it('renders component as unsent', () => { + const result = renderComponent( + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
+
+ +
+
+ +
+
+
+ my-file-name-goes-here-an-isnt-truncated + .zip +
+

+ 2 KB +

+
+
+
+
({ @@ -52,6 +54,10 @@ const FileComponentStyled = styled('div')(({ theme }) => ({ [`& .${classes.filename}`]: { marginLeft: '16px', }, + + [`& .${classes.unsent}`]: { + opacity: 0.5, + }, })) const ActionIndicatorStyled = styled('div')(() => ({ @@ -126,6 +132,7 @@ const ActionIndicator: React.FC<{ export interface FileComponentProps { message: DisplayableMessage downloadStatus?: DownloadStatus + isUnsent: boolean } export interface FileActionsProps { @@ -137,6 +144,7 @@ export interface FileActionsProps { export const FileComponent: React.FC = ({ message, downloadStatus, + isUnsent, openContainingFolder, downloadFile, cancelDownload, @@ -343,7 +351,7 @@ export const FileComponent: React.FC = ({ } placement='top' > -
+
{renderIcon()}
@@ -362,6 +370,7 @@ export const FileComponent: React.FC = ({ width: 'fit-content', display: downloadState ? 'block' : 'none', }} + className={classNames({ [classes.unsent]: isUnsent })} > {renderActionIndicator()}
diff --git a/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.test.tsx b/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.test.tsx index be24ec1abf..5d0b38eeb9 100644 --- a/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.test.tsx +++ b/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.test.tsx @@ -52,7 +52,7 @@ describe('UploadedFile', () => {
{
{ `) }) + + it('renders an unsent placeholder if image is not finished downloading yet', () => { + const result = renderComponent( + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
+
+
+

+ test.png +

+
+ +
+ +
+ + + + + +
+
+
+
+
+
+
+ + `) + }) + it('renders unsent image if image is downloaded', () => { + // @ts-expect-error + message.media.path = 'path/to/file/test.png' + // @ts-expect-error + message.media.message = { + id: 'string', + channelId: 'general', + } + const result = renderComponent( + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
+
+
+
+

+ test.png +

+ +
+
+
+
+ + `) + }) }) diff --git a/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.tsx b/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.tsx index e93c3beb23..5afadfb30c 100644 --- a/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.tsx +++ b/packages/desktop/src/renderer/components/Channel/File/UploadedImage/UploadedImage.tsx @@ -1,6 +1,9 @@ import React, { useEffect, useState } from 'react' +import classNames from 'classnames' import { styled } from '@mui/material/styles' + import { DownloadStatus, FileMetadata } from '@quiet/types' + import { UseModalType } from '../../../../containers/hooks' import UploadedFileModal from './UploadedImagePreview' import { UploadedFilename, UploadedImagePlaceholder } from '../UploadedImagePlaceholder/UploadedImagePlaceholder' @@ -10,6 +13,8 @@ const PREFIX = 'UploadedImage' const classes = { image: `${PREFIX}image`, container: `${PREFIX}container`, + pending: `${PREFIX}pending`, + unsent: `${PREFIX}unsent`, } const Root = styled('div')(() => ({ @@ -22,6 +27,14 @@ const Root = styled('div')(() => ({ maxWidth: '400px', cursor: 'pointer', }, + + [`& .${classes.pending}`]: { + opacity: 0.5, + }, + + [`& .${classes.unsent}`]: { + opacity: 0.5, + }, })) export interface UploadedImageProps { @@ -31,9 +44,10 @@ export interface UploadedImageProps { }> downloadStatus?: DownloadStatus + isUnsent?: boolean } -export const UploadedImage: React.FC = ({ media, uploadedFileModal, downloadStatus }) => { +export const UploadedImage: React.FC = ({ media, uploadedFileModal, downloadStatus, isUnsent }) => { const [showImage, setShowImage] = useState(false) const { cid, path, name, ext } = media @@ -62,7 +76,11 @@ export const UploadedImage: React.FC = ({ media, uploadedFil {path ? ( <>
{ setShowImage(true) }} diff --git a/packages/desktop/src/renderer/components/ContextMenu/ContextMenu.stories.tsx b/packages/desktop/src/renderer/components/ContextMenu/ContextMenu.stories.tsx index cb75dfd915..5156fd5306 100644 --- a/packages/desktop/src/renderer/components/ContextMenu/ContextMenu.stories.tsx +++ b/packages/desktop/src/renderer/components/ContextMenu/ContextMenu.stories.tsx @@ -14,7 +14,7 @@ const channel_items: ContextMenuItemProps[] = [ { title: 'Delete', action: () => { - console.log('clicked on delete channel') + console.info('clicked on delete channel') }, }, ] @@ -24,7 +24,7 @@ const args: ContextMenuProps = { children: , visible: true, handleClose: () => { - console.log('closing menu') + console.info('closing menu') }, } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.stories.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.stories.tsx index 01188395c6..ced87aadb3 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.stories.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.stories.tsx @@ -16,10 +16,10 @@ const args: PerformCommunityActionProps = { open: true, communityOwnership: CommunityOwnership.Owner, handleCommunityAction: function (value: string): void { - console.log('Creating community: ', value) + console.info('Creating community: ', value) }, handleRedirection: function (): void { - console.log('Redirected to join community') + console.info('Redirected to join community') }, handleClose: function (): void {}, isCloseDisabled: false, diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.test.tsx index aef565bf9a..970a329aac 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/CreateCommunity/CreateCommunity.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import '@testing-library/jest-dom/extend-expect' import { screen, waitFor } from '@testing-library/dom' -import { act } from 'react-dom/test-utils' +import { act } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderComponent } from '../../../testUtils/renderComponent' import { prepareStore } from '../../../testUtils/prepareStore' diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.stories.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.stories.tsx index 070d6e674f..594bd57329 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.stories.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.stories.tsx @@ -17,10 +17,10 @@ const args: PerformCommunityActionProps = { open: true, communityOwnership: CommunityOwnership.User, handleCommunityAction: function (value: string): void { - console.log('Joining community: ', value) + console.info('Joining community: ', value) }, handleRedirection: function (): void { - console.log('Redirected to create community') + console.info('Redirected to create community') }, handleClose: function (): void {}, isCloseDisabled: false, diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx index 9d6b94520b..ca5360049b 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import '@testing-library/jest-dom/extend-expect' import { screen, waitFor } from '@testing-library/dom' -import { act } from 'react-dom/test-utils' +import { act } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderComponent } from '../../../testUtils/renderComponent' import { prepareStore } from '../../../testUtils/prepareStore' diff --git a/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.stories.tsx b/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.stories.tsx index e298655534..44ce06a310 100644 --- a/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.stories.tsx +++ b/packages/desktop/src/renderer/components/CreateUsername/CreateUsername.stories.tsx @@ -15,7 +15,7 @@ const args: CreateUsernameComponentProps = { open: true, handleClose: function (): void {}, registerUsername: function (nickname: string): void { - console.log('Registering username: ', nickname) + console.info('Registering username: ', nickname) }, } diff --git a/packages/desktop/src/renderer/components/LoadingPanel/LoadingPanel.stories.tsx b/packages/desktop/src/renderer/components/LoadingPanel/LoadingPanel.stories.tsx index 1107935fab..78e65feb65 100644 --- a/packages/desktop/src/renderer/components/LoadingPanel/LoadingPanel.stories.tsx +++ b/packages/desktop/src/renderer/components/LoadingPanel/LoadingPanel.stories.tsx @@ -18,7 +18,7 @@ export const StartingPanel = StartingPanelTemplate.bind({}) const JoiningPanelArgs: JoiningPanelComponentProps = { open: true, handleClose: function (): void {}, - openUrl: () => console.log('OpenURL'), + openUrl: () => console.info('OpenURL'), connectionInfo: { number: 10, text: ConnectionProcessInfo.BACKEND_MODULES }, isOwner: false, } diff --git a/packages/desktop/src/renderer/components/MathMessage/MathMessageComponent.test.tsx b/packages/desktop/src/renderer/components/MathMessage/MathMessageComponent.test.tsx index 3a4a5afeb4..472beaec1f 100644 --- a/packages/desktop/src/renderer/components/MathMessage/MathMessageComponent.test.tsx +++ b/packages/desktop/src/renderer/components/MathMessage/MathMessageComponent.test.tsx @@ -1,19 +1,25 @@ import React from 'react' -import { act } from 'react-dom/test-utils' +import { act } from '@testing-library/react' import { renderComponent } from '../../testUtils/renderComponent' import { MathMessageComponent } from './MathMessageComponent' describe('MathMessageComponent', () => { it('renders tex', async () => { const result = renderComponent( - {}} /> + {}} + /> ) await act(async () => {}) expect(result.baseElement).toMatchInlineSnapshot(`
{ message={'It is $$a + b = c$$ and $$a - b = d$$'} messageId={'1'} pending={false} + isUnsent={false} openUrl={() => {}} onMathMessageRendered={onMathMessageRendered} /> @@ -107,13 +114,13 @@ describe('MathMessageComponent', () => {
It is { and { message={String.raw`$$sum_{i=0}^n i = \frac{n(n+1)}{2}$$ - look`} messageId={'1'} pending={false} + isUnsent={false} openUrl={() => {}} onMathMessageRendered={onMathMessageRendered} /> @@ -287,7 +295,7 @@ describe('MathMessageComponent', () => {
{
    ({ color: theme.palette.colors.lightGray, }, + [`&.${classes.unsent}`]: { + opacity: 0.5, + }, + [`&.${classes.middle}`]: { margin: '0 5px 0 5px', }, @@ -46,6 +51,7 @@ const MathComponent: React.FC = ({ onMathMessageRendered, messageId, pending, + isUnsent, openUrl, index, }) => { @@ -82,6 +88,7 @@ const MathComponent: React.FC = ({ const className = { [classes.message]: true, [classes.pending]: pending, + [classes.unsent]: isUnsent, [classes.beginning]: index === 0, [classes.middle]: index !== 0, } @@ -92,6 +99,7 @@ const MathComponent: React.FC = ({ message={message} messageId={`${messageId}-${index}`} pending={pending} + isUnsent={isUnsent} openUrl={openUrl} key={`${messageId}-${index}`} /> @@ -107,6 +115,7 @@ export const MathMessageComponent: React.FC + return ( + + ) } return ( @@ -129,6 +146,7 @@ export const MathMessageComponent: React.FC { + it('renders text followed by animated ellipsis', () => { + const result = renderComponent( + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
    +
    +

    + Sending +

    +

    + . +

    +

    + . +

    +

    + . +

    +
    +
    +
    + + `) + }) +}) diff --git a/packages/desktop/src/renderer/components/ui/AnimatedEllipsis/AnimatedEllipsis.tsx b/packages/desktop/src/renderer/components/ui/AnimatedEllipsis/AnimatedEllipsis.tsx new file mode 100644 index 0000000000..d4398aa3dd --- /dev/null +++ b/packages/desktop/src/renderer/components/ui/AnimatedEllipsis/AnimatedEllipsis.tsx @@ -0,0 +1,187 @@ +import React from 'react' +import Typography from '@mui/material/Typography' +import { Grid } from '@mui/material' +import { styled } from '@mui/material/styles' +import classNames from 'classnames' + +const PREFIX = 'AnimatedEllipsis' + +const classes = { + wrapper: `${PREFIX}-wrapper`, + content: `${PREFIX}-content`, + dot1: `${PREFIX}-dot1`, + dot2: `${PREFIX}-dot2`, + dot3: `${PREFIX}-dot3`, +} + +const getAnimationName = (className: string) => `${className}-visibility-anim` +const getAnimationProperties = (className: string) => { + return { + animationName: getAnimationName(className), + animationDuration: '1800ms', + animationTimingFunction: 'ease-in-out', + animationIterationCount: 'infinite', + } +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.wrapper}`]: { + display: 'flex', + flexDirection: 'row', + }, + + // dot 1 + + [`& .${classes.dot1}`]: getAnimationProperties(classes.dot1), + + [`@keyframes ${getAnimationName(classes.dot1)}`]: { + '0%': { + opacity: 1, + }, + '65%': { + opacity: 1, + }, + '66%': { + opacity: 0.5, + }, + '75%': { + opacity: 0.3, + }, + '90%': { + opacity: 0.1, + }, + '100%': { + opacity: 0, + }, + }, + + // dot2 + + [`& .${classes.dot2}`]: getAnimationProperties(classes.dot2), + + [`@keyframes ${getAnimationName(classes.dot2)}`]: { + '0%': { + opacity: 0, + }, + '5%': { + opacity: 0.1, + }, + '15%': { + opacity: 0.25, + }, + '18%': { + opacity: 0.5, + }, + '20%': { + opacity: 0.75, + }, + '22%': { + opacity: 1, + }, + '65%': { + opacity: 1, + }, + '66%': { + opacity: 0.5, + }, + '75%': { + opacity: 0.3, + }, + '90%': { + opacity: 0.1, + }, + '100%': { + opacity: 0, + }, + }, + + // dot 3 + + [`& .${classes.dot3}`]: getAnimationProperties(classes.dot3), + + [`@keyframes ${getAnimationName(classes.dot3)}`]: { + '0%': { + opacity: 0, + }, + '25%': { + opacity: 0.1, + }, + '35%': { + opacity: 0.25, + }, + '39%': { + opacity: 0.5, + }, + '43%': { + opacity: 0.75, + }, + '44%': { + opacity: 1, + }, + '65%': { + opacity: 1, + }, + '66%': { + opacity: 0.5, + }, + '75%': { + opacity: 0.3, + }, + '90%': { + opacity: 0.1, + }, + '100%': { + opacity: 0, + }, + }, +})) + +interface AnimatedEllipsis { + content: string + color: string + fontSize: number + fontWeight: string +} + +export const AnimatedEllipsis: React.FC = ({ content, color, fontSize, fontWeight }) => { + return ( + + + + {content} + + + . + + + . + + + . + + + + ) +} + +export default AnimatedEllipsis diff --git a/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.stories.tsx b/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.stories.tsx index 1654d78466..5a6d152717 100644 --- a/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.stories.tsx +++ b/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.stories.tsx @@ -18,7 +18,7 @@ const args: TextWithLinkProps = { tag: 'a', label: 'linked', action: () => { - console.log('link clicked') + console.info('link clicked') }, }, ], diff --git a/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.test.tsx b/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.test.tsx index 6ac48c706c..cdf9026b9d 100644 --- a/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.test.tsx +++ b/packages/desktop/src/renderer/components/ui/TextWithLink/TextWithLink.test.tsx @@ -13,7 +13,7 @@ describe('TextWithLink', () => { tag: 'simple', label: 'simple', action: () => { - console.log('linked clicked') + console.info('linked clicked') }, }, ]} diff --git a/packages/desktop/src/renderer/components/widgets/WarningModal/WarningModal.stories.tsx b/packages/desktop/src/renderer/components/widgets/WarningModal/WarningModal.stories.tsx index ec618f4f40..7d84e917fc 100644 --- a/packages/desktop/src/renderer/components/widgets/WarningModal/WarningModal.stories.tsx +++ b/packages/desktop/src/renderer/components/widgets/WarningModal/WarningModal.stories.tsx @@ -13,7 +13,7 @@ export const Component = Template.bind({}) const args: WarningModalComponentProps = { open: true, handleClose: function (): void { - console.log('Closed modal') + console.info('Closed modal') }, title: 'Warning title', subtitle: 'Warning description', diff --git a/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.test.tsx b/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.test.tsx index 2b9beeff3d..e19bd43ffc 100644 --- a/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.test.tsx +++ b/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.test.tsx @@ -14,394 +14,1072 @@ describe('BasicMessage', () => { jest.spyOn(DateTime, 'utc').mockImplementationOnce(() => DateTime.utc(2019, 3, 7, 13, 3, 48)) }) - it('renders component', async () => { - const messages = generateMessages() - const result = renderComponent( - - - - - - ) - expect(result.baseElement).toMatchInlineSnapshot(` - -
    -
  • -
    { + it('renders component', async () => { + const messages = generateMessages() + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • - Jdenticon +
    + Jdenticon +
    -
    -
    -

    +

    + gringo +

    +
    +
    - gringo -

    +

    + string +

    +
    +
    +
    -

    - string -

    + message0 +
    +
    +
  • + +
+ + `) + }) + + it('renders component with multiple messages', async () => { + const messages = generateMessages({ amount: 2 }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
+
  • +
    +
    +
    +
    + Jdenticon +
    +
    +
    +
    +

    + gringo +

    +
    +
    +

    + string +

    +
    +
    +
    +
    - + + message0 + +
    +
    - message0 - + + message1 + +
  • -
    - -
    - - `) - }) - it('renders component with multiple messages', async () => { - const messages = generateMessages({ amount: 2 }) - const result = renderComponent( - - - - - - ) - expect(result.baseElement).toMatchInlineSnapshot(` - -
    -
  • -
    +
    + + `) + }) + + it('renders with separate info messages', async () => { + const messages = generateMessages({ amount: 2, type: 3 }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • - Jdenticon +
    + +
    -
    -
    -

    - gringo -

    +

    + Quiet +

    +
    +
    +

    + string +

    +
    +
    +
    -

    - string -

    + message0 + +
    +
    + + message1 +
    +
    +
  • + +
    + + `) + }) + + it('renders with basic message and info message', async () => { + const message1 = generateMessages() + const message2 = generateMessages({ type: 3 }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • +
    +
    - +
    +
    +
    +
    - message0 - +
    +

    + gringo +

    +
    +
    +

    + string +

    +
    +
    - - message1 - + + message0 + +
    +
    + + message0 + +
    -
  • - -
    - - `) - }) - it('renders with separate info messages', async () => { - const messages = generateMessages({ amount: 2, type: 3 }) - const result = renderComponent( - - - - - - ) - expect(result.baseElement).toMatchInlineSnapshot(` - -
    -
  • -
    +
    + + `) + }) + + it('renders info messages as sent even when other messages would be unsent', async () => { + const nowSeconds = DateTime.utc().toSeconds() + const messages = generateMessages({ amount: 2, type: 3, createdAtSeconds: nowSeconds }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • - +
    + +
    -
    -
    +
    +
    +

    + Quiet +

    +
    +
    +

    + string +

    +
    +
    +
    +
    -

    - Quiet -

    + message0 +
    -

    - string -

    + message1 +
    +
    +
    +
  • +
    + + `) + }) + + it('renders messages as sent when no peers are connected but community is fresh', async () => { + const nowSeconds = DateTime.utc().toSeconds() + const messages = generateMessages({ amount: 2, createdAtSeconds: nowSeconds }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • +
    +
    +
    +
    + Jdenticon +
    +
    - - message0 - +
    +

    + gringo +

    +
    +
    +

    + string +

    +
    +
    - + + message0 + +
    +
    - message1 - + + message1 + +
  • -
    - -
    - - `) + +
    + + `) + }) }) - it('renders with basic message and info message', async () => { - const message1 = generateMessages() - const message2 = generateMessages({ type: 3 }) - const result = renderComponent( - - - - - - ) - expect(result.baseElement).toMatchInlineSnapshot(` - -
    -
  • -
    { + it('renders component with unsent messages when peers disconnected this session', async () => { + const nowSeconds = DateTime.utc().toSeconds() + const messages = generateMessages({ createdAtSeconds: nowSeconds }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • +
    + Jdenticon +
    +
    +
    - Jdenticon +
    +
    +
    +

    + gringo +

    +
    +
    +

    + string +

    +
    +
    +
    +
    +

    + Sending +

    +

    + . +

    +

    + . +

    +

    + . +

    +
    +
    +
    +
    +
    +
    +
    + + message0 + +
    +
    +
    +
  • +
    + + `) + }) + + it('renders component with unsent messages when no peers seen this session', async () => { + const nowSeconds = DateTime.utc().toSeconds() + const messages = generateMessages({ createdAtSeconds: nowSeconds }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • +
    + Jdenticon +
    +
    +
    +
    -

    +

    + gringo +

    +
    +
    - gringo -

    +

    + string +

    +
    +
    +
    +
    +

    + Sending +

    +

    + . +

    +

    + . +

    +

    + . +

    +
    +
    +
    +
    +
    -

    - string -

    + message0 +
    +
    +
  • + +
    + + `) + }) + + it('renders component with multiple unsent messages', async () => { + const nowSeconds = DateTime.utc().toSeconds() + const messages = generateMessages({ createdAtSeconds: nowSeconds, amount: 5 }) + const result = renderComponent( + + + + + + ) + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
  • +
    +
    +
    +
    + Jdenticon +
    +
    - - message0 - +
    +

    + gringo +

    +
    +
    +

    + string +

    +
    +
    +
    +
    +

    + Sending +

    +

    + . +

    +

    + . +

    +

    + . +

    +
    +
    +
    +
    - + + message0 + +
    +
    + + message1 + +
    +
    + + message2 + +
    +
    - message0 - + + message3 + +
    +
    + + message4 + +
  • -
    - -
    - - `) + +
    + + `) + }) }) }) diff --git a/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.tsx b/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.tsx index 1a43ad6625..3b8f9ff205 100644 --- a/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.tsx +++ b/packages/desktop/src/renderer/components/widgets/channels/BasicMessage.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { DateTime } from 'luxon' import { styled } from '@mui/material/styles' import { Dictionary } from '@reduxjs/toolkit' import classNames from 'classnames' @@ -23,6 +24,8 @@ import Icon from '../../ui/Icon/Icon' import { UseModalType } from '../../../containers/hooks' import { HandleOpenModalType, UserLabelType } from '../userLabel/UserLabel.types' import UserLabel from '../userLabel/UserLabel.component' +import AnimatedEllipsis from '../../ui/AnimatedEllipsis/AnimatedEllipsis' +import { isMessageUnsent } from '@quiet/state-manager' const PREFIX = 'BasicMessageComponent' @@ -37,6 +40,7 @@ const classes = { broadcasted: `${PREFIX}broadcasted`, failed: `${PREFIX}failed`, avatar: `${PREFIX}avatar`, + avatarUnsent: `${PREFIX}avatar-unsent`, alignAvatar: `${PREFIX}alignAvatar`, moderation: `${PREFIX}moderation`, time: `${PREFIX}time`, @@ -44,6 +48,8 @@ const classes = { pending: `${PREFIX}pending`, info: `${PREFIX}info`, infoIcon: `${PREFIX}infoIcon`, + unsent: `${PREFIX}unsent`, + sending: `${PREFIX}sending`, } const StyledListItem = styled(ListItem)(({ theme }) => ({ @@ -116,6 +122,7 @@ const StyledListItem = styled(ListItem)(({ theme }) => ({ color: theme.palette.colors.lightGray, fontSize: 14, marginTop: -2, + marginRight: 5, }, [`& .${classes.iconBox}`]: { @@ -126,6 +133,15 @@ const StyledListItem = styled(ListItem)(({ theme }) => ({ color: theme.palette.colors.lightGray, }, + [`& .${classes.unsent}`]: { + opacity: 0.5, + }, + + [`& .${classes.sending}`]: { + color: theme.palette.colors.darkGray, + marginTop: -2, + }, + [`& .${classes.info}`]: { color: theme.palette.colors.white, }, @@ -161,6 +177,10 @@ const MessageProfilePhoto: React.FC<{ message: DisplayableMessage }> = ({ messag export interface BasicMessageProps { messages: DisplayableMessage[] pendingMessages?: Dictionary + connectedPeers: string[] | undefined + communityPeerList: string[] | undefined + lastConnectedTime: number + allPeersDisconnectedTime: number | undefined openUrl: (url: string) => void downloadStatuses?: Dictionary uploadedFileModal?: UseModalType<{ @@ -174,6 +194,10 @@ export interface BasicMessageProps { export const BasicMessageComponent: React.FC = ({ messages, pendingMessages = {}, + connectedPeers = [], + communityPeerList = [], + lastConnectedTime, + allPeersDisconnectedTime, downloadStatuses = {}, uploadedFileModal, onMathMessageRendered, @@ -196,6 +220,13 @@ export const BasicMessageComponent: React.FC - +
    {infoMessage ? ( @@ -225,10 +260,11 @@ export const BasicMessageComponent: React.FC {infoMessage ? 'Quiet' : messageDisplayData.nickname} @@ -249,12 +285,23 @@ export const BasicMessageComponent: React.FC {messageDisplayData.date} )} + {isUnsent && ( + + + + )} { duplicatedUsernameModalHandleOpen={jest.fn()} unregisteredUsernameModalHandleOpen={jest.fn()} messages={messages} + lastConnectedTime={1636995489} + allPeersDisconnectedTime={undefined} + connectedPeers={['foobar']} + communityPeerList={['foobar', 'barbaz']} scrollbarRef={React.createRef()} onScroll={jest.fn()} openUrl={jest.fn()} @@ -83,7 +87,7 @@ describe('ChannelMessages', () => {
  • { >
    { >

    string

    @@ -136,10 +142,196 @@ describe('ChannelMessages', () => { style="margin-top: -3px;" >
    + string + +
    +
    +
    +
    +
  • + + + + + + + `) + }) + + it('renders component with unsent messages', async () => { + const now = DateTime.utc().toSeconds() + const message = { + id: 'string', + type: 1, + message: 'string', + createdAt: now, + date: 'string', + nickname: 'string', + isDuplicated: false, + isRegistered: true, + pubKey: 'string', + } + + jest.spyOn(DateTime, 'utc').mockImplementationOnce(() => DateTime.utc(2019, 3, 7, 13, 3, 48)) + + const messages = { + Today: [[message]], + } + + const result = renderComponent( + + ) + + expect(result.baseElement).toMatchInlineSnapshot(` + +
    +
    +