diff --git a/package.json b/package.json index 09a3b98bf9..79b1789c64 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "@fetch-mock/jest": "^0.2.20", "@jest/globals": "^30.2.0", "@peculiar/webcrypto": "^1.4.3", - "@playwright/test": "1.57.0", + "@playwright/test": "1.58.1", "@principalstudio/html-webpack-inject-preload": "^1.2.7", "@sentry/webpack-plugin": "^4.0.0", "@stylistic/eslint-plugin": "^5.0.0", @@ -247,14 +247,14 @@ "knip": "^5.36.2", "lint-staged": "^16.0.0", "matrix-web-i18n": "3.6.0", - "mini-css-extract-plugin": "2.9.2", + "mini-css-extract-plugin": "2.10.0", "minimist": "^1.2.6", "modernizr": "^3.12.0", - "postcss": "8.4.46", + "postcss": "8.5.6", "postcss-easings": "4.0.0", "postcss-hexrgba": "2.1.0", - "postcss-import": "16.1.0", - "postcss-loader": "8.1.1", + "postcss-import": "16.1.1", + "postcss-loader": "8.2.0", "postcss-mixins": "12.0.0", "postcss-nested": "7.0.2", "postcss-preset-env": "11.1.1", diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/default-auto.png index 53a2ebad9e..7de0a32d50 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/has-error-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/has-error-auto.png index 40780744aa..1b867daf22 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/has-error-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/has-error-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-media-name-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-media-name-auto.png index 10b13a2596..da721483de 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-media-name-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-media-name-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-size-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-size-auto.png index c8f672ccd7..0e6321ef4a 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-size-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/AudioPlayerView/AudioPlayerView.stories.tsx/no-size-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/default-auto.png index 7e727a80d0..f53a9d0f89 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/lot-of-seconds-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/lot-of-seconds-auto.png index 41d3bb5784..34f40b8165 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/lot-of-seconds-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/Clock/Clock.stories.tsx/lot-of-seconds-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/default-auto.png index 3c1c35b2d3..2d275e14bb 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/disabled-auto.png b/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/disabled-auto.png index 24ceee9310..bd3a4ac90b 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/disabled-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/audio/SeekBar/SeekBar.stories.tsx/disabled-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx/default-auto.png index 9d558cc762..3733a95b01 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/critical-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/critical-auto.png index d248fc94cf..afe84b3579 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/critical-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/critical-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/default-auto.png index e967598620..3e3bca2a35 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/info-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/info-auto.png index 50109032d9..023e6af54c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/info-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/info-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/success-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/success-auto.png index e47d77bc42..9b6d88e610 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/success-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/success-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-action-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-action-auto.png index 24c4f77fdc..c2e523db13 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-action-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-action-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-avatar-image-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-avatar-image-auto.png index 4f3ebb33f2..994a3426f6 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-avatar-image-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-avatar-image-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-loads-of-content-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-loads-of-content-auto.png index 97592a756d..68d4d0f60e 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-loads-of-content-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/with-loads-of-content-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/without-close-auto.png b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/without-close-auto.png index 43792a86a3..13a4980352 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/without-close-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/composer/Banner/Banner.stories.tsx/without-close-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/default-auto.png index 8e27242e47..0c58b2b70c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-albanian-auto.png b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-albanian-auto.png index c845bdeabe..c57847ce6b 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-albanian-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-albanian-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-german-auto.png b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-german-auto.png index 6565cf86b7..e230daefef 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-german-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/crypto/SasEmoji/SasEmoji.stories.tsx/worst-case-german-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/default-auto.png index 8a8c8bd9b0..1bb98ff6da 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-children-auto.png b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-children-auto.png index 9c397d3a2b..b7509b00a1 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-children-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-children-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-lock-solid-icon-auto.png b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-lock-solid-icon-auto.png index 3013eba1b4..99683701c9 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-lock-solid-icon-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/has-lock-solid-icon-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/is-crypto-event-bubble-auto.png b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/is-crypto-event-bubble-auto.png index d7af848ce2..2767b4dd1c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/is-crypto-event-bubble-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/EventTileBubble/EventTileBubble.stories.tsx/is-crypto-event-bubble-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/TextualEventView/TextualEventView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/TextualEventView/TextualEventView.stories.tsx/default-auto.png index ef1ac10465..7f7b1736bc 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/event-tiles/TextualEventView/TextualEventView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/event-tiles/TextualEventView/TextualEventView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png index 27809a1621..78ab5b60ea 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-false-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-false-auto.png index 4b752bc958..5752391671 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-false-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-false-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-true-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-true-auto.png index 27809a1621..78ab5b60ea 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-true-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-true-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-block-icon-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-block-icon-auto.png index 51d6af59b8..d1c5f6f7c0 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-block-icon-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-block-icon-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-class-name-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-class-name-auto.png index b83cd9932d..b5f7f810de 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-class-name-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-class-name-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png index 27809a1621..78ab5b60ea 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/MediaBody/MediaBody.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/MediaBody/MediaBody.stories.tsx/default-auto.png index 320d80fc6f..e229052444 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/MediaBody/MediaBody.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/MediaBody/MediaBody.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/default-auto.png index 92a5f4d367..1bd6b0e55a 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/many-senders-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/many-senders-auto.png index 237f1a8bfd..8776eee112 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/many-senders-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/many-senders-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/no-tooltip-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/no-tooltip-auto.png index dcbe7310d6..04edc17bee 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/no-tooltip-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/no-tooltip-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/without-caption-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/without-caption-auto.png index e711eb4649..4d75f74cb5 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/without-caption-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx/without-caption-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/default-auto.png index 9830d14901..9cc04c2006 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-date-event-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-date-event-auto.png index 0c14346698..88f95f804f 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-date-event-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-date-event-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-html-child-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-html-child-auto.png index a6182a84ab..6be0311bab 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-html-child-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-html-child-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-late-event-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-late-event-auto.png index 9cb04d9757..a9d968d969 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-late-event-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/message-body/TimelineSeparator/TimelineSeparator.stories.tsx/with-late-event-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/default-auto.png index 5877ff657b..c849fa7c0e 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/without-close-button-auto.png b/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/without-close-button-auto.png index a12a0ba0e3..c3f7c0d041 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/without-close-button-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/pill-input/Pill/Pill.stories.tsx/without-close-button-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/default-auto.png index 09815ae73e..992bc2713c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/no-child-auto.png b/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/no-child-auto.png index 8f7d91a3a9..cf7fd6ed38 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/no-child-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/pill-input/PillInput/PillInput.stories.tsx/no-child-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/default-auto.png index 1532118e7c..43df9d7ffc 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/hover-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/hover-auto.png index 1532118e7c..43df9d7ffc 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/hover-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/hover-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/selected-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/selected-auto.png index 99eecbcb75..54f9f9d172 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/selected-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/selected-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/separator-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/separator-auto.png index 19d11bfda1..277400aa56 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/separator-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/separator-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/without-timestamp-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/without-timestamp-auto.png index f2e179cbdd..815ab86b1d 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/without-timestamp-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichItem/RichItem.stories.tsx/without-timestamp-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/default-auto.png index ca4053aee7..c91ac330b1 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/empty-auto.png b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/empty-auto.png index a23672245d..7610eb2b09 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/empty-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/rich-list/RichList/RichList.stories.tsx/empty-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/default-auto.png index 01b549ca72..27dc35b48c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/only-basic-modification-auto.png b/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/only-basic-modification-auto.png index 10444ee6b4..1efbec2117 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/only-basic-modification-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx/only-basic-modification-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/default-auto.png index 300e2237bc..0510570c85 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png index 5564b64357..62a6db0401 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-space-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-space-menu-auto.png index ef05e424a5..3396e4d7af 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-space-menu-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-space-menu-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/activity-indicator-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/activity-indicator-auto.png new file mode 100644 index 0000000000..6ac546adbc Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/activity-indicator-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/invited-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/invited-auto.png new file mode 100644 index 0000000000..cfd3f0f556 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/invited-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-auto.png new file mode 100644 index 0000000000..bacce8176c Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-with-count-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-with-count-auto.png new file mode 100644 index 0000000000..571b86c600 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/mention-with-count-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-auto.png new file mode 100644 index 0000000000..d4dbca6e39 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-without-activity-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-without-activity-auto.png new file mode 100644 index 0000000000..d4dbca6e39 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/muted-without-activity-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/no-notification-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/no-notification-auto.png new file mode 100644 index 0000000000..9f58a62407 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/no-notification-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/notification-with-count-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/notification-with-count-auto.png new file mode 100644 index 0000000000..0c2c4bbe5a Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/notification-with-count-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/unsent-message-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/unsent-message-auto.png new file mode 100644 index 0000000000..c2bcf320c5 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/unsent-message-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-auto.png new file mode 100644 index 0000000000..9a6f8b5b35 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-without-activity-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-without-activity-auto.png new file mode 100644 index 0000000000..9a6f8b5b35 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/video-call-without-activity-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/voice-call-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/voice-call-auto.png new file mode 100644 index 0000000000..86f5cde837 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx/voice-call-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png new file mode 100644 index 0000000000..7c5fa14a48 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/default-auto.png new file mode 100644 index 0000000000..e045a22515 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/invitation-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/invitation-auto.png new file mode 100644 index 0000000000..b9613435e8 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/invitation-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/no-message-preview-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/no-message-preview-auto.png new file mode 100644 index 0000000000..c1caeadc05 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/no-message-preview-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/selected-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/selected-auto.png new file mode 100644 index 0000000000..c44d733d2f Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/selected-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/unsent-message-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/unsent-message-auto.png new file mode 100644 index 0000000000..30ad387147 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/unsent-message-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png new file mode 100644 index 0000000000..e045a22515 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-mention-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-mention-auto.png new file mode 100644 index 0000000000..345a8775f8 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-mention-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png new file mode 100644 index 0000000000..d6ff5c8493 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/without-hover-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/without-hover-menu-auto.png new file mode 100644 index 0000000000..e045a22515 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/without-hover-menu-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png new file mode 100644 index 0000000000..95078643c6 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png new file mode 100644 index 0000000000..08f95e5684 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-container-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-with-active-wrapping-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-with-active-wrapping-filter-auto.png new file mode 100644 index 0000000000..a9d71aeb4f Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/narrow-with-active-wrapping-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png new file mode 100644 index 0000000000..9f58a62407 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/no-filters-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/people-selected-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/people-selected-auto.png new file mode 100644 index 0000000000..44c69d4b65 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx/people-selected-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/all-buttons-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/all-buttons-auto.png index 35205a70e1..3c16c7aac7 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/all-buttons-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/all-buttons-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/default-auto.png index d61a99a268..0d390fb218 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/with-dial-pad-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/with-dial-pad-auto.png index 35205a70e1..3c16c7aac7 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/with-dial-pad-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/with-dial-pad-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/without-explore-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/without-explore-auto.png index 74468944e0..3fa2ac2799 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/without-explore-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSearchView/RoomListSearchView.stories.tsx/without-explore-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/default-auto.png new file mode 100644 index 0000000000..980c20f8f5 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-auto.png new file mode 100644 index 0000000000..65f8c7b203 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-favourite-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-favourite-filter-auto.png new file mode 100644 index 0000000000..6b342ecc84 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-favourite-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-invites-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-invites-filter-auto.png new file mode 100644 index 0000000000..f6e995e46b Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-invites-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-low-priority-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-low-priority-filter-auto.png new file mode 100644 index 0000000000..af7e857088 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-low-priority-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-mentions-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-mentions-filter-auto.png new file mode 100644 index 0000000000..a654ab1e94 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-mentions-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-people-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-people-filter-auto.png new file mode 100644 index 0000000000..1ff8fed3ea Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-people-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-rooms-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-rooms-filter-auto.png new file mode 100644 index 0000000000..5a6cbf6cae Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-rooms-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-unread-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-unread-filter-auto.png new file mode 100644 index 0000000000..2d829787b2 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-unread-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-without-create-permission-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-without-create-permission-auto.png new file mode 100644 index 0000000000..fac07b40c4 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/empty-without-create-permission-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png new file mode 100644 index 0000000000..980c20f8f5 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/loading-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/loading-auto.png new file mode 100644 index 0000000000..bace5dba52 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/loading-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png new file mode 100644 index 0000000000..dd4d0a4bac Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-active-filter-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-active-filter-auto.png new file mode 100644 index 0000000000..c8087956e2 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-active-filter-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-selection-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-selection-auto.png new file mode 100644 index 0000000000..ba35806f60 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/with-selection-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/default-auto.png new file mode 100644 index 0000000000..0447c2d348 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png index 6e1e62c3e7..27ae2f14a1 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png index 6e1e62c3e7..086460d8e6 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png index cc31e1b3a9..2b47574de8 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png index d59731e529..76a4eaf89c 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-connection-lost-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-connection-lost-auto.png index 227e21000e..cf66b145d0 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-connection-lost-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-connection-lost-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-consent-link-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-consent-link-auto.png index eeca3d8763..ec27e69163 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-consent-link-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-consent-link-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-local-room-retry-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-local-room-retry-auto.png index 2415c1f469..eab66ee802 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-local-room-retry-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-local-room-retry-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-resource-limit-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-resource-limit-auto.png index e81f7dee80..74ab83fab2 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-resource-limit-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-resource-limit-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-auto.png index 32963c4fd3..a08a9d4155 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-sending-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-sending-auto.png index 41f37ca7e5..4c8736bcec 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-sending-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/room/RoomStatusBar/RoomStatusBarView.stories.tsx/with-unsent-messages-sending-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/utils/VirtualizedList/VirtualizedList.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/utils/VirtualizedList/VirtualizedList.stories.tsx/default-auto.png index 03ef57533e..c71aaab6b1 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/utils/VirtualizedList/VirtualizedList.stories.tsx/default-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/utils/VirtualizedList/VirtualizedList.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/package.json b/packages/shared-components/package.json index c3ad5b4b93..5746ad270f 100644 --- a/packages/shared-components/package.json +++ b/packages/shared-components/package.json @@ -64,7 +64,7 @@ "@element-hq/element-web-playwright-common": "2.2.5", "@fetch-mock/vitest": "^0.2.18", "@matrix-org/react-sdk-module-api": "^2.5.0", - "@playwright/test": "1.57.0", + "@playwright/test": "1.58.1", "@storybook/addon-a11y": "^10.0.7", "@storybook/addon-designs": "^11.0.1", "@storybook/addon-docs": "^10.0.7", diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index 04bc3596ec..2ba72dd1c5 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -25,6 +25,12 @@ "left_panel": { "open_dial_pad": "Open dial pad" }, + "notifications": { + "all_messages": "All messages", + "default_settings": "Match default settings", + "mentions_keywords": "Mentions and keywords", + "mute_room": "Mute room" + }, "room": { "context_menu": { "title": "Room options" @@ -50,8 +56,63 @@ } }, "room_list": { + "a11y": { + "default": "Open room %(roomName)s", + "invitation": "Open room %(roomName)s invitation.", + "mention": { + "one": "Open room %(roomName)s with 1 unread mention.", + "other": "Open room %(roomName)s with %(count)s unread mentions." + }, + "unread": { + "one": "Open room %(roomName)s with 1 unread message.", + "other": "Open room %(roomName)s with %(count)s unread messages." + }, + "unsent_message": "Open room %(roomName)s with an unsent message." + }, "appearance": "Appearance", + "collapse_filters": "Collapse filter list", + "empty": { + "no_chats": "No chats yet", + "no_chats_description": "Get started by messaging someone or by creating a room", + "no_chats_description_no_room_rights": "Get started by messaging someone", + "no_favourites": "You don't have favourite chats yet", + "no_favourites_description": "You can add a chat to your favourites in the chat settings", + "no_invites": "You don't have any unread invites", + "no_lowpriority": "You don't have any low priority rooms", + "no_mentions": "You don't have any unread mentions", + "no_people": "You don’t have direct chats with anyone yet", + "no_people_description": "You can deselect filters in order to see your other chats", + "no_rooms": "You’re not in any room yet", + "no_rooms_description": "You can deselect filters in order to see your other chats", + "no_unread": "Congrats! You don’t have any unread messages", + "show_activity": "See all activity", + "show_chats": "Show all chats" + }, + "expand_filters": "Expand filter list", + "filters": { + "favourite": "Favourites", + "invites": "Invites", + "low_priority": "Low priority", + "mentions": "Mentions", + "people": "People", + "rooms": "Rooms", + "unread": "Unreads" + }, + "list_title": "Room list", + "more_options": { + "copy_link": "Copy room link", + "favourited": "Favourited", + "leave_room": "Leave room", + "low_priority": "Low priority", + "mark_read": "Mark as read", + "mark_unread": "Mark as unread" + }, + "notification_options": "Notification options", "open_space_menu": "Open space menu", + "primary_filters": "Room list filters", + "room": { + "more_options": "More Options" + }, "room_options": "Room Options", "show_message_previews": "Show message previews", "sort": "Sort", diff --git a/packages/shared-components/src/i18n/strings/et.json b/packages/shared-components/src/i18n/strings/et.json index 171e4ed0c8..6310387318 100644 --- a/packages/shared-components/src/i18n/strings/et.json +++ b/packages/shared-components/src/i18n/strings/et.json @@ -29,6 +29,11 @@ "context_menu": { "title": "Jututoa eelistused" }, + "history_visibility_badge": { + "private": "Uued liikmed ei näe ajalugu", + "shared": "Uued liikmed näevad ajalugu", + "world_readable": "Kõik võivad ajalugu näha" + }, "status_bar": { "delete_all": "Kustuta kõik", "exceeded_resource_limit_description": "Teenuse kasutamise jätkamiseks võta ühendust oma teenuse haldajaga või peakasutajaga.", diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 0efa6698f6..96216b8ee2 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -27,6 +27,10 @@ export * from "./rich-list/RichItem"; export * from "./rich-list/RichList"; export * from "./room-list/RoomListHeaderView"; export * from "./room-list/RoomListSearchView"; +export * from "./room-list/RoomListView"; +export * from "./room-list/RoomListItem"; +export * from "./room-list/RoomListPrimaryFilters"; +export * from "./room-list/VirtualizedRoomListView"; export * from "./utils/Box"; export * from "./utils/Flex"; export * from "./right-panel/WidgetContextMenu"; diff --git a/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap b/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap index 441ed87491..b66ad0b495 100644 --- a/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap +++ b/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap @@ -4,7 +4,7 @@ exports[`RichItem > renders the item in default state 1`] = `
+`; + +exports[` > renders UnsentMessage story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; + +exports[` > renders WithHoverMenu story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders WithMention story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; + +exports[` > renders WithNotification story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; diff --git a/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts new file mode 100644 index 0000000000..b5e263567f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomNotifs"; + +export const mockRoom = { name: "General" }; + +export const defaultSnapshot: RoomListItemSnapshot = { + id: "!room:server", + room: mockRoom, + name: "General", + isBold: false, + messagePreview: "Alice: Hey everyone!", + notification: { + hasAnyNotificationOrActivity: false, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/index.ts b/packages/shared-components/src/room-list/RoomListItem/index.ts new file mode 100644 index 0000000000..edf17066b8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListItemView } from "./RoomListItem"; +export type { + Room, + RoomListItemSnapshot, + RoomItemViewModel, + RoomListItemActions, + RoomListItemViewProps, +} from "./RoomListItem"; +export { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +export type { RoomListItemNotificationMenuProps } from "./RoomListItemNotificationMenu"; +export { RoomListItemMoreOptionsMenu, MoreOptionContent } from "./RoomListItemMoreOptionsMenu"; +export type { RoomListItemMoreOptionsMenuProps } from "./RoomListItemMoreOptionsMenu"; +export { RoomListItemHoverMenu } from "./RoomListItemHoverMenu"; +export type { RoomListItemHoverMenuProps } from "./RoomListItemHoverMenu"; +export { RoomListItemContextMenu } from "./RoomListItemContextMenu"; +export type { RoomListItemContextMenuProps } from "./RoomListItemContextMenu"; +export { NotificationDecoration } from "./NotificationDecoration"; +export type { NotificationDecorationProps, NotificationDecorationData } from "./NotificationDecoration"; +export { RoomNotifState } from "./RoomNotifs"; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css new file mode 100644 index 0000000000..29db6d1bd6 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.roomListPrimaryFilters { + padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x); +} + +/* Hide filters that are wrapping when collapsed */ +.roomListPrimaryFilters :global(.wrapping) { + display: none; +} + +.list { + /** + * The InteractionObserver needs the height to be set to work properly. + */ + height: 100%; + flex: 1; +} + +/* IconButton styles for chevron */ +.iconButton svg { + transition: transform 0.1s linear; +} + +.iconButton[aria-expanded="true"] svg { + transform: rotate(180deg); +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx new file mode 100644 index 0000000000..a1a80334b9 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +import type { FilterId } from "./useVisibleFilters"; + +const meta: Meta = { + title: "Room List/RoomListPrimaryFilters", + component: RoomListPrimaryFilters, + tags: ["autodocs"], + args: { + onToggleFilter: fn(), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// All available filter IDs +const allFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite", "mentions", "invites", "low_priority"]; + +// Subset of filters for narrow container tests +const fewFilterIds: FilterId[] = ["people", "rooms", "unread"]; + +export const Default: Story = { + args: { + filterIds: allFilterIds, + }, +}; + +export const PeopleSelected: Story = { + args: { + filterIds: allFilterIds, + activeFilterId: "people", + }, +}; + +export const NoFilters: Story = { + args: { + filterIds: [], + }, +}; + +/** + * Narrow container that causes filters to wrap. + * The chevron button should appear to expand/collapse the filter list. + */ +export const NarrowContainer: Story = { + args: { + filterIds: fewFilterIds, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +/** + * Narrow container with active filter that would wrap. + * When collapsed, the active filter should move to the front. + */ +export const NarrowWithActiveWrappingFilter: Story = { + args: { + filterIds: fewFilterIds, + activeFilterId: "unread", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx new file mode 100644 index 0000000000..a86181da15 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { act } from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import * as stories from "./RoomListPrimaryFilters.stories"; + +const { Default, PeopleSelected, NoFilters, NarrowContainer, NarrowWithActiveWrappingFilter } = composeStories(stories); + +describe(" stories", () => { + describe("snapshots", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders PeopleSelected story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NoFilters story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowContainer story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowWithActiveWrappingFilter story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("behavior", () => { + it("should call onToggleFilter when a filter is clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("option", { name: "People" })); + + expect(Default.args.onToggleFilter).toHaveBeenCalled(); + }); + }); + + describe("resize behavior", () => { + let resizeCallback: ResizeObserverCallback; + + beforeEach(() => { + globalThis.ResizeObserver = class MockResizeObserver { + public constructor(callback: ResizeObserverCallback) { + resizeCallback = callback; + } + public observe = vi.fn(); + public unobserve = vi.fn(); + public disconnect = vi.fn(); + } as unknown as typeof ResizeObserver; + }); + + function mockFiltersNotWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + function mockUnreadWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + it("should hide wrapping filters and show chevron", () => { + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + + it("should expand and collapse filter list with chevron button", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible(); + + await user.click(screen.getByRole("button", { name: "Collapse filter list" })); + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + }); + + it("should move active filter to front when collapsed and wrapping", () => { + render(); + mockUnreadWrapping(); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "Unreads" })); + }); + + it("should restore original filter order when expanded", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "People" })); + }); + + it("should handle resize from non-wrapping to wrapping", () => { + render(); + mockFiltersNotWrapping(); + + expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull(); + + mockUnreadWrapping(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx new file mode 100644 index 0000000000..561544a3a5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, useId, useState } from "react"; +import { ChatFilter, IconButton } from "@vector-im/compound-web"; +import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; + +import { Flex } from "../../utils/Flex"; +import { _t } from "../../utils/i18n"; +import { useCollapseFilters } from "./useCollapseFilters"; +import { useVisibleFilters, type FilterId } from "./useVisibleFilters"; +import styles from "./RoomListPrimaryFilters.module.css"; + +/** + * Maps filter IDs to translated labels + */ +const filterIdToLabel = (filterId: FilterId): string => { + switch (filterId) { + case "unread": + return _t("room_list|filters|unread"); + case "people": + return _t("room_list|filters|people"); + case "rooms": + return _t("room_list|filters|rooms"); + case "favourite": + return _t("room_list|filters|favourite"); + case "mentions": + return _t("room_list|filters|mentions"); + case "invites": + return _t("room_list|filters|invites"); + case "low_priority": + return _t("room_list|filters|low_priority"); + } +}; + +/** + * Props for RoomListPrimaryFilters component + */ +export interface RoomListPrimaryFiltersProps { + /** Array of filter IDs to display */ + filterIds: FilterId[]; + /** Currently active filter ID (if any) */ + activeFilterId?: FilterId; + /** Callback when a filter is toggled */ + onToggleFilter: (filterId: FilterId) => void; +} + +/** + * The primary filters component for the room list. + * Displays a collapsible list of filters with expand/collapse functionality. + */ +export const RoomListPrimaryFilters: React.FC = ({ + filterIds, + activeFilterId, + onToggleFilter, +}): JSX.Element | null => { + const id = useId(); + const [isExpanded, setIsExpanded] = useState(false); + + const { + ref, + isWrapping: displayChevron, + wrappingIndex, + } = useCollapseFilters(isExpanded, "wrapping"); + const visibleFilterIds = useVisibleFilters(filterIds, activeFilterId, wrappingIndex); + + return ( + + {displayChevron && ( + setIsExpanded((expanded) => !expanded)} + > + + + )} + + {visibleFilterIds.map((filterId, index) => ( + onToggleFilter(filterId)} + > + {filterIdToLabel(filterId)} + + ))} + + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap new file mode 100644 index 0000000000..74c281bde5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap @@ -0,0 +1,388 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` stories > snapshots > renders Default story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; + +exports[` stories > snapshots > renders NarrowContainer story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NarrowWithActiveWrappingFilter story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NoFilters story 1`] = ` +
+
+
+
+
+`; + +exports[` stories > snapshots > renders PeopleSelected story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx new file mode 100644 index 0000000000..7697d4829c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +export type { RoomListPrimaryFiltersProps } from "./RoomListPrimaryFilters"; +export { useCollapseFilters } from "./useCollapseFilters"; +export { useVisibleFilters } from "./useVisibleFilters"; +export type { FilterId } from "./useVisibleFilters"; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts new file mode 100644 index 0000000000..e3fbf74e54 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useRef, useState, type RefObject } from "react"; + +/** + * A hook to manage the wrapping of filters in the room list. + * It observes the filter list and hides filters that are wrapping when the list is not expanded. + * @param isExpanded + * @param wrappingClassName - the CSS class to apply to wrapping filters + * @returns an object containing: + * - `ref`: a ref to put on the filter list element + * - `isWrapping`: a boolean indicating if the filters are wrapping + * - `wrappingIndex`: the index of the first filter that is wrapping + */ +export function useCollapseFilters( + isExpanded: boolean, + wrappingClassName: string, +): { + ref: RefObject; + isWrapping: boolean; + wrappingIndex: number; +} { + const ref = useRef(null); + const [isWrapping, setIsWrapping] = useState(false); + const [wrappingIndex, setWrappingIndex] = useState(-1); + + useEffect(() => { + if (!ref.current) return; + + const hideFilters = (list: Element): void => { + let isWrapping = false; + Array.from(list.children).forEach((node, i): void => { + const child = node as HTMLElement; + child.setAttribute("aria-hidden", "false"); + child.classList.remove(wrappingClassName); + + // If the filter list is expanded, all filters are visible + if (isExpanded) return; + + // If the previous element is on the left element of the current one, it means that the filter is wrapping + const previousSibling = child.previousElementSibling as HTMLElement | null; + if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) { + if (!isWrapping) setWrappingIndex(i); + isWrapping = true; + } + + // If the filter is wrapping, we hide it + child.classList.toggle(wrappingClassName, isWrapping); + child.setAttribute("aria-hidden", isWrapping.toString()); + }); + + if (!isWrapping) setWrappingIndex(-1); + setIsWrapping(isExpanded || isWrapping); + }; + + hideFilters(ref.current); + const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target))); + + observer.observe(ref.current); + return () => { + observer.disconnect(); + }; + }, [isExpanded, wrappingClassName]); + + return { ref, isWrapping, wrappingIndex }; +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts new file mode 100644 index 0000000000..73a580b4d9 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useState } from "react"; + +/** + * Standard filter identifiers that can be used across implementations. + * These are stable keys - the view layer maps them to translated labels. + */ +export type FilterId = "unread" | "people" | "rooms" | "favourite" | "mentions" | "invites" | "low_priority"; + +/** + * A hook to sort the filter IDs by active state. + * The list is sorted if the active filter index is greater than or equal to the wrapping index. + * If the wrapping index is -1, the filters are not sorted. + * + * @param filterIds - the list of filter IDs to sort. + * @param activeFilterId - the currently active filter ID (if any). + * @param wrappingIndex - the index of the first filter that is wrapping. + */ +export function useVisibleFilters( + filterIds: FilterId[], + activeFilterId: FilterId | undefined, + wrappingIndex: number, +): FilterId[] { + // By default, the filters are not sorted + const [sortedFilterIds, setSortedFilterIds] = useState(filterIds); + + useEffect(() => { + const activeIndex = activeFilterId ? filterIds.indexOf(activeFilterId) : -1; + const isActiveFilterWrapping = activeIndex >= wrappingIndex; + // If the active filter is not wrapping, we don't need to sort the filters + if (!isActiveFilterWrapping || wrappingIndex === -1) { + setSortedFilterIds(filterIds); + return; + } + + // Sort the filters with the active filter at first position + setSortedFilterIds( + filterIds.slice().sort((filterA, filterB) => { + // If the filter is active, it should be at the top of the list + if (filterA === activeFilterId && filterB !== activeFilterId) return -1; + if (filterA !== activeFilterId && filterB === activeFilterId) return 1; + // If both filters are active or not, keep their original order + return 0; + }), + ); + }, [filterIds, activeFilterId, wrappingIndex]); + + return sortedFilterIds; +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css new file mode 100644 index 0000000000..204e7615a4 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.genericPlaceholder { + align-self: center; + /** It should take 2/3 of the width **/ + width: 66%; + /** It should be positioned at 1/3 of the height **/ + padding-top: 33%; +} + +.title { + font: var(--cpd-font-body-lg-semibold); + text-align: center; +} + +.description { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + text-align: center; +} + +.defaultPlaceholder { + margin-top: var(--cpd-space-4x); +} + +.genericPlaceholder button { + width: 100%; +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx new file mode 100644 index 0000000000..12d26517b3 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx @@ -0,0 +1,182 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, type PropsWithChildren, type ReactNode } from "react"; +import { Button } from "@vector-im/compound-web"; +import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; +import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room"; + +import { Flex } from "../../utils/Flex"; +import { _t } from "../../utils/i18n"; +import { useViewModel } from "../../viewmodel"; +import type { RoomListViewModel } from "./RoomListView"; +import styles from "./RoomListEmptyStateView.module.css"; + +/** + * Props for RoomListEmptyStateView component + */ +export interface RoomListEmptyStateViewProps { + /** The view model containing all data and callbacks */ + vm: RoomListViewModel; +} + +/** + * Empty state component for the room list. + * Displays appropriate message and actions based on the active filter. + */ +export const RoomListEmptyStateView: React.FC = ({ vm }): JSX.Element => { + const snapshot = useViewModel(vm); + + // If there is no active filter, show the default empty state + if (!snapshot.activeFilterId) { + return ( + + + + {snapshot.canCreateRoom && ( + + )} + + + ); + } + + // Handle different filter cases based on filter ID + switch (snapshot.activeFilterId) { + case "favourite": + return ( + + ); + case "people": + return ( + + ); + case "rooms": + return ( + + ); + case "unread": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "invites": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "mentions": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "low_priority": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + default: + return ( + + ); + } +}; + +interface GenericPlaceholderProps { + /** The title of the placeholder */ + title: string; + /** The description of the placeholder */ + description?: string; + /** Optional children (e.g., action buttons) */ + children?: ReactNode; +} + +/** + * A generic placeholder for the room list + */ +function GenericPlaceholder({ title, description, children }: PropsWithChildren): JSX.Element { + return ( + + {title} + {description && {description}} + {children} + + ); +} + +interface ActionPlaceholderProps { + /** The title to display */ + title: string; + /** The action button text */ + action: string; + /** Callback when the action button is clicked */ + onAction?: () => void; +} + +/** + * A placeholder for the room list when a filter is active + * The user can take action to toggle the filter + */ +function ActionPlaceholder({ title, action, onAction }: ActionPlaceholderProps): JSX.Element { + return ( + + {onAction && ( + + )} + + ); +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css new file mode 100644 index 0000000000..2f65f7969d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.skeleton { + position: relative; + margin-left: 4px; + height: 100%; + flex: 1; +} + +.skeleton::before { + background-color: var(--cpd-color-bg-subtle-secondary); + width: 100%; + height: 100%; + content: ""; + position: absolute; + mask-repeat: repeat-y; + mask-size: auto 96px; + mask-image: url("./assets/skeleton.svg"); +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx new file mode 100644 index 0000000000..6ab8b80de3 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx @@ -0,0 +1,18 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; + +import styles from "./RoomListLoadingSkeleton.module.css"; + +/** + * Loading skeleton component for the room list. + * Displays a repeating skeleton pattern while rooms are being fetched. + */ +export const RoomListLoadingSkeleton: React.FC = (): JSX.Element => { + return
; +}; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx new file mode 100644 index 0000000000..206307262e --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx @@ -0,0 +1,221 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Room } from "../RoomListItem/RoomListItem"; +import type { FilterId } from "../RoomListPrimaryFilters"; +import { RoomListView, type RoomListSnapshot, type RoomListViewActions } from "./RoomListView"; +import { useMockedViewModel } from "../../viewmodel"; +import { + renderAvatar, + createGetRoomItemViewModel, + mockRoomIds, + smallListRoomIds, + largeListRoomIds, +} from "../story-mocks"; + +type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement }; + +const mockFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite"]; + +// Wrapper component that creates a mocked ViewModel +const RoomListViewWrapper = ({ + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListViewProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + }); + return ; +}; + +const meta = { + title: "Room List/RoomListView", + component: RoomListViewWrapper, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + // Snapshot properties (state) + isLoadingRooms: false, + isRoomListEmpty: false, + filterIds: mockFilterIds, + activeFilterId: undefined, + roomListState: { + activeRoomIndex: undefined, + spaceId: "!space:server", + filterKeys: undefined, + }, + roomIds: mockRoomIds, + canCreateRoom: true, + // Action properties (callbacks) + onToggleFilter: fn(), + createChatRoom: fn(), + createRoom: fn(), + getRoomItemViewModel: createGetRoomItemViewModel(mockRoomIds), + updateVisibleRooms: fn(), + renderAvatar, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19126", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Loading: Story = { + args: { + isLoadingRooms: true, + }, +}; + +export const Empty: Story = { + args: { + isRoomListEmpty: true, + }, +}; + +export const EmptyWithoutCreatePermission: Story = { + args: { + isRoomListEmpty: true, + canCreateRoom: false, + }, +}; + +export const WithActiveFilter: Story = { + args: { + filterIds: ["unread", "people", "rooms", "favourite"], + activeFilterId: "favourite", + roomListState: { + activeRoomIndex: undefined, + spaceId: "!space:server", + filterKeys: ["favourites"], + }, + }, +}; + +export const WithSelection: Story = { + args: { + roomListState: { + activeRoomIndex: 0, + spaceId: "!space:server", + filterKeys: undefined, + }, + }, +}; + +export const EmptyFavouriteFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["favourite", "people"], + activeFilterId: "favourite", + }, +}; + +export const EmptyPeopleFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["people", "rooms"], + activeFilterId: "people", + }, +}; + +export const EmptyRoomsFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["rooms", "people"], + activeFilterId: "rooms", + }, +}; + +export const EmptyUnreadFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["unread", "people"], + activeFilterId: "unread", + }, +}; + +export const EmptyInvitesFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["invites", "people"], + activeFilterId: "invites", + }, +}; + +export const EmptyMentionsFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["mentions", "people"], + activeFilterId: "mentions", + }, +}; + +export const EmptyLowPriorityFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["low_priority", "people"], + activeFilterId: "low_priority", + }, +}; + +export const SmallList: Story = { + args: { + roomIds: smallListRoomIds, + getRoomItemViewModel: createGetRoomItemViewModel(smallListRoomIds), + }, +}; + +export const LargeList: Story = { + args: { + roomIds: largeListRoomIds, + getRoomItemViewModel: createGetRoomItemViewModel(largeListRoomIds), + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx new file mode 100644 index 0000000000..15237eed7e --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { VirtuosoMockContext } from "react-virtuoso"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./RoomListView.stories"; + +const { + Default, + Loading, + Empty, + EmptyWithoutCreatePermission, + WithActiveFilter, + SmallList, + LargeList, + EmptyFavouriteFilter, + EmptyPeopleFilter, + EmptyRoomsFilter, + EmptyUnreadFilter, + EmptyInvitesFilter, + EmptyMentionsFilter, + EmptyLowPriorityFilter, +} = composeStories(stories); + +const renderWithMockContext = (component: React.ReactElement): ReturnType => { + return render(component, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + +describe("", () => { + it("renders Default story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders Loading story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders Empty story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyWithoutCreatePermission story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithActiveFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders SmallList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders LargeList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyFavouriteFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyPeopleFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyRoomsFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyUnreadFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyInvitesFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyMentionsFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyLowPriorityFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("should call onToggleFilter when filter is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("option", { name: "People" })); + + expect(Default.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call createRoom when New room button is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "New room" })); + + expect(Empty.args.createRoom).toHaveBeenCalled(); + }); + + it("should call createChatRoom when Start chat button is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "Start chat" })); + + expect(Empty.args.createChatRoom).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when Show all chats is clicked in unread empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "Show all chats" })); + + expect(EmptyUnreadFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in invites empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyInvitesFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in mentions empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyMentionsFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in low priority empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyLowPriorityFilter.args.onToggleFilter).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx new file mode 100644 index 0000000000..491c28d7d1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, type ReactNode } from "react"; + +import { useViewModel, type ViewModel } from "../../viewmodel"; +import { RoomListPrimaryFilters, type FilterId } from "../RoomListPrimaryFilters"; +import { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton"; +import { RoomListEmptyStateView } from "./RoomListEmptyStateView"; +import { VirtualizedRoomListView, type RoomListViewState } from "../VirtualizedRoomListView"; +import { type Room } from "../RoomListItem"; + +/** + * Snapshot for the room list view + */ +export type RoomListSnapshot = { + /** Whether the rooms are currently loading */ + isLoadingRooms: boolean; + /** Whether the room list is empty */ + isRoomListEmpty: boolean; + /** Array of filter IDs */ + filterIds: FilterId[]; + /** Currently active filter ID (if any) */ + activeFilterId?: FilterId; + /** Room list state */ + roomListState: RoomListViewState; + /** Array of room IDs for virtualization */ + roomIds: string[]; + /** Optional description for the empty state */ + emptyStateDescription?: string; + /** Optional action element for the empty state */ + emptyStateAction?: ReactNode; + /** Whether the user can create rooms */ + canCreateRoom?: boolean; +}; + +/** + * Actions interface for room list operations + */ +export interface RoomListViewActions { + /** Called when a filter is toggled */ + onToggleFilter: (filterId: FilterId) => void; + /** Called to create a new chat room */ + createChatRoom: () => void; + /** Called to create a new room */ + createRoom: () => void; + /** Get view model for a specific room (virtualization API) */ + getRoomItemViewModel: (roomId: string) => any; + /** Called when the visible range changes (virtualization API) */ + updateVisibleRooms: (startIndex: number, endIndex: number) => void; +} + +/** + * The view model type for the room list view + */ +export type RoomListViewModel = ViewModel & RoomListViewActions; + +/** + * Props for RoomListView component + */ +export interface RoomListViewProps { + /** The view model containing all data and callbacks */ + vm: RoomListViewModel; + /** Render function for room avatar */ + renderAvatar: (room: Room) => ReactNode; + /** Optional callback for keyboard events on the room list */ + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +/** + * Room list view component that manages filters, loading states, empty states, and the room list. + */ +export const RoomListView: React.FC = ({ vm, renderAvatar, onKeyDown }): JSX.Element => { + const snapshot = useViewModel(vm); + let listBody: ReactNode; + + if (snapshot.isLoadingRooms) { + listBody = ; + } else if (snapshot.isRoomListEmpty) { + listBody = ; + } else { + listBody = ; + } + + return ( + <> +
+ +
+ {listBody} + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap new file mode 100644 index 0000000000..c518632039 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap @@ -0,0 +1,11387 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Default story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; + +exports[` > renders Empty story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+ + No chats yet + + + Get started by messaging someone or by creating a room + +
+ + +
+
+
+
+`; + +exports[` > renders EmptyFavouriteFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have favourite chats yet + + + You can add a chat to your favourites in the chat settings + +
+
+
+`; + +exports[` > renders EmptyInvitesFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any unread invites + + +
+
+
+`; + +exports[` > renders EmptyLowPriorityFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any low priority rooms + + +
+
+
+`; + +exports[` > renders EmptyMentionsFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any unread mentions + + +
+
+
+`; + +exports[` > renders EmptyPeopleFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don’t have direct chats with anyone yet + + + You can deselect filters in order to see your other chats + +
+
+
+`; + +exports[` > renders EmptyRoomsFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You’re not in any room yet + + + You can deselect filters in order to see your other chats + +
+
+
+`; + +exports[` > renders EmptyUnreadFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + Congrats! You don’t have any unread messages + + +
+
+
+`; + +exports[` > renders EmptyWithoutCreatePermission story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+ + No chats yet + + + Get started by messaging someone + +
+ +
+
+
+
+`; + +exports[` > renders LargeList story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; + +exports[` > renders Loading story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+`; + +exports[` > renders SmallList story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + + + +`; + +exports[` > renders WithActiveFilter story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; diff --git a/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg b/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg new file mode 100644 index 0000000000..adf56e4ed8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/shared-components/src/room-list/RoomListView/index.tsx b/packages/shared-components/src/room-list/RoomListView/index.tsx new file mode 100644 index 0000000000..405b94bf2f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListView } from "./RoomListView"; +export type { RoomListViewProps, RoomListViewModel, RoomListSnapshot, RoomListViewActions } from "./RoomListView"; +export { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton"; +export { RoomListEmptyStateView } from "./RoomListEmptyStateView"; +export type { RoomListEmptyStateViewProps } from "./RoomListEmptyStateView"; diff --git a/res/css/views/rooms/RoomListPanel/_RoomList.pcss b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css similarity index 63% rename from res/css/views/rooms/RoomListPanel/_RoomList.pcss rename to packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css index 54798f1ea9..c444c8c1cd 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomList.pcss +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css @@ -1,10 +1,14 @@ /* - * Copyright 2025 New Vector Ltd. + * Copyright 2025 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -.mx_RoomList { +/** + * Room list container styles + */ +.roomList { height: 100%; + width: 100%; } diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx new file mode 100644 index 0000000000..3ea908cb55 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Room } from "../RoomListItem/RoomListItem"; +import { VirtualizedRoomListView, type RoomListViewState } from "./VirtualizedRoomListView"; +import type { RoomListSnapshot, RoomListViewActions } from "../RoomListView"; +import { useMockedViewModel } from "../../viewmodel"; +import type { FilterId } from "../RoomListPrimaryFilters"; +import { renderAvatar, createGetRoomItemViewModel, mockRoomIds } from "../story-mocks"; + +type RoomListStoryProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement }; + +// Use first 10 room IDs for this story +const storyRoomIds = mockRoomIds.slice(0, 10); + +// Wrapper component that creates a mocked ViewModel +const RoomListWrapper = ({ + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListStoryProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + }); + + return ( +
+ +
+ ); +}; + +const mockFilterIds: FilterId[] = ["unread", "people"]; + +const defaultRoomListState: RoomListViewState = { + activeRoomIndex: 0, + spaceId: "!space:server", + filterKeys: undefined, +}; + +const meta: Meta = { + title: "Room List/VirtualizedRoomListView", + component: RoomListWrapper, + tags: ["autodocs"], + args: { + isLoadingRooms: false, + isRoomListEmpty: false, + filterIds: mockFilterIds, + activeFilterId: undefined, + roomIds: storyRoomIds, + roomListState: defaultRoomListState, + canCreateRoom: true, + onToggleFilter: fn(), + createChatRoom: fn(), + createRoom: fn(), + getRoomItemViewModel: createGetRoomItemViewModel(storyRoomIds), + updateVisibleRooms: fn(), + renderAvatar, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4", + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx new file mode 100644 index 0000000000..23f384554d --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen, fireEvent } from "@test-utils"; +import { VirtuosoMockContext } from "react-virtuoso"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./VirtualizedRoomListView.stories"; + +const { Default } = composeStories(stories); + +const renderWithMockContext = (component: React.ReactElement): ReturnType => { + return render(component, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + +describe("", () => { + it("renders Default story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("should render the room list listbox", () => { + renderWithMockContext(); + expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument(); + }); + + it("should render room items", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + expect(items.length).toBeGreaterThan(0); + }); + + it("should mark selected room with aria-selected true", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + // The first item (index 0) should be selected based on Default story (activeRoomIndex: 0) + expect(items[0]).toHaveAttribute("aria-selected", "true"); + }); + + it("should handle focus state correctly", () => { + renderWithMockContext(); + + const listbox = screen.getByRole("listbox", { name: "Room list" }); + fireEvent.focus(listbox); + + const items = screen.getAllByRole("option"); + // First item should have tabIndex 0 (focusable) when list is focused + expect(items[0]).toHaveAttribute("tabIndex", "0"); + }); + + it("should call updateVisibleRooms on render", () => { + renderWithMockContext(); + expect(Default.args.updateVisibleRooms).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx new file mode 100644 index 0000000000..7b27df0f28 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx @@ -0,0 +1,198 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useCallback, useMemo, useRef, type JSX, type ReactNode } from "react"; +import { type ScrollIntoViewLocation } from "react-virtuoso"; +import { isEqual } from "lodash"; + +import type { Room } from "../RoomListItem/RoomListItem"; +import { useViewModel } from "../../viewmodel"; +import { _t } from "../../utils/i18n"; +import { VirtualizedList, type VirtualizedListContext } from "../../utils/VirtualizedList"; +import { RoomListItemView } from "../RoomListItem"; +import type { RoomListViewModel } from "../RoomListView"; + +/** + * Filter key type - opaque string type for filter identifiers + */ +export type FilterKey = string; + +/** + * State for the room list data (nested within RoomListSnapshot) + */ +export interface RoomListViewState { + /** Optional active room index for keyboard navigation */ + activeRoomIndex?: number; + /** Space ID for context tracking */ + spaceId?: string; + /** Active filter keys for context tracking */ + filterKeys?: FilterKey[]; +} + +/** + * Props for the VirtualizedRoomListView component + */ +export interface VirtualizedRoomListViewProps { + /** + * The view model containing all room list data and callbacks + */ + vm: RoomListViewModel; + + /** + * Render function for room avatar + * @param room - The opaque Room object from the client + */ + renderAvatar: (room: Room) => ReactNode; + + /** + * Optional callback for keyboard key down events + */ + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +/** Height of a single room list item in pixels */ +const ROOM_LIST_ITEM_HEIGHT = 48; + +/** + * Type for context used in ListView + */ +type Context = { spaceId: string; filterKeys: FilterKey[] | undefined }; + +/** + * Amount to extend the top and bottom of the viewport by. + * From manual testing and user feedback 25 items is reported to be enough to avoid blank space + * when using the mouse wheel, and the trackpad scrolling at a slow to moderate speed where you + * can still see/read the content. Using the trackpad to sling through a large percentage of the + * list quickly will still show blank space. We would likely need to simplify the item content to + * improve this case. + */ +const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT; + +/** + * A virtualized list of rooms. + * This component provides efficient rendering of large room lists using virtualization, + * and renders RoomListItemView components for each room. + * + * @example + * ```tsx + * } /> + * ``` + */ +export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: VirtualizedRoomListViewProps): JSX.Element { + const snapshot = useViewModel(vm); + const { roomListState, roomIds } = snapshot; + const activeRoomIndex = roomListState.activeRoomIndex; + const lastSpaceId = useRef(undefined); + const lastFilterKeys = useRef(undefined); + const roomCount = roomIds.length; + + /** + * Callback when the visible range changes + * Notifies the view model which rooms are visible + */ + const rangeChanged = useCallback( + (range: { startIndex: number; endIndex: number }) => { + vm.updateVisibleRooms(range.startIndex, range.endIndex); + }, + [vm], + ); + + /** + * Get the item component for a specific index + * Gets the room's view model and passes it to RoomListItemView + */ + const getItemComponent = useCallback( + ( + index: number, + roomId: string, + context: VirtualizedListContext, + onFocus: (item: string, e: React.FocusEvent) => void, + ): JSX.Element => { + const isSelected = activeRoomIndex === index; + const roomItemVM = vm.getRoomItemViewModel(roomId); + + // Item is focused when the list has focus AND this item's key matches tabIndexKey + // This matches the old RoomList implementation's roving tabindex pattern + const isFocused = context.focused && context.tabIndexKey === roomId; + + return ( + + ); + }, + [activeRoomIndex, roomCount, renderAvatar, vm], + ); + + /** + * Get the key for a room item + * Since we're using virtualization, items are always room ID strings + */ + const getItemKey = useCallback((item: string): string => { + return item; + }, []); + + const context = useMemo( + () => ({ spaceId: roomListState.spaceId || "", filterKeys: roomListState.filterKeys }), + [roomListState.spaceId, roomListState.filterKeys], + ); + + /** + * Determine if we should scroll the active index into view + * This happens when the space or filters change + */ + const scrollIntoViewOnChange = useCallback( + (params: { + context: VirtualizedListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>; + }): ScrollIntoViewLocation | null | undefined | false => { + const { spaceId, filterKeys } = params.context.context; + const shouldScrollIndexIntoView = + lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys); + lastFilterKeys.current = filterKeys; + lastSpaceId.current = spaceId; + + if (shouldScrollIndexIntoView) { + return { + align: "start", + index: activeRoomIndex || 0, + behavior: "auto", + }; + } + return false; + }, + [activeRoomIndex], + ); + + return ( + true} + rangeChanged={rangeChanged} + onKeyDown={onKeyDown} + increaseViewportBy={{ + bottom: EXTENDED_VIEWPORT_HEIGHT, + top: EXTENDED_VIEWPORT_HEIGHT, + }} + /> + ); +} diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap b/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap new file mode 100644 index 0000000000..85cd5f6d08 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap @@ -0,0 +1,1277 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Default story 1`] = ` +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + + +`; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts b/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts new file mode 100644 index 0000000000..da5840ada5 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { VirtualizedRoomListView } from "./VirtualizedRoomListView"; +export type { VirtualizedRoomListViewProps, RoomListViewState, FilterKey } from "./VirtualizedRoomListView"; diff --git a/packages/shared-components/src/room-list/story-mocks.tsx b/packages/shared-components/src/room-list/story-mocks.tsx new file mode 100644 index 0000000000..83a8eb1b94 --- /dev/null +++ b/packages/shared-components/src/room-list/story-mocks.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { Room } from "./RoomListItem/RoomListItem"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomListItem/RoomNotifs"; + +/** + * Mock avatar component for stories + */ +export const mockAvatar = (name: string): React.ReactElement => ( +
+ {name.substring(0, 2).toUpperCase()} +
+); + +/** + * Render avatar function for stories + */ +export const renderAvatar = (room: Room): React.ReactElement => { + // Cast to any to access properties - in real usage, the room object from the SDK will have these + return mockAvatar((room as any)?.name || "Room"); +}; + +/** + * Room names used for mock data + */ +const roomNames = [ + "General", + "Random", + "Engineering", + "Design", + "Product", + "Marketing", + "Sales", + "Support", + "Announcements", + "Off-topic", + "Team Alpha", + "Team Beta", + "Project X", + "Project Y", + "Water Cooler", + "Feedback", + "Ideas", + "Bugs", + "Features", + "Releases", +]; + +/** + * Create a mock room item snapshot for stories + */ +export const createMockRoomSnapshot = (id: string, name: string, index: number): RoomListItemSnapshot => ({ + id, + room: { name }, + name, + isBold: index % 3 === 0, + messagePreview: index % 2 === 0 ? `Last message in ${name}` : undefined, + notification: { + hasAnyNotificationOrActivity: index % 5 === 0, + isUnsentMessage: false, + invited: false, + isMention: index % 5 === 0, + isActivityNotification: false, + isNotification: index % 5 === 0, + hasUnreadCount: index % 5 === 0, + count: index % 5 === 0 ? index : 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}); + +/** + * Create a mock getRoomItemViewModel function for stories + */ +export const createGetRoomItemViewModel = (roomIds: string[]): ((roomId: string) => any) => { + const viewModels = new Map(); + roomIds.forEach((roomId, index) => { + const name = roomNames[index % roomNames.length]; + const snapshot = createMockRoomSnapshot(roomId, name, index); + + const mockViewModel = { + getSnapshot: () => snapshot, + subscribe: fn(), + unsubscribe: fn(), + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + }; + viewModels.set(roomId, mockViewModel); + }); + + return (roomId: string) => viewModels.get(roomId); +}; + +/** + * Mock room IDs for different list sizes + */ +export const mockRoomIds = Array.from({ length: 20 }, (_, i) => `!room${i}:server`); +export const smallListRoomIds = mockRoomIds.slice(0, 5); +export const largeListRoomIds = Array.from({ length: 100 }, (_, i) => `!room${i}:server`); diff --git a/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx index 20e191ba38..adea593d07 100644 --- a/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx +++ b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx @@ -1,9 +1,9 @@ /* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ import React, { useRef, type JSX, useCallback, useEffect, useState, useMemo } from "react"; import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso"; @@ -95,6 +95,19 @@ export interface IVirtualizedListProps extends Omit< * @returns */ onKeyDown?: (e: React.KeyboardEvent) => void; + + /** + * Optional total count of items (for virtualization with partial data loading). + * If provided, this will be used instead of items.length for the total count. + */ + totalCount?: number; + + /** + * Optional callback when the visible range of items changes. + * Useful for loading data on-demand as the user scrolls. + * @param range - The new visible range with startIndex and endIndex + */ + rangeChanged?: (range: ListRange) => void; } /** @@ -113,7 +126,17 @@ export type ScrollIntoViewOnChange = NonNullable< */ export function VirtualizedList(props: IVirtualizedListProps): React.ReactElement { // Extract our custom props to avoid conflicts with Virtuoso props - const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props; + const { + items, + getItemComponent, + isItemFocusable, + getItemKey, + context, + onKeyDown, + totalCount, + rangeChanged, + ...virtuosoProps + } = props; /** Reference to the Virtuoso component for programmatic scrolling */ const virtuosoHandleRef = useRef(null); /** Reference to the DOM element containing the virtualized list */ @@ -324,6 +347,15 @@ export function VirtualizedList(props: IVirtualizedListProp [tabIndexKey, isFocused, props.context], ); + // Combine internal range tracking with optional external callback + const handleRangeChanged = useCallback( + (range: ListRange) => { + setVisibleRange(range); + rangeChanged?.(range); + }, + [rangeChanged], + ); + return ( (props: IVirtualizedListProp scrollerRef={scrollerRef} onKeyDown={keyDownCallback} context={listContext} - rangeChanged={setVisibleRange} + rangeChanged={handleRangeChanged} // virtuoso errors internally if you pass undefined. overscan={props.overscan || 0} data={props.items} + totalCount={totalCount} onFocus={onFocus} onBlur={onBlur} itemContent={getItemComponentInternal} diff --git a/packages/shared-components/src/viewmodel/index.ts b/packages/shared-components/src/viewmodel/index.ts index 7936e535a2..25e5ec60b8 100644 --- a/packages/shared-components/src/viewmodel/index.ts +++ b/packages/shared-components/src/viewmodel/index.ts @@ -12,5 +12,5 @@ export * from "./ViewModelSubscriptions"; export type * from "./ViewModel"; export * from "./MockViewModel"; export * from "./useCreateAutoDisposedViewModel"; -export * from "./useViewModel"; export * from "./useMockedViewModel"; +export * from "./useViewModel"; diff --git a/packages/shared-components/vitest.config.ts b/packages/shared-components/vitest.config.ts index 2d5d55bc16..975ced5ae3 100644 --- a/packages/shared-components/vitest.config.ts +++ b/packages/shared-components/vitest.config.ts @@ -17,6 +17,7 @@ import { nodePolyfills } from "vite-plugin-node-polyfills"; import { InlineConfig } from "vite"; import { Reporter } from "vitest/reporters"; import { env } from "process"; +import { BrowserContextOptions } from "playwright-core"; const dirname = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url)); @@ -60,6 +61,19 @@ if (env["GITHUB_ACTIONS"] !== undefined) { } } +const commonContextOptions: Omit = { + reducedMotion: "reduce", + // Force consistent font rendering + colorScheme: "light", + // Disable font smoothing for consistent rendering + deviceScaleFactor: 1, +}; + +const commonLaunchOptions = { + // Options to try to make font rendering more consistent + args: ["--font-render-hinting=none", "--disable-font-subpixel-positioning", "--disable-lcd-text"], +}; + export default defineConfig({ test: { coverage: { @@ -91,7 +105,10 @@ export default defineConfig({ browser: { enabled: true, headless: true, - provider: playwright({ contextOptions: { reducedMotion: "reduce" } }), + provider: playwright({ + contextOptions: commonContextOptions, + launchOptions: commonLaunchOptions, + }), instances: [{ browser: "chromium" }], }, setupFiles: [".storybook/vitest.setup.ts"], @@ -105,7 +122,12 @@ export default defineConfig({ browser: { enabled: true, headless: true, - provider: playwright({}), + provider: playwright({ + // These tests don't actually take screenshots (at least at time of writing) + // but let's pass these options everywhere for consistency + contextOptions: commonContextOptions, + launchOptions: commonLaunchOptions, + }), instances: [{ browser: "chromium" }], }, setupFiles: ["src/test/setupTests.ts"], diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index eb0f759652..5907ad6d97 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -123,11 +123,12 @@ test.describe("Room list", () => { // It should make the room muted await page.getByRole("menuitem", { name: "Mute room" }).click(); - // Not hovered, the room decoration should be the muted icon - await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); - // Put focus on the room list await roomListView.getByRole("option", { name: "Open room room28" }).click(); + await roomItem.scrollIntoViewIfNeeded(); + + // Not hovered, the room decoration should be the muted icon + await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); // During hover the room decoration should still be the muted icon await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts index 405cdf22ed..c13730107c 100644 --- a/playwright/e2e/read-receipts/room-list-order.spec.ts +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -18,19 +18,28 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { util, msg, page, + app, + bot, }) => { + // Create a third room to navigate to + const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] }); + await bot.awaitRoomMembership(room3Id); + const room3 = { name: "Room Gamma", roomId: room3Id }; + await util.goTo(room2); // Display the unread first room + await util.receiveMessages(room2, ["Msg2"]); await util.receiveMessages(room1, ["Msg1"]); await page.reload(); - // switch rooms so they can re-order in the list - await util.goTo(room1); + // Switch to room3 so neither room1 nor room2 is selected/sticky + // This allows them to reorder based on activity + await util.goTo(room3); // Room 1 has an unread message and should be displayed first // (as the default is to sort by activity) - await util.assertRoomListOrder([room1, room2]); + await util.assertRoomListOrder([room1, room2, room3]); }); test("Rooms with unread threads appear at the top of room list with default 'activity' order", async ({ @@ -38,18 +47,29 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { roomBeta: room2, util, msg, + app, + bot, }) => { + // Create a third room to navigate to + const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] }); + await bot.awaitRoomMembership(room3Id); + const room3 = { name: "Room Gamma", roomId: room3Id }; + await util.goTo(room2); await util.receiveMessages(room1, ["Msg1"]); + await util.receiveMessages(room2, ["Msg2"]); await util.markAsRead(room1); await util.assertRead(room1); - // Display the unread first room + // Display the unread first room (room1 moves above room2 as it has an unread thread) await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]); await util.saveAndReload(); + // Switch to room3 so neither room1 nor room2 is selected/sticky + await util.goTo(room3); + // Room 1 has an unread message and should be displayed first - await util.assertRoomListOrder([room1, room2]); + await util.assertRoomListOrder([room1, room2, room3]); }); }); }); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index b31deadace..38aff5928d 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -38,7 +38,7 @@ const test = base.extend<{ test.describe("Sliding Sync", () => { const checkOrder = async (wantOrder: string[], page: Page) => { - await expect(page.getByTestId("room-list").locator(".mx_RoomListItemView_text")).toHaveText(wantOrder); + await expect(page.getByTestId("room-list").getByTestId("room-name")).toHaveText(wantOrder); }; const bumpRoom = async (roomId: string, app: ElementAppPage) => { diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 061a19617e..3e68b09ad6 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png index 721130747c..68762d038d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index f9a865a0f4..cdebb2131d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index caca293378..85f11918f6 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -10,7 +10,7 @@ import { type StartedPostgreSqlContainer, } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; -const TAG = "main@sha256:d0d03f9067c7977807131a9c739c9ed9f081063d1a1c21bee66204e40c44aa50"; +const TAG = "main@sha256:2eec80f8348ed1f78414cc9258399b9329b53e6e9e2e634da484568996c1da8f"; /** * MatrixAuthenticationServiceContainer which freezes the docker digest to diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 059ef00c6e..bc13c3582e 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; -const TAG = "develop@sha256:4620e446582e79a3942f5438ebf714da18c281143496e53be318334b4697b449"; +const TAG = "develop@sha256:f0de453dbb284112c4bc91be01345f189f218bf51ee09ed565dab295c0a72b44"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1556eab737..37b02fe0bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,6 @@ overrides: wrap-ansi: npm:wrap-ansi@^7.0.0 matrix-widget-api: ^1.16.1 -pnpmfileChecksum: sha256-tdCiV0ni/ieyXxF+TWmrqHp2UKm/PELWSu657Ob2j7I= - patchedDependencies: '@matrix-org/react-sdk-module-api': hash: 016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96 @@ -333,7 +331,7 @@ importers: version: 0.16.3 '@element-hq/element-web-playwright-common': specifier: 2.2.5 - version: 2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.57.0)(playwright-core@1.57.0) + version: 2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.1)(playwright-core@1.58.1) '@fetch-mock/jest': specifier: ^0.2.20 version: 0.2.20(@jest/globals@30.2.0)(jest@30.2.0(@types/node@22.19.8)) @@ -344,8 +342,8 @@ importers: specifier: ^1.4.3 version: 1.5.0 '@playwright/test': - specifier: 1.57.0 - version: 1.57.0 + specifier: 1.58.1 + version: 1.58.1 '@principalstudio/html-webpack-inject-preload': specifier: ^1.2.7 version: 1.2.7(html-webpack-plugin@5.6.6(webpack@5.105.0))(webpack@5.105.0) @@ -575,8 +573,8 @@ importers: specifier: 3.6.0 version: 3.6.0 mini-css-extract-plugin: - specifier: 2.9.2 - version: 2.9.2(webpack@5.105.0) + specifier: 2.10.0 + version: 2.10.0(webpack@5.105.0) minimist: specifier: ^1.2.6 version: 1.2.8 @@ -584,35 +582,35 @@ importers: specifier: ^3.12.0 version: 3.13.1 postcss: - specifier: 8.4.46 - version: 8.4.46 + specifier: 8.5.6 + version: 8.5.6 postcss-easings: specifier: 4.0.0 - version: 4.0.0(postcss@8.4.46) + version: 4.0.0(postcss@8.5.6) postcss-hexrgba: specifier: 2.1.0 - version: 2.1.0(postcss@8.4.46) + version: 2.1.0(postcss@8.5.6) postcss-import: - specifier: 16.1.0 - version: 16.1.0(postcss@8.4.46) + specifier: 16.1.1 + version: 16.1.1(postcss@8.5.6) postcss-loader: - specifier: 8.1.1 - version: 8.1.1(postcss@8.4.46)(typescript@5.9.3)(webpack@5.105.0) + specifier: 8.2.0 + version: 8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.0) postcss-mixins: specifier: 12.0.0 - version: 12.0.0(postcss@8.4.46) + version: 12.0.0(postcss@8.5.6) postcss-nested: specifier: 7.0.2 - version: 7.0.2(postcss@8.4.46) + version: 7.0.2(postcss@8.5.6) postcss-preset-env: specifier: 11.1.1 - version: 11.1.1(postcss@8.4.46) + version: 11.1.1(postcss@8.5.6) postcss-scss: specifier: 4.0.9 - version: 4.0.9(postcss@8.4.46) + version: 4.0.9(postcss@8.5.6) postcss-simple-vars: specifier: 7.0.1 - version: 7.0.1(postcss@8.4.46) + version: 7.0.1(postcss@8.5.6) prettier: specifier: 3.8.1 version: 3.8.1 @@ -715,7 +713,7 @@ importers: devDependencies: '@element-hq/element-web-playwright-common': specifier: 2.2.5 - version: 2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.57.0)(playwright-core@1.57.0) + version: 2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.1)(playwright-core@1.58.1) '@fetch-mock/vitest': specifier: ^0.2.18 version: 0.2.18(vitest@4.0.18) @@ -723,8 +721,8 @@ importers: specifier: ^2.5.0 version: 2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4) '@playwright/test': - specifier: 1.57.0 - version: 1.57.0 + specifier: 1.58.1 + version: 1.58.1 '@storybook/addon-a11y': specifier: ^10.0.7 version: 10.2.5(storybook@10.2.5(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) @@ -784,7 +782,7 @@ importers: version: 5.1.3(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2)) '@vitest/browser-playwright': specifier: ^4.0.17 - version: 4.0.18(playwright@1.57.0)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) + version: 4.0.18(playwright@1.58.1)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) '@vitest/coverage-v8': specifier: ^4.0.17 version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18) @@ -2850,8 +2848,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@playwright/test@1.58.1': + resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} engines: {node: '>=18'} hasBin: true @@ -7082,10 +7080,6 @@ packages: node-notifier: optional: true - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -7517,8 +7511,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - mini-css-extract-plugin@2.9.2: - resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + mini-css-extract-plugin@2.10.0: + resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 @@ -7932,13 +7926,13 @@ packages: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} engines: {node: '>=16.0.0'} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.1: + resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.1: + resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} engines: {node: '>=18'} hasBin: true @@ -8106,8 +8100,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-import@16.1.0: - resolution: {integrity: sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==} + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} engines: {node: '>=18.0.0'} peerDependencies: postcss: ^8.0.0 @@ -8124,8 +8118,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-loader@8.1.1: - resolution: {integrity: sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==} + postcss-loader@8.2.0: + resolution: {integrity: sha512-tHX+RkpsXVcc7st4dSdDGliI+r4aAQDuv+v3vFYHixb6YgjreG5AG4SEB0kDK8u2s6htqEEpKlkhSBUTvWKYnA==} engines: {node: '>= 18.12.0'} peerDependencies: '@rspack/core': 0.x || 1.x @@ -8382,10 +8376,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.46: - resolution: {integrity: sha512-73x4XLhY0QNN+87/u6F7TRq+yl3xPAjlbRRvhly1mAKJgNO4q5fiqegez/Yi3u+ez8wbBXXqY9N1+RAJAVCzEw==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -10219,10 +10209,10 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@axe-core/playwright@4.11.1(playwright-core@1.57.0)': + '@axe-core/playwright@4.11.1(playwright-core@1.58.1)': dependencies: axe-core: 4.11.1 - playwright-core: 1.57.0 + playwright-core: 1.58.1 '@babel/code-frame@7.29.0': dependencies: @@ -11170,278 +11160,278 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-alpha-function@2.0.2(postcss@8.4.46)': + '@csstools/postcss-alpha-function@2.0.2(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-cascade-layers@6.0.0(postcss@8.4.46)': + '@csstools/postcss-cascade-layers@6.0.0(postcss@8.5.6)': dependencies: '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - '@csstools/postcss-color-function-display-p3-linear@2.0.1(postcss@8.4.46)': + '@csstools/postcss-color-function-display-p3-linear@2.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-color-function@5.0.1(postcss@8.4.46)': + '@csstools/postcss-color-function@5.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-color-mix-function@4.0.1(postcss@8.4.46)': + '@csstools/postcss-color-mix-function@4.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-color-mix-variadic-function-arguments@2.0.1(postcss@8.4.46)': + '@csstools/postcss-color-mix-variadic-function-arguments@2.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-content-alt-text@3.0.0(postcss@8.4.46)': + '@csstools/postcss-content-alt-text@3.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-contrast-color-function@3.0.1(postcss@8.4.46)': + '@csstools/postcss-contrast-color-function@3.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-exponential-functions@3.0.0(postcss@8.4.46)': + '@csstools/postcss-exponential-functions@3.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-font-format-keywords@5.0.0(postcss@8.4.46)': + '@csstools/postcss-font-format-keywords@5.0.0(postcss@8.5.6)': dependencies: - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@3.0.1(postcss@8.4.46)': + '@csstools/postcss-gamut-mapping@3.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-gradients-interpolation-method@6.0.1(postcss@8.4.46)': + '@csstools/postcss-gradients-interpolation-method@6.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-hwb-function@5.0.1(postcss@8.4.46)': + '@csstools/postcss-hwb-function@5.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-ic-unit@5.0.0(postcss@8.4.46)': + '@csstools/postcss-ic-unit@5.0.0(postcss@8.5.6)': dependencies: - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@3.0.0(postcss@8.4.46)': + '@csstools/postcss-initial@3.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-is-pseudo-class@6.0.0(postcss@8.4.46)': + '@csstools/postcss-is-pseudo-class@6.0.0(postcss@8.5.6)': dependencies: '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - '@csstools/postcss-light-dark-function@3.0.0(postcss@8.4.46)': + '@csstools/postcss-light-dark-function@3.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-logical-float-and-clear@4.0.0(postcss@8.4.46)': + '@csstools/postcss-logical-float-and-clear@4.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-logical-overflow@3.0.0(postcss@8.4.46)': + '@csstools/postcss-logical-overflow@3.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-logical-overscroll-behavior@3.0.0(postcss@8.4.46)': + '@csstools/postcss-logical-overscroll-behavior@3.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-logical-resize@4.0.0(postcss@8.4.46)': + '@csstools/postcss-logical-resize@4.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@4.0.0(postcss@8.4.46)': + '@csstools/postcss-logical-viewport-units@4.0.0(postcss@8.5.6)': dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-media-minmax@3.0.0(postcss@8.4.46)': + '@csstools/postcss-media-minmax@3.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0(postcss@8.4.46)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-mixins@1.0.0(postcss@8.4.46)': + '@csstools/postcss-mixins@1.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-nested-calc@5.0.0(postcss@8.4.46)': + '@csstools/postcss-nested-calc@5.0.0(postcss@8.5.6)': dependencies: - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@5.0.1(postcss@8.4.46)': + '@csstools/postcss-normalize-display-values@5.0.1(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@5.0.1(postcss@8.4.46)': + '@csstools/postcss-oklab-function@5.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-position-area-property@2.0.0(postcss@8.4.46)': + '@csstools/postcss-position-area-property@2.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-progressive-custom-properties@5.0.0(postcss@8.4.46)': + '@csstools/postcss-progressive-custom-properties@5.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@2.0.0(postcss@8.4.46)': + '@csstools/postcss-property-rule-prelude-list@2.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-random-function@3.0.0(postcss@8.4.46)': + '@csstools/postcss-random-function@3.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-relative-color-syntax@4.0.1(postcss@8.4.46)': + '@csstools/postcss-relative-color-syntax@4.0.1(postcss@8.5.6)': dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - '@csstools/postcss-scope-pseudo-class@5.0.0(postcss@8.4.46)': + '@csstools/postcss-scope-pseudo-class@5.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - '@csstools/postcss-sign-functions@2.0.0(postcss@8.4.46)': + '@csstools/postcss-sign-functions@2.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-stepped-value-functions@5.0.0(postcss@8.4.46)': + '@csstools/postcss-stepped-value-functions@5.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0(postcss@8.4.46)': + '@csstools/postcss-syntax-descriptor-syntax-production@2.0.0(postcss@8.5.6)': dependencies: '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-system-ui-font-family@2.0.0(postcss@8.4.46)': + '@csstools/postcss-system-ui-font-family@2.0.0(postcss@8.5.6)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-text-decoration-shorthand@5.0.1(postcss@8.4.46)': + '@csstools/postcss-text-decoration-shorthand@5.0.1(postcss@8.5.6)': dependencies: '@csstools/color-helpers': 6.0.1 - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@5.0.0(postcss@8.4.46)': + '@csstools/postcss-trigonometric-functions@5.0.0(postcss@8.5.6)': dependencies: '@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - '@csstools/postcss-unset-value@5.0.0(postcss@8.4.46)': + '@csstools/postcss-unset-value@5.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': dependencies: @@ -11451,9 +11441,9 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 - '@csstools/utilities@3.0.0(postcss@8.4.46)': + '@csstools/utilities@3.0.0(postcss@8.5.6)': dependencies: - postcss: 8.4.46 + postcss: 8.5.6 '@discoveryjs/json-ext@0.5.7': {} @@ -11470,16 +11460,16 @@ snapshots: '@matrix-org/react-sdk-module-api': 2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4) matrix-web-i18n: 3.6.0 - '@element-hq/element-web-playwright-common@2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.57.0)(playwright-core@1.57.0)': + '@element-hq/element-web-playwright-common@2.2.5(@element-hq/element-web-module-api@1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.1)(playwright-core@1.58.1)': dependencies: - '@axe-core/playwright': 4.11.1(playwright-core@1.57.0) + '@axe-core/playwright': 4.11.1(playwright-core@1.58.1) '@element-hq/element-web-module-api': 1.9.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4) - '@playwright/test': 1.57.0 + '@playwright/test': 1.58.1 '@testcontainers/postgresql': 11.11.0 glob: 13.0.1 lodash-es: 4.17.23 mailpit-api: 1.7.0 - playwright-core: 1.57.0 + playwright-core: 1.58.1 strip-ansi: 7.1.2 testcontainers: 11.11.0 yaml: 2.8.2 @@ -12492,9 +12482,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.57.0': + '@playwright/test@1.58.1': dependencies: - playwright: 1.57.0 + playwright: 1.58.1 '@polka/url@1.0.0-next.29': {} @@ -13159,7 +13149,7 @@ snapshots: storybook: 10.2.5(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) - '@vitest/browser-playwright': 4.0.18(playwright@1.57.0)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.58.1)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) '@vitest/runner': 4.0.18 vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.8)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: @@ -13881,11 +13871,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser-playwright@4.0.18(playwright@1.57.0)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18)': + '@vitest/browser-playwright@4.0.18(playwright@1.58.1)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18)': dependencies: '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2)) - playwright: 1.57.0 + playwright: 1.58.1 tinyrainbow: 3.0.3 vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.8)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: @@ -14421,13 +14411,13 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.24(postcss@8.4.46): + autoprefixer@10.4.24(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001766 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -15123,36 +15113,36 @@ snapshots: randombytes: 2.1.0 randomfill: 1.0.4 - css-blank-pseudo@8.0.1(postcss@8.4.46): + css-blank-pseudo@8.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 - css-declaration-sorter@7.3.1(postcss@8.4.46): + css-declaration-sorter@7.3.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 css-functions-list@3.2.3: {} - css-has-pseudo@8.0.0(postcss@8.4.46): + css-has-pseudo@8.0.0(postcss@8.5.6): dependencies: '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 css-loader@7.1.3(webpack@5.105.0): dependencies: - icss-utils: 5.1.0(postcss@8.4.46) - postcss: 8.4.46 - postcss-modules-extract-imports: 3.1.0(postcss@8.4.46) - postcss-modules-local-by-default: 4.2.0(postcss@8.4.46) - postcss-modules-scope: 3.2.1(postcss@8.4.46) - postcss-modules-values: 4.0.0(postcss@8.4.46) + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 semver: 7.7.3 optionalDependencies: @@ -15161,16 +15151,16 @@ snapshots: css-minimizer-webpack-plugin@7.0.4(webpack@5.105.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 7.1.2(postcss@8.4.46) + cssnano: 7.1.2(postcss@8.5.6) jest-worker: 30.2.0 - postcss: 8.4.46 + postcss: 8.5.6 schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.105.0(webpack-cli@6.0.1) - css-prefers-color-scheme@11.0.0(postcss@8.4.46): + css-prefers-color-scheme@11.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 css-select@4.3.0: dependencies: @@ -15213,49 +15203,49 @@ snapshots: cssfontparser@1.2.1: {} - cssnano-preset-default@7.0.10(postcss@8.4.46): + cssnano-preset-default@7.0.10(postcss@8.5.6): dependencies: browserslist: 4.28.1 - css-declaration-sorter: 7.3.1(postcss@8.4.46) - cssnano-utils: 5.0.1(postcss@8.4.46) - postcss: 8.4.46 - postcss-calc: 10.1.1(postcss@8.4.46) - postcss-colormin: 7.0.5(postcss@8.4.46) - postcss-convert-values: 7.0.8(postcss@8.4.46) - postcss-discard-comments: 7.0.5(postcss@8.4.46) - postcss-discard-duplicates: 7.0.2(postcss@8.4.46) - postcss-discard-empty: 7.0.1(postcss@8.4.46) - postcss-discard-overridden: 7.0.1(postcss@8.4.46) - postcss-merge-longhand: 7.0.5(postcss@8.4.46) - postcss-merge-rules: 7.0.7(postcss@8.4.46) - postcss-minify-font-values: 7.0.1(postcss@8.4.46) - postcss-minify-gradients: 7.0.1(postcss@8.4.46) - postcss-minify-params: 7.0.5(postcss@8.4.46) - postcss-minify-selectors: 7.0.5(postcss@8.4.46) - postcss-normalize-charset: 7.0.1(postcss@8.4.46) - postcss-normalize-display-values: 7.0.1(postcss@8.4.46) - postcss-normalize-positions: 7.0.1(postcss@8.4.46) - postcss-normalize-repeat-style: 7.0.1(postcss@8.4.46) - postcss-normalize-string: 7.0.1(postcss@8.4.46) - postcss-normalize-timing-functions: 7.0.1(postcss@8.4.46) - postcss-normalize-unicode: 7.0.5(postcss@8.4.46) - postcss-normalize-url: 7.0.1(postcss@8.4.46) - postcss-normalize-whitespace: 7.0.1(postcss@8.4.46) - postcss-ordered-values: 7.0.2(postcss@8.4.46) - postcss-reduce-initial: 7.0.5(postcss@8.4.46) - postcss-reduce-transforms: 7.0.1(postcss@8.4.46) - postcss-svgo: 7.1.0(postcss@8.4.46) - postcss-unique-selectors: 7.0.4(postcss@8.4.46) + css-declaration-sorter: 7.3.1(postcss@8.5.6) + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 10.1.1(postcss@8.5.6) + postcss-colormin: 7.0.5(postcss@8.5.6) + postcss-convert-values: 7.0.8(postcss@8.5.6) + postcss-discard-comments: 7.0.5(postcss@8.5.6) + postcss-discard-duplicates: 7.0.2(postcss@8.5.6) + postcss-discard-empty: 7.0.1(postcss@8.5.6) + postcss-discard-overridden: 7.0.1(postcss@8.5.6) + postcss-merge-longhand: 7.0.5(postcss@8.5.6) + postcss-merge-rules: 7.0.7(postcss@8.5.6) + postcss-minify-font-values: 7.0.1(postcss@8.5.6) + postcss-minify-gradients: 7.0.1(postcss@8.5.6) + postcss-minify-params: 7.0.5(postcss@8.5.6) + postcss-minify-selectors: 7.0.5(postcss@8.5.6) + postcss-normalize-charset: 7.0.1(postcss@8.5.6) + postcss-normalize-display-values: 7.0.1(postcss@8.5.6) + postcss-normalize-positions: 7.0.1(postcss@8.5.6) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) + postcss-normalize-string: 7.0.1(postcss@8.5.6) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) + postcss-normalize-unicode: 7.0.5(postcss@8.5.6) + postcss-normalize-url: 7.0.1(postcss@8.5.6) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) + postcss-ordered-values: 7.0.2(postcss@8.5.6) + postcss-reduce-initial: 7.0.5(postcss@8.5.6) + postcss-reduce-transforms: 7.0.1(postcss@8.5.6) + postcss-svgo: 7.1.0(postcss@8.5.6) + postcss-unique-selectors: 7.0.4(postcss@8.5.6) - cssnano-utils@5.0.1(postcss@8.4.46): + cssnano-utils@5.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - cssnano@7.1.2(postcss@8.4.46): + cssnano@7.1.2(postcss@8.5.6): dependencies: - cssnano-preset-default: 7.0.10(postcss@8.4.46) + cssnano-preset-default: 7.0.10(postcss@8.5.6) lilconfig: 3.1.3 - postcss: 8.4.46 + postcss: 8.5.6 csso@5.0.5: dependencies: @@ -16673,9 +16663,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.4.46): + icss-utils@5.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 identity-obj-proxy@3.0.0: dependencies: @@ -17319,8 +17309,6 @@ snapshots: - supports-color - ts-node - jiti@1.21.7: {} - jiti@2.6.1: {} jju@1.4.0: {} @@ -17788,7 +17776,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.105.0): + mini-css-extract-plugin@2.10.0(webpack@5.105.0): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 @@ -18236,11 +18224,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.57.0: {} + playwright-core@1.58.1: {} - playwright@1.57.0: + playwright@1.58.1: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.1 optionalDependencies: fsevents: 2.3.2 @@ -18258,428 +18246,428 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-attribute-case-insensitive@8.0.0(postcss@8.4.46): + postcss-attribute-case-insensitive@8.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-calc@10.1.1(postcss@8.4.46): + postcss-calc@10.1.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.4.46): + postcss-clamp@4.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@8.0.1(postcss@8.4.46): + postcss-color-functional-notation@8.0.1(postcss@8.5.6): dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - postcss-color-hex-alpha@11.0.0(postcss@8.4.46): + postcss-color-hex-alpha@11.0.0(postcss@8.5.6): dependencies: - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@11.0.0(postcss@8.4.46): + postcss-color-rebeccapurple@11.0.0(postcss@8.5.6): dependencies: - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.5(postcss@8.4.46): + postcss-colormin@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.8(postcss@8.4.46): + postcss-convert-values@7.0.8(postcss@8.5.6): dependencies: browserslist: 4.28.1 - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-custom-media@12.0.0(postcss@8.4.46): + postcss-custom-media@12.0.0(postcss@8.5.6): dependencies: '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - postcss: 8.4.46 + postcss: 8.5.6 - postcss-custom-properties@15.0.0(postcss@8.4.46): + postcss-custom-properties@15.0.0(postcss@8.5.6): dependencies: '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-custom-selectors@9.0.0(postcss@8.4.46): + postcss-custom-selectors@9.0.0(postcss@8.5.6): dependencies: '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-dir-pseudo-class@10.0.0(postcss@8.4.46): + postcss-dir-pseudo-class@10.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-discard-comments@7.0.5(postcss@8.4.46): + postcss-discard-comments@7.0.5(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@7.0.2(postcss@8.4.46): + postcss-discard-duplicates@7.0.2(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-discard-empty@7.0.1(postcss@8.4.46): + postcss-discard-empty@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-discard-overridden@7.0.1(postcss@8.4.46): + postcss-discard-overridden@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-double-position-gradients@7.0.0(postcss@8.4.46): + postcss-double-position-gradients@7.0.0(postcss@8.5.6): dependencies: - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-easings@4.0.0(postcss@8.4.46): + postcss-easings@4.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-focus-visible@11.0.0(postcss@8.4.46): + postcss-focus-visible@11.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-focus-within@10.0.0(postcss@8.4.46): + postcss-focus-within@10.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-font-variant@5.0.0(postcss@8.4.46): + postcss-font-variant@5.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-gap-properties@7.0.0(postcss@8.4.46): + postcss-gap-properties@7.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-hexrgba@2.1.0(postcss@8.4.46): + postcss-hexrgba@2.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-image-set-function@8.0.0(postcss@8.4.46): + postcss-image-set-function@8.0.0(postcss@8.5.6): dependencies: - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-import@16.1.0(postcss@8.4.46): + postcss-import@16.1.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.4.46): + postcss-js@4.1.0(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.46 + postcss: 8.5.6 - postcss-lab-function@8.0.1(postcss@8.4.46): + postcss-lab-function@8.0.1(postcss@8.5.6): dependencies: '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/utilities': 3.0.0(postcss@8.4.46) - postcss: 8.4.46 + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/utilities': 3.0.0(postcss@8.5.6) + postcss: 8.5.6 - postcss-loader@8.1.1(postcss@8.4.46)(typescript@5.9.3)(webpack@5.105.0): + postcss-loader@8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.0): dependencies: cosmiconfig: 9.0.0(typescript@5.9.3) - jiti: 1.21.7 - postcss: 8.4.46 + jiti: 2.6.1 + postcss: 8.5.6 semver: 7.7.3 optionalDependencies: webpack: 5.105.0(webpack-cli@6.0.1) transitivePeerDependencies: - typescript - postcss-logical@9.0.0(postcss@8.4.46): + postcss-logical@9.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 postcss-media-query-parser@0.2.3: {} - postcss-merge-longhand@7.0.5(postcss@8.4.46): + postcss-merge-longhand@7.0.5(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - stylehacks: 7.0.7(postcss@8.4.46) + stylehacks: 7.0.7(postcss@8.5.6) - postcss-merge-rules@7.0.7(postcss@8.4.46): + postcss-merge-rules@7.0.7(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.4.46) - postcss: 8.4.46 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@7.0.1(postcss@8.4.46): + postcss-minify-font-values@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.1(postcss@8.4.46): + postcss-minify-gradients@7.0.1(postcss@8.5.6): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.4.46) - postcss: 8.4.46 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.5(postcss@8.4.46): + postcss-minify-params@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.28.1 - cssnano-utils: 5.0.1(postcss@8.4.46) - postcss: 8.4.46 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.5(postcss@8.4.46): + postcss-minify-selectors@7.0.5(postcss@8.5.6): dependencies: cssesc: 3.0.0 - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-mixins@12.0.0(postcss@8.4.46): + postcss-mixins@12.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 - postcss-js: 4.1.0(postcss@8.4.46) - postcss-simple-vars: 7.0.1(postcss@8.4.46) - sugarss: 5.0.1(postcss@8.4.46) + postcss: 8.5.6 + postcss-js: 4.1.0(postcss@8.5.6) + postcss-simple-vars: 7.0.1(postcss@8.5.6) + sugarss: 5.0.1(postcss@8.5.6) tinyglobby: 0.2.15 - postcss-modules-extract-imports@3.1.0(postcss@8.4.46): + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-modules-local-by-default@4.2.0(postcss@8.4.46): + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): dependencies: - icss-utils: 5.1.0(postcss@8.4.46) - postcss: 8.4.46 + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.4.46): + postcss-modules-scope@3.2.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.4.46): + postcss-modules-values@4.0.0(postcss@8.5.6): dependencies: - icss-utils: 5.1.0(postcss@8.4.46) - postcss: 8.4.46 + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 - postcss-nested@7.0.2(postcss@8.4.46): + postcss-nested@7.0.2(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-nesting@14.0.0(postcss@8.4.46): + postcss-nesting@14.0.0(postcss@8.5.6): dependencies: '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-normalize-charset@7.0.1(postcss@8.4.46): + postcss-normalize-charset@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-normalize-display-values@7.0.1(postcss@8.4.46): + postcss-normalize-display-values@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.4.46): + postcss-normalize-positions@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.4.46): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.4.46): + postcss-normalize-string@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.4.46): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.5(postcss@8.4.46): + postcss-normalize-unicode@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.28.1 - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.4.46): + postcss-normalize-url@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.4.46): + postcss-normalize-whitespace@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.4.46): + postcss-opacity-percentage@3.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-ordered-values@7.0.2(postcss@8.4.46): + postcss-ordered-values@7.0.2(postcss@8.5.6): dependencies: - cssnano-utils: 5.0.1(postcss@8.4.46) - postcss: 8.4.46 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@7.0.0(postcss@8.4.46): + postcss-overflow-shorthand@7.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.4.46): + postcss-page-break@3.0.4(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-place@11.0.0(postcss@8.4.46): + postcss-place@11.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-preset-env@11.1.1(postcss@8.4.46): + postcss-preset-env@11.1.1(postcss@8.5.6): dependencies: - '@csstools/postcss-alpha-function': 2.0.2(postcss@8.4.46) - '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.4.46) - '@csstools/postcss-color-function': 5.0.1(postcss@8.4.46) - '@csstools/postcss-color-function-display-p3-linear': 2.0.1(postcss@8.4.46) - '@csstools/postcss-color-mix-function': 4.0.1(postcss@8.4.46) - '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.1(postcss@8.4.46) - '@csstools/postcss-content-alt-text': 3.0.0(postcss@8.4.46) - '@csstools/postcss-contrast-color-function': 3.0.1(postcss@8.4.46) - '@csstools/postcss-exponential-functions': 3.0.0(postcss@8.4.46) - '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.4.46) - '@csstools/postcss-gamut-mapping': 3.0.1(postcss@8.4.46) - '@csstools/postcss-gradients-interpolation-method': 6.0.1(postcss@8.4.46) - '@csstools/postcss-hwb-function': 5.0.1(postcss@8.4.46) - '@csstools/postcss-ic-unit': 5.0.0(postcss@8.4.46) - '@csstools/postcss-initial': 3.0.0(postcss@8.4.46) - '@csstools/postcss-is-pseudo-class': 6.0.0(postcss@8.4.46) - '@csstools/postcss-light-dark-function': 3.0.0(postcss@8.4.46) - '@csstools/postcss-logical-float-and-clear': 4.0.0(postcss@8.4.46) - '@csstools/postcss-logical-overflow': 3.0.0(postcss@8.4.46) - '@csstools/postcss-logical-overscroll-behavior': 3.0.0(postcss@8.4.46) - '@csstools/postcss-logical-resize': 4.0.0(postcss@8.4.46) - '@csstools/postcss-logical-viewport-units': 4.0.0(postcss@8.4.46) - '@csstools/postcss-media-minmax': 3.0.0(postcss@8.4.46) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 4.0.0(postcss@8.4.46) - '@csstools/postcss-mixins': 1.0.0(postcss@8.4.46) - '@csstools/postcss-nested-calc': 5.0.0(postcss@8.4.46) - '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.4.46) - '@csstools/postcss-oklab-function': 5.0.1(postcss@8.4.46) - '@csstools/postcss-position-area-property': 2.0.0(postcss@8.4.46) - '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.4.46) - '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.4.46) - '@csstools/postcss-random-function': 3.0.0(postcss@8.4.46) - '@csstools/postcss-relative-color-syntax': 4.0.1(postcss@8.4.46) - '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.4.46) - '@csstools/postcss-sign-functions': 2.0.0(postcss@8.4.46) - '@csstools/postcss-stepped-value-functions': 5.0.0(postcss@8.4.46) - '@csstools/postcss-syntax-descriptor-syntax-production': 2.0.0(postcss@8.4.46) - '@csstools/postcss-system-ui-font-family': 2.0.0(postcss@8.4.46) - '@csstools/postcss-text-decoration-shorthand': 5.0.1(postcss@8.4.46) - '@csstools/postcss-trigonometric-functions': 5.0.0(postcss@8.4.46) - '@csstools/postcss-unset-value': 5.0.0(postcss@8.4.46) - autoprefixer: 10.4.24(postcss@8.4.46) + '@csstools/postcss-alpha-function': 2.0.2(postcss@8.5.6) + '@csstools/postcss-cascade-layers': 6.0.0(postcss@8.5.6) + '@csstools/postcss-color-function': 5.0.1(postcss@8.5.6) + '@csstools/postcss-color-function-display-p3-linear': 2.0.1(postcss@8.5.6) + '@csstools/postcss-color-mix-function': 4.0.1(postcss@8.5.6) + '@csstools/postcss-color-mix-variadic-function-arguments': 2.0.1(postcss@8.5.6) + '@csstools/postcss-content-alt-text': 3.0.0(postcss@8.5.6) + '@csstools/postcss-contrast-color-function': 3.0.1(postcss@8.5.6) + '@csstools/postcss-exponential-functions': 3.0.0(postcss@8.5.6) + '@csstools/postcss-font-format-keywords': 5.0.0(postcss@8.5.6) + '@csstools/postcss-gamut-mapping': 3.0.1(postcss@8.5.6) + '@csstools/postcss-gradients-interpolation-method': 6.0.1(postcss@8.5.6) + '@csstools/postcss-hwb-function': 5.0.1(postcss@8.5.6) + '@csstools/postcss-ic-unit': 5.0.0(postcss@8.5.6) + '@csstools/postcss-initial': 3.0.0(postcss@8.5.6) + '@csstools/postcss-is-pseudo-class': 6.0.0(postcss@8.5.6) + '@csstools/postcss-light-dark-function': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-float-and-clear': 4.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overflow': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overscroll-behavior': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-resize': 4.0.0(postcss@8.5.6) + '@csstools/postcss-logical-viewport-units': 4.0.0(postcss@8.5.6) + '@csstools/postcss-media-minmax': 3.0.0(postcss@8.5.6) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 4.0.0(postcss@8.5.6) + '@csstools/postcss-mixins': 1.0.0(postcss@8.5.6) + '@csstools/postcss-nested-calc': 5.0.0(postcss@8.5.6) + '@csstools/postcss-normalize-display-values': 5.0.1(postcss@8.5.6) + '@csstools/postcss-oklab-function': 5.0.1(postcss@8.5.6) + '@csstools/postcss-position-area-property': 2.0.0(postcss@8.5.6) + '@csstools/postcss-progressive-custom-properties': 5.0.0(postcss@8.5.6) + '@csstools/postcss-property-rule-prelude-list': 2.0.0(postcss@8.5.6) + '@csstools/postcss-random-function': 3.0.0(postcss@8.5.6) + '@csstools/postcss-relative-color-syntax': 4.0.1(postcss@8.5.6) + '@csstools/postcss-scope-pseudo-class': 5.0.0(postcss@8.5.6) + '@csstools/postcss-sign-functions': 2.0.0(postcss@8.5.6) + '@csstools/postcss-stepped-value-functions': 5.0.0(postcss@8.5.6) + '@csstools/postcss-syntax-descriptor-syntax-production': 2.0.0(postcss@8.5.6) + '@csstools/postcss-system-ui-font-family': 2.0.0(postcss@8.5.6) + '@csstools/postcss-text-decoration-shorthand': 5.0.1(postcss@8.5.6) + '@csstools/postcss-trigonometric-functions': 5.0.0(postcss@8.5.6) + '@csstools/postcss-unset-value': 5.0.0(postcss@8.5.6) + autoprefixer: 10.4.24(postcss@8.5.6) browserslist: 4.28.1 - css-blank-pseudo: 8.0.1(postcss@8.4.46) - css-has-pseudo: 8.0.0(postcss@8.4.46) - css-prefers-color-scheme: 11.0.0(postcss@8.4.46) + css-blank-pseudo: 8.0.1(postcss@8.5.6) + css-has-pseudo: 8.0.0(postcss@8.5.6) + css-prefers-color-scheme: 11.0.0(postcss@8.5.6) cssdb: 8.7.1 - postcss: 8.4.46 - postcss-attribute-case-insensitive: 8.0.0(postcss@8.4.46) - postcss-clamp: 4.1.0(postcss@8.4.46) - postcss-color-functional-notation: 8.0.1(postcss@8.4.46) - postcss-color-hex-alpha: 11.0.0(postcss@8.4.46) - postcss-color-rebeccapurple: 11.0.0(postcss@8.4.46) - postcss-custom-media: 12.0.0(postcss@8.4.46) - postcss-custom-properties: 15.0.0(postcss@8.4.46) - postcss-custom-selectors: 9.0.0(postcss@8.4.46) - postcss-dir-pseudo-class: 10.0.0(postcss@8.4.46) - postcss-double-position-gradients: 7.0.0(postcss@8.4.46) - postcss-focus-visible: 11.0.0(postcss@8.4.46) - postcss-focus-within: 10.0.0(postcss@8.4.46) - postcss-font-variant: 5.0.0(postcss@8.4.46) - postcss-gap-properties: 7.0.0(postcss@8.4.46) - postcss-image-set-function: 8.0.0(postcss@8.4.46) - postcss-lab-function: 8.0.1(postcss@8.4.46) - postcss-logical: 9.0.0(postcss@8.4.46) - postcss-nesting: 14.0.0(postcss@8.4.46) - postcss-opacity-percentage: 3.0.0(postcss@8.4.46) - postcss-overflow-shorthand: 7.0.0(postcss@8.4.46) - postcss-page-break: 3.0.4(postcss@8.4.46) - postcss-place: 11.0.0(postcss@8.4.46) - postcss-pseudo-class-any-link: 11.0.0(postcss@8.4.46) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.46) - postcss-selector-not: 9.0.0(postcss@8.4.46) + postcss: 8.5.6 + postcss-attribute-case-insensitive: 8.0.0(postcss@8.5.6) + postcss-clamp: 4.1.0(postcss@8.5.6) + postcss-color-functional-notation: 8.0.1(postcss@8.5.6) + postcss-color-hex-alpha: 11.0.0(postcss@8.5.6) + postcss-color-rebeccapurple: 11.0.0(postcss@8.5.6) + postcss-custom-media: 12.0.0(postcss@8.5.6) + postcss-custom-properties: 15.0.0(postcss@8.5.6) + postcss-custom-selectors: 9.0.0(postcss@8.5.6) + postcss-dir-pseudo-class: 10.0.0(postcss@8.5.6) + postcss-double-position-gradients: 7.0.0(postcss@8.5.6) + postcss-focus-visible: 11.0.0(postcss@8.5.6) + postcss-focus-within: 10.0.0(postcss@8.5.6) + postcss-font-variant: 5.0.0(postcss@8.5.6) + postcss-gap-properties: 7.0.0(postcss@8.5.6) + postcss-image-set-function: 8.0.0(postcss@8.5.6) + postcss-lab-function: 8.0.1(postcss@8.5.6) + postcss-logical: 9.0.0(postcss@8.5.6) + postcss-nesting: 14.0.0(postcss@8.5.6) + postcss-opacity-percentage: 3.0.0(postcss@8.5.6) + postcss-overflow-shorthand: 7.0.0(postcss@8.5.6) + postcss-page-break: 3.0.4(postcss@8.5.6) + postcss-place: 11.0.0(postcss@8.5.6) + postcss-pseudo-class-any-link: 11.0.0(postcss@8.5.6) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6) + postcss-selector-not: 9.0.0(postcss@8.5.6) - postcss-pseudo-class-any-link@11.0.0(postcss@8.4.46): + postcss-pseudo-class-any-link@11.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-reduce-initial@7.0.5(postcss@8.4.46): + postcss-reduce-initial@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - postcss: 8.4.46 + postcss: 8.5.6 - postcss-reduce-transforms@7.0.1(postcss@8.4.46): + postcss-reduce-transforms@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.4.46): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-resolve-nested-selector@0.1.6: {} @@ -18687,13 +18675,13 @@ snapshots: dependencies: postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.4.46): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-selector-not@9.0.0(postcss@8.4.46): + postcss-selector-not@9.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-selector-parser@7.1.1: @@ -18701,29 +18689,23 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.4.46): + postcss-simple-vars@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 - postcss-svgo@7.1.0(postcss@8.4.46): + postcss-svgo@7.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-value-parser: 4.2.0 svgo: 4.0.0 - postcss-unique-selectors@7.0.4(postcss@8.4.46): + postcss-unique-selectors@7.0.4(postcss@8.5.6): dependencies: - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.4.46: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -19327,7 +19309,7 @@ snapshots: htmlparser2: 8.0.2 is-plain-object: 5.0.0 parse-srcset: 1.0.2 - postcss: 8.4.46 + postcss: 8.5.6 sax@1.4.4: {} @@ -19822,10 +19804,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@7.0.7(postcss@8.4.46): + stylehacks@7.0.7(postcss@8.5.6): dependencies: browserslist: 4.28.1 - postcss: 8.4.46 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 stylelint-config-recommended@18.0.0(stylelint@17.1.1(typescript@5.9.3)): @@ -19899,14 +19881,9 @@ snapshots: - supports-color - typescript - sugarss@5.0.1(postcss@8.4.46): - dependencies: - postcss: 8.4.46 - sugarss@5.0.1(postcss@8.5.6): dependencies: postcss: 8.5.6 - optional: true super-regex@0.2.0: dependencies: @@ -20498,7 +20475,7 @@ snapshots: type-plus: 8.0.0-beta.7 vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.8)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f))(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2) optionalDependencies: - '@vitest/browser-playwright': 4.0.18(playwright@1.57.0)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.58.1)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) transitivePeerDependencies: - babel-plugin-macros @@ -20531,7 +20508,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.8 - '@vitest/browser-playwright': 4.0.18(playwright@1.57.0)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.58.1)(vite@7.3.1(@types/node@22.19.8)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) '@vitest/ui': 4.0.18(vitest@4.0.18) jsdom: 26.1.0(patch_hash=040623e87b1c8b676c2a705513c0276c0704dd1b23fc3a1bb77cde8128b64b5f) transitivePeerDependencies: diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 28458c899f..53f99f8a35 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -263,14 +263,7 @@ @import "./views/right_panel/_VerificationPanel.pcss"; @import "./views/right_panel/_WidgetCard.pcss"; @import "./views/room_settings/_AliasSettings.pcss"; -@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss"; -@import "./views/rooms/RoomListPanel/_RoomList.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListItemView.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListSkeleton.pcss"; @import "./views/rooms/_AppsDrawer.pcss"; @import "./views/rooms/_Autocomplete.pcss"; @import "./views/rooms/_AuxPanel.pcss"; diff --git a/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss b/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss deleted file mode 100644 index a0fbfdaea7..0000000000 --- a/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_EmptyRoomList_GenericPlaceholder { - align-self: center; - /** It should take 2/3 of the width **/ - width: 66%; - /** It should be positioned at 1/3 of the height **/ - padding-top: 33%; - - .mx_EmptyRoomList_GenericPlaceholder_title { - font: var(--cpd-font-body-lg-semibold); - text-align: center; - } - - .mx_EmptyRoomList_GenericPlaceholder_description { - font: var(--cpd-font-body-sm-regular); - color: var(--cpd-color-text-secondary); - text-align: center; - } - - .mx_EmptyRoomList_DefaultPlaceholder { - margin-top: var(--cpd-space-4x); - } - - button { - width: 100%; - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss deleted file mode 100644 index cabd9b2d20..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListItemMenuView { - svg { - fill: var(--cpd-color-icon-primary); - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss deleted file mode 100644 index 4a7eb23b18..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -/** - * The RoomListItemView has the following structure: - * button--------------------------------------------------| - * | <-12px-> container------------------------------------| - * | | room avatar <-8px-> content----------------| - * | | | room_name <- 20px ->| - * | | | --------------------| <-- border - * |-------------------------------------------------------| - */ -.mx_RoomListItemView { - /* Remove button default style */ - color: inherit; - background: unset; - border: none; - padding: 0; - text-align: unset; - - cursor: pointer; - height: 48px; - width: 100%; - - padding-left: var(--cpd-space-3x); - font: var(--cpd-font-body-md-regular); - - /* Hide the menu by default */ - .mx_RoomListItemView_menu { - display: none; - } - - &:hover, - &:focus-visible, - /* When the context menu is opened */ - &[data-state="open"], - /* When the options and notifications menu are opened */ - &:has(.mx_RoomListItemMenuView > button[data-state="open"]) { - background-color: var(--cpd-color-bg-action-secondary-hovered); - - .mx_RoomListItemView_menu { - display: flex; - } - - &.mx_RoomListItemView_has_menu { - /** - * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 - * the icon size of the menu is 18px instead of 20px with a different internal padding - * We need to use 18px to align the icon with the others icons - * 18px is not available in compound spacing - */ - .mx_RoomListItemView_content { - padding-right: 18px; - } - - /* When the menu is visible, hide the notification decoration to avoid clutter */ - .mx_RoomListItemView_notificationDecoration { - display: none; - } - } - } - - .mx_RoomListItemView_content { - height: 100%; - flex: 1; - /* The border is only under the room name and the future hover menu */ - border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); - box-sizing: border-box; - min-width: 0; - padding-right: var(--cpd-space-5x); - - .mx_RoomListItemView_text { - min-width: 0; - } - - .mx_RoomListItemView_roomName { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .mx_RoomListItemView_messagePreview { - font: var(--cpd-font-body-sm-regular); - color: var(--cpd-color-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } -} - -.mx_RoomListItemView_selected { - background-color: var(--cpd-color-bg-action-secondary-pressed); -} - -.mx_RoomListItemView_bold .mx_RoomListItemView_roomName { - font: var(--cpd-font-body-md-semibold); -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss deleted file mode 100644 index 378f2e75da..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListPrimaryFilters { - padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x); - - .mx_RoomListPrimaryFilters_wrapping { - display: none; - } - - .mx_RoomListPrimaryFilters_list { - /** - * The InteractionObserver needs the height to be set to work properly. - */ - height: 100%; - flex: 1; - } - - .mx_RoomListPrimaryFilters_IconButton { - svg { - transition: transform 0.1s linear; - } - } - - .mx_RoomListPrimaryFilters_IconButton[aria-expanded="true"] { - svg { - transform: rotate(180deg); - } - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss deleted file mode 100644 index 0fa8dc12ae..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListSecondaryFilters { - font: var(--cpd-font-body-md-medium); - margin: var(--cpd-space-2x); - margin-left: var(--cpd-space-1x); -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss deleted file mode 100644 index 248f260262..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListSkeleton { - position: relative; - margin-left: 4px; - height: 100%; - - &::before { - background-color: var(--cpd-color-bg-subtle-secondary); - width: 100%; - height: 100%; - - content: ""; - position: absolute; - mask-repeat: repeat-y; - mask-size: auto 96px; - mask-image: url("/res/img/element-icons/roomlist/room-list-item-skeleton.svg"); - } -} diff --git a/src/Searching.ts b/src/Searching.ts index d507bd10ef..28f67522ad 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -175,6 +175,26 @@ async function localSearch( throw new Error("Local search failed"); } + // Fix state_key: null issue - Seshat includes "state_key": null for non-state events, + // which causes matrix-js-sdk to incorrectly treat them as state events + if (localResult.results) { + for (const searchResult of localResult.results) { + const event = searchResult.result as unknown as Record; + if (event?.state_key === null) delete event.state_key; + // Also fix context events + if (searchResult.context) { + for (const ctxEvent of searchResult.context.events_before || []) { + const ev = ctxEvent as unknown as Record; + if (ev?.state_key === null) delete ev.state_key; + } + for (const ctxEvent of searchResult.context.events_after || []) { + const ev = ctxEvent as unknown as Record; + if (ev?.state_key === null) delete ev.state_key; + } + } + } + } + searchArgs.next_batch = localResult.next_batch; const result = { diff --git a/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx b/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx deleted file mode 100644 index 9e141c1379..0000000000 --- a/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useState } from "react"; - -import type { Room } from "matrix-js-sdk/src/matrix"; -import { type MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; - -interface MessagePreviewViewState { - /** - * A string representation of the message preview if available. - */ - message?: string; -} - -/** - * View model for rendering a message preview for a given room list item. - * @param room The room for which we're rendering the message preview. - * @see {@link MessagePreviewViewState} for what this view model returns. - */ -export function useMessagePreviewViewModel(room: Room): MessagePreviewViewState { - const [messagePreview, setMessagePreview] = useState(null); - - const updatePreview = useCallback(async (): Promise => { - /** - * The second argument to getPreviewForRoom is a tag id which doesn't really make - * much sense within the context of the new room list. We can pass an empty string - * to match all tags for now but we should remember to actually change the implementation - * in the store once we remove the legacy room list. - */ - const newPreview = await MessagePreviewStore.instance.getPreviewForRoom(room, ""); - setMessagePreview(newPreview); - }, [room]); - - /** - * Update when the message preview has changed for this room. - */ - useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { - updatePreview(); - }); - - /** - * Do an initial fetch of the message preview. - */ - useEffect(() => { - updatePreview(); - }, [updatePreview]); - - return { - message: messagePreview?.text, - }; -} diff --git a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx deleted file mode 100644 index 738a05b8c3..0000000000 --- a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; - -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; -import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import { DefaultTagID } from "../../../stores/room-list/models"; -import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; -import { UIComponent } from "../../../settings/UIFeature"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { tagRoom } from "../../../utils/room/tagRoom"; -import { RoomNotifState } from "../../../RoomNotifs"; -import { useNotificationState } from "../../../hooks/useRoomNotificationState"; - -export interface RoomListItemMenuViewState { - /** - * Whether the more options menu should be shown. - */ - showMoreOptionsMenu: boolean; - /** - * Whether the notification menu should be shown. - */ - showNotificationMenu: boolean; - /** - * Whether the room is a favourite room. - */ - isFavourite: boolean; - /** - * Whether the room is a low priority room. - */ - isLowPriority: boolean; - /** - * Can invite other user's in the room. - */ - canInvite: boolean; - /** - * Can copy the room link. - */ - canCopyRoomLink: boolean; - /** - * Can mark the room as read. - */ - canMarkAsRead: boolean; - /** - * Can mark the room as unread. - */ - canMarkAsUnread: boolean; - /** - * Whether the notification is set to all messages. - */ - isNotificationAllMessage: boolean; - /** - * Whether the notification is set to all messages loud. - */ - isNotificationAllMessageLoud: boolean; - /** - * Whether the notification is set to mentions and keywords only. - */ - isNotificationMentionOnly: boolean; - /** - * Whether the notification is muted. - */ - isNotificationMute: boolean; - /** - * Mark the room as read. - * @param evt - */ - markAsRead: (evt: Event) => void; - /** - * Mark the room as unread. - * @param evt - */ - markAsUnread: (evt: Event) => void; - /** - * Toggle the room as favourite. - * @param evt - */ - toggleFavorite: (evt: Event) => void; - /** - * Toggle the room as low priority. - */ - toggleLowPriority: () => void; - /** - * Invite other users in the room. - * @param evt - */ - invite: (evt: Event) => void; - /** - * Copy the room link in the clipboard. - * @param evt - */ - copyRoomLink: (evt: Event) => void; - /** - * Leave the room. - * @param evt - */ - leaveRoom: (evt: Event) => void; - /** - * Set the room notification state. - * @param state - */ - setRoomNotifState: (state: RoomNotifState) => void; -} - -export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState { - const matrixClient = useMatrixClientContext(); - const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); - const { level: notificationLevel } = useUnreadNotifications(room); - - const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); - const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]); - const isLowPriority = Boolean(roomTags[DefaultTagID.LowPriority]); - const isArchived = Boolean(roomTags[DefaultTagID.Archived]); - - const showMoreOptionsMenu = hasAccessToOptionsMenu(room); - const showNotificationMenu = hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived); - - const canMarkAsRead = notificationLevel > NotificationLevel.None; - const canMarkAsUnread = !canMarkAsRead && !isArchived; - - const canInvite = - room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); - const canCopyRoomLink = !isDm; - - const [roomNotifState, setRoomNotifState] = useNotificationState(room); - const isNotificationAllMessage = roomNotifState === RoomNotifState.AllMessages; - const isNotificationAllMessageLoud = roomNotifState === RoomNotifState.AllMessagesLoud; - const isNotificationMentionOnly = roomNotifState === RoomNotifState.MentionsOnly; - const isNotificationMute = roomNotifState === RoomNotifState.Mute; - - // Actions - - const markAsRead = useCallback( - async (evt: Event): Promise => { - await clearRoomNotification(room, matrixClient); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", evt); - }, - [room, matrixClient], - ); - - const markAsUnread = useCallback( - async (evt: Event): Promise => { - await setMarkedUnreadState(room, matrixClient, true); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", evt); - }, - [room, matrixClient], - ); - - const toggleFavorite = useCallback( - (evt: Event): void => { - tagRoom(room, DefaultTagID.Favourite); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); - }, - [room], - ); - - const toggleLowPriority = useCallback((): void => tagRoom(room, DefaultTagID.LowPriority), [room]); - - const invite = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: "view_invite", - roomId: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", evt); - }, - [room], - ); - - const copyRoomLink = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: "copy_room", - room_id: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); - }, - [room], - ); - - const leaveRoom = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: isArchived ? "forget_room" : "leave_room", - room_id: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", evt); - }, - [room, isArchived], - ); - - return { - showMoreOptionsMenu, - showNotificationMenu, - isFavourite, - isLowPriority, - canInvite, - canCopyRoomLink, - canMarkAsRead, - canMarkAsUnread, - isNotificationAllMessage, - isNotificationAllMessageLoud, - isNotificationMentionOnly, - isNotificationMute, - markAsRead, - markAsUnread, - toggleFavorite, - toggleLowPriority, - invite, - copyRoomLink, - leaveRoom, - setRoomNotifState, - }; -} diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx deleted file mode 100644 index 30576e2dc2..0000000000 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useMemo, useState } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; - -import dispatcher from "../../../dispatcher/dispatcher"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; -import { _t } from "../../../languageHandler"; -import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; -import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter"; -import { DefaultTagID } from "../../../stores/room-list/models"; -import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall"; -import { CallEvent, type ConnectionState } from "../../../models/Call"; -import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; -import { useMessagePreviewToggle } from "./useMessagePreviewToggle"; - -export interface RoomListItemViewState { - /** - * The name of the room. - */ - name: string; - /** - * Whether the context menu should be shown. - */ - showContextMenu: boolean; - /** - * Whether the hover menu should be shown. - */ - showHoverMenu: boolean; - /** - * Open the room having given roomId. - */ - openRoom: () => void; - /** - * The a11y label for the room list item. - */ - a11yLabel: string; - /** - * The notification state of the room. - */ - notificationState: RoomNotificationState; - /** - * Whether the room should be bolded. - */ - isBold: boolean; - /** - * Whether the room is a video room - */ - isVideoRoom: boolean; - /** - * The connection state of the call. - * `null` if there is no call in the room. - */ - callConnectionState: ConnectionState | null; - /** - * Whether there are participants in the call. - */ - hasParticipantInCall: boolean; - /** - * Whether the call is a voice or video call. - */ - callType: CallType | undefined; - /** - * Pre-rendered and translated preview for the latest message in the room, or undefined - * if no preview should be shown. - */ - messagePreview: string | undefined; - /** - * Whether the notification decoration should be shown. - */ - showNotificationDecoration: boolean; -} - -/** - * View model for the room list item - * @see {@link RoomListItemViewState} for more information about what this view model returns. - */ -export function useRoomListItemViewModel(room: Room): RoomListItemViewState { - const matrixClient = useMatrixClientContext(); - const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); - const isArchived = Boolean(roomTags[DefaultTagID.Archived]); - const name = useEventEmitterState(room, RoomEvent.Name, () => room.name); - - const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); - - const [a11yLabel, setA11yLabel] = useState(getA11yLabel(name, notificationState)); - const [{ isBold, invited, hasVisibleNotification }, setNotificationValues] = useState( - getNotificationValues(notificationState), - ); - useEffect(() => { - setA11yLabel(getA11yLabel(name, notificationState)); - }, [name, notificationState]); - - // Listen to changes in the notification state and update the values - useTypedEventEmitter(notificationState, NotificationStateEvents.Update, () => { - setA11yLabel(getA11yLabel(name, notificationState)); - setNotificationValues(getNotificationValues(notificationState)); - }); - - // If the notification reference change due to room change, update the values - useEffect(() => { - setNotificationValues(getNotificationValues(notificationState)); - }, [notificationState]); - - // We don't want to show the menus if - // - there is an invitation for this room - // - the user doesn't have access to notification and more options menus - const showContextMenu = !invited && hasAccessToOptionsMenu(room); - const showHoverMenu = - !invited && (showContextMenu || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); - - const messagePreview = useRoomMessagePreview(room); - - // Video room - const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom(); - // EC video call or video room - const call = useCall(room.roomId); - const connectionState = useConnectionState(call); - const participantCount = useParticipantCount(call); - const callConnectionState = call ? connectionState : null; - - const showNotificationDecoration = hasVisibleNotification || participantCount > 0; - - // Actions - - const openRoom = useCallback((): void => { - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "RoomList", - }); - }, [room]); - - const [callType, setCallType] = useState(CallType.Video); - useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType); - - return { - name, - notificationState, - showContextMenu, - showHoverMenu, - openRoom, - a11yLabel, - isBold, - isVideoRoom, - callConnectionState, - hasParticipantInCall: participantCount > 0, - messagePreview, - showNotificationDecoration, - callType: call ? callType : undefined, - }; -} - -/** - * Calculate the values from the notification state - * @param notificationState - */ -function getNotificationValues(notificationState: RoomNotificationState): { - computeA11yLabel: (name: string) => string; - isBold: boolean; - invited: boolean; - hasVisibleNotification: boolean; -} { - const invited = notificationState.invited; - const computeA11yLabel = (name: string): string => getA11yLabel(name, notificationState); - const isBold = notificationState.hasAnyNotificationOrActivity; - - const hasVisibleNotification = notificationState.hasAnyNotificationOrActivity || notificationState.muted; - - return { - computeA11yLabel, - isBold, - invited, - hasVisibleNotification, - }; -} - -/** - * Get the a11y label for the room list item - * @param roomName - * @param notificationState - */ -function getA11yLabel(roomName: string, notificationState: RoomNotificationState): string { - if (notificationState.isUnsentMessage) { - return _t("a11y|room_messsage_not_sent", { - roomName, - }); - } else if (notificationState.invited) { - return _t("a11y|room_n_unread_invite", { - roomName, - }); - } else if (notificationState.isMention) { - return _t("a11y|room_n_unread_messages_mentions", { - roomName, - count: notificationState.count, - }); - } else if (notificationState.hasUnreadCount) { - return _t("a11y|room_n_unread_messages", { - roomName, - count: notificationState.count, - }); - } else { - return _t("room_list|room|open_room", { roomName }); - } -} - -function useRoomMessagePreview(room: Room): string | undefined { - const { shouldShowMessagePreview } = useMessagePreviewToggle(); - const [previewText, setPreviewText] = useState(undefined); - - const updatePreview = useCallback(async () => { - if (!shouldShowMessagePreview) { - setPreviewText(undefined); - return; - } - - const roomIsDM = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); - // For the tag, we only care about whether the room is a DM or not as we don't show - // display names in previewsd for DMs, so anything else we just say is 'untagged' - // (even though it could actually be have other tags: we don't care about them). - const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom( - room, - roomIsDM ? DefaultTagID.DM : DefaultTagID.Untagged, - ); - setPreviewText(messagePreview?.text); - }, [room, shouldShowMessagePreview]); - - // MessagePreviewStore and the other AsyncStores need to be converted to TypedEventEmitter - useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { - updatePreview(); - }); - - useEffect(() => { - updatePreview(); - }, [updatePreview]); - - return previewText; -} diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx deleted file mode 100644 index a48d973b23..0000000000 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { useCallback } from "react"; - -import type { Room } from "matrix-js-sdk/src/matrix"; -import { type PrimaryFilter, useFilteredRooms } from "./useFilteredRooms"; -import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useStickyRoomList } from "./useStickyRoomList"; -import { useRoomListNavigation } from "./useRoomListNavigation"; -import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3"; - -export interface RoomListViewState { - /** - * Whether the list of rooms is being loaded. - */ - isLoadingRooms: boolean; - - /** - * The room results to be displayed (along with the spaceId and filter keys at the time of query) - */ - roomsResult: RoomsResult; - - /** - * Create a chat room - * @param e - The click event - */ - createChatRoom: () => void; - - /** - * Whether the user can create a room in the current space - */ - canCreateRoom: boolean; - - /** - * Create a room - * @param e - The click event - */ - createRoom: () => void; - - /** - * A list of objects that provide the view enough information - * to render primary room filters. - */ - primaryFilters: PrimaryFilter[]; - - /** - * The currently active primary filter. - * If no primary filter is active, this will be undefined. - */ - activePrimaryFilter?: PrimaryFilter; - - /** - * The index of the active room in the room list. - */ - activeIndex: number | undefined; -} - -/** - * View model for the new room list - * @see {@link RoomListViewState} for more information about what this view model returns. - */ -export function useRoomListViewModel(): RoomListViewState { - const matrixClient = useMatrixClientContext(); - const { isLoadingRooms, primaryFilters, activePrimaryFilter, roomsResult: filteredRooms } = useFilteredRooms(); - const { activeIndex, roomsResult } = useStickyRoomList(filteredRooms); - - useRoomListNavigation(roomsResult.rooms); - - const currentSpace = useEventEmitterState( - SpaceStore.instance, - UPDATE_SELECTED_SPACE, - () => SpaceStore.instance.activeSpaceRoom, - ); - const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace); - - const createChatRoom = useCallback(() => dispatcher.fire(Action.CreateChat), []); - const createRoom = useCallback(() => createRoomFunc(currentSpace), [currentSpace]); - - return { - isLoadingRooms, - roomsResult, - canCreateRoom, - createRoom, - createChatRoom, - primaryFilters, - activePrimaryFilter, - activeIndex, - }; -} diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx deleted file mode 100644 index a0e36dc668..0000000000 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters"; -import { _t, _td } from "../../../languageHandler"; -import RoomListStoreV3, { - LISTS_LOADED_EVENT, - LISTS_UPDATE_EVENT, - type RoomsResult, -} from "../../../stores/room-list-v3/RoomListStoreV3"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; - -/** - * Provides information about a primary filter. - * A primary filter is a commonly used filter that is given - * more precedence in the UI. For eg, primary filters may be - * rendered as pills above the room list. - */ -export interface PrimaryFilter { - // A function to toggle this filter on and off. - toggle: () => void; - // Whether this filter is currently applied - active: boolean; - // Text that can be used in the UI to represent this filter. - name: string; - // The key of the filter - key: FilterKey; -} - -interface FilteredRooms { - primaryFilters: PrimaryFilter[]; - isLoadingRooms: boolean; - roomsResult: RoomsResult; - /** - * The currently active primary filter. - * If no primary filter is active, this will be undefined. - */ - activePrimaryFilter?: PrimaryFilter; -} - -const filterKeyToNameMap: Map = new Map([ - [FilterKey.UnreadFilter, _td("room_list|filters|unread")], - [FilterKey.PeopleFilter, _td("room_list|filters|people")], - [FilterKey.RoomsFilter, _td("room_list|filters|rooms")], - [FilterKey.FavouriteFilter, _td("room_list|filters|favourite")], - [FilterKey.MentionsFilter, _td("room_list|filters|mentions")], - [FilterKey.InvitesFilter, _td("room_list|filters|invites")], - [FilterKey.LowPriorityFilter, _td("room_list|filters|low_priority")], -]); - -/** - * Track available filters and provide a filtered list of rooms. - */ -export function useFilteredRooms(): FilteredRooms { - /** - * Primary filter refers to the pill based filters - * rendered above the room list. - */ - const [primaryFilter, setPrimaryFilter] = useState(); - - const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace()); - const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms); - - const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => { - const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters); - setRoomsResult(newRooms); - }, []); - - const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] => - array.filter((f) => f !== undefined) as FilterKey[]; - - const getAppliedFilters = useCallback((): FilterKey[] => { - return filterUndefined([primaryFilter]); - }, [primaryFilter]); - - useEffect(() => { - // Update the rooms state when the primary filter changes - const filters = getAppliedFilters(); - updateRoomsFromStore(filters); - }, [getAppliedFilters, updateRoomsFromStore]); - - useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => { - const filters = getAppliedFilters(); - updateRoomsFromStore(filters); - }); - - useEventEmitter(RoomListStoreV3.instance, LISTS_LOADED_EVENT, () => { - setIsLoadingRooms(false); - }); - - /** - * This tells the view which primary filters are available, how to toggle them - * and whether a given primary filter is active. @see {@link PrimaryFilter} - */ - const primaryFilters = useMemo(() => { - const createPrimaryFilter = (key: FilterKey, name: string): PrimaryFilter => { - return { - toggle: () => { - setPrimaryFilter((currentFilter) => { - const filter = currentFilter === key ? undefined : key; - updateRoomsFromStore(filterUndefined([filter])); - return filter; - }); - }, - active: primaryFilter === key, - name, - key, - }; - }; - const filters: PrimaryFilter[] = []; - for (const [key, name] of filterKeyToNameMap.entries()) { - filters.push(createPrimaryFilter(key, _t(name))); - } - return filters; - }, [primaryFilter, updateRoomsFromStore]); - - const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]); - - return { - isLoadingRooms, - primaryFilters, - activePrimaryFilter, - roomsResult, - }; -} diff --git a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx deleted file mode 100644 index efb58b3e04..0000000000 --- a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ -import { useCallback } from "react"; - -import SettingsStore from "../../../settings/SettingsStore"; -import { SettingLevel } from "../../../settings/SettingLevel"; -import { useSettingValue } from "../../../hooks/useSettings"; - -interface MessagePreviewToggleState { - shouldShowMessagePreview: boolean; - toggleMessagePreview: () => void; -} - -/** - * This hook: - * - Provides a state that tracks whether message previews are turned on or off. - * - Provides a function to toggle message previews. - */ -export function useMessagePreviewToggle(): MessagePreviewToggleState { - const shouldShowMessagePreview = useSettingValue("RoomList.showMessagePreview"); - - const toggleMessagePreview = useCallback((): void => { - const toggled = !shouldShowMessagePreview; - SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled); - }, [shouldShowMessagePreview]); - - return { toggleMessagePreview, shouldShowMessagePreview }; -} diff --git a/src/components/viewmodels/roomlist/useRoomListNavigation.ts b/src/components/viewmodels/roomlist/useRoomListNavigation.ts deleted file mode 100644 index 5ef979e79c..0000000000 --- a/src/components/viewmodels/roomlist/useRoomListNavigation.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { type Room } from "matrix-js-sdk/src/matrix"; - -import dispatcher from "../../../dispatcher/dispatcher"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { type ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; - -/** - * Hook to navigate the room list using keyboard shortcuts. - * It listens to the ViewRoomDelta action and updates the room list accordingly. - * @param rooms - */ -export function useRoomListNavigation(rooms: Room[]): void { - useDispatcher(dispatcher, (payload) => { - if (payload.action !== Action.ViewRoomDelta) return; - const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); - if (!roomId) return; - - const { delta, unread } = payload as ViewRoomDeltaPayload; - const filteredRooms = unread - ? // Filter the rooms to only include unread ones and the active room - rooms.filter((room) => { - const state = RoomNotificationStateStore.instance.getRoomState(room); - return room.roomId === roomId || state.isUnread; - }) - : rooms; - - const currentIndex = filteredRooms.findIndex((room) => room.roomId === roomId); - if (currentIndex === -1) return; - - // Get the next/previous new room according to the delta - // Use slice to loop on the list - // If delta is -1 at the start of the list, it will go to the end - // If delta is 1 at the end of the list, it will go to the start - const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length); - if (!newRoom) return; - - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: newRoom.roomId, - show_room_tile: true, // to make sure the room gets scrolled into view - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }); - }); -} diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx deleted file mode 100644 index 355e09a292..0000000000 --- a/src/components/viewmodels/roomlist/useStickyRoomList.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useRef, useState } from "react"; - -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import type { Room } from "matrix-js-sdk/src/matrix"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; -import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3"; - -function getIndexByRoomId(rooms: Room[], roomId: string): number | undefined { - const index = rooms.findIndex((room) => room.roomId === roomId); - return index === -1 ? undefined : index; -} - -function getRoomsWithStickyRoom( - rooms: Room[], - oldIndex: number | undefined, - newIndex: number | undefined, - isRoomChange: boolean, -): { newRooms: Room[]; newIndex: number | undefined } { - const updated = { newIndex, newRooms: rooms }; - if (isRoomChange) { - /* - * When opening another room, the index should obviously change. - */ - return updated; - } - if (newIndex === undefined || oldIndex === undefined) { - /* - * If oldIndex is undefined, then there was no active room before. - * So nothing to do in regards to sticky room. - * Similarly, if newIndex is undefined, there's no active room anymore. - */ - return updated; - } - if (newIndex === oldIndex) { - /* - * If the index hasn't changed, we have nothing to do. - */ - return updated; - } - if (oldIndex > rooms.length - 1) { - /* - * If the old index falls out of the bounds of the rooms array - * (usually because rooms were removed), we can no longer place - * the active room in the same old index. - */ - return updated; - } - - /* - * Making the active room sticky is as simple as removing it from - * its new index and placing it in the old index. - */ - const newRooms = [...rooms]; - const [newRoom] = newRooms.splice(newIndex, 1); - newRooms.splice(oldIndex, 0, newRoom); - - return { newIndex: oldIndex, newRooms }; -} - -export interface StickyRoomListResult { - /** - * The rooms result with the active sticky room applied - */ - roomsResult: RoomsResult; - /** - * Index of the active room in the room list. - */ - activeIndex: number | undefined; -} - -/** - * - Provides a list of rooms such that the active room is sticky i.e the active room is kept - * in the same index even when the order of rooms in the list changes. - * - Provides the index of the active room. - * @param rooms list of rooms - * @see {@link StickyRoomListResult} details what this hook returns.. - */ -export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult { - const [listState, setListState] = useState({ - activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()!), - roomsResult: roomsResult, - }); - - const currentSpaceRef = useRef(SpaceStore.instance.activeSpace); - - const updateRoomsAndIndex = useCallback( - (newRoomId: string | null, isRoomChange: boolean = false) => { - setListState((current) => { - const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); - const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId!); - const oldIndex = current.activeIndex; - const { newIndex, newRooms } = getRoomsWithStickyRoom( - roomsResult.rooms, - oldIndex, - newActiveIndex, - isRoomChange, - ); - return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } }; - }); - }, - [roomsResult], - ); - - // Re-calculate the index when the active room has changed. - useDispatcher(dispatcher, (payload) => { - if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true); - }); - - // Re-calculate the index when the list of rooms has changed. - useEffect(() => { - let newRoomId: string | null = null; - let isRoomChange = false; - if (currentSpaceRef.current !== roomsResult.spaceId) { - /* - If the space has changed, we check if we can immediately set the active - index to the last opened room in that space. Otherwise, we might see a - flicker because of the delay between the space change event and - active room change dispatch. - */ - newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId); - isRoomChange = true; - currentSpaceRef.current = roomsResult.spaceId; - } - updateRoomsAndIndex(newRoomId, isRoomChange); - }, [roomsResult, updateRoomsAndIndex]); - - return listState; -} diff --git a/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx b/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx deleted file mode 100644 index 8c1d04b8c5..0000000000 --- a/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, type PropsWithChildren } from "react"; -import { Button } from "@vector-im/compound-web"; -import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; -import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room"; -import { Flex } from "@element-hq/web-shared-components"; - -import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; -import { _t } from "../../../../languageHandler"; -import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters"; -import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms"; - -interface EmptyRoomListProps { - /** - * The view model for the room list - */ - vm: RoomListViewState; -} - -/** - * The empty state for the room list - */ -export function EmptyRoomList({ vm }: EmptyRoomListProps): JSX.Element | undefined { - // If there is no active primary filter, show the default empty state - if (!vm.activePrimaryFilter) return ; - - switch (vm.activePrimaryFilter.key) { - case FilterKey.FavouriteFilter: - return ( - - ); - case FilterKey.PeopleFilter: - return ( - - ); - case FilterKey.RoomsFilter: - return ( - - ); - case FilterKey.UnreadFilter: - return ( - - ); - case FilterKey.InvitesFilter: - return ( - - ); - case FilterKey.MentionsFilter: - return ( - - ); - case FilterKey.LowPriorityFilter: - return ( - - ); - default: - return undefined; - } -} - -interface GenericPlaceholderProps { - /** - * The title of the placeholder - */ - title: string; - /** - * The description of the placeholder - */ - description?: string; -} - -/** - * A generic placeholder for the room list - */ -function GenericPlaceholder({ title, description, children }: PropsWithChildren): JSX.Element { - return ( - - {title} - {description && {description}} - {children} - - ); -} - -interface DefaultPlaceholderProps { - /** - * The view model for the room list - */ - vm: RoomListViewState; -} - -/** - * The default empty state for the room list when no primary filter is active - * The user can create chat or room (if they have the permission) - */ -function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element { - return ( - - - - {vm.canCreateRoom && ( - - )} - - - ); -} - -interface ActionPlaceholderProps { - filter: PrimaryFilter; - title: string; - action: string; -} - -/** - * A placeholder for the room list when a filter is active - * The user can take action to toggle the filter - */ -function ActionPlaceholder({ filter, title, action }: ActionPlaceholderProps): JSX.Element { - return ( - - - - ); -} diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx deleted file mode 100644 index c946695b39..0000000000 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { useCallback, useRef, type JSX, useMemo } from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; -import { isEqual } from "lodash"; -import { - type VirtualizedListContext, - VirtualizedList, - type ScrollIntoViewOnChange, -} from "@element-hq/web-shared-components"; - -import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; -import { _t } from "../../../../languageHandler"; -import { RoomListItemView } from "./RoomListItemView"; -import { type FilterKey } from "../../../../stores/room-list-v3/skip-list/filters"; -import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; -import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; -import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation"; - -interface RoomListProps { - /** - * The view model state for the room list. - */ - vm: RoomListViewState; -} - -type Context = { - spaceId: string; - filterKeys: FilterKey[] | undefined; -}; - -/** - * Height of a single room list item - */ -const ROOM_LIST_ITEM_HEIGHT = 48; -/** - * Amount to extend the top and bottom of the viewport by. - * From manual testing and user feedback 25 items is reported to be enough to avoid blank space when using the mouse wheel, - * and the trackpad scrolling at a slow to moderate speed where you can still see/read the content. - * Using the trackpad to sling through a large percentage of the list quickly will still show blank space. - * We would likely need to simplify the item content to improve this case. - */ -const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT; -/** - * A virtualized list of rooms. - */ -export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): JSX.Element { - const lastSpaceId = useRef(undefined); - const lastFilterKeys = useRef(undefined); - const roomCount = roomsResult.rooms.length; - const getItemComponent = useCallback( - ( - index: number, - item: Room, - context: VirtualizedListContext, - onFocus: (item: Room, e: React.FocusEvent) => void, - ): JSX.Element => { - const itemKey = item.roomId; - const isRovingItem = itemKey === context.tabIndexKey; - const isFocused = isRovingItem && context.focused; - const isSelected = activeIndex === index; - return ( - - ); - }, - [activeIndex, roomCount], - ); - - const getItemKey = useCallback((item: Room): string => { - return item.roomId; - }, []); - - const scrollIntoViewOnChange = useCallback>( - (params) => { - const { spaceId, filterKeys } = params.context.context; - const shouldScrollIndexIntoView = - lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys); - lastFilterKeys.current = filterKeys; - lastSpaceId.current = spaceId; - - if (shouldScrollIndexIntoView) { - return { - align: `start`, - index: activeIndex || 0, - behavior: "auto", - }; - } - return false; - }, - [activeIndex], - ); - - const keyDownCallback = useCallback((ev: React.KeyboardEvent) => { - const navAction = getKeyBindingsManager().getNavigationAction(ev); - if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) { - LandmarkNavigation.findAndFocusNextLandmark( - Landmark.ROOM_LIST, - navAction === KeyBindingAction.PreviousLandmark, - ); - ev.stopPropagation(); - ev.preventDefault(); - return; - } - }, []); - const context = useMemo( - () => ({ spaceId: roomsResult.spaceId, filterKeys: roomsResult.filterKeys }), - [roomsResult.spaceId, roomsResult.filterKeys], - ); - - return ( - true} - onKeyDown={keyDownCallback} - increaseViewportBy={{ - bottom: EXTENDED_VIEWPORT_HEIGHT, - top: EXTENDED_VIEWPORT_HEIGHT, - }} - /> - ); -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx deleted file mode 100644 index f3ba4167e7..0000000000 --- a/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { type Room } from "matrix-js-sdk/src/matrix"; -import { type JSX, type PropsWithChildren } from "react"; -import { ContextMenu } from "@vector-im/compound-web"; -import React from "react"; - -import { _t } from "../../../../languageHandler"; -import { MoreOptionContent } from "./RoomListItemMenuView"; -import { useRoomListItemMenuViewModel } from "../../../viewmodels/roomlist/RoomListItemMenuViewModel"; - -interface RoomListItemContextMenuViewProps { - /** - * The room to display the menu for. - */ - room: Room; -} - -/** - * A view for the room list item context menu. - */ -export function RoomListItemContextMenuView({ - room, - children, -}: PropsWithChildren): JSX.Element { - const vm = useRoomListItemMenuViewModel(room); - - return ( - - - - ); -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx deleted file mode 100644 index 7c5dd5ba1a..0000000000 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, useState } from "react"; -import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem } from "@vector-im/compound-web"; -import { - MarkAsReadIcon, - MarkAsUnreadIcon, - FavouriteIcon, - ArrowDownIcon, - UserAddIcon, - LinkIcon, - LeaveIcon, - OverflowHorizontalIcon, - NotificationsSolidIcon, - NotificationsOffSolidIcon, - CheckIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; -import { type Room } from "matrix-js-sdk/src/matrix"; -import { Flex } from "@element-hq/web-shared-components"; -import classNames from "classnames"; - -import { _t } from "../../../../languageHandler"; -import { - type RoomListItemMenuViewState, - useRoomListItemMenuViewModel, -} from "../../../viewmodels/roomlist/RoomListItemMenuViewModel"; -import { RoomNotifState } from "../../../../RoomNotifs"; - -interface RoomListItemMenuViewProps { - /** - * Additional class name for the root element. - */ - className?: string; - - /** - * The room to display the menu for. - */ - room: Room; -} - -/** - * A view for the room list item menu. - */ -export function RoomListItemMenuView({ room, className }: RoomListItemMenuViewProps): JSX.Element { - const vm = useRoomListItemMenuViewModel(room); - - return ( - - {vm.showMoreOptionsMenu && } - {vm.showNotificationMenu && } - - ); -} - -interface MoreOptionsMenuProps { - /** - * The view model state for the menu. - */ - vm: RoomListItemMenuViewState; -} - -/** - * The more options menu for the room list item. - */ -function MoreOptionsMenu({ vm }: MoreOptionsMenuProps): JSX.Element { - const [open, setOpen] = useState(false); - - return ( - - - - } - > - - - ); -} - -interface MoreOptionContentProps { - /** - * The view model state for the menu. - */ - vm: RoomListItemMenuViewState; -} - -export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { - return ( -
e.stopPropagation()} - > - {vm.canMarkAsRead && ( - evt.stopPropagation()} - hideChevron={true} - /> - )} - {vm.canMarkAsUnread && ( - evt.stopPropagation()} - hideChevron={true} - /> - )} - evt.stopPropagation()} - /> - evt.stopPropagation()} - /> - {vm.canInvite && ( - evt.stopPropagation()} - hideChevron={true} - /> - )} - {vm.canCopyRoomLink && ( - evt.stopPropagation()} - hideChevron={true} - /> - )} - - evt.stopPropagation()} - hideChevron={true} - /> -
- ); -} - -interface NotificationMenuProps { - /** - * The view model state for the menu. - */ - vm: RoomListItemMenuViewState; -} - -function NotificationMenu({ vm }: NotificationMenuProps): JSX.Element { - const [open, setOpen] = useState(false); - const checkComponent = ; - - return ( - - {vm.isNotificationMute ? : } - - } - > -
e.stopPropagation()} - > - vm.setRoomNotifState(RoomNotifState.AllMessages)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationAllMessage && checkComponent} - - vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationAllMessageLoud && checkComponent} - - vm.setRoomNotifState(RoomNotifState.MentionsOnly)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationMentionOnly && checkComponent} - - vm.setRoomNotifState(RoomNotifState.Mute)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationMute && checkComponent} - -
-
- ); -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx deleted file mode 100644 index d87da9c034..0000000000 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, memo, useEffect, useRef } from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; -import classNames from "classnames"; -import { Flex } from "@element-hq/web-shared-components"; - -import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel"; -import { RoomListItemMenuView } from "./RoomListItemMenuView"; -import { NotificationDecoration } from "../NotificationDecoration"; -import { RoomAvatarView } from "../../avatars/RoomAvatarView"; -import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView"; - -interface RoomListItemViewProps extends Omit, "onFocus"> { - /** - * The room to display - */ - room: Room; - /** - * Whether the room is selected - */ - isSelected: boolean; - /** - * Whether the room is focused - */ - isFocused: boolean; - /** - * A callback that indicates the item has received focus - */ - onFocus: (room: Room, e: React.FocusEvent) => void; - /** - * The index of the room in the list - */ - roomIndex: number; - /** - * The total number of rooms in the list - */ - roomCount: number; -} - -/** - * An item in the room list - */ -export const RoomListItemView = memo(function RoomListItemView({ - room, - isSelected, - isFocused, - onFocus, - roomIndex: index, - roomCount: count, - ...props -}: RoomListItemViewProps): JSX.Element { - const ref = useRef(null); - const vm = useRoomListItemViewModel(room); - - useEffect(() => { - if (isFocused) { - ref.current?.focus({ preventScroll: true, focusVisible: true }); - } - }, [isFocused]); - - const content = ( - vm.openRoom()} - onFocus={(e: React.FocusEvent) => onFocus(room, e)} - tabIndex={isFocused ? 0 : -1} - {...props} - > - - - {/* We truncate the room name when too long. Title here is to show the full name on hover */} -
-
- {vm.name} -
- {vm.messagePreview && ( -
- {vm.messagePreview} -
- )} -
- {vm.showHoverMenu && } - - {/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */} - {vm.showNotificationDecoration && ( - - )} -
-
- ); - - if (!vm.showContextMenu) return content; - return {content}; -}); diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx deleted file mode 100644 index 44f19a86da..0000000000 --- a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, useEffect, useId, useRef, useState, type RefObject } from "react"; -import { ChatFilter, IconButton } from "@vector-im/compound-web"; -import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; -import { Flex } from "@element-hq/web-shared-components"; - -import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; -import { _t } from "../../../../languageHandler"; - -interface RoomListPrimaryFiltersProps { - /** - * The view model for the room list - */ - vm: RoomListViewState; -} - -/** - * The primary filters for the room list - */ -export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element { - const id = useId(); - const [isExpanded, setIsExpanded] = useState(false); - - const { ref, isWrapping: displayChevron, wrappingIndex } = useCollapseFilters(isExpanded); - const filters = useVisibleFilters(vm.primaryFilters, wrappingIndex); - - return ( - - {displayChevron && ( - setIsExpanded((_expanded) => !_expanded)} - > - - - )} - - {filters.map((filter, i) => ( - filter.toggle()}> - {filter.name} - - ))} - - - ); -} - -/** - * A hook to manage the wrapping of filters in the room list. - * It observes the filter list and hides filters that are wrapping when the list is not expanded. - * @param isExpanded - * @returns an object containing: - * - `ref`: a ref to put on the filter list element - * - `isWrapping`: a boolean indicating if the filters are wrapping - * - `wrappingIndex`: the index of the first filter that is wrapping - */ -function useCollapseFilters( - isExpanded: boolean, -): { ref: RefObject; isWrapping: boolean; wrappingIndex: number } { - const ref = useRef(null); - const [isWrapping, setIsWrapping] = useState(false); - const [wrappingIndex, setWrappingIndex] = useState(-1); - - useEffect(() => { - if (!ref.current) return; - - const hideFilters = (list: Element): void => { - let isWrapping = false; - Array.from(list.children).forEach((node, i): void => { - const child = node as HTMLElement; - const wrappingClass = "mx_RoomListPrimaryFilters_wrapping"; - child.setAttribute("aria-hidden", "false"); - child.classList.remove(wrappingClass); - - // If the filter list is expanded, all filters are visible - if (isExpanded) return; - - // If the previous element is on the left element of the current one, it means that the filter is wrapping - const previousSibling = child.previousElementSibling as HTMLElement | null; - if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) { - if (!isWrapping) setWrappingIndex(i); - isWrapping = true; - } - - // If the filter is wrapping, we hide it - child.classList.toggle(wrappingClass, isWrapping); - child.setAttribute("aria-hidden", isWrapping.toString()); - }); - - if (!isWrapping) setWrappingIndex(-1); - setIsWrapping(isExpanded || isWrapping); - }; - - hideFilters(ref.current); - const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target))); - - observer.observe(ref.current); - return () => { - observer.disconnect(); - }; - }, [isExpanded]); - - return { ref, isWrapping, wrappingIndex }; -} - -/** - * A hook to sort the filters by active state. - * The list is sorted if the current filter index is greater than or equal to the wrapping index. - * If the wrapping index is -1, the filters are not sorted. - * - * @param filters - the list of filters to sort. - * @param wrappingIndex - the index of the first filter that is wrapping. - */ -export function useVisibleFilters( - filters: RoomListViewState["primaryFilters"], - wrappingIndex: number, -): RoomListViewState["primaryFilters"] { - // By default, the filters are not sorted - const [sortedFilters, setSortedFilters] = useState(filters); - - useEffect(() => { - const isActiveFilterWrapping = filters.findIndex((f) => f.active) >= wrappingIndex; - // If the active filter is not wrapping, we don't need to sort the filters - if (!isActiveFilterWrapping || wrappingIndex === -1) { - setSortedFilters(filters); - return; - } - - // Sort the filters with the current filter at first position - setSortedFilters( - filters.slice().sort((filterA, filterB) => { - // If the filter is active, it should be at the top of the list - if (filterA.active && !filterB.active) return -1; - if (!filterA.active && filterB.active) return 1; - // If both filters are active or not, keep their original order - return 0; - }), - ); - }, [filters, wrappingIndex]); - - return sortedFilters; -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index b29affc0be..50dd83e505 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -5,33 +5,47 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX } from "react"; +import React, { useCallback, type JSX, type ReactNode } from "react"; +import { + RoomListView as SharedRoomListView, + useCreateAutoDisposedViewModel, + type Room as SharedRoom, +} from "@element-hq/web-shared-components"; +import { type Room } from "matrix-js-sdk/src/matrix"; -import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel"; -import { RoomList } from "./RoomList"; -import { EmptyRoomList } from "./EmptyRoomList"; -import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { RoomAvatarView } from "../../avatars/RoomAvatarView"; +import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; +import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; +import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation"; +import { RoomListViewViewModel } from "../../../../viewmodels/room-list/RoomListViewViewModel"; /** - * Host the room list and the (future) room filters + * RoomListView component using shared components with proper MVVM pattern. */ export function RoomListView(): JSX.Element { - const vm = useRoomListViewModel(); - const isRoomListEmpty = vm.roomsResult.rooms.length === 0; - let listBody; - if (vm.isLoadingRooms) { - listBody =
; - } else if (isRoomListEmpty) { - listBody = ; - } else { - listBody = ; - } - return ( - <> -
- -
- {listBody} - - ); + const matrixClient = useMatrixClientContext(); + + // Create and auto-dispose ViewModel instance + const vm = useCreateAutoDisposedViewModel(() => new RoomListViewViewModel({ client: matrixClient })); + + // Render avatar for each room - memoized to prevent re-renders + const renderAvatar = useCallback((room: SharedRoom): ReactNode => { + return ; + }, []); + + // Handle keyboard navigation for landmarks + const onKeyDown = useCallback((ev: React.KeyboardEvent) => { + const navAction = getKeyBindingsManager().getNavigationAction(ev); + if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) { + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.ROOM_LIST, + navAction === KeyBindingAction.PreviousLandmark, + ); + ev.stopPropagation(); + ev.preventDefault(); + } + }, []); + + return ; } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 4301446794..82358af880 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -12,16 +12,6 @@ "one": "Nepřečtená zmínka." }, "recent_rooms": "Nedávné místnosti", - "room_messsage_not_sent": "Otevřít místnost %(roomName)s s nenastavenou zprávou.", - "room_n_unread_invite": "Otevřít pozvánku do místnosti %(roomName)s.", - "room_n_unread_messages": { - "one": "Otevřít místnost %(roomName)s s 1 nepřečtenou zprávou.", - "other": "Otevřít místnost %(roomName)s s %(count)s nepřečtenými zprávami." - }, - "room_n_unread_messages_mentions": { - "one": "Otevřít místnost %(roomName)s s 1 nepřečtenou zmínkou.", - "other": "Otevřít místnost %(roomName)s s %(count)s nepřečtenými zprávami včetně zmínek." - }, "room_name": "Místnost %(name)s", "room_status_bar": "Stavový řádek místnosti", "seek_bar_label": "Panel posunu zvuku", @@ -1718,7 +1708,6 @@ "class_global": "Globální", "class_other": "Další možnosti", "default": "Výchozí", - "default_settings": "Shoda výchozího nastavení", "email_pusher_app_display_name": "E-mailová oznámení", "enable_prompt_toast_description": "Povolit oznámení na ploše", "enable_prompt_toast_title": "Oznámení", @@ -1737,8 +1726,7 @@ "mentions_and_keywords_description": "Dostávat oznámení pouze o zmínkách a klíčových slovech podle nastavení", "mentions_keywords": "Zmínky a klíčová slova", "message_didnt_send": "Zpráva se neodeslala. Klikněte pro informace.", - "mute_description": "Nebudete dostávat žádná oznámení", - "mute_room": "Ztlumit místnost" + "mute_description": "Nebudete dostávat žádná oznámení" }, "notifier": { "m.key.verification.request": "%(name)s žádá o ověření" @@ -2160,37 +2148,9 @@ "add_space_label": "Přidat prostor", "breadcrumbs_empty": "Žádné nedávno navštívené místnosti", "breadcrumbs_label": "Nedávno navštívené místnosti", - "collapse_filters": "Sbalit seznam filtrů", - "empty": { - "no_chats": "Zatím žádné chaty", - "no_chats_description": "Začněte tím, že někomu pošlete zprávu nebo vytvoříte místnost", - "no_chats_description_no_room_rights": "Začněte tím, že někomu pošlete zprávu", - "no_favourites": "Zatím nemáte oblíbený chat", - "no_favourites_description": "Chat si můžete přidat do oblíbených v nastavení chatu", - "no_invites": "Nemáte žádné nepřečtené pozvánky", - "no_lowpriority": "Nemáte žádné místnosti s nízkou prioritou", - "no_mentions": "Nemáte žádné nepřečtené zmínky", - "no_people": "Zatím s nikým nemáte přímé chaty", - "no_people_description": "Můžete zrušit výběr filtrů, abyste viděli ostatní chaty", - "no_rooms": "Ještě nejste v žádné místnosti", - "no_rooms_description": "Můžete zrušit výběr filtrů, abyste viděli své další chaty", - "no_unread": "Gratulujeme! Nemáte žádné nepřečtené zprávy", - "show_activity": "Zobrazit veškerou aktivitu", - "show_chats": "Zobrazit všechny chaty" - }, - "expand_filters": "Rozbalit seznam filtrů", "failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti", "failed_remove_tag": "Nepodařilo se odstranit štítek %(tagName)s z místnosti", "failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy", - "filters": { - "favourite": "Oblíbené", - "invites": "Pozvánky", - "low_priority": "Nízká priorita", - "mentions": "Zmínky", - "people": "Lidé", - "rooms": "Místnosti", - "unread": "Nepřečtené" - }, "home_menu_label": "Možnosti domovské obrazovky", "join_public_room_label": "Připojit se k veřejné místnosti", "joining_rooms_status": { @@ -2199,23 +2159,13 @@ }, "list_title": "Seznam místností", "more_options": { - "copy_link": "Kopírovat odkaz na místnost", - "favourited": "Oblíbené", - "leave_room": "Opustit místnost", - "low_priority": "Nízká priorita", - "mark_read": "Označit jako přečtené", - "mark_unread": "Označit jako nepřečtené" + "leave_room": "Opustit místnost" }, "notification_options": "Možnosti oznámení", - "primary_filters": "Filtry seznamu místností", "redacting_messages_status": { "one": "Momentálně se odstraňují zprávy v %(count)s místnosti", "other": "Momentálně se odstraňují zprávy v %(count)s místnostech" }, - "room": { - "more_options": "Více možností", - "open_room": "Otevřít místnost %(roomName)s" - }, "show_less": "Zobrazit méně", "show_n_more": { "other": "Zobrazit %(count)s dalších", diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index 8a7d596990..b4e9978f47 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -14,18 +14,6 @@ "%(count)s crybwylliad heb eu darllen": "other" }, "recent_rooms": "Ystafelloedd diweddar", - "room_messsage_not_sent": "Agor ystafell %(roomName)s gyda neges heb ei gosod.", - "room_n_unread_invite": "Agor gwahoddiad i ystafell %(roomName)s.", - "room_n_unread_messages": { - "Ystafell agored%(roomName)s gyda %(count)s negeseuon heb eu darllen.": "zero", - "Ystafell agored %(roomName)s gydag 1 neges heb ei darllen.": "one", - "Ystafell agored%(roomName)s gyda %(count)s neges heb eu darllen.": "other" - }, - "room_n_unread_messages_mentions": { - "Ystafell agored %(roomName)s gyda %(count)s negeseuon heb eu darllen gan gynnwys crybwylliadau.": "zero", - "Ystafell agored %(roomName)s gydag 1 crybwylliad heb ei ddarllen.": "one", - "Ystafell agored %(roomName)s gyda %(count)s neges heb eu darllen gan gynnwys crybwylliadau.": "other" - }, "room_name": "Ystafell %(matere)s", "room_status_bar": "Bar statws ystafell", "seek_bar_label": "Bar chwilio sain", @@ -1730,7 +1718,6 @@ "class_global": "Eang", "class_other": "Arall", "default": "Rhagosodedig", - "default_settings": "Cydweddu'r gosodiadau rhagosodedig", "email_pusher_app_display_name": "Hysbysiadau E-bost", "enable_prompt_toast_description": "Galluogi hysbysiadau bwrdd gwaith", "enable_prompt_toast_title": "Hysbysiadau", @@ -1749,8 +1736,7 @@ "mentions_and_keywords_description": "Dim ond gyda chyfeiriadau ac allweddeiriau fel y'u gosodwyd yn eich gosodiadau y cewch eich hysbysu", "mentions_keywords": "Crybwylliadau ac allweddeiriau", "message_didnt_send": "Heb anfon y neges. Cliciwch am wybodaeth.", - "mute_description": "Fyddwch chi ddim yn cael unrhyw hysbysiadau", - "mute_room": "Tewi'r ystafell" + "mute_description": "Fyddwch chi ddim yn cael unrhyw hysbysiadau" }, "notifier": { "m.key.verification.request": "Mae %(matere)s yn gofyn am ddilysiad" @@ -2163,37 +2149,9 @@ "add_space_label": "Ychwanegu gofod", "breadcrumbs_empty": "Dim ystafelloedd yr ymwelwyd â nhw yn ddiweddar", "breadcrumbs_label": "Ymwelwyd ag ystafelloedd yn ddiweddar", - "collapse_filters": "Cwympo rhestr hidlo", - "empty": { - "no_chats": "Dim sgyrsiau eto", - "no_chats_description": "Dechreuwch drwy anfon neges at rywun neu drwy greu ystafell", - "no_chats_description_no_room_rights": "Dechreuwch trwy anfon neges at rywun", - "no_favourites": "Nid oes gennych hoff sgwrs eto", - "no_favourites_description": "Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio", - "no_invites": "Does gennych chi ddim gwahoddiadau heb eu darllen", - "no_lowpriority": "Nid oes gennych unrhyw ystafelloedd blaenoriaeth isel", - "no_mentions": "Does gennych chi ddim crybwylliadau heb eu darllen", - "no_people": "Nid oes gennych chi sgyrsiau uniongyrchol gydag unrhyw un eto", - "no_people_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", - "no_rooms": "Nid ydych mewn unrhyw ystafell eto", - "no_rooms_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", - "no_unread": "Llongyfarchiadau! Nid oes gennych unrhyw negeseuon heb eu darllen", - "show_activity": "Gweld yr holl weithgarwch", - "show_chats": "Dangos pob sgwrs" - }, - "expand_filters": "Ehangu rhestr hidlo", "failed_add_tag": "Wedi methu ag ychwanegu tag %(tagName)s i'r ystafell", "failed_remove_tag": "Wedi methu â thynnu'r tag %(tagName)s o'r ystafell", "failed_set_dm_tag": "Wedi methu gosod tag neges uniongyrchol", - "filters": { - "favourite": "Ffefrynnau", - "invites": "Gwahoddiadau", - "low_priority": "Blaenoriaeth isel", - "mentions": "Crybwylliadau", - "people": "Pobl", - "rooms": "Ystafelloedd", - "unread": "Heb eu darllen" - }, "home_menu_label": "Dewisiadau cartref", "join_public_room_label": "Ymuno â'r ystafell gyhoeddus", "joining_rooms_status": { @@ -2201,22 +2159,12 @@ }, "list_title": "Rhestr ystafelloedd", "more_options": { - "copy_link": "Copïo dolen ystafell", - "favourited": "Ffafrio", - "leave_room": "Gadael yr ystafell", - "low_priority": "Blaenoriaeth isel", - "mark_read": "Marcio fel wedi'i ddarllen", - "mark_unread": "Marcio fel heb ei ddarllen" + "leave_room": "Gadael yr ystafell" }, "notification_options": "Dewisiadau hysbysu", - "primary_filters": "Hidlau rhestr ystafelloedd", "redacting_messages_status": { "Yn tynnu negeseuon mewn %(count)s ystafell": "other" }, - "room": { - "more_options": "Rhagor o Ddewisiadau", - "open_room": "Agor ystafell %(roomName)s" - }, "show_less": "Dangos llai", "show_n_more": { "Dangos %(count)s yn rhagor": "other" diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index e3e8ef4d07..8385d2cd51 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -12,8 +12,6 @@ "other": "%(count)s ulæste beskeder, herunder omtaler." }, "recent_rooms": "Nylige rum", - "room_messsage_not_sent": "Åbent rum %(roomName)s med en usendt besked.", - "room_n_unread_invite": "Invitation til det åbne rum %(roomName)s.", "room_name": "Rum %(name)s", "room_status_bar": "Statusbjælke for rum", "seek_bar_label": "Progressionsmarkør for lydafspiller", @@ -1844,15 +1842,9 @@ "add_space_label": "Tilføj gruppe", "breadcrumbs_empty": "Ingen nyligt besøgte rum", "breadcrumbs_label": "Nyligt besøgte rum", - "empty": { - "no_rooms": "Du er ikke i noget rum endnu" - }, "failed_add_tag": "Kunne ikke tilføje tag(s): %(tagName)s til gruppen", "failed_remove_tag": "Kunne ikke fjerne tag(s): %(tagName)s fra gruppen", "failed_set_dm_tag": "Kunne ikke indstille tagget til direkte beskeder", - "filters": { - "people": "Brugere" - }, "home_menu_label": "Hjemmeindstillinger", "join_public_room_label": "Deltag i offentligt rum", "joining_rooms_status": { diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 326da77589..fbbe9f14de 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -12,16 +12,6 @@ "one": "1 ungelesene Erwähnung." }, "recent_rooms": "Zuletzt besuchte Chats", - "room_messsage_not_sent": "Öffne den Chat „%(roomName)s” mit einer ungesendeten Nachricht.", - "room_n_unread_invite": "Offene Einladung zum Chat %(roomName)s", - "room_n_unread_messages": { - "one": "Chat %(roomName)s öffnen mit 1 ungelesenen Nachricht.", - "other": "Chat %(roomName)s öffnen mit %(count)s ungelesene Nachrichten." - }, - "room_n_unread_messages_mentions": { - "one": "Öffne den den Chat %(roomName)s mit 1 ungelesenen Erwähnung.", - "other": "Öffne den Chat %(roomName)s mit %(count)s ungelesenen Nachrichten einschließlich Erwähnungen." - }, "room_name": "Chat %(name)s", "room_status_bar": "Chat-Statusleiste", "seek_bar_label": "Audio-Suchleiste", @@ -1717,7 +1707,6 @@ "class_global": "Global", "class_other": "Sonstiges", "default": "Standard", - "default_settings": "Standardeinstellungen verwenden", "email_pusher_app_display_name": "E-Mail-Benachrichtigungen", "enable_prompt_toast_description": "Aktiviere Desktopbenachrichtigungen", "enable_prompt_toast_title": "Benachrichtigungen", @@ -1736,8 +1725,7 @@ "mentions_and_keywords_description": "Nur bei Erwähnungen und Schlüsselwörtern benachrichtigen, die du in den Einstellungen konfigurieren kannst", "mentions_keywords": "Erwähnungen und Schlüsselwörter", "message_didnt_send": "Nachricht nicht gesendet. Klicke für Details.", - "mute_description": "Du wirst keine Benachrichtigungen erhalten", - "mute_room": "Chat stummschalten" + "mute_description": "Du wirst keine Benachrichtigungen erhalten" }, "notifier": { "m.key.verification.request": "%(name)s fordert eine Verifizierung an" @@ -2153,37 +2141,9 @@ "add_space_label": "Space hinzufügen", "breadcrumbs_empty": "Keine kürzlich besuchten Chats", "breadcrumbs_label": "Kürzlich besuchte Chats", - "collapse_filters": "Filterliste einklappen", - "empty": { - "no_chats": "Noch keine Chats", - "no_chats_description": "Leg los, indem du jemandem eine Nachricht schickst oder einen Chat erstellst", - "no_chats_description_no_room_rights": "Leg los, indem du jemandem eine Nachricht schickst", - "no_favourites": "Du hast noch keine Chats als Favorit markiert", - "no_favourites_description": "In den Chat Einstellungen kannst du einen Chat als Favorit markieren", - "no_invites": "Du hast keine ungelesenen Einladungen", - "no_lowpriority": "Du hast keine Chats mit niedriger Priorität.", - "no_mentions": "Du hast keine ungelesenen Erwähnungen", - "no_people": "Du hast noch keine Direktnachrichten", - "no_people_description": "Wähle Filter ab, um Chats zu sehen.", - "no_rooms": "Du bist noch in keinem Chat", - "no_rooms_description": "Wähle Filter ab, um Chats zu sehen.", - "no_unread": "Glückwunsch! Du hast keine ungelesenen Nachrichten.", - "show_activity": "Alle Aktivitäten ansehen", - "show_chats": "Alle Chats anzeigen" - }, - "expand_filters": "Filterliste ausklappen", "failed_add_tag": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an den Chat", "failed_remove_tag": "Entfernen des Chat-Tags %(tagName)s fehlgeschlagen", "failed_set_dm_tag": "Fehler beim Setzen der Nachrichtenmarkierung", - "filters": { - "favourite": "Favoriten", - "invites": "Einladungen", - "low_priority": "Niedrige Priorität", - "mentions": "Erwähnungen", - "people": "Personen", - "rooms": "Gruppen", - "unread": "Ungelesen" - }, "home_menu_label": "Startseiteneinstellungen", "join_public_room_label": "Öffentlichem Chat beitreten", "joining_rooms_status": { @@ -2192,23 +2152,13 @@ }, "list_title": "Chatliste", "more_options": { - "copy_link": "Chatlink kopieren", - "favourited": "Favorisiert", - "leave_room": "Chat verlassen", - "low_priority": "Niedrige Priorität", - "mark_read": "Als gelesen markieren", - "mark_unread": "Als ungelesen markieren" + "leave_room": "Chat verlassen" }, "notification_options": "Benachrichtigungsoptionen", - "primary_filters": "Filter für die Chatliste", "redacting_messages_status": { "one": "Entferne Nachrichten in %(count)s Chat", "other": "Entferne Nachrichten in %(count)s Chats" }, - "room": { - "more_options": "Weitere Optionen", - "open_room": "Öffne Chat %(roomName)s" - }, "show_less": "Weniger anzeigen", "show_n_more": { "other": "%(count)s weitere anzeigen", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4f6fe35959..b498e20d72 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -12,16 +12,6 @@ "other": "%(count)s unread messages including mentions." }, "recent_rooms": "Recent rooms", - "room_messsage_not_sent": "Open room %(roomName)s with an unsent message.", - "room_n_unread_invite": "Open room %(roomName)s invitation.", - "room_n_unread_messages": { - "one": "Open room %(roomName)s with 1 unread message.", - "other": "Open room %(roomName)s with %(count)s unread messages." - }, - "room_n_unread_messages_mentions": { - "one": "Open room %(roomName)s with 1 unread mention.", - "other": "Open room %(roomName)s with %(count)s unread messages including mentions." - }, "room_name": "Room %(name)s", "room_status_bar": "Room status bar", "seek_bar_label": "Audio seek bar", @@ -1717,7 +1707,6 @@ "class_global": "Global", "class_other": "Other", "default": "Default", - "default_settings": "Match default settings", "email_pusher_app_display_name": "Email Notifications", "enable_prompt_toast_description": "Enable desktop notifications", "enable_prompt_toast_title": "Notifications", @@ -1736,8 +1725,7 @@ "mentions_and_keywords_description": "Get notified only with mentions and keywords as set up in your settings", "mentions_keywords": "Mentions and keywords", "message_didnt_send": "Message didn't send. Click for info.", - "mute_description": "You won't get any notifications", - "mute_room": "Mute room" + "mute_description": "You won't get any notifications" }, "notifier": { "m.key.verification.request": "%(name)s is requesting verification" @@ -2154,37 +2142,9 @@ "add_space_label": "Add space", "breadcrumbs_empty": "No recently visited rooms", "breadcrumbs_label": "Recently visited rooms", - "collapse_filters": "Collapse filter list", - "empty": { - "no_chats": "No chats yet", - "no_chats_description": "Get started by messaging someone or by creating a room", - "no_chats_description_no_room_rights": "Get started by messaging someone", - "no_favourites": "You don't have favourite chats yet", - "no_favourites_description": "You can add a chat to your favourites in the chat settings", - "no_invites": "You don't have any unread invites", - "no_lowpriority": "You don't have any low priority rooms", - "no_mentions": "You don't have any unread mentions", - "no_people": "You don’t have direct chats with anyone yet", - "no_people_description": "You can deselect filters in order to see your other chats", - "no_rooms": "You’re not in any room yet", - "no_rooms_description": "You can deselect filters in order to see your other chats", - "no_unread": "Congrats! You don’t have any unread messages", - "show_activity": "See all activity", - "show_chats": "Show all chats" - }, - "expand_filters": "Expand filter list", "failed_add_tag": "Failed to add tag %(tagName)s to room", "failed_remove_tag": "Failed to remove tag %(tagName)s from room", "failed_set_dm_tag": "Failed to set direct message tag", - "filters": { - "favourite": "Favourites", - "invites": "Invites", - "low_priority": "Low priority", - "mentions": "Mentions", - "people": "People", - "rooms": "Rooms", - "unread": "Unreads" - }, "home_menu_label": "Home options", "join_public_room_label": "Join public room", "joining_rooms_status": { @@ -2193,23 +2153,13 @@ }, "list_title": "Room list", "more_options": { - "copy_link": "Copy room link", - "favourited": "Favourited", - "leave_room": "Leave room", - "low_priority": "Low priority", - "mark_read": "Mark as read", - "mark_unread": "Mark as unread" + "leave_room": "Leave room" }, "notification_options": "Notification options", - "primary_filters": "Room list filters", "redacting_messages_status": { "one": "Currently removing messages in %(count)s room", "other": "Currently removing messages in %(count)s rooms" }, - "room": { - "more_options": "More Options", - "open_room": "Open room %(roomName)s" - }, "show_less": "Show less", "show_n_more": { "one": "Show %(count)s more", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index c62d9adcfb..1c800dda1c 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -12,8 +12,6 @@ "one": "1 mención sin leer." }, "recent_rooms": "Salas recientes", - "room_messsage_not_sent": "Abrir sala %(roomName)s con un mensaje no enviado.", - "room_n_unread_invite": "Abrir invitación de sala %(roomName)s.", "room_name": "Sala %(name)s", "room_status_bar": "Barra de estado de la sala", "seek_bar_label": "Barra de búsqueda de audio", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 0cd207acbc..84916f2787 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -12,16 +12,6 @@ "other": "%(count)s lugemata sõnumit kaasa arvatud mainimised." }, "recent_rooms": "Hiljuti kasutatud jututoad", - "room_messsage_not_sent": "Ava „%(roomName)s“ jututuba saatmata sõnumiga.", - "room_n_unread_invite": "Ava %(roomName)s jututoa kutse.", - "room_n_unread_messages": { - "one": "Ava %(roomName)s jututuba 1 lugemata sõnumiga.", - "other": "Ava %(roomName)s jututuba %(count)s lugemata sõnumiga." - }, - "room_n_unread_messages_mentions": { - "one": "Ava %(roomName)s jututuba 1 lugemata mainimisega.", - "other": "Ava %(roomName)s jututuba %(count)s lugemata sõnumiga, mille hulgas on ka mainimisi." - }, "room_name": "Jututuba %(name)s", "room_status_bar": "Jututoa olekuriba", "seek_bar_label": "Heli kerimisriba", @@ -1717,7 +1707,6 @@ "class_global": "Üldised", "class_other": "Muud", "default": "Tavaline", - "default_settings": "Sobita vaikimisi seadistustega", "email_pusher_app_display_name": "E-posti teel saadetavad teavitused", "enable_prompt_toast_description": "Võta kasutusele töölauakeskkonna teavitused", "enable_prompt_toast_title": "Teavitused", @@ -1736,8 +1725,7 @@ "mentions_and_keywords_description": "Soovin teavitusi sellisena mainimiste ja võtmesõnade puhul, nagu ma neid olen seadistanud", "mentions_keywords": "Mainimised ja märksõnad", "message_didnt_send": "Sõnum jäi saatmata. Lisateabe saamiseks klõpsi.", - "mute_description": "Sa ei saa üldse teavitusi", - "mute_room": "Summuta jututuba" + "mute_description": "Sa ei saa üldse teavitusi" }, "notifier": { "m.key.verification.request": "%(name)s soovib verifitseerimist" @@ -2153,37 +2141,9 @@ "add_space_label": "Lisa kogukond", "breadcrumbs_empty": "Hiljuti külastatud jututubasid ei leidu", "breadcrumbs_label": "Hiljuti külastatud jututoad", - "collapse_filters": "Ahenda filtriloendit", - "empty": { - "no_chats": "Vestlusi veel ei leidu", - "no_chats_description": "Alusta sellest, et leia mõni vestluspartner või loo oma jututuba", - "no_chats_description_no_room_rights": "Alusta sellest, et leia mõni vestluspartner", - "no_favourites": "Sa pole veel ühtegi vestlust märkinud lemmikuks", - "no_favourites_description": "Vestluse saad märkida lemmikuks tema seadistustest", - "no_invites": "Sul pole lugemata kutseid", - "no_lowpriority": "Sul pole ühtegi vähetähtsat jututuba", - "no_mentions": "Sul pole lugemata mainimisi", - "no_people": "Sul pole veel ühtegi otsevestlust kellegagi", - "no_people_description": "Kõikide muude vestluste nägemiseks eemalda otsingufiltrid", - "no_rooms": "Sa veel ei osale mitte üheski jututoas", - "no_rooms_description": "Kõikide oma muude vestluste nägemiseks eemalda otsingufiltrid", - "no_unread": "Õnnitlused! Sul pole ühtegi lugemata sõnumit", - "show_activity": "Vaata kõiki tegevusi", - "show_chats": "Näita kõiki vestlusi" - }, - "expand_filters": "Laienda filtriloendit", "failed_add_tag": "Sildi %(tagName)s lisamine jututoale ebaõnnestus", "failed_remove_tag": "Sildi %(tagName)s eemaldamine jututoast ebaõnnestus", "failed_set_dm_tag": "Otsevestluse sildi seadmine ei õnnestunud", - "filters": { - "favourite": "Lemmikud", - "invites": "Kutsed", - "low_priority": "Vähetähtis", - "mentions": "Mainimised", - "people": "Inimesed", - "rooms": "Jututoad", - "unread": "Lugemata" - }, "home_menu_label": "Avalehe valikud", "join_public_room_label": "Liitu avaliku jututoaga", "joining_rooms_status": { @@ -2192,23 +2152,13 @@ }, "list_title": "Jututubade loend", "more_options": { - "copy_link": "Kopeeri jututoa link", - "favourited": "Määratud lemmikuks", - "leave_room": "Lahku jututoast", - "low_priority": "Vähetähtis", - "mark_read": "Märgi loetuks", - "mark_unread": "Märgi mitteloetuks" + "leave_room": "Lahku jututoast" }, "notification_options": "Teavituste eelistused", - "primary_filters": "Jututubade loendi filtrid", "redacting_messages_status": { "other": "Kustutame sõnumeid %(count)s jututoas", "one": "Kustutame sõnumeid %(count)s jututoas" }, - "room": { - "more_options": "Täiendavad seadistused", - "open_room": "Ava jututuba: %(roomName)s" - }, "show_less": "Näita vähem", "show_n_more": { "one": "Näita veel %(count)s vestlust", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index dfdebb2553..42a1ed4d85 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -12,7 +12,6 @@ "one": "Yksi lukematon maininta." }, "recent_rooms": "Viimeisimmät huoneet", - "room_n_unread_invite": "Avaa huoneen %(roomName)s kutsu.", "room_name": "Huone %(name)s", "room_status_bar": "Huoneen tilapalkki", "seek_bar_label": "Äänen siirtymispalkki", @@ -1414,8 +1413,7 @@ "mentions_and_keywords_description": "Vastaanota ilmoitukset maininnoista ja asiasanoista asetuksissa määrittämälläsi tavalla", "mentions_keywords": "Maininnat ja avainsanat", "message_didnt_send": "Viestiä ei lähetetty. Lisätietoa napsauttamalla.", - "mute_description": "Et saa ilmoituksia", - "mute_room": "Mykistä huone" + "mute_description": "Et saa ilmoituksia" }, "notifier": { "m.key.verification.request": "%(name)s pyytää varmennusta" @@ -1760,22 +1758,8 @@ "add_space_label": "Lisää avaruus", "breadcrumbs_empty": "Ei hiljattain vierailtuja huoneita", "breadcrumbs_label": "Hiljattain vieraillut huoneet", - "empty": { - "no_chats": "Ei keskusteluja vielä", - "no_chats_description": "Aloita lähettämällä viestejä jollekin henkilölle tai luomalla huone", - "no_chats_description_no_room_rights": "Aloita lähettämällä viesti jollekin", - "no_favourites": "Sinulla ei ole vielä suosikkikeskustelua", - "no_rooms": "Et ole vielä missään huoneessa", - "no_unread": "Onnittelut! Sinulla ei ole lukemattomia viestejä", - "show_chats": "Näytä kaikki keskustelut" - }, "failed_add_tag": "Tagin %(tagName)s lisääminen huoneeseen epäonnistui", "failed_remove_tag": "Tagin %(tagName)s poistaminen huoneesta epäonnistui", - "filters": { - "favourite": "Suosikit", - "people": "Ihmiset", - "rooms": "Huoneet" - }, "home_menu_label": "Etusivun valinnat", "join_public_room_label": "Liity julkiseen huoneeseen", "joining_rooms_status": { @@ -1784,19 +1768,13 @@ }, "list_title": "Huoneluettelo", "more_options": { - "copy_link": "Kopioi huoneen linkki", - "leave_room": "Poistu huoneesta", - "mark_read": "Merkitse luetuksi", - "mark_unread": "Merkitse lukemattomaksi" + "leave_room": "Poistu huoneesta" }, "notification_options": "Ilmoitusasetukset", "redacting_messages_status": { "one": "Poistetaan parhaillaan viestejä yhdessä huoneessa", "other": "Poistetaan parhaillaan viestejä %(count)s huoneesta" }, - "room": { - "open_room": "Avoin huone %(roomName)s" - }, "show_less": "Näytä vähemmän", "show_n_more": { "one": "Näytä %(count)s lisää", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 09efeb2e97..d354d07291 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -12,16 +12,6 @@ "one": "1 mention non lue." }, "recent_rooms": "Salons récents", - "room_messsage_not_sent": "Ouvrir le salon %(roomName)s avec un message non envoyé.", - "room_n_unread_invite": "Ouvrir l'invitation du salon %(roomName)s.", - "room_n_unread_messages": { - "one": "Ouvrir salon %(roomName)s avec 1 message non lu.", - "other": "Ouvrir salon %(roomName)s avec %(count)s messages non lus." - }, - "room_n_unread_messages_mentions": { - "one": "Ouvrir salon %(roomName)s avec 1 mention non lue.", - "other": "Ouvrir salon %(roomName)s avec %(count)s messages non lus comprenant des mentions." - }, "room_name": "Salon %(name)s", "room_status_bar": "Barre de statut du salon", "seek_bar_label": "Barre de recherche audio", @@ -1717,7 +1707,6 @@ "class_global": "Global", "class_other": "Autre", "default": "Par défaut", - "default_settings": "Correspondre aux paramètres par défaut", "email_pusher_app_display_name": "Notifications par courriel", "enable_prompt_toast_description": "Activer les notifications sur le bureau", "enable_prompt_toast_title": "Notifications", @@ -1736,8 +1725,7 @@ "mentions_and_keywords_description": "Recevoir des notifications uniquement pour les mentions et mot-clés comme défini dans vos paramètres", "mentions_keywords": "Mentions et mots-clés", "message_didnt_send": "Le message n’a pas été envoyé. Cliquer pour plus d’info.", - "mute_description": "Vous n’aurez aucune notification", - "mute_room": "Rendre le salon muet" + "mute_description": "Vous n’aurez aucune notification" }, "notifier": { "m.key.verification.request": "%(name)s demande une vérification" @@ -2151,37 +2139,9 @@ "add_space_label": "Ajouter un espace", "breadcrumbs_empty": "Aucun salon visité récemment", "breadcrumbs_label": "Salons visités récemment", - "collapse_filters": "Réduire la liste des filtres", - "empty": { - "no_chats": "Pas encore de discussions", - "no_chats_description": "Commencez par envoyer un message à quelqu'un ou en créant un salon", - "no_chats_description_no_room_rights": "Commencez par envoyer un message à quelqu'un", - "no_favourites": "Vous n'avez pas encore de discussion favorite", - "no_favourites_description": "Vous pouvez ajouter une discussion à vos favoris dans les paramètres de discussion", - "no_invites": "Vous n'avez aucune invitation non lue", - "no_lowpriority": "Vous n'avez aucun salon avec une priorité basse", - "no_mentions": "Vous n'avez aucune mention non lue", - "no_people": "Vous n'avez encore de discussions", - "no_people_description": "Veuillez désélectionner des filtres pour voir vos discussions", - "no_rooms": "Vous n’êtes membre d’aucun salon", - "no_rooms_description": "Veuillez désélectionner des filtres pour voir vos discussions", - "no_unread": "Félicitations ! Vous n'avez aucun message non lu", - "show_activity": "Voir toutes les activités", - "show_chats": "Afficher toutes les discussions" - }, - "expand_filters": "Développer la liste des filtres", "failed_add_tag": "Échec de l’ajout de l’étiquette %(tagName)s au salon", "failed_remove_tag": "Échec de la suppression de l’étiquette %(tagName)s du salon", "failed_set_dm_tag": "Échec de l’ajout de l’étiquette de conversation privée", - "filters": { - "favourite": "Favoris", - "invites": "Invitations", - "low_priority": "Priorité basse", - "mentions": "Mentions", - "people": "Personnes", - "rooms": "Salons", - "unread": "Non-lus" - }, "home_menu_label": "Options de l’accueil", "join_public_room_label": "Rejoindre le salon public", "joining_rooms_status": { @@ -2190,23 +2150,13 @@ }, "list_title": "Liste de salons", "more_options": { - "copy_link": "Copier le lien du salon", - "favourited": "Favorisé", - "leave_room": "Quitter le salon", - "low_priority": "Priorité basse", - "mark_read": "Marquer comme lu", - "mark_unread": "Marquer comme non lu" + "leave_room": "Quitter le salon" }, "notification_options": "Paramètres de notifications", - "primary_filters": "Filtre de la liste des salons", "redacting_messages_status": { "one": "Actuellement en train de supprimer les messages dans %(count)s salon", "other": "Actuellement en train de supprimer les messages dans %(count)s salons" }, - "room": { - "more_options": "Plus d’options", - "open_room": "Ouvrir salon %(roomName)s" - }, "show_less": "En voir moins", "show_n_more": { "other": "En afficher %(count)s de plus", diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json index d2a680a414..6702fbaf09 100644 --- a/src/i18n/strings/hr.json +++ b/src/i18n/strings/hr.json @@ -14,18 +14,6 @@ "other": "%(count)s nepročitanih poruka, uključujući spominjanja." }, "recent_rooms": "Nedavne sobe", - "room_messsage_not_sent": "Otvori sobu %(roomName)s s porukom koja nije poslana.", - "room_n_unread_invite": "Otvorite pozivnicu za sobu %(roomName)s.", - "room_n_unread_messages": { - "one": "Otvori sobu %(roomName)s s 1 nepročitanom porukom.", - "few": "Otvori sobu %(roomName)s s %(count)s nepročitane poruke.", - "other": "Otvori sobu %(roomName)s s %(count)s nepročitanih poruka." - }, - "room_n_unread_messages_mentions": { - "one": "Otvori sobu %(roomName)s s 1 nepročitanim spominjanjem.", - "few": "Otvori sobu %(roomName)s s %(count)s nepročitane poruke, uključujući spominjanja.", - "other": "Otvori sobu %(roomName)s s %(count)s nepročitanih poruka, uključujući spominjanja." - }, "room_name": "Soba %(name)s", "room_status_bar": "Traka statusa sobe", "seek_bar_label": "Traka za traženje zvuka", @@ -1733,7 +1721,6 @@ "class_global": "Globalno", "class_other": "Ostalo", "default": "Zadano", - "default_settings": "Uskladi zadane postavke", "email_pusher_app_display_name": "Obavijesti e-poštom", "enable_prompt_toast_description": "Omogući obavijesti na radnoj površini", "enable_prompt_toast_title": "Obavijesti", @@ -1752,8 +1739,7 @@ "mentions_and_keywords_description": "Primajte obavijesti samo o spominjanjima i ključnim riječima kako je postavljeno u vašim postavkama", "mentions_keywords": "Spominjanja i ključne riječi", "message_didnt_send": "Poruka nije poslana. Kliknite za informacije.", - "mute_description": "Nećete primati nikakve obavijesti", - "mute_room": "Utišaj sobu" + "mute_description": "Nećete primati nikakve obavijesti" }, "notifier": { "m.key.verification.request": "%(name)s traži potvrdu" @@ -2183,37 +2169,9 @@ "add_space_label": "Dodaj prostor", "breadcrumbs_empty": "Nema nedavno posjećenih soba", "breadcrumbs_label": "Nedavno posjećene sobe", - "collapse_filters": "Sažmi popis filtara", - "empty": { - "no_chats": "Još nema razgovora", - "no_chats_description": "Započnite tako da nekome pošaljete poruku ili izradite sobu", - "no_chats_description_no_room_rights": "Započnite tako da nekome pošaljete poruku", - "no_favourites": "Još nemate omiljenih razgovora", - "no_favourites_description": "Razgovor možete dodati u favorite u postavkama razgovora", - "no_invites": "Nemate nepročitanih pozivnica", - "no_lowpriority": "Nemate sobe niskog prioriteta", - "no_mentions": "Nemate nepročitanih spominjanja", - "no_people": "Još nemate izravne razgovore ni s kim", - "no_people_description": "Možete poništiti odabir filtara kako biste vidjeli ostale razgovore", - "no_rooms": "Niste još ni u jednoj sobi", - "no_rooms_description": "Možete poništiti odabir filtera kako biste vidjeli ostale razgovore", - "no_unread": "Čestitamo! Nemate nepročitanih poruka", - "show_activity": "Prikaži sve aktivnosti", - "show_chats": "Prikaži sve razgovore" - }, - "expand_filters": "Proširi popis filtara", "failed_add_tag": "Nije uspjelo dodavanje oznake %(tagName)s na sobu", "failed_remove_tag": "Nije uspjelo uklanjanje oznake %(tagName)s sa sobe", "failed_set_dm_tag": "Nije uspjelo postavljanje oznake za izravnu poruku", - "filters": { - "favourite": "Favoriti", - "invites": "Pozivnice", - "low_priority": "Niski prioritet", - "mentions": "Spominjanja", - "people": "Osobe", - "rooms": "Sobe", - "unread": "Nepročitano" - }, "home_menu_label": "Mogućnosti početne stranice", "join_public_room_label": "Pridruži se javnoj sobi", "joining_rooms_status": { @@ -2223,24 +2181,14 @@ }, "list_title": "Popis soba", "more_options": { - "copy_link": "Kopiraj poveznicu na sobu", - "favourited": "Označeno kao favorit", - "leave_room": "Napusti sobu", - "low_priority": "Niski prioritet", - "mark_read": "Označi kao pročitano", - "mark_unread": "Označi kao nepročitano" + "leave_room": "Napusti sobu" }, "notification_options": "Mogućnosti obavijesti", - "primary_filters": "Filtri popisa soba", "redacting_messages_status": { "one": "Trenutačno se uklanjaju poruke u %(count)s sobi", "few": "Trenutačno se uklanjaju poruke u %(count)s sobe", "other": "Trenutačno se uklanjaju poruke u %(count)s soba" }, - "room": { - "more_options": "Više mogućnosti", - "open_room": "Otvori sobu %(roomName)s" - }, "show_less": "Prikaži manje", "show_n_more": { "one": "Prikaži još %(count)s", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 33a2ac2003..2e34730273 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -12,16 +12,6 @@ "1 olvasatlan megemlítés.": "one" }, "recent_rooms": "Legutóbbi szobák", - "room_messsage_not_sent": "A(z) %(roomName)s szoba megnyitása nem beállított üzenettel.", - "room_n_unread_invite": "A(z) %(roomName)s szoba meghívásának megnyitása.", - "room_n_unread_messages": { - "A(z) %(roomName)s szoba megnyitása 1 olvasatlan üzenettel.": "one", - "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan üzenettel.": "other" - }, - "room_n_unread_messages_mentions": { - "A(z) %(roomName)s szoba megnyitása 1 olvasatlan megemlítéssel.": "one", - "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan megemlítéssel.": "other" - }, "room_name": "Szoba: %(name)s", "room_status_bar": "Szoba állapotsora", "seek_bar_label": "Hang keresősávja", @@ -1708,7 +1698,6 @@ "class_global": "Globális", "class_other": "Egyéb", "default": "Alapértelmezett", - "default_settings": "Megegyezik az alapértelmezett beállításokkal", "email_pusher_app_display_name": "E-mail értesítések", "enable_prompt_toast_description": "Asztali értesítések engedélyezése", "enable_prompt_toast_title": "Értesítések", @@ -1727,8 +1716,7 @@ "mentions_and_keywords_description": "Értesítések fogadása csak megemlítéseknél és kulcsszavaknál, a beállításokban megadottak szerint", "mentions_keywords": "Megemlítések és kulcsszavak", "message_didnt_send": "Az üzenet nincs elküldve. Kattintson az információkért.", - "mute_description": "Nem kap semmilyen értesítést", - "mute_room": "Szoba némítása" + "mute_description": "Nem kap semmilyen értesítést" }, "notifier": { "m.key.verification.request": "%(name)s ellenőrzést kér" @@ -2137,37 +2125,9 @@ "add_space_label": "Tér hozzáadása", "breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák", "breadcrumbs_label": "Nemrég meglátogatott szobák", - "collapse_filters": "Szűrőlista összecsukása", - "empty": { - "no_chats": "Még nincsenek csevegések", - "no_chats_description": "Kezdje azzal, hogy üzenetet küld valakinek, vagy létrehoz egy szobát", - "no_chats_description_no_room_rights": "Kezdje azzal, hogy üzenetet küld valakinek", - "no_favourites": "Még nincs kedvenc csevegése", - "no_favourites_description": "A csevegési beállításokban adhat hozzá csevegést a kedvencekhez", - "no_invites": "Nincs olvasatlan meghívója", - "no_lowpriority": "Nincs alacsony prioritású szobája", - "no_mentions": "Nincs olvasatlan említése", - "no_people": "Még nincs közvetlen csevegése senkivel", - "no_people_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez", - "no_rooms": "Még nincs egy szobában sem", - "no_rooms_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez", - "no_unread": "Gratulálunk! Nincsenek olvasatlan üzenetei.", - "show_activity": "Összes tevékenység megtekintése", - "show_chats": "Összes csevegés megjelenítése" - }, - "expand_filters": "Szűrőlista kibontása", "failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s", "failed_remove_tag": "Nem sikerült a szobáról eltávolítani ezt: %(tagName)s", "failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani", - "filters": { - "favourite": "Kedvencek", - "invites": "Meghívók", - "low_priority": "Alacsony prioritás", - "mentions": "Említések", - "people": "Emberek", - "rooms": "Szobák", - "unread": "Olvasatlan" - }, "home_menu_label": "Kezdőlap beállítások", "join_public_room_label": "Belépés nyilvános szobába", "joining_rooms_status": { @@ -2175,22 +2135,12 @@ }, "list_title": "Szobalista", "more_options": { - "copy_link": "Szoba hivatkozásának másolása", - "favourited": "Kedvencnek jelölve", - "leave_room": "Szoba elhagyása", - "low_priority": "Alacsony prioritás", - "mark_read": "Megjelölés olvasottként", - "mark_unread": "Megjelölés olvasatlanként" + "leave_room": "Szoba elhagyása" }, "notification_options": "Értesítési beállítások", - "primary_filters": "Szobalistaszűrők", "redacting_messages_status": { "Üzenet törlése %(count)s szobából": "other" }, - "room": { - "more_options": "További lehetőségek", - "open_room": "A(z) %(roomName)s szoba megnyitása" - }, "show_less": "Kevesebb megjelenítése", "show_n_more": { "Még %(count)s megjelenítése": "one" diff --git a/src/i18n/strings/hy.json b/src/i18n/strings/hy.json index 4478bad9da..d291ad6885 100644 --- a/src/i18n/strings/hy.json +++ b/src/i18n/strings/hy.json @@ -12,16 +12,6 @@ "other": "%(count)s չկարդացված հաղորդագրություններ, ներառյալ հիշատակումները։" }, "recent_rooms": "Վերջին սենյակները", - "room_messsage_not_sent": "Բացել %(roomName)s սենյակը՝ չուղարկված հաղորդագրությամբ", - "room_n_unread_invite": "Բացել %(roomName)s սենյակի հրավերը", - "room_n_unread_messages": { - "one": "Բացել %(roomName)s սենյակը՝ 1 չկարդացած հաղորդագրությամբ", - "other": "Բացել %(roomName)s սենյակը՝ %(count)s չկարդացած հաղորդագրություններով" - }, - "room_n_unread_messages_mentions": { - "one": "Բացել %(roomName)s սենյակը՝ 1 չկարդացած հիշեցմամբ", - "other": "Բացել %(roomName)s սենյակը՝ %(count)s չկարդացած հաղորդագրություններով, ներառյալ հիշեցումները" - }, "room_name": "Սենյակ %(name)s", "room_status_bar": "Սենյակի կարգավիճակի գոտի/վահանակ", "seek_bar_label": "Աուդիո որոնման գոտի", @@ -1659,7 +1649,6 @@ "class_global": "Գլոբալ", "class_other": "Այլ", "default": "Լռելյայն", - "default_settings": "Համապատասխանեցնել լռելյայն կարգավորումները", "email_pusher_app_display_name": "Ծանուցումներ էլ.փոստով", "enable_prompt_toast_description": "Միացնել աշխատասեղանի ծանուցումները", "enable_prompt_toast_title": "Ծանուցումներ", @@ -1678,8 +1667,7 @@ "mentions_and_keywords_description": "Ստանալ ծանուցում միայն հիշատակումներով և բանալի բառերով, ինչպես սահմանված են ձեր կարգաբերումներում", "mentions_keywords": "Նշումներ/հիշատակումներ և բանալի բառեր", "message_didnt_send": "Հաղորդագրությունը չի ուղարկվել: Սեղմեք տեղեկությունների համար:", - "mute_description": "Դուք չեք ստանա ոչ մի ծանուցում", - "mute_room": "\"Խլացնել\" սենյակի ձայնը" + "mute_description": "Դուք չեք ստանա ոչ մի ծանուցում" }, "notifier": { "m.key.verification.request": "%(name)s-ը պահանջում է ստուգում" @@ -2077,37 +2065,9 @@ "add_space_label": "Ավելացնել տարածք", "breadcrumbs_empty": "Վերջերս այցելած սենյակներ չկան", "breadcrumbs_label": "Վերջերս այցելած սենյակներ", - "collapse_filters": "Ծալել ֆիլտրերի ցանկը", - "empty": { - "no_chats": "Դեռևս զրույցներ չկան", - "no_chats_description": "Սկսեք՝ ուղարկելով հաղորդագրություն ինչ-որ մեկին կամ ստեղծելով սենյակ", - "no_chats_description_no_room_rights": "Սկսեք՝ հաղորդագրություն ուղարկելով ինչ-որ մեկին։", - "no_favourites": "Դուք դեռ չունեք սիրելի զրույց", - "no_favourites_description": "Զրույցը նախընտրածների մեջ ավելացնելու համար օգտագործեք զրույցի կարգավորումները։", - "no_invites": "Դուք չունեք չկարդացված հրավերներ", - "no_lowpriority": "Դուք ցածր առաջնահերթության սենյակներ չունեք", - "no_mentions": "Դուք չունեք չկարդացված հիշատակումներ", - "no_people": "Դուք դեռ ոչ մեկի հետ անհատական զրույց չունեք", - "no_people_description": "Դուք կարող եք անջատել ֆիլտրերը՝ ձեր մյուս զրույցները տեսնելու համար", - "no_rooms": "Դուք դեռ որևէ սենյակում չեք գտնվում", - "no_rooms_description": "Դուք կարող եք անջատել ֆիլտրերը՝ ձեր մյուս զրույցները տեսնելու համար", - "no_unread": "Շնորհավորանքներ։ Դուք չունեք չկարդացված հաղորդագրություններ։", - "show_activity": "Տեսնել ամբողջ ակտիվությունը", - "show_chats": "Ցուցադրել բոլոր զրույցները" - }, - "expand_filters": "Ընդարձակել ֆիլտրերի ցանկը", "failed_add_tag": "Չհաջողվեց %(tagName)s պիտակը(tag) ավելացնել սենյակին", "failed_remove_tag": "Չհաջողվեց հեռացնել %(tagName)s պիտակը(tag) սենյակից", "failed_set_dm_tag": "Չհաջողվեց սահմանել ուղիղ հաղորդագրության պիտակը(tag)", - "filters": { - "favourite": "Ընտրյալներ", - "invites": "Հրավերներ", - "low_priority": "Ցածր առաջնահերթություն", - "mentions": "Հիշատակումներ", - "people": "Մարդիկ", - "rooms": "Սենյակներ", - "unread": "Չկարդացվածներ" - }, "home_menu_label": "Գլխավոր էջի ընտրանքներ", "join_public_room_label": "Միանալ հանրային սենյակին", "joining_rooms_status": { @@ -2116,23 +2076,13 @@ }, "list_title": "Սենյակների ցանկ", "more_options": { - "copy_link": "Պատճենել սենյակի հղումը", - "favourited": "Ավելացված է ընտրյալների մեջ", - "leave_room": "Լքել սենյակը", - "low_priority": "Ցածր առաջնահերթություն", - "mark_read": "Նշել որպես կարդացված", - "mark_unread": "Նշել որպես չկարդացված" + "leave_room": "Լքել սենյակը" }, "notification_options": "Ծանուցման ընտրանքներ", - "primary_filters": "Սենյակների ցանկի ֆիլտրեր", "redacting_messages_status": { "one": "Ներկայումս ջնջվում են %(count)s սենյակում", "other": "Ներկայումս ջնջվում են %(count)s սենյակներում" }, - "room": { - "more_options": "Լրացուցիչ ընտրանքներ", - "open_room": "Բացել %(roomName)s սենյակը" - }, "show_less": "Ցուցադրել ավելի քիչ", "show_n_more": { "one": "Ցուցադրել ևս %(count)s", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 3666a705ba..04f49a6ee5 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -12,14 +12,6 @@ "other": "%(count)s pesan yang belum dibaca termasuk sebutan." }, "recent_rooms": "Ruangan terkini", - "room_messsage_not_sent": "Buka ruangan %(roomName)s dengan pesan yang belum diatur.", - "room_n_unread_invite": "Buka undangan ruangan %(roomName)s.", - "room_n_unread_messages": { - "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca." - }, - "room_n_unread_messages_mentions": { - "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca termasuk sebutan." - }, "room_name": "Ruangan %(name)s", "room_status_bar": "Bilah status ruangan", "seek_bar_label": "Bilah pencarian audio", @@ -1713,7 +1705,6 @@ "class_global": "Global", "class_other": "Lainnya", "default": "Bawaan", - "default_settings": "Cocokkan pengaturan bawaan", "email_pusher_app_display_name": "Notifikasi Surel", "enable_prompt_toast_description": "Aktifkan notifikasi desktop", "enable_prompt_toast_title": "Notifikasi", @@ -1732,8 +1723,7 @@ "mentions_and_keywords_description": "Dapatkan notifikasi hanya dengan sebutan dan kata kunci yang diatur di pengaturan Anda", "mentions_keywords": "Sebutan dan kata kunci", "message_didnt_send": "Pesan tidak terkirim. Klik untuk informasi.", - "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun", - "mute_room": "Bisukan ruangan" + "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun" }, "notifier": { "m.key.verification.request": "%(name)s meminta verifikasi" @@ -2140,37 +2130,9 @@ "add_space_label": "Tambahkan space", "breadcrumbs_empty": "Tidak ada ruangan yang baru saja dilihat", "breadcrumbs_label": "Ruangan yang baru saja dilihat", - "collapse_filters": "Tutup daftar filter", - "empty": { - "no_chats": "Belum ada obrolan", - "no_chats_description": "Mulailah dengan mengirim pesan kepada seseorang atau dengan membuat ruangan", - "no_chats_description_no_room_rights": "Mulailah dengan mengirim pesan kepada seseorang", - "no_favourites": "Anda belum memiliki obrolan favorit", - "no_favourites_description": "Anda dapat menambahkan obrolan ke favorit Anda di pengaturan obrolan", - "no_invites": "Anda tidak memiliki undangan yang belum dibaca", - "no_lowpriority": "Anda tidak memiliki ruangan dengan prioritas rendah", - "no_mentions": "Anda tidak memiliki sebutan yang belum dibaca", - "no_people": "Anda belum memiliki obrolan langsung dengan siapa pun", - "no_people_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain", - "no_rooms": "Anda belum berada di ruangan mana pun", - "no_rooms_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain", - "no_unread": "Selamat! Anda tidak memiliki pesan yang belum dibaca", - "show_activity": "Lihat semua aktivitas", - "show_chats": "Tampilkan semua obrolan" - }, - "expand_filters": "Buka daftar filter", "failed_add_tag": "Gagal menambahkan tag %(tagName)s ke ruangan", "failed_remove_tag": "Gagal menghapus tanda %(tagName)s dari ruangan", "failed_set_dm_tag": "Gagal menetapkan tanda pesan langsung", - "filters": { - "favourite": "Favorit", - "invites": "Undangan", - "low_priority": "Prioritas rendah", - "mentions": "Sebutan", - "people": "Orang", - "rooms": "Ruangan", - "unread": "Belum dibaca" - }, "home_menu_label": "Opsi Beranda", "join_public_room_label": "Bergabung dengan ruangan publik", "joining_rooms_status": { @@ -2179,23 +2141,13 @@ }, "list_title": "Daftar ruangan", "more_options": { - "copy_link": "Salin tautan ruangan", - "favourited": "Difavorit", - "leave_room": "Tinggalkan ruangan", - "low_priority": "Prioritas rendah", - "mark_read": "Tandai sebagai dibaca", - "mark_unread": "Tandai sebagai belum dibaca" + "leave_room": "Tinggalkan ruangan" }, "notification_options": "Opsi notifikasi", - "primary_filters": "Filter daftar ruangan", "redacting_messages_status": { "one": "Saat ini menghapus pesan-pesan di %(count)s ruangan", "other": "Saat ini menghapus pesan-pesan di %(count)s ruangan" }, - "room": { - "more_options": "Opsi Lainnya", - "open_room": "Buka ruangan %(roomName)s" - }, "show_less": "Tampilkan lebih sedikit", "show_n_more": { "one": "Tampilkan %(count)s lagi", diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 420548c3b4..64bfe96d6b 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -10,14 +10,6 @@ "other": "읽지 않은 메시지 %(count)s개 (멘션 포함)." }, "recent_rooms": "최근 방", - "room_messsage_not_sent": "%(roomName)s에 미발송 메시지가 있는 방을 엽니다.", - "room_n_unread_invite": "공개 방 %(roomName)s에 초대 되었습니다.", - "room_n_unread_messages": { - "other": "읽지 않은 메시지 %(count)s개가 있는 채팅방 %(roomName)s 열기" - }, - "room_n_unread_messages_mentions": { - "other": "멘션을 포함해 읽지 않은 메시지 %(count)s개가 있는 채팅방 %(roomName)s 열기" - }, "room_name": "%(name)s 방", "room_status_bar": "방 상태 표시줄", "seek_bar_label": "오디오 탐색 바", @@ -1700,7 +1692,6 @@ "class_global": "글로벌", "class_other": "기타", "default": "기본", - "default_settings": "기본 설정과 일치", "email_pusher_app_display_name": "이메일 알림", "enable_prompt_toast_description": "데스크톱 알림 활성화", "enable_prompt_toast_title": "알림", @@ -1719,8 +1710,7 @@ "mentions_and_keywords_description": "설정에서 지정한 멘션과 키워드인 경우에만 알림을 받습니다", "mentions_keywords": "멘션 및 키워드", "message_didnt_send": "메시지가 전송되지 않았습니다. 자세한 내용은 클릭하세요.", - "mute_description": "어떤 알람도 받지 않습니다", - "mute_room": "채팅방 음소거" + "mute_description": "어떤 알람도 받지 않습니다" }, "notifier": { "m.key.verification.request": "%(name)s님이 인증을 요청하고 있습니다" @@ -2113,37 +2103,9 @@ "add_space_label": "스페이스 추가하기", "breadcrumbs_empty": "최근에 방문하지 않은 방 목록", "breadcrumbs_label": "최근 방문한 방 목록", - "collapse_filters": "필터 목록 접기", - "empty": { - "no_chats": "아직 채팅이 없습니다.", - "no_chats_description": "누군가에게 메시지를 보내거나 채팅방을 생성하여 시작하세요", - "no_chats_description_no_room_rights": "누군가에게 메시지를 보내서 시작하세요", - "no_favourites": "아직 즐겨찾는 채팅이 없습니다.", - "no_favourites_description": "채팅 설정에서 채팅을 즐겨찾기에 추가할 수 있습니다", - "no_invites": "읽지 않은 초대장이 없습니다", - "no_lowpriority": "우선순위가 낮은 채팅방이 없습니다.", - "no_mentions": "읽지 않은 멘션이 없습니다.", - "no_people": "아직 누구와도 직접 채팅을 하지 않았습니다", - "no_people_description": "다른 채팅을 보려면 필터 선택을 해제하세요.", - "no_rooms": "아직 어떤 채팅방에도 있지 않습니다", - "no_rooms_description": "다른 채팅을 보려면 필터 선택을 해제하세요.", - "no_unread": "축하합니다! 읽지 않은 메시지가 없습니다.", - "show_activity": "모든 활동 보기", - "show_chats": "모든 채팅 보기" - }, - "expand_filters": "필터 목록 확장", "failed_add_tag": "방에 %(tagName)s 태그 추가에 실패함", "failed_remove_tag": "방에 %(tagName)s 태그 제거에 실패함", "failed_set_dm_tag": "다이렉트 메시지 태그 설정에 실패했습니다", - "filters": { - "favourite": "즐겨찾기", - "invites": "초대", - "low_priority": "낮은 우선순위", - "mentions": "멘션", - "people": "사람", - "rooms": "채팅방", - "unread": "읽지 않은 항목" - }, "home_menu_label": "홈 옵션", "join_public_room_label": "공개 방 참가하기", "joining_rooms_status": { @@ -2151,22 +2113,12 @@ }, "list_title": "채팅방 목록", "more_options": { - "copy_link": "채팅방 링크 복사", - "favourited": "즐겨찾기 됨", - "leave_room": "채팅방 떠나기", - "low_priority": "낮은 우선순위", - "mark_read": "읽음으로 표시", - "mark_unread": "읽지 않음으로 표시" + "leave_room": "채팅방 떠나기" }, "notification_options": "알림 옵션", - "primary_filters": "채팅방 목록 필터", "redacting_messages_status": { "other": "현재 %(count)s 방에서 메시지를 삭제 중입니다" }, - "room": { - "more_options": "옵션 더보기", - "open_room": "채팅방 %(roomName)s 열기" - }, "show_less": "간단히 표시", "show_n_more": { "other": "%(count)s개 더 보기" diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 2269299115..3a2993ea87 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -12,16 +12,6 @@ "other": "%(count)s uleste meldinger inkludert der du nevnes." }, "recent_rooms": "Nylige rom", - "room_messsage_not_sent": "Åpent rom %(roomName)s med en usendt melding.", - "room_n_unread_invite": "Invitasjon til det åpne rommet %(roomName)s.", - "room_n_unread_messages": { - "one": "Åpne room %(roomName)s med 1 ulest message.", - "other": "Åpne room %(roomName)s med %(count)s uleste messages." - }, - "room_n_unread_messages_mentions": { - "one": "Åpne room %(roomName)s med 1 ulest omtale.", - "other": "Åpne room %(roomName)s med %(count)s uleste meldinger inkludert omtaler." - }, "room_name": "Rom %(name)s", "room_status_bar": "Statuslinje for rommet", "seek_bar_label": "Søkelinje for lyd", @@ -1711,7 +1701,6 @@ "class_global": "Globalt", "class_other": "Andre", "default": "Standard", - "default_settings": "Match standardinnstillingene", "email_pusher_app_display_name": "E-postvarsler", "enable_prompt_toast_description": "Aktiver skrivebordsvarsler", "enable_prompt_toast_title": "Varsler", @@ -1730,8 +1719,7 @@ "mentions_and_keywords_description": "Bli varslet bare med omtaler og nøkkelord som konfigurert i innstillingene dine ", "mentions_keywords": "Omtaler og nøkkelord", "message_didnt_send": "Meldingen ble ikke sendt. Klikk for informasjon.", - "mute_description": "Du vil ikke få noen varsler", - "mute_room": "Demp rommet" + "mute_description": "Du vil ikke få noen varsler" }, "notifier": { "m.key.verification.request": "%(name)s ber om verifisering" @@ -2145,37 +2133,9 @@ "add_space_label": "Legg til område", "breadcrumbs_empty": "Ingen nylig besøkte rom", "breadcrumbs_label": "Nylig besøkte rom", - "collapse_filters": "Skjul filterlisten", - "empty": { - "no_chats": "Ingen chatter ennå", - "no_chats_description": "Kom i gang ved å sende meldinger til noen eller ved å opprette et rom", - "no_chats_description_no_room_rights": "Kom i gang med å sende meldinger til noen", - "no_favourites": "Du har ikke favorittchat ennå", - "no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene", - "no_invites": "Du har ingen uleste invitasjoner", - "no_lowpriority": "Du har ingen rom med lav prioritet", - "no_mentions": "Du har ingen uleste omtaler", - "no_people": "Du har ikke direkte chatter med noen ennå", - "no_people_description": "Du kan fjerne merket for filtre for å se de andre chattene dine", - "no_rooms": "Du er ikke med i noen rom ennå", - "no_rooms_description": "Du kan fjerne merket for filtre for å se de andre chattene dine", - "no_unread": "Gratulerer! Du har ingen uleste meldinger", - "show_activity": "Se alle aktiviteter", - "show_chats": "Vis alle chatter" - }, - "expand_filters": "Utvid filterlisten", "failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom", "failed_remove_tag": "Kunne ikke fjerne tagg %(tagName)s fra rommet", "failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen", - "filters": { - "favourite": "Favoritter", - "invites": "Invitasjoner", - "low_priority": "Lav prioritet", - "mentions": "Omtaler", - "people": "Personer", - "rooms": "Rom", - "unread": "Uleste" - }, "home_menu_label": "Hjem alternativer", "join_public_room_label": "Bli med i offentlig rom", "joining_rooms_status": { @@ -2184,23 +2144,13 @@ }, "list_title": "Romliste", "more_options": { - "copy_link": "Kopier romlenke", - "favourited": "Favorittmerket", - "leave_room": "Forlat rommet", - "low_priority": "Lav prioritet", - "mark_read": "Marker som lest", - "mark_unread": "Marker som ulest" + "leave_room": "Forlat rommet" }, "notification_options": "Varselsinnstillinger", - "primary_filters": "Filtre for romliste", "redacting_messages_status": { "one": "Fjerner for øyeblikket meldinger i %(count)s rom", "other": "Fjerner for øyeblikket meldinger i %(count)s rom" }, - "room": { - "more_options": "Flere alternativer", - "open_room": "Åpne rom %(roomName)s" - }, "show_less": "Vis mindre", "show_n_more": { "Vis %(count)s til": "Vis %(count)s mer" diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 4480de3c1e..95651e177e 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -12,18 +12,6 @@ "one": "1 nieprzeczytana wzmianka." }, "recent_rooms": "Ostatnie pokoje", - "room_messsage_not_sent": "Otwórz pokój %(roomName)s z niewysłaną wiadomością.room", - "room_n_unread_invite": "Otwórz zaproszenie pokoju %(roomName)s.", - "room_n_unread_messages": { - "one": "Otwórz pokój %(roomName)s z 1 nieprzeczytaną wiadomością.", - "few": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wiadomościami.", - "many": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wiadomościami." - }, - "room_n_unread_messages_mentions": { - "one": "Otwórz pokój %(roomName)s z 1 nieprzeczytaną wzmianką.", - "few": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wzmiankami.", - "many": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wzmiankami." - }, "room_name": "Pokój %(name)s", "room_status_bar": "Pasek stanu pokoju", "seek_bar_label": "Pasek wyszukiwania audio", @@ -1695,7 +1683,6 @@ "class_global": "Globalne", "class_other": "Inne", "default": "Domyślne", - "default_settings": "Ustawienia domyślne", "email_pusher_app_display_name": "Powiadomienia e-mail", "enable_prompt_toast_description": "Włącz powiadomienia na pulpicie", "enable_prompt_toast_title": "Powiadomienia", @@ -1714,8 +1701,7 @@ "mentions_and_keywords_description": "Otrzymuj powiadomienia tylko z wzmiankami i słowami kluczowymi zgodnie z Twoimi ustawieniami", "mentions_keywords": "Wzmianki i słowa kluczowe", "message_didnt_send": "Nie wysłano wiadomości. Kliknij po więcej informacji.", - "mute_description": "Nie otrzymasz żadnych powiadomień", - "mute_room": "Wycisz pokój" + "mute_description": "Nie otrzymasz żadnych powiadomień" }, "notifier": { "m.key.verification.request": "%(name)s prosi o weryfikację" @@ -2119,37 +2105,9 @@ "add_space_label": "Dodaj przestrzeń", "breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów", "breadcrumbs_label": "Ostatnio odwiedzane pokoje", - "collapse_filters": "Zwiń listę filtrów", - "empty": { - "no_chats": "Nie ma jeszcze czatów", - "no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju", - "no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.", - "no_favourites": "Nie masz jeszcze ulubionego czatu", - "no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu", - "no_invites": "Nie masz żadnych nieprzeczytanych zaproszeń", - "no_lowpriority": "Nie masz pokoi o niskim priorytecie", - "no_mentions": "Nie masz żadnych nieprzeczytanych wzmianek", - "no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych", - "no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty", - "no_rooms": "Nie jesteś jeszcze w żadnym pokoju", - "no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty", - "no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości", - "show_activity": "Wyświetl całą aktywność", - "show_chats": "Pokaż wszystkie czaty" - }, - "expand_filters": "Rozwiń listę filtrów", "failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju", "failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju", "failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych", - "filters": { - "favourite": "Ulubione", - "invites": "Zaproszenia", - "low_priority": "Niski priorytet", - "mentions": "Wzmianki", - "people": "Osoby", - "rooms": "Pokoje", - "unread": "Nieprzeczytane" - }, "home_menu_label": "Opcje głównej", "join_public_room_label": "Dołącz do publicznego pokoju", "joining_rooms_status": { @@ -2158,23 +2116,13 @@ }, "list_title": "Lista pokojów", "more_options": { - "copy_link": "Kopiuj link do pokoju", - "favourited": "Ulubione", - "leave_room": "Opuść pokój", - "low_priority": "Niski priorytet", - "mark_read": "Oznacz jako przeczytane", - "mark_unread": "Oznacz jako nieprzeczytane" + "leave_room": "Opuść pokój" }, "notification_options": "Opcje powiadomień", - "primary_filters": "Filtry listy pomieszczeń", "redacting_messages_status": { "one": "Aktualnie usuwanie wiadomości z %(count)s pokoju", "other": "Aktualnie usuwanie wiadomości z %(count)s pokoi" }, - "room": { - "more_options": "Więcej opcji", - "open_room": "Otwórz pokój %(roomName)s" - }, "show_less": "Pokaż mniej", "show_n_more": { "one": "Pokaż %(count)s więcej", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index fa4bad8825..d34c3c2d43 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -2004,28 +2004,9 @@ "add_space_label": "Adiciona espaço", "breadcrumbs_empty": "Nenhuma sala visitada recentemente", "breadcrumbs_label": "Salas visitadas recentemente", - "empty": { - "no_chats": "Ainda sem conversas", - "no_chats_description": "Começa a enviar mensagens a alguém ou a crie uma sala", - "no_chats_description_no_room_rights": "Começa por enviar uma mensagem a alguém", - "no_favourites": "Ainda não tem um conversa favorita", - "no_favourites_description": "Pode adicionar uma conversa aos seus favoritos nas definições de conversa", - "no_people": "Ainda não tem conversas diretas com ninguém", - "no_people_description": "Pode desseleccionar filtros para veres as suas outras conversas", - "no_rooms": "Você ainda não está em nenhuma sala", - "no_rooms_description": "Pode desmarcar filtros para ver as suas outras conversas", - "no_unread": "Parabéns! Não tens nenhuma mensagem por ler", - "show_chats": "Mostra todas as conversas" - }, "failed_add_tag": "Falha ao adicionar %(tagName)s à sala", "failed_remove_tag": "Não foi possível remover a marcação %(tagName)s desta sala", "failed_set_dm_tag": "Falha ao definir a etiqueta de mensagem direta", - "filters": { - "favourite": "Favoritos", - "people": "Pessoas", - "rooms": "Salas", - "unread": "Não lido" - }, "home_menu_label": "Opções de casa", "join_public_room_label": "Participa na sala pública", "joining_rooms_status": { @@ -2034,23 +2015,13 @@ }, "list_title": "Lista de salas", "more_options": { - "copy_link": "Copiar link da sala", - "favourited": "Adicionado aos favoritos", - "leave_room": "Sair da sala", - "low_priority": "Baixa prioridade", - "mark_read": "Marcar como lido", - "mark_unread": "Marcar como não lido" + "leave_room": "Sair da sala" }, "notification_options": "Opções de notificação", - "primary_filters": "Filtros da lista de salas", "redacting_messages_status": { "one": "Atualmente removendo mensagens na %(count)s sala", "other": "Atualmente removendo mensagens em %(count)s salas" }, - "room": { - "more_options": "Mais opções", - "open_room": "Abrir a sala %(roomName)s" - }, "show_less": "Mostrar menos", "show_n_more": { "one": "Mostrar %(count)s mais", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 1656263d6e..e10ea69e53 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -12,16 +12,6 @@ "one": "1 menção não lida." }, "recent_rooms": "Salas recentes", - "room_messsage_not_sent": "Abra a sala %(roomName)s com uma mensagem não enviada.", - "room_n_unread_invite": "Abra o convite da sala %(roomName)s.", - "room_n_unread_messages": { - "one": "Sala aberta %(roomName)s com 1 mensagem não lida.", - "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas." - }, - "room_n_unread_messages_mentions": { - "one": "Sala aberta %(roomName)s com 1 menção não lida.", - "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas, incluindo menções." - }, "room_name": "Sala %(name)s", "room_status_bar": "Barra de status da sala", "seek_bar_label": "Barra de busca de áudio", @@ -1708,7 +1698,6 @@ "class_global": "Global", "class_other": "Outros", "default": "Padrão", - "default_settings": "Corresponder às configurações padrão", "email_pusher_app_display_name": "Notificações por e-mail", "enable_prompt_toast_description": "Ativar notificações na área de trabalho", "enable_prompt_toast_title": "Notificações", @@ -1727,8 +1716,7 @@ "mentions_and_keywords_description": "Receba notificações apenas com menções e palavras-chave conforme definido em suas configurações", "mentions_keywords": "Menções e palavras-chave!", "message_didnt_send": "A mensagem não foi enviada. Clique para mais informações.", - "mute_description": "Você não receberá nenhuma notificação", - "mute_room": "Silenciar sala" + "mute_description": "Você não receberá nenhuma notificação" }, "notifier": { "m.key.verification.request": "%(name)s está solicitando confirmação" @@ -2129,37 +2117,9 @@ "add_space_label": "Adicionar espaço", "breadcrumbs_empty": "Nenhuma sala foi visitada recentemente", "breadcrumbs_label": "Salas visitadas recentemente", - "collapse_filters": "Recolher lista de filtros", - "empty": { - "no_chats": "Ainda não há conversas.", - "no_chats_description": "Comece enviando uma mensagem para alguém ou criando uma sala", - "no_chats_description_no_room_rights": "Comece enviando uma mensagem para alguém", - "no_favourites": "Você ainda não tem o bate-papo favorito", - "no_favourites_description": "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo", - "no_invites": "Você não tem nenhum convite não lido", - "no_lowpriority": "Você não tem nenhuma sala de baixa prioridade", - "no_mentions": "Você não tem nenhuma menção não lida", - "no_people": "Você ainda não tem conversas diretas com ninguém", - "no_people_description": "Você pode desmarcar os filtros para ver suas outras conversas", - "no_rooms": "Você não está em nenhuma sala ainda", - "no_rooms_description": "Você pode desmarcar os filtros para ver suas outras conversas.", - "no_unread": "Parabéns! Você não tem nenhuma mensagem não lida", - "show_activity": "Ver todas as atividades", - "show_chats": "Mostrar todas as conversas" - }, - "expand_filters": "Expandir lista de filtros", "failed_add_tag": "Falha ao adicionar a tag %(tagName)s para a sala", "failed_remove_tag": "Falha ao remover a tag %(tagName)s da sala", "failed_set_dm_tag": "Falha ao definir a marca de mensagem direta", - "filters": { - "favourite": "Favoritos", - "invites": "Convites", - "low_priority": "Baixa prioridade", - "mentions": "Menções", - "people": "Pessoas", - "rooms": "Salas", - "unread": "Não lido" - }, "home_menu_label": "Opções do Início", "join_public_room_label": "Entrar na sala pública", "joining_rooms_status": { @@ -2168,23 +2128,13 @@ }, "list_title": "Lista de salas", "more_options": { - "copy_link": "Copiar link da sala", - "favourited": "Favoritado", - "leave_room": "Sair da sala", - "low_priority": "Baixa prioridade", - "mark_read": "Marcar como lido", - "mark_unread": "Marcar como não lido" + "leave_room": "Sair da sala" }, "notification_options": "Alterar notificações", - "primary_filters": "Filtros da lista de salas", "redacting_messages_status": { "one": "Atualmente removendo mensagens em %(count)s sala", "other": "Atualmente removendo mensagens em %(count)s salas" }, - "room": { - "more_options": "Mais opções", - "open_room": "Abrir sala %(roomName)s" - }, "show_less": "Mostrar menos", "show_n_more": { "other": "Mostrar %(count)s a mais", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 20c66a6333..e042f8f940 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -12,18 +12,6 @@ "one": "1 непрочитанное упоминание." }, "recent_rooms": "Недавние комнаты", - "room_messsage_not_sent": "Открыть комнату %(roomName)s с неотправленным сообщением.", - "room_n_unread_invite": "Открыть приглашение в комнату %(roomName)s.", - "room_n_unread_messages": { - "one": "Открыть комнату %(roomName)s с 1 непрочитанным сообщением.", - "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями.", - "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями." - }, - "room_n_unread_messages_mentions": { - "one": "Открыть комнату %(roomName)s с 1 непрочитанным упоминанием.", - "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями.", - "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями." - }, "room_name": "Комната %(name)s", "room_status_bar": "Строка состояния комнаты", "seek_bar_label": "Панель поиска аудио", @@ -1716,7 +1704,6 @@ "class_global": "Глобально", "class_other": "Другие", "default": "По умолчанию", - "default_settings": "Соответствует настройкам по умолчанию", "email_pusher_app_display_name": "Уведомления по электронной почте", "enable_prompt_toast_description": "Включить уведомления на рабочем столе", "enable_prompt_toast_title": "Уведомления", @@ -1735,8 +1722,7 @@ "mentions_and_keywords_description": "Получать уведомления только по упоминаниям и ключевым словам, установленным в ваших настройках", "mentions_keywords": "Упоминания и ключевые слова", "message_didnt_send": "Сообщение не отправлено. Нажмите для получения информации.", - "mute_description": "Вы не будете получать никаких уведомлений", - "mute_room": "Заглушить комнату" + "mute_description": "Вы не будете получать никаких уведомлений" }, "notifier": { "m.key.verification.request": "%(name)s запрашивает проверку" @@ -2147,37 +2133,9 @@ "add_space_label": "Добавить пространство", "breadcrumbs_empty": "Нет недавно посещенных комнат", "breadcrumbs_label": "Недавно посещённые комнаты", - "collapse_filters": "Свернуть список фильтров", - "empty": { - "no_chats": "Пока нет доступных чатов", - "no_chats_description": "Начните с отправки сообщений или создания комнаты", - "no_chats_description_no_room_rights": "Начните переписку с отправки сообщения", - "no_favourites": "У вас пока нет чатов в Избранное", - "no_favourites_description": "Вы можете добавить в Избранное в настройках чата", - "no_invites": "У вас нет непрочитанных приглашений", - "no_lowpriority": "У вас нет комнат с низким приоритетом", - "no_mentions": "У вас нет непрочитанных упоминаний", - "no_people": "У вас пока нет личных чатов", - "no_people_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты", - "no_rooms": "Вы еще не находитесь ни в одной комнате", - "no_rooms_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты", - "no_unread": "Поздравляю! У вас нет непрочитанных сообщений", - "show_activity": "Посмотреть всю активность", - "show_chats": "Показать все чаты" - }, - "expand_filters": "Развернуть список фильтров", "failed_add_tag": "Не удалось добавить тег %(tagName)s в комнату", "failed_remove_tag": "Не удалось удалить тег %(tagName)s из комнаты", "failed_set_dm_tag": "Не удалось установить метку личного сообщения", - "filters": { - "favourite": "Избранное", - "invites": "Приглашения", - "low_priority": "Низкий приоритет", - "mentions": "Упоминания", - "people": "Люди", - "rooms": "Комнаты", - "unread": "Непрочитанные" - }, "home_menu_label": "Параметры раздела \"Главная\"", "join_public_room_label": "Присоединиться к публичной комнате", "joining_rooms_status": { @@ -2186,23 +2144,13 @@ }, "list_title": "Список комнат", "more_options": { - "copy_link": "Скопировать ссылку на комнату", - "favourited": "Избранное", - "leave_room": "Покинуть комнату", - "low_priority": "Низкий приоритет", - "mark_read": "Отметить как прочитанное", - "mark_unread": "Отметить как непрочитанное" + "leave_room": "Покинуть комнату" }, "notification_options": "Настройки уведомлений", - "primary_filters": "Фильтры комнат", "redacting_messages_status": { "one": "Удаляются сообщения в %(count)s комнате", "other": "Удаляются сообщения в %(count)s комнатах" }, - "room": { - "more_options": "Дополнительные параметры", - "open_room": "Открыть комнату %(roomName)s" - }, "show_less": "Показать меньше", "show_n_more": { "other": "Показать ещё %(count)s", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 9717f39a28..ccf06da08e 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -14,18 +14,6 @@ "other": "%(count)s neprečítaných správ vrátane zmienok." }, "recent_rooms": "Nedávne miestnosti", - "room_messsage_not_sent": "Otvoriť miestnosť %(roomName)s s neodoslanou správou.", - "room_n_unread_invite": "Otvoriť pozvánku miestnosti %(roomName)s.", - "room_n_unread_messages": { - "one": "Otvoriť miestnosť %(roomName)s s 1 neprečítanou správou.", - "few": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami.", - "other": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami." - }, - "room_n_unread_messages_mentions": { - "one": "Otvoriť miestnosť %(roomName)s s 1 neprečítanou zmienkou.", - "few": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami vrátane zmienok.", - "other": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami vrátane zmienok." - }, "room_name": "Miestnosť %(name)s", "room_status_bar": "Stavový riadok miestnosti", "seek_bar_label": "Panel vyhľadávania zvuku", @@ -1734,7 +1722,6 @@ "class_global": "Celosystémové", "class_other": "Ďalšie", "default": "Predvolené", - "default_settings": "Zhoda s predvolenými nastaveniami", "email_pusher_app_display_name": "Emailové oznámenia", "enable_prompt_toast_description": "Povoliť oznámenia na ploche", "enable_prompt_toast_title": "Oznámenia", @@ -1753,8 +1740,7 @@ "mentions_and_keywords_description": "Dostávajte upozornenia len na zmienky a kľúčové slová nastavené vo vašich nastaveniach", "mentions_keywords": "Zmienky a kľúčové slová", "message_didnt_send": "Správa sa neodoslala. Kliknite pre informácie.", - "mute_description": "Nebudete dostávať žiadne oznámenia", - "mute_room": "Stlmiť miestnosť" + "mute_description": "Nebudete dostávať žiadne oznámenia" }, "notifier": { "m.key.verification.request": "%(name)s žiada o overenie" @@ -2184,37 +2170,9 @@ "add_space_label": "Pridať priestor", "breadcrumbs_empty": "Žiadne nedávno navštívené miestnosti", "breadcrumbs_label": "Nedávno navštívené miestnosti", - "collapse_filters": "Zbaliť zoznam filtrov", - "empty": { - "no_chats": "Zatiaľ žiadne konverzácie", - "no_chats_description": "Začnite tým, že niekomu napíšete správu alebo vytvoríte miestnosť", - "no_chats_description_no_room_rights": "Začnite tým, že niekomu napíšete správu", - "no_favourites": "Zatiaľ nemáte obľúbenú konverzáciu", - "no_favourites_description": "V nastaveniach konverzácií môžete pridať konverzáciu medzi obľúbené", - "no_invites": "Nemáte žiadne neprečítané pozvánky", - "no_lowpriority": "Nemáte žiadne miestnosti s nízkou prioritou", - "no_mentions": "Nemáte žiadne neprečítané zmienky", - "no_people": "Zatiaľ s nikým nemáte priame konverzácie", - "no_people_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie", - "no_rooms": "Zatiaľ ešte nie ste v žiadnej miestnosti", - "no_rooms_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie", - "no_unread": "Gratulujeme! Nemáte žiadne neprečítané správy", - "show_activity": "Zobraziť všetku aktivitu", - "show_chats": "Zobraziť všetky konverzácie" - }, - "expand_filters": "Rozbaliť zoznam filtrov", "failed_add_tag": "Miestnosti sa nepodarilo pridať značku %(tagName)s", "failed_remove_tag": "Z miestnosti sa nepodarilo odstrániť značku %(tagName)s", "failed_set_dm_tag": "Nepodarilo sa nastaviť značku priamej správy", - "filters": { - "favourite": "Obľúbené", - "invites": "Pozvánky", - "low_priority": "Nízka priorita", - "mentions": "Zmienky", - "people": "Ľudia", - "rooms": "Miestnosti", - "unread": "Neprečítané" - }, "home_menu_label": "Možnosti domovskej obrazovky", "join_public_room_label": "Pripojiť sa k verejnej miestnosti", "joining_rooms_status": { @@ -2224,24 +2182,14 @@ }, "list_title": "Zoznam miestností", "more_options": { - "copy_link": "Kopírovať odkaz na miestnosť", - "favourited": "Obľúbené", - "leave_room": "Opustiť miestnosť", - "low_priority": "Nízka priorita", - "mark_read": "Označiť ako prečítané", - "mark_unread": "Označiť ako neprečítané" + "leave_room": "Opustiť miestnosť" }, "notification_options": "Možnosti oznámenia", - "primary_filters": "Filtre zoznamu miestností", "redacting_messages_status": { "one": "V súčasnosti sa odstraňujú správy v %(count)s miestnosti", "few": "V súčasnosti sa odstraňujú správy v %(count)s miestnostiach", "other": "V súčasnosti sa odstraňujú správy v %(count)s miestnostiach" }, - "room": { - "more_options": "Viac možností", - "open_room": "Otvoriť miestnosť %(roomName)s" - }, "show_less": "Zobraziť menej", "show_n_more": { "one": "Zobraziť %(count)s ďalšiu", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index ede4e07e1f..fbbd7563a0 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -12,16 +12,6 @@ "one": "1 oläst omnämnande." }, "recent_rooms": "Nyliga rum", - "room_messsage_not_sent": "Öppna rummet %(roomName)s med ett osänt meddelande.", - "room_n_unread_invite": "Öppna inbjudan till rummet %(roomName)s.", - "room_n_unread_messages": { - "one": "Öppna rummet %(roomName)s med 1 oläst meddelande.", - "other": "Öppna rummet %(roomName)s med %(count)s olästa meddelanden." - }, - "room_n_unread_messages_mentions": { - "one": "Öppna rummet %(roomName)s med 1 oläst omnämnande.", - "other": "Öppna rummet %(roomName)s med %(count)s olästa meddelanden inklusive omnämnanden." - }, "room_name": "Rum %(name)s", "room_status_bar": "Rumsstatusfält", "seek_bar_label": "Förloppsfält för ljud", @@ -1658,7 +1648,6 @@ "class_global": "Globalt", "class_other": "Annat", "default": "Standard", - "default_settings": "Matcha standardinställningar", "email_pusher_app_display_name": "E-postaviseringar", "enable_prompt_toast_description": "Aktivera skrivbordsaviseringar", "enable_prompt_toast_title": "Aviseringar", @@ -1677,8 +1666,7 @@ "mentions_and_keywords_description": "Bli endast aviserad om omnämnanden och nyckelord i enlighet med dina inställningar", "mentions_keywords": "Omnämnanden & nyckelord", "message_didnt_send": "Meddelande skickades inte. Klicka för info.", - "mute_description": "Du får inga aviseringar", - "mute_room": "Tysta rum" + "mute_description": "Du får inga aviseringar" }, "notifier": { "m.key.verification.request": "%(name)s begär verifiering" @@ -2075,37 +2063,9 @@ "add_space_label": "Lägg till utrymme", "breadcrumbs_empty": "Inga nyligen besökta rum", "breadcrumbs_label": "Nyligen besökta rum", - "collapse_filters": "Kollapsa filterlista", - "empty": { - "no_chats": "Inga chattar än", - "no_chats_description": "Kom igång genom att skicka meddelanden till någon eller genom att skapa ett rum", - "no_chats_description_no_room_rights": "Kom igång genom att skicka meddelanden till någon", - "no_favourites": "Du har ingen favoritchatt än", - "no_favourites_description": "Du kan lägga till en chatt till dina favoriter i chattinställningarna", - "no_invites": "Du har inga olästa inbjudningar", - "no_lowpriority": "Du har inga lågprioriterade rum.", - "no_mentions": "Du har inga olästa omnämnanden", - "no_people": "Du har inte direktchattar med någon ännu", - "no_people_description": "Du kan avmarkera filter för att se dina andra chattar", - "no_rooms": "Du är inte i något rum än", - "no_rooms_description": "Du kan avmarkera filter för att se dina andra chattar", - "no_unread": "Grattis! Du har inga olästa meddelanden", - "show_activity": "Visa all aktivitet", - "show_chats": "Visa alla chattar" - }, - "expand_filters": "Expandera filterlista", "failed_add_tag": "Misslyckades att lägga till etiketten %(tagName)s till rummet", "failed_remove_tag": "Misslyckades att radera etiketten %(tagName)s från rummet", "failed_set_dm_tag": "Misslyckades att sätta direktmeddelandetagg", - "filters": { - "favourite": "Favoriter", - "invites": "Inbjudningar", - "low_priority": "Låg prioritet", - "mentions": "Omnämnanden", - "people": "Personer", - "rooms": "Rum", - "unread": "Olästa" - }, "home_menu_label": "Hemalternativ", "join_public_room_label": "Gå med i offentligt rum", "joining_rooms_status": { @@ -2114,23 +2074,13 @@ }, "list_title": "Rumslista", "more_options": { - "copy_link": "Kopiera rumslänk", - "favourited": "Favoritmarkerad", - "leave_room": "Lämna rum", - "low_priority": "Låg prioritet", - "mark_read": "Markera som läst", - "mark_unread": "Markera som oläst" + "leave_room": "Lämna rum" }, "notification_options": "Aviseringsinställningar", - "primary_filters": "Filter för rumslista", "redacting_messages_status": { "one": "Tar just nu bort meddelanden i %(count)s rum", "other": "Tar just nu bort meddelanden i %(count)s rum" }, - "room": { - "more_options": "Fler alternativ", - "open_room": "Öppet rummet %(roomName)s" - }, "show_less": "Visa mindre", "show_n_more": { "other": "Visa %(count)s till", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index e4b6fcca76..e5a9548339 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -2005,12 +2005,6 @@ "failed_add_tag": "%(tagName)s etiketi odaya eklenemedi", "failed_remove_tag": "Odadan %(tagName)s etiketi kaldırılamadı", "failed_set_dm_tag": "Doğrudan mesaj etiketi ayarlanamadı", - "filters": { - "favourite": "Favoriler", - "people": "Kişiler", - "rooms": "Odalar", - "unread": "Okunmamış" - }, "home_menu_label": "Ana sayfa seçenekleri", "join_public_room_label": "Herkese açık odaya katıl", "joining_rooms_status": { @@ -2019,14 +2013,10 @@ }, "list_title": "Oda listesi", "notification_options": "Bildirim ayarları", - "primary_filters": "Oda listesi filtreleri", "redacting_messages_status": { "one": "Şu anda %(count)s odadaki mesajlar kaldırılıyor", "other": "Şu anda %(count)s odadaki mesajlar kaldırılıyor" }, - "room": { - "open_room": "Açık oda %(roomName)s" - }, "show_less": "Daha az göster", "show_n_more": { "one": "%(count)s adet daha fazla göster", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 641f6dbe4b..08b4d8b2d1 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -12,18 +12,6 @@ "one": "1 непрочитана згадка." }, "recent_rooms": "Недавні кімнати", - "room_messsage_not_sent": "Відкрити кімнату %(roomName)s з не надісланим повідомленням.", - "room_n_unread_invite": "Відкрити запрошення кімнати %(roomName)s.", - "room_n_unread_messages": { - "one": "Відкрити кімнату %(roomName)s з 1 непрочитаним повідомленням.", - "few": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними повідомленнями.", - "many": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними повідомленнями." - }, - "room_n_unread_messages_mentions": { - "one": "Відкрити кімнату %(roomName)s з 1 непрочитаною згадкою.", - "few": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними згадками.", - "many": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними згадками." - }, "room_name": "Кімната %(name)s", "room_status_bar": "Панель стану кімнати", "seek_bar_label": "Панель гортання аудіо", @@ -1715,7 +1703,6 @@ "class_global": "Глобально", "class_other": "Інше", "default": "Типовий", - "default_settings": "Згідно з усталеними налаштуваннями", "email_pusher_app_display_name": "Сповіщення е-поштою", "enable_prompt_toast_description": "Увімкнути сповіщення стільниці", "enable_prompt_toast_title": "Сповіщення", @@ -1734,8 +1721,7 @@ "mentions_and_keywords_description": "Отримувати лише вказані у ваших налаштуваннях згадки й ключові слова", "mentions_keywords": "Згадки та ключові слова", "message_didnt_send": "Повідомлення не надіслане. Натисніть, щоб дізнатись більше.", - "mute_description": "Ви не отримуватимете жодних сповіщень", - "mute_room": "Вимкнути сповіщення кімнати" + "mute_description": "Ви не отримуватимете жодних сповіщень" }, "notifier": { "m.key.verification.request": "%(name)s робить запит на звірення" @@ -2151,37 +2137,9 @@ "add_space_label": "Додати простір", "breadcrumbs_empty": "Немає недавно відвіданих кімнат", "breadcrumbs_label": "Недавно відвідані кімнати", - "collapse_filters": "Згорнути список фільтрів", - "empty": { - "no_chats": "Ще немає бесід", - "no_chats_description": "Почніть користування, надіславши комусь повідомлення або створивши кімнату", - "no_chats_description_no_room_rights": "Розпочніть користування, написавши комусь повідомлення", - "no_favourites": "У вас ще немає обраних бесід", - "no_favourites_description": "Ви можете додати бесіду до обраних у її налаштуваннях", - "no_invites": "У вас немає непрочитаних запрошень", - "no_lowpriority": "У вас немає неважливих кімнат", - "no_mentions": "У вас немає непрочитаних згадок", - "no_people": "У вас ще немає особистих бесід", - "no_people_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди", - "no_rooms": "Ви ще не входили до кімнат", - "no_rooms_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди", - "no_unread": "Вітаємо! У вас немає непрочитаних повідомлень", - "show_activity": "Переглянути всю діяльність", - "show_chats": "Показати всі бесіди" - }, - "expand_filters": "Розгорнути список фільтрів", "failed_add_tag": "Не вдалось додати до кімнати мітку %(tagName)s", "failed_remove_tag": "Не вдалося прибрати з кімнати мітку %(tagName)s", "failed_set_dm_tag": "Не вдалося встановити мітку особистого повідомлення", - "filters": { - "favourite": "Обрані", - "invites": "Запрошення", - "low_priority": "Неважливі", - "mentions": "Згадування", - "people": "Люди", - "rooms": "Кімнати", - "unread": "Непрочитані" - }, "home_menu_label": "Параметри домівки", "join_public_room_label": "Приєднатись до загальнодоступної кімнати", "joining_rooms_status": { @@ -2190,23 +2148,13 @@ }, "list_title": "Список кімнат", "more_options": { - "copy_link": "Копіювати посилання на кімнату", - "favourited": "Обране", - "leave_room": "Вийти з кімнати", - "low_priority": "Неважливі", - "mark_read": "Позначити прочитаним", - "mark_unread": "Позначити непрочитаним" + "leave_room": "Вийти з кімнати" }, "notification_options": "Параметри сповіщень", - "primary_filters": "Фільтри списку кімнат", "redacting_messages_status": { "one": "Триває видалення повідомлень в %(count)s кімнаті", "other": "Триває видалення повідомлень у %(count)s кімнатах" }, - "room": { - "more_options": "Інші опції", - "open_room": "Відкрити кімнату %(roomName)s" - }, "show_less": "Згорнути", "show_n_more": { "other": "Показати ще %(count)s", diff --git a/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/src/viewmodels/room-list/RoomListHeaderViewModel.ts index fee0c954c3..e99268190c 100644 --- a/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -26,11 +26,11 @@ import { showSpaceSettings, } from "../../utils/space"; import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; -import { createRoom, hasCreateRoomRights } from "../../components/viewmodels/roomlist/utils"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters"; import { SettingLevel } from "../../settings/SettingLevel"; +import { createRoom, hasCreateRoomRights } from "./utils"; export interface Props { /** diff --git a/src/viewmodels/room-list/RoomListItemViewModel.ts b/src/viewmodels/room-list/RoomListItemViewModel.ts new file mode 100644 index 0000000000..d7ce4e6e7f --- /dev/null +++ b/src/viewmodels/room-list/RoomListItemViewModel.ts @@ -0,0 +1,327 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { + BaseViewModel, + RoomNotifState, + type RoomListItemSnapshot, + type RoomListItemActions, +} from "@element-hq/web-shared-components"; +import { RoomEvent } from "matrix-js-sdk/src/matrix"; +import { CallType } from "matrix-js-sdk/src/webrtc/call"; + +import type { Room, MatrixClient } from "matrix-js-sdk/src/matrix"; +import type { RoomNotificationState } from "../../stores/notifications/RoomNotificationState"; +import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; +import { NotificationStateEvents } from "../../stores/notifications/NotificationState"; +import { MessagePreviewStore } from "../../stores/room-list/MessagePreviewStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import { DefaultTagID } from "../../stores/room-list/models"; +import DMRoomMap from "../../utils/DMRoomMap"; +import SettingsStore from "../../settings/SettingsStore"; +import { NotificationLevel } from "../../stores/notifications/NotificationLevel"; +import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; +import { EchoChamber } from "../../stores/local-echo/EchoChamber"; +import { RoomNotifState as ElementRoomNotifState } from "../../RoomNotifs"; +import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../settings/UIFeature"; +import { CallStore, CallStoreEvent } from "../../stores/CallStore"; +import { clearRoomNotification, setMarkedUnreadState } from "../../utils/notifications"; +import { tagRoom } from "../../utils/room/tagRoom"; +import dispatcher from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; +import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import PosthogTrackers from "../../PosthogTrackers"; + +interface RoomItemProps { + room: Room; + client: MatrixClient; +} + +/** + * View model for an individual room list item. + * Manages per-room subscriptions and updates only when this specific room's data changes. + * Implements RoomListItemActions to provide interaction callbacks. + */ +export class RoomListItemViewModel + extends BaseViewModel + implements RoomListItemActions +{ + private notifState: RoomNotificationState; + + public constructor(props: RoomItemProps) { + // Get notification state first so we can generate a complete initial snapshot + const notifState = RoomNotificationStateStore.instance.getRoomState(props.room); + const initialItem = RoomListItemViewModel.generateItemSync(props.room, props.client, notifState); + super(props, initialItem); + + this.notifState = notifState; + + // Subscribe to notification state changes for this room + this.disposables.trackListener(this.notifState, NotificationStateEvents.Update, this.onNotificationChanged); + + // Subscribe to message preview changes (will filter to this room) + this.disposables.trackListener(MessagePreviewStore.instance, UPDATE_EVENT, this.onMessagePreviewChanged); + + // Subscribe to settings changes for message preview toggle + const settingsWatchRef = SettingsStore.watchSetting( + "RoomList.showMessagePreview", + null, + this.onMessagePreviewSettingChanged, + ); + this.disposables.track(() => { + SettingsStore.unwatchSetting(settingsWatchRef); + }); + + // Subscribe to call state changes + this.disposables.trackListener(CallStore.instance, CallStoreEvent.ConnectedCalls, this.onCallStateChanged); + + // Subscribe to room-specific events + this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomChanged); + this.disposables.trackListener(props.room, RoomEvent.Tags, this.onRoomChanged); + + // Load message preview asynchronously (sync data is already complete) + void this.loadAndSetMessagePreview(); + } + + private onNotificationChanged = (): void => { + this.updateItem(); + }; + + private onMessagePreviewChanged = (): void => { + void this.loadAndSetMessagePreview(); + }; + + private onMessagePreviewSettingChanged = (): void => { + void this.loadAndSetMessagePreview(); + }; + + private onCallStateChanged = (): void => { + // Only update if call state for this room actually changed + const call = CallStore.instance.getCall(this.props.room.roomId); + const currentCallType = this.snapshot.current.notification.callType; + const newCallType = + call && call.participants.size > 0 ? (call.callType === CallType.Voice ? "voice" : "video") : undefined; + + if (currentCallType !== newCallType) { + this.updateItem(); + } + }; + + private onRoomChanged = (): void => { + this.updateItem(); + }; + + /** + * Update the item snapshot with current sync data. + * Preserves the message preview which is managed separately. + */ + private updateItem(): void { + const newItem = RoomListItemViewModel.generateItemSync(this.props.room, this.props.client, this.notifState); + // Preserve message preview - it's managed separately by loadAndSetMessagePreview + this.snapshot.set({ ...newItem, messagePreview: this.snapshot.current.messagePreview }); + } + + private getMessagePreviewTag(): string { + const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId)); + return isDm ? DefaultTagID.DM : DefaultTagID.Untagged; + } + + /** + * Load the message preview for this room if enabled. + * Returns undefined if previews are disabled or couldn't be loaded. + */ + private async loadMessagePreview(): Promise { + const shouldShowMessagePreview = SettingsStore.getValue("RoomList.showMessagePreview"); + if (!shouldShowMessagePreview) { + return undefined; + } + + const messagePreviewTag = this.getMessagePreviewTag(); + const preview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, messagePreviewTag); + return preview?.text; + } + + /** + * Load and set the message preview if it differs from current. + */ + private async loadAndSetMessagePreview(): Promise { + const messagePreview = await this.loadMessagePreview(); + if (messagePreview !== this.snapshot.current.messagePreview) { + this.snapshot.merge({ messagePreview }); + } + } + + /** + * Generate a complete RoomListItem with all synchronous data. + * Message preview is loaded separately to avoid blocking initial render. + */ + private static generateItemSync( + room: Room, + client: MatrixClient, + notifState: RoomNotificationState, + ): RoomListItemSnapshot { + // Get room tags for menu state + const roomTags = room.tags; + const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); + + // Message preview will be loaded asynchronously and updated separately + const messagePreview = undefined; + + const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]); + const isLowPriority = Boolean(roomTags[DefaultTagID.LowPriority]); + const isArchived = Boolean(roomTags[DefaultTagID.Archived]); + + // More options menu state + const showMoreOptionsMenu = hasAccessToOptionsMenu(room); + const showNotificationMenu = hasAccessToNotificationMenu(room, client.isGuest(), isArchived); + + // Notification levels + const canMarkAsRead = notifState.level > NotificationLevel.None; + const canMarkAsUnread = !canMarkAsRead && !isArchived; + + const canInvite = room.canInvite(client.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); + const canCopyRoomLink = !isDm; + + // Get the current room notification state from EchoChamber + const echoChamber = EchoChamber.forRoom(room); + const elementRoomNotifState = echoChamber.notificationVolume; + + // Convert element-web RoomNotifState to shared-components RoomNotifState + let roomNotifState: RoomNotifState; + switch (elementRoomNotifState) { + case ElementRoomNotifState.AllMessages: + roomNotifState = RoomNotifState.AllMessages; + break; + case ElementRoomNotifState.AllMessagesLoud: + roomNotifState = RoomNotifState.AllMessagesLoud; + break; + case ElementRoomNotifState.MentionsOnly: + roomNotifState = RoomNotifState.MentionsOnly; + break; + case ElementRoomNotifState.Mute: + roomNotifState = RoomNotifState.Mute; + break; + default: + roomNotifState = RoomNotifState.AllMessages; + } + + const isNotificationMute = elementRoomNotifState === ElementRoomNotifState.Mute; + + // Video room and call state tracking + const call = CallStore.instance.getCall(room.roomId); + const participantCount = call?.participants.size ?? 0; + const hasParticipantsInCall = participantCount > 0; + const callType = + call?.callType === CallType.Voice ? "voice" : call?.callType === CallType.Video ? "video" : undefined; + + return { + id: room.roomId, + room, + name: room.name, + isBold: notifState.hasAnyNotificationOrActivity, + messagePreview, + notification: { + hasAnyNotificationOrActivity: notifState.hasAnyNotificationOrActivity || hasParticipantsInCall, + isUnsentMessage: notifState.isUnsentMessage, + invited: notifState.invited, + isMention: notifState.isMention, + isActivityNotification: notifState.isActivityNotification, + isNotification: notifState.isNotification, + hasUnreadCount: notifState.hasUnreadCount, + count: notifState.count, + muted: isNotificationMute, + callType: hasParticipantsInCall ? callType : undefined, + }, + showMoreOptionsMenu, + showNotificationMenu, + isFavourite, + isLowPriority, + canInvite, + canCopyRoomLink, + canMarkAsRead, + canMarkAsUnread, + roomNotifState, + }; + } + + public onOpenRoom = (): void => { + dispatcher.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + metricsTrigger: "RoomList", + }); + }; + + public onMarkAsRead = async (): Promise => { + await clearRoomNotification(this.props.room, this.props.client); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead"); + }; + + public onMarkAsUnread = async (): Promise => { + await setMarkedUnreadState(this.props.room, this.props.client, true); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread"); + }; + + public onToggleFavorite = (): void => { + tagRoom(this.props.room, DefaultTagID.Favourite); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle"); + }; + + public onToggleLowPriority = (): void => { + tagRoom(this.props.room, DefaultTagID.LowPriority); + }; + + public onInvite = (): void => { + dispatcher.dispatch({ + action: "view_invite", + roomId: this.props.room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem"); + }; + + public onCopyRoomLink = (): void => { + dispatcher.dispatch({ + action: "copy_room", + room_id: this.props.room.roomId, + }); + }; + + public onLeaveRoom = (): void => { + const isArchived = Boolean(this.props.room.tags[DefaultTagID.Archived]); + dispatcher.dispatch({ + action: isArchived ? "forget_room" : "leave_room", + room_id: this.props.room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem"); + }; + + public onSetRoomNotifState = (notifState: RoomNotifState): void => { + // Convert shared-components RoomNotifState to element-web RoomNotifState + let elementNotifState: ElementRoomNotifState; + switch (notifState) { + case "all_messages": + elementNotifState = ElementRoomNotifState.AllMessages; + break; + case "all_messages_loud": + elementNotifState = ElementRoomNotifState.AllMessagesLoud; + break; + case "mentions_only": + elementNotifState = ElementRoomNotifState.MentionsOnly; + break; + case "mute": + elementNotifState = ElementRoomNotifState.Mute; + break; + default: + elementNotifState = ElementRoomNotifState.AllMessages; + } + + // Set the notification state using EchoChamber + const echoChamber = EchoChamber.forRoom(this.props.room); + echoChamber.notificationVolume = elementNotifState; + }; +} diff --git a/src/viewmodels/room-list/RoomListViewViewModel.ts b/src/viewmodels/room-list/RoomListViewViewModel.ts new file mode 100644 index 0000000000..a3618b93af --- /dev/null +++ b/src/viewmodels/room-list/RoomListViewViewModel.ts @@ -0,0 +1,450 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { + BaseViewModel, + type RoomListSnapshot, + type FilterId, + type RoomListViewActions, + type RoomListViewState, +} from "@element-hq/web-shared-components"; +import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; + +import { Action } from "../../dispatcher/actions"; +import dispatcher from "../../dispatcher/dispatcher"; +import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import SpaceStore from "../../stores/spaces/SpaceStore"; +import RoomListStoreV3, { RoomListStoreV3Event, type RoomsResult } from "../../stores/room-list-v3/RoomListStoreV3"; +import { FilterKey } from "../../stores/room-list-v3/skip-list/filters"; +import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; +import { RoomListItemViewModel } from "./RoomListItemViewModel"; +import { SdkContextClass } from "../../contexts/SDKContext"; +import { hasCreateRoomRights } from "./utils"; + +interface RoomListViewViewModelProps { + client: MatrixClient; +} + +const filterKeyToIdMap: Map = new Map([ + [FilterKey.UnreadFilter, "unread"], + [FilterKey.PeopleFilter, "people"], + [FilterKey.RoomsFilter, "rooms"], + [FilterKey.FavouriteFilter, "favourite"], + [FilterKey.MentionsFilter, "mentions"], + [FilterKey.InvitesFilter, "invites"], + [FilterKey.LowPriorityFilter, "low_priority"], +]); + +export class RoomListViewViewModel + extends BaseViewModel + implements RoomListViewActions +{ + // State tracking + private activeFilter: FilterKey | undefined = undefined; + private roomsResult: RoomsResult; + private lastActiveRoomIndex: number | undefined = undefined; + + // Child view model management + private roomItemViewModels = new Map(); + private roomsMap = new Map(); + + public constructor(props: RoomListViewViewModelProps) { + const activeSpace = SpaceStore.instance.activeSpaceRoom; + + // Get initial rooms + const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined); + const canCreateRoom = hasCreateRoomRights(props.client, activeSpace); + const filterIds = [...filterKeyToIdMap.values()]; + + super(props, { + // Initial view state - start with empty, will populate in async init + isLoadingRooms: RoomListStoreV3.instance.isLoadingRooms, + isRoomListEmpty: roomsResult.rooms.length === 0, + filterIds, + activeFilterId: undefined, + roomListState: { + activeRoomIndex: undefined, + spaceId: roomsResult.spaceId, + filterKeys: undefined, + }, + roomIds: roomsResult.rooms.map((room) => room.roomId), + canCreateRoom, + }); + + this.roomsResult = roomsResult; + + // Build initial roomsMap from roomsResult + this.updateRoomsMap(roomsResult); + + // Subscribe to room list updates + this.disposables.trackListener( + RoomListStoreV3.instance, + RoomListStoreV3Event.ListsUpdate as any, + this.onListsUpdate, + ); + + // Subscribe to room list loaded + this.disposables.trackListener( + RoomListStoreV3.instance, + RoomListStoreV3Event.ListsLoaded as any, + this.onListsLoaded, + ); + + // Subscribe to active room changes to update selected room + const dispatcherRef = dispatcher.register(this.onDispatch); + this.disposables.track(() => { + dispatcher.unregister(dispatcherRef); + }); + + // Track cleanup of all child view models + this.disposables.track(() => { + for (const viewModel of this.roomItemViewModels.values()) { + viewModel.dispose(); + } + this.roomItemViewModels.clear(); + }); + } + + public onToggleFilter = (filterId: FilterId): void => { + // Find the FilterKey by matching the filter ID + let filterKey: FilterKey | undefined = undefined; + for (const [key, id] of filterKeyToIdMap.entries()) { + if (id === filterId) { + filterKey = key; + break; + } + } + + if (filterKey === undefined) return; + + // Toggle the filter - if it's already active, deactivate it + const newFilter = this.activeFilter === filterKey ? undefined : filterKey; + this.activeFilter = newFilter; + + // Update rooms result with new filter + const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined; + this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + + // Update roomsMap immediately before clearing VMs + this.updateRoomsMap(this.roomsResult); + + // Clear view models since room list changed + this.clearViewModels(); + + this.updateRoomListData(); + }; + + /** + * Rebuild roomsMap when roomsResult changes. + * This maintains a quick lookup for room objects. + */ + private updateRoomsMap(roomsResult: RoomsResult): void { + this.roomsMap.clear(); + for (const room of roomsResult.rooms) { + this.roomsMap.set(room.roomId, room); + } + } + + /** + * Clear all child view models. + * Called when the room list structure changes (space change, filter change, etc.) + */ + private clearViewModels(): void { + for (const viewModel of this.roomItemViewModels.values()) { + viewModel.dispose(); + } + this.roomItemViewModels.clear(); + } + + /** + * Get the ordered list of room IDs. + */ + public get roomIds(): string[] { + return this.roomsResult.rooms.map((room) => room.roomId); + } + + /** + * Get a RoomListItemViewModel for a specific room. + * Creates a RoomListItemViewModel if needed, which manages per-room subscriptions. + * The view should call this only for visible rooms from the roomIds list. + * @throws Error if room is not found in roomsMap (indicates a programming error) + */ + public getRoomItemViewModel(roomId: string): RoomListItemViewModel { + // Check if we have a view model for this room + let viewModel = this.roomItemViewModels.get(roomId); + + if (!viewModel) { + const room = this.roomsMap.get(roomId); + if (!room) { + throw new Error(`Room ${roomId} not found in roomsMap`); + } + + // Create new view model + viewModel = new RoomListItemViewModel({ + room, + client: this.props.client, + }); + + this.roomItemViewModels.set(roomId, viewModel); + } + + // Return the view model - the view will call useViewModel() on it + return viewModel; + } + + /** + * Update which rooms are currently visible. + * Called by the view when scroll position changes. + * Disposes of view models for rooms no longer visible. + */ + public updateVisibleRooms(startIndex: number, endIndex: number): void { + const allRoomIds = this.roomIds; + const newVisibleIds = allRoomIds.slice(startIndex, Math.min(endIndex, allRoomIds.length)); + + const newVisibleSet = new Set(newVisibleIds); + + // Dispose view models for rooms no longer visible + for (const [roomId, viewModel] of this.roomItemViewModels.entries()) { + if (!newVisibleSet.has(roomId)) { + viewModel.dispose(); + this.roomItemViewModels.delete(roomId); + } + } + } + + private onDispatch = (payload: any): void => { + if (payload.action === Action.ActiveRoomChanged) { + // When the active room changes, update the room list data to reflect the new selected room + // Pass isRoomChange=true so sticky logic doesn't prevent the index from updating + this.updateRoomListData(true); + } else if (payload.action === Action.ViewRoomDelta) { + // Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) + // This was previously handled by useRoomListNavigation hook + this.handleViewRoomDelta(payload as ViewRoomDeltaPayload); + } + }; + + /** + * Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) to move between rooms. + * Supports both regular navigation and unread-only navigation. + * Migrated from useRoomListNavigation hook. + */ + private handleViewRoomDelta(payload: ViewRoomDeltaPayload): void { + const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); + if (!currentRoomId) return; + + const { delta, unread } = payload; + const rooms = this.roomsResult.rooms; + + const filteredRooms = unread + ? // Filter the rooms to only include unread ones and the active room + rooms.filter((room) => { + const state = RoomNotificationStateStore.instance.getRoomState(room); + return room.roomId === currentRoomId || state.isUnread; + }) + : rooms; + + const currentIndex = filteredRooms.findIndex((room) => room.roomId === currentRoomId); + if (currentIndex === -1) return; + + // Get the next/previous new room according to the delta + // Use slice to loop on the list + // If delta is -1 at the start of the list, it will go to the end + // If delta is 1 at the end of the list, it will go to the start + const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length); + if (!newRoom) return; + + dispatcher.dispatch({ + action: Action.ViewRoom, + room_id: newRoom.roomId, + show_room_tile: true, // to make sure the room gets scrolled into view + metricsTrigger: "WebKeyboardShortcut", + metricsViaKeyboard: true, + }); + } + + /** + * Handle room list updates from RoomListStoreV3. + * + * This event fires when: + * - Room order changes (new messages, manual reordering) + * - Active space changes + * - Filters are applied + * - Rooms are added/removed + * + * Space changes are detected by comparing old vs new spaceId. + * This matches the old hook pattern where space changes were handled + * indirectly through room list updates. + */ + private onListsUpdate = (): void => { + const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined; + const oldSpaceId = this.roomsResult.spaceId; + + // Refresh room data from store + this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + this.updateRoomsMap(this.roomsResult); + + const newSpaceId = this.roomsResult.spaceId; + + // Clear view models since room list structure changed + this.clearViewModels(); + + // Detect space change + if (oldSpaceId !== newSpaceId) { + // Space changed - get the last selected room for the new space to prevent flicker + const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId); + + this.updateRoomListData(true, lastSelectedRoom); + return; + } + + // Normal room list update (not a space change) + this.updateRoomListData(); + }; + + private onListsLoaded = (): void => { + // Room lists have finished loading + this.snapshot.merge({ + isLoadingRooms: false, + }); + }; + + /** + * Calculate the active room index based on the currently viewed room. + * Returns undefined if no room is selected or if the selected room is not in the current list. + * + * @param roomId - The room ID to find the index for (can be null/undefined) + */ + private getActiveRoomIndex(roomId: string | null | undefined): number | undefined { + if (!roomId) { + return undefined; + } + + const index = this.roomsResult.rooms.findIndex((room) => room.roomId === roomId); + return index >= 0 ? index : undefined; + } + + /** + * Apply sticky room logic to keep the active room at the same index position. + * When the room list updates, this prevents the selected room from jumping around in the UI. + * + * @param isRoomChange - Whether this update is due to a room change (not a list update) + * @param roomId - The room ID to apply sticky logic for (can be null/undefined) + * @returns The modified rooms array with sticky positioning applied + */ + private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] { + const rooms = this.roomsResult.rooms; + + if (!roomId) { + return rooms; + } + + const newIndex = rooms.findIndex((room) => room.roomId === roomId); + const oldIndex = this.lastActiveRoomIndex; + + // When opening another room, the index should obviously change + if (isRoomChange) { + return rooms; + } + + // If oldIndex is undefined, then there was no active room before + // Similarly, if newIndex is -1, the active room is not in the current list + if (newIndex === -1 || oldIndex === undefined) { + return rooms; + } + + // If the index hasn't changed, we have nothing to do + if (newIndex === oldIndex) { + return rooms; + } + + // If the old index falls out of the bounds of the rooms array + // (usually because rooms were removed), we can no longer place + // the active room in the same old index + if (oldIndex > rooms.length - 1) { + return rooms; + } + + // Making the active room sticky is as simple as removing it from + // its new index and placing it in the old index + const newRooms = [...rooms]; + const [stickyRoom] = newRooms.splice(newIndex, 1); + newRooms.splice(oldIndex, 0, stickyRoom); + + return newRooms; + } + + private async updateRoomListData( + isRoomChange: boolean = false, + roomIdOverride: string | null = null, + ): Promise { + // Determine the room ID to use for calculations + // Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore + const roomId = roomIdOverride ?? SdkContextClass.instance.roomViewStore.getRoomId(); + + // Apply sticky room logic to keep selected room at same position + const stickyRooms = this.applyStickyRoom(isRoomChange, roomId); + + // Update roomsResult with sticky rooms + this.roomsResult = { + ...this.roomsResult, + rooms: stickyRooms, + }; + + // Rebuild roomsMap with the reordered rooms + this.updateRoomsMap(this.roomsResult); + + // Calculate the active room index after applying sticky logic + const activeRoomIndex = this.getActiveRoomIndex(roomId); + + // Track the current active room index for future sticky calculations + this.lastActiveRoomIndex = activeRoomIndex; + + // Build the complete state atomically to ensure consistency + // roomIds and roomListState must always be in sync + const roomIds = this.roomIds; + const roomListState: RoomListViewState = { + activeRoomIndex, + spaceId: this.roomsResult.spaceId, + filterKeys: this.roomsResult.filterKeys?.map((k) => String(k)), + }; + + const filterIds = [...filterKeyToIdMap.values()]; + const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined; + const isRoomListEmpty = roomIds.length === 0; + const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms; + + // Single atomic snapshot update + this.snapshot.merge({ + isLoadingRooms, + isRoomListEmpty, + filterIds, + activeFilterId, + roomListState, + roomIds, + }); + } + + public createChatRoom = (): void => { + dispatcher.fire(Action.CreateChat); + }; + + public createRoom = (): void => { + const activeSpace = SpaceStore.instance.activeSpaceRoom; + if (activeSpace) { + dispatcher.dispatch({ + action: Action.CreateRoom, + parent_space: activeSpace, + }); + } else { + dispatcher.dispatch({ + action: Action.CreateRoom, + }); + } + }; +} diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/viewmodels/room-list/utils.ts similarity index 83% rename from src/components/viewmodels/roomlist/utils.ts rename to src/viewmodels/room-list/utils.ts index dfa20e0d1c..5cd2f58678 100644 --- a/src/components/viewmodels/roomlist/utils.ts +++ b/src/viewmodels/room-list/utils.ts @@ -7,12 +7,12 @@ import { type Room, KnownMembership, EventTimeline, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { isKnockDenied } from "../../../utils/membership"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; -import { UIComponent } from "../../../settings/UIFeature"; -import { showCreateNewRoom } from "../../../utils/space"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; +import { isKnockDenied } from "../../utils/membership"; +import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../settings/UIFeature"; +import { showCreateNewRoom } from "../../utils/space"; +import dispatcher from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; /** * Check if the user has access to the options menu. diff --git a/test/unit-tests/Searching-test.ts b/test/unit-tests/Searching-test.ts new file mode 100644 index 0000000000..60b34fb345 --- /dev/null +++ b/test/unit-tests/Searching-test.ts @@ -0,0 +1,262 @@ +/* +Copyright 2025 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type IResultRoomEvents } from "matrix-js-sdk/src/matrix"; + +import eventSearch from "../../src/Searching"; +import EventIndexPeg from "../../src/indexing/EventIndexPeg"; +import { createTestClient } from "../test-utils"; + +describe("Searching", () => { + const mockClient = createTestClient(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("localSearch", () => { + it("removes state_key: null from search results", async () => { + // Mock search results from Seshat that include state_key: null + const mockSearchResults: IResultRoomEvents = { + count: 2, + results: [ + { + rank: 1, + result: { + event_id: "$event1", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567890, + content: { body: "test message 1", msgtype: "m.text" }, + // Seshat incorrectly includes state_key: null for non-state events + state_key: null, + } as any, + context: { + events_before: [ + { + event_id: "$before1", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567889, + content: { body: "before message", msgtype: "m.text" }, + state_key: null, + } as any, + ], + events_after: [ + { + event_id: "$after1", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567891, + content: { body: "after message", msgtype: "m.text" }, + state_key: null, + } as any, + ], + profile_info: {}, + }, + }, + { + rank: 2, + result: { + event_id: "$event2", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567880, + content: { body: "test message 2", msgtype: "m.text" }, + state_key: null, + } as any, + context: { + events_before: [], + events_after: [], + profile_info: {}, + }, + }, + ], + highlights: ["test"], + }; + + // Mock EventIndex.search to return results with state_key: null + const mockEventIndex = { + search: jest.fn().mockResolvedValue(mockSearchResults), + }; + jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any); + + // Mock crypto to indicate room is encrypted + jest.spyOn(mockClient, "getCrypto").mockReturnValue({ + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true), + } as any); + + // Perform search in an encrypted room + const roomId = "!room:example.org"; + await eventSearch(mockClient, "test", roomId); + + // Verify that state_key: null was removed from the search arguments passed to search + expect(mockEventIndex.search).toHaveBeenCalled(); + + // Get the mock search results that were passed to processRoomEventsSearch + // The state_key should have been deleted from the original results object + const mainEventResult = mockSearchResults.results![0].result as unknown as Record; + expect(mainEventResult.state_key).toBeUndefined(); + + const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record< + string, + unknown + >; + expect(beforeEvent.state_key).toBeUndefined(); + + const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record< + string, + unknown + >; + expect(afterEvent.state_key).toBeUndefined(); + + const secondResult = mockSearchResults.results![1].result as unknown as Record; + expect(secondResult.state_key).toBeUndefined(); + }); + + it("does not modify events without state_key: null", async () => { + const mockSearchResults: IResultRoomEvents = { + count: 1, + results: [ + { + rank: 1, + result: { + event_id: "$event1", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567890, + content: { body: "test message", msgtype: "m.text" }, + // No state_key property at all (correct behavior) + } as any, + context: { + events_before: [], + events_after: [], + profile_info: {}, + }, + }, + ], + highlights: ["test"], + }; + + const mockEventIndex = { + search: jest.fn().mockResolvedValue(mockSearchResults), + }; + jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any); + + jest.spyOn(mockClient, "getCrypto").mockReturnValue({ + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true), + } as any); + + const roomId = "!room:example.org"; + await eventSearch(mockClient, "test", roomId); + + // Verify state_key is still undefined (not accidentally set to something) + const eventResult = mockSearchResults.results![0].result as unknown as Record; + expect("state_key" in eventResult).toBe(false); + }); + + it("handles missing context fields and empty result sets", async () => { + const mockSearchResults: IResultRoomEvents = { + count: 3, + results: [ + { + rank: 1, + result: { + event_id: "$event1", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567890, + content: { body: "test message", msgtype: "m.text" }, + state_key: null, + } as any, + context: { + events_before: [{ event_id: "$before1", state_key: "not-null" } as any], + events_after: [{ event_id: "$after1", state_key: "not-null" } as any], + profile_info: {}, + }, + }, + { + rank: 2, + result: { + event_id: "$event2", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567891, + content: { body: "test message 2", msgtype: "m.text" }, + state_key: null, + } as any, + context: { + profile_info: {}, + } as any, + }, + { + rank: 3, + result: { + event_id: "$event3", + room_id: "!room:example.org", + sender: "@user:example.org", + type: "m.room.message", + origin_server_ts: 1234567892, + content: { body: "test message 3", msgtype: "m.text" }, + state_key: null, + } as any, + context: undefined as any, + }, + ], + highlights: ["test"], + }; + + const mockEventIndex = { + search: jest + .fn() + .mockResolvedValueOnce(mockSearchResults) + .mockResolvedValueOnce({ count: 0, highlights: ["test"] } as IResultRoomEvents), + }; + jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any); + + jest.spyOn(mockClient, "getCrypto").mockReturnValue({ + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true), + } as any); + + const roomId = "!room:example.org"; + await eventSearch(mockClient, "test", roomId); + await eventSearch(mockClient, "test", roomId); + + const firstMainEvent = mockSearchResults.results![0].result as unknown as Record; + expect(firstMainEvent.state_key).toBeUndefined(); + + const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record< + string, + unknown + >; + expect(beforeEvent.state_key).toBe("not-null"); + + const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record< + string, + unknown + >; + expect(afterEvent.state_key).toBe("not-null"); + + const secondMainEvent = mockSearchResults.results![1].result as unknown as Record; + expect(secondMainEvent.state_key).toBeUndefined(); + + const thirdMainEvent = mockSearchResults.results![2].result as unknown as Record; + expect(thirdMainEvent.state_key).toBeUndefined(); + }); + }); +}); diff --git a/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx deleted file mode 100644 index 6c5b121022..0000000000 --- a/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { renderHook, waitFor } from "jest-matrix-react"; -import { type Room } from "matrix-js-sdk/src/matrix"; - -import { createTestClient, mkStubRoom } from "../../../../test-utils"; -import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore"; -import { useMessagePreviewViewModel } from "../../../../../src/components/viewmodels/roomlist/MessagePreviewViewModel"; - -describe("MessagePreviewViewModel", () => { - let room: Room; - - beforeEach(() => { - const matrixClient = createTestClient(); - room = mkStubRoom("roomId", "roomName", matrixClient); - }); - - it("should do an initial fetch of the message preview", async () => { - // Mock the store to return some text. - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => { - return { text: "Hello world!" } as MessagePreview; - }); - - const { result: vm } = renderHook(() => useMessagePreviewViewModel(room)); - - // Eventually, vm.message should have the text from the store. - await waitFor(() => { - expect(vm.current.message).toEqual("Hello world!"); - }); - }); - - it("should fetch message preview again on update from store", async () => { - // Mock the store to return the text in variable message. - let message = "Hello World!"; - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => { - return { text: message } as MessagePreview; - }); - jest.spyOn(MessagePreviewStore, "getPreviewChangedEventName").mockImplementation((room) => { - return "UPDATE"; - }); - - const { result: vm } = renderHook(() => useMessagePreviewViewModel(room)); - - // Let's assume the message changed. - message = "New message!"; - MessagePreviewStore.instance.emit("UPDATE"); - - /// vm.message should be the updated message. - await waitFor(() => { - expect(vm.current.message).toEqual(message); - }); - }); -}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx deleted file mode 100644 index d017084db5..0000000000 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { renderHook } from "jest-matrix-react"; -import { mocked } from "jest-mock"; -import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; - -import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils"; -import { useRoomListItemMenuViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel"; -import { - hasAccessToNotificationMenu, - hasAccessToOptionsMenu, -} from "../../../../../src/components/viewmodels/roomlist/utils"; -import DMRoomMap from "../../../../../src/utils/DMRoomMap"; -import { DefaultTagID } from "../../../../../src/stores/room-list/models"; -import { useUnreadNotifications } from "../../../../../src/hooks/useUnreadNotifications"; -import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel"; -import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications"; -import { tagRoom } from "../../../../../src/utils/room/tagRoom"; -import dispatcher from "../../../../../src/dispatcher/dispatcher"; -import { useNotificationState } from "../../../../../src/hooks/useRoomNotificationState"; -import { RoomNotifState } from "../../../../../src/RoomNotifs"; - -jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ - hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), - hasAccessToNotificationMenu: jest.fn().mockReturnValue(false), -})); - -jest.mock("../../../../../src/hooks/useUnreadNotifications", () => ({ - useUnreadNotifications: jest.fn(), -})); - -jest.mock("../../../../../src/hooks/useRoomNotificationState", () => ({ - useNotificationState: jest.fn(), -})); - -jest.mock("../../../../../src/utils/notifications", () => ({ - clearRoomNotification: jest.fn(), - setMarkedUnreadState: jest.fn(), -})); - -jest.mock("../../../../../src/utils/room/tagRoom", () => ({ - tagRoom: jest.fn(), -})); - -describe("RoomListItemMenuViewModel", () => { - let matrixClient: MatrixClient; - let room: Room; - - beforeEach(() => { - matrixClient = stubClient(); - room = mkStubRoom("roomId", "roomName", matrixClient); - - DMRoomMap.makeShared(matrixClient); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined); - - mocked(useUnreadNotifications).mockReturnValue({ symbol: null, count: 0, level: NotificationLevel.None }); - mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, jest.fn()]); - jest.spyOn(dispatcher, "dispatch"); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - function render() { - return renderHook(() => useRoomListItemMenuViewModel(room), withClientContextRenderOptions(matrixClient)); - } - - it("default", () => { - const { result } = render(); - expect(result.current.showMoreOptionsMenu).toBe(false); - expect(result.current.canInvite).toBe(false); - expect(result.current.isFavourite).toBe(false); - expect(result.current.canCopyRoomLink).toBe(true); - expect(result.current.canMarkAsRead).toBe(false); - expect(result.current.canMarkAsUnread).toBe(true); - }); - - it("should has showMoreOptionsMenu to be true", () => { - mocked(hasAccessToOptionsMenu).mockReturnValue(true); - const { result } = render(); - expect(result.current.showMoreOptionsMenu).toBe(true); - }); - - it("should has showNotificationMenu to be true", () => { - mocked(hasAccessToNotificationMenu).mockReturnValue(true); - const { result } = render(); - expect(result.current.showNotificationMenu).toBe(true); - }); - - it("should be able to invite", () => { - jest.spyOn(room, "canInvite").mockReturnValue(true); - const { result } = render(); - expect(result.current.canInvite).toBe(true); - }); - - it("should be a favourite", () => { - room.tags = { [DefaultTagID.Favourite]: { order: 0 } }; - const { result } = render(); - expect(result.current.isFavourite).toBe(true); - }); - - it("should not be able to copy the room link", () => { - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("userId"); - const { result } = render(); - expect(result.current.canCopyRoomLink).toBe(false); - }); - - it("should be able to mark as read", () => { - // Add a notification - mocked(useUnreadNotifications).mockReturnValue({ - symbol: null, - count: 1, - level: NotificationLevel.Notification, - }); - const { result } = render(); - expect(result.current.canMarkAsRead).toBe(true); - expect(result.current.canMarkAsUnread).toBe(false); - }); - - it("should has isNotificationAllMessage to be true", () => { - const { result } = render(); - expect(result.current.isNotificationAllMessage).toBe(true); - }); - - it("should has isNotificationAllMessageLoud to be true", () => { - mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessagesLoud, jest.fn()]); - const { result } = render(); - expect(result.current.isNotificationAllMessageLoud).toBe(true); - }); - - it("should has isNotificationMentionOnly to be true", () => { - mocked(useNotificationState).mockReturnValue([RoomNotifState.MentionsOnly, jest.fn()]); - const { result } = render(); - expect(result.current.isNotificationMentionOnly).toBe(true); - }); - - it("should has isNotificationMute to be true", () => { - mocked(useNotificationState).mockReturnValue([RoomNotifState.Mute, jest.fn()]); - const { result } = render(); - expect(result.current.isNotificationMute).toBe(true); - }); - - // Actions - - it("should mark as read", () => { - const { result } = render(); - result.current.markAsRead(new Event("click")); - expect(mocked(clearRoomNotification)).toHaveBeenCalledWith(room, matrixClient); - }); - - it("should mark as unread", () => { - const { result } = render(); - result.current.markAsUnread(new Event("click")); - expect(mocked(setMarkedUnreadState)).toHaveBeenCalledWith(room, matrixClient, true); - }); - - it("should tag a room as favourite", () => { - const { result } = render(); - result.current.toggleFavorite(new Event("click")); - expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.Favourite); - }); - - it("should tag a room as low priority", () => { - const { result } = render(); - result.current.toggleLowPriority(); - expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.LowPriority); - }); - - it("should dispatch invite action", () => { - const { result } = render(); - result.current.invite(new Event("click")); - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: "view_invite", - roomId: room.roomId, - }); - }); - - it("should dispatch a copy room action", () => { - const { result } = render(); - result.current.copyRoomLink(new Event("click")); - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: "copy_room", - room_id: room.roomId, - }); - }); - - it("should dispatch forget room action", () => { - // forget room is only available for archived rooms - room.tags = { [DefaultTagID.Archived]: { order: 0 } }; - - const { result } = render(); - result.current.leaveRoom(new Event("click")); - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: "forget_room", - room_id: room.roomId, - }); - }); - - it("should dispatch leave room action", () => { - const { result } = render(); - result.current.leaveRoom(new Event("click")); - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: "leave_room", - room_id: room.roomId, - }); - }); - - it("should call setRoomNotifState", () => { - const setRoomNotifState = jest.fn(); - mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, setRoomNotifState]); - const { result } = render(); - result.current.setRoomNotifState(RoomNotifState.Mute); - expect(setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); - }); -}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx deleted file mode 100644 index 96bc53016e..0000000000 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { renderHook, waitFor } from "jest-matrix-react"; -import { type Room } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; - -import dispatcher from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { useRoomListItemViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel"; -import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils"; -import { - hasAccessToNotificationMenu, - hasAccessToOptionsMenu, -} from "../../../../../src/components/viewmodels/roomlist/utils"; -import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; -import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; -import * as UseCallModule from "../../../../../src/hooks/useCall"; -import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore"; -import DMRoomMap from "../../../../../src/utils/DMRoomMap"; -import { useMessagePreviewToggle } from "../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle"; - -jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ - hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), - hasAccessToNotificationMenu: jest.fn().mockReturnValue(false), -})); - -jest.mock("../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle", () => ({ - useMessagePreviewToggle: jest.fn().mockReturnValue({ shouldShowMessagePreview: true }), -})); - -describe("RoomListItemViewModel", () => { - let room: Room; - - beforeEach(() => { - const matrixClient = createTestClient(); - room = mkStubRoom("roomId", "roomName", matrixClient); - - const dmRoomMap = { - getUserIdForRoomId: jest.fn(), - getDMRoomsForUserId: jest.fn(), - } as unknown as DMRoomMap; - DMRoomMap.setShared(dmRoomMap); - - mocked(useMessagePreviewToggle).mockReturnValue({ - shouldShowMessagePreview: false, - toggleMessagePreview: jest.fn(), - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it("should dispatch view room action on openRoom", async () => { - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - - const fn = jest.spyOn(dispatcher, "dispatch"); - vm.current.openRoom(); - expect(fn).toHaveBeenCalledWith( - expect.objectContaining({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "RoomList", - }), - ); - }); - - it("should show context menu if user has access to options menu", async () => { - mocked(hasAccessToOptionsMenu).mockReturnValue(true); - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showContextMenu).toBe(true); - }); - - it("should show hover menu if user has access to options menu", async () => { - mocked(hasAccessToOptionsMenu).mockReturnValue(true); - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showHoverMenu).toBe(true); - }); - - it("should show hover menu if user has access to notification menu", async () => { - mocked(hasAccessToNotificationMenu).mockReturnValue(true); - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showHoverMenu).toBe(true); - }); - - it("should not show hover menu if user has an invitation notification", async () => { - mocked(hasAccessToOptionsMenu).mockReturnValue(true); - - const notificationState = new RoomNotificationState(room, false); - jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); - jest.spyOn(notificationState, "invited", "get").mockReturnValue(false); - - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showHoverMenu).toBe(true); - }); - - it("should return a message preview if one is available and they are enabled", async () => { - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ - text: "Message look like this", - } as MessagePreview); - mocked(useMessagePreviewToggle).mockReturnValue({ - shouldShowMessagePreview: true, - toggleMessagePreview: jest.fn(), - }); - - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this")); - }); - - it("should hide message previews when disabled", async () => { - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ - text: "Message look like this", - } as MessagePreview); - - const { result: vm, rerender } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - - // This doesn't seem to test that the hook actually triggers an update, - // but I can't see how to test that. - rerender(); - - expect(vm.current.messagePreview).toBe(undefined); - }); - - it("should check message preview when room change", async () => { - const otherRoom = mkStubRoom("roomId2", "roomName2", room.client); - - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ - text: "Message look like this", - } as MessagePreview); - mocked(useMessagePreviewToggle).mockReturnValue({ - shouldShowMessagePreview: true, - toggleMessagePreview: jest.fn(), - }); - - const { result: vm, rerender } = renderHook((props) => useRoomListItemViewModel(props), { - initialProps: room, - ...withClientContextRenderOptions(room.client), - }); - await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this")); - - jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null); - rerender(otherRoom); - await waitFor(() => expect(vm.current.messagePreview).toBe(undefined)); - }); - - describe("notification", () => { - let notificationState: RoomNotificationState; - beforeEach(() => { - notificationState = new RoomNotificationState(room, false); - jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); - }); - - it("should show notification decoration if there is call has participant", () => { - jest.spyOn(UseCallModule, "useParticipantCount").mockReturnValue(1); - - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showNotificationDecoration).toBe(true); - }); - - it.each([ - { - label: "hasAnyNotificationOrActivity", - mock: () => jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true), - }, - { label: "muted", mock: () => jest.spyOn(notificationState, "muted", "get").mockReturnValue(true) }, - ])("should show notification decoration if $label=true", ({ mock }) => { - mock(); - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.showNotificationDecoration).toBe(true); - }); - - it("should be bold if there is a notification", () => { - jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); - - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.isBold).toBe(true); - }); - - it("should recompute notification state when room changes", () => { - const newRoom = mkStubRoom("room2", "Room 2", room.client); - const newNotificationState = new RoomNotificationState(newRoom, false); - - const { result, rerender } = renderHook((room) => useRoomListItemViewModel(room), { - ...withClientContextRenderOptions(room.client), - initialProps: room, - }); - - expect(result.current.showNotificationDecoration).toBe(false); - - jest.spyOn(newNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); - jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(newNotificationState); - rerender(newRoom); - - expect(result.current.showNotificationDecoration).toBe(true); - }); - }); - - describe("a11yLabel", () => { - let notificationState: RoomNotificationState; - beforeEach(() => { - notificationState = new RoomNotificationState(room, false); - jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); - }); - - it.each([ - { - label: "unsent message", - mock: () => jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true), - expected: "Open room roomName with an unsent message.", - }, - { - label: "invitation", - mock: () => jest.spyOn(notificationState, "invited", "get").mockReturnValue(true), - expected: "Open room roomName invitation.", - }, - { - label: "mention", - mock: () => { - jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true); - jest.spyOn(notificationState, "count", "get").mockReturnValue(3); - }, - expected: "Open room roomName with 3 unread messages including mentions.", - }, - { - label: "unread", - mock: () => { - jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(true); - jest.spyOn(notificationState, "count", "get").mockReturnValue(3); - }, - expected: "Open room roomName with 3 unread messages.", - }, - { - label: "default", - expected: "Open room roomName", - }, - ])("should return the $label label", ({ mock, expected }) => { - mock?.(); - const { result: vm } = renderHook( - () => useRoomListItemViewModel(room), - withClientContextRenderOptions(room.client), - ); - expect(vm.current.a11yLabel).toBe(expected); - }); - }); -}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx deleted file mode 100644 index c8ede64320..0000000000 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ /dev/null @@ -1,341 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { range } from "lodash"; -import { act, renderHook, waitFor } from "jest-matrix-react"; -import { mocked } from "jest-mock"; - -import RoomListStoreV3, { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list-v3/RoomListStoreV3"; -import { mkStubRoom } from "../../../../test-utils"; -import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; -import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters"; -import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils"; -import dispatcher from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; -import { UPDATE_SELECTED_SPACE } from "../../../../../src/stores/spaces"; - -jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ - hasCreateRoomRights: jest.fn().mockReturnValue(false), - createRoom: jest.fn(), -})); - -describe("RoomListViewModel", () => { - function mockAndCreateRooms() { - const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined)); - const fn = jest - .spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace") - .mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] })); - return { rooms, fn }; - } - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it("should return a list of rooms", async () => { - const { rooms } = mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - - expect(vm.current.roomsResult.rooms).toHaveLength(10); - for (const room of rooms) { - expect(vm.current.roomsResult.rooms).toContain(room); - } - }); - - it("should update list of rooms on event from room list store", async () => { - const { rooms } = mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - - const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined); - rooms.push(newRoom); - await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - - await waitFor(() => { - expect(vm.current.roomsResult.rooms).toContain(newRoom); - }); - }); - - describe("Filters", () => { - it("should provide list of available filters", () => { - mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - // should have 6 filters - expect(vm.current.primaryFilters).toHaveLength(7); - // check the order - for (const [i, name] of [ - "Unreads", - "People", - "Rooms", - "Favourites", - "Mentions", - "Invites", - "Low priority", - ].entries()) { - expect(vm.current.primaryFilters[i].name).toEqual(name); - expect(vm.current.primaryFilters[i].active).toEqual(false); - } - }); - - it("should get filtered rooms from RLS on toggle", () => { - const { fn } = mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - // Let's say we toggle the People toggle - const i = vm.current.primaryFilters.findIndex((f) => f.name === "People"); - act(() => { - vm.current.primaryFilters[i].toggle(); - }); - expect(fn).toHaveBeenCalledWith([FilterKey.PeopleFilter]); - expect(vm.current.primaryFilters[i].active).toEqual(true); - }); - - it("should change active property on toggle", () => { - mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - // Let's say we toggle the People filter - const i = vm.current.primaryFilters.findIndex((f) => f.name === "People"); - expect(vm.current.primaryFilters[i].active).toEqual(false); - act(() => { - vm.current.primaryFilters[i].toggle(); - }); - expect(vm.current.primaryFilters[i].active).toEqual(true); - - // Let's say that we toggle the Favourite filter - const j = vm.current.primaryFilters.findIndex((f) => f.name === "Favourites"); - act(() => { - vm.current.primaryFilters[j].toggle(); - }); - expect(vm.current.primaryFilters[i].active).toEqual(false); - expect(vm.current.primaryFilters[j].active).toEqual(true); - }); - - it("should return the current active primary filter", async () => { - // Let's say that the user's preferred sorting is alphabetic - mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - // Toggle people filter - const i = vm.current.primaryFilters.findIndex((f) => f.name === "People"); - expect(vm.current.primaryFilters[i].active).toEqual(false); - act(() => vm.current.primaryFilters[i].toggle()); - - // The active primary filter should be the People filter - expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]); - }); - - it("should not remove all filters when active space is changed", async () => { - mockAndCreateRooms(); - const { result: vm } = renderHook(() => useRoomListViewModel()); - - // Let's first toggle the People filter - const i = vm.current.primaryFilters.findIndex((f) => f.name === "People"); - act(() => { - vm.current.primaryFilters[i].toggle(); - }); - expect(vm.current.primaryFilters[i].active).toEqual(true); - - // Simulate a space change - await act(() => SpaceStore.instance.emit(UPDATE_SELECTED_SPACE)); - - // Primary filter should remain unchanged - expect(vm.current.activePrimaryFilter?.name).toEqual("People"); - }); - }); - - describe("Create room and chat", () => { - it("should be canCreateRoom=false if hasCreateRoomRights=false", () => { - mocked(hasCreateRoomRights).mockReturnValue(false); - const { result } = renderHook(() => useRoomListViewModel()); - expect(result.current.canCreateRoom).toBe(false); - }); - - it("should be canCreateRoom=true if hasCreateRoomRights=true", () => { - mocked(hasCreateRoomRights).mockReturnValue(true); - const { result } = renderHook(() => useRoomListViewModel()); - expect(result.current.canCreateRoom).toBe(true); - }); - - it("should call createRoom", () => { - const { result } = renderHook(() => useRoomListViewModel()); - result.current.createRoom(); - expect(mocked(createRoom)).toHaveBeenCalled(); - }); - - it("should dispatch Action.CreateChat", () => { - const spy = jest.spyOn(dispatcher, "fire"); - const { result } = renderHook(() => useRoomListViewModel()); - result.current.createChatRoom(); - expect(spy).toHaveBeenCalledWith(Action.CreateChat); - }); - }); - - describe("Sticky room and active index", () => { - function expectActiveRoom(vm: ReturnType, i: number, roomId: string) { - expect(vm.activeIndex).toEqual(i); - expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId); - } - - it("active index is calculated with the last opened room in a space", () => { - // Let's say there's two spaces: !space1:matrix.org and !space2:matrix.org - // Let's also say that the current active space is !space1:matrix.org - let currentSpace = "!space1:matrix.org"; - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => currentSpace); - - const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined)); - // Let's say all the rooms are in space1 - const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] }; - // Let's say all rooms with even index are in space 2 - const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) }; - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() => - currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2, - ); - - // Let's say that the room at index 4 is currently active - const roomId = rooms[4].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expect(vm.current.activeIndex).toEqual(4); - - // Let's say that space is changed to "!space2:matrix.org" - currentSpace = "!space2:matrix.org"; - // Let's say that room[6] is active in space 2 - const activeRoomIdInSpace2 = rooms[6].roomId; - jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockImplementation( - () => activeRoomIdInSpace2, - ); - act(() => { - RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT); - }); - - // Active index should be 3 even without the room change event. - expectActiveRoom(vm.current, 3, activeRoomIdInSpace2); - }); - - it("active room and active index are retained on order change", () => { - const { rooms } = mockAndCreateRooms(); - - // Let's say that the room at index 5 is active - const roomId = rooms[5].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expect(vm.current.activeIndex).toEqual(5); - - // Let's say that room at index 9 moves to index 5 - const room9 = rooms[9]; - rooms.splice(9, 1); - rooms.splice(5, 0, room9); - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - - // Active room index should still be 5 - expectActiveRoom(vm.current, 5, roomId); - - // Let's add 2 new rooms from index 0 - const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined); - const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined); - rooms.unshift(newRoom1, newRoom2); - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - - // Active room index should still be 5 - expectActiveRoom(vm.current, 5, roomId); - }); - - it("active room and active index are updated when another room is opened", () => { - const { rooms } = mockAndCreateRooms(); - const roomId = rooms[5].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expectActiveRoom(vm.current, 5, roomId); - - // Let's say that room at index 9 becomes active - const room = rooms[9]; - act(() => { - dispatcher.dispatch( - { - action: Action.ActiveRoomChanged, - oldRoomId: null, - newRoomId: room.roomId, - }, - true, - ); - }); - - // Active room index should change to reflect new room - expectActiveRoom(vm.current, 9, room.roomId); - }); - - it("active room and active index are updated when active index spills out of rooms array bounds", () => { - const { rooms } = mockAndCreateRooms(); - // Let's say that the room at index 5 is active - const roomId = rooms[5].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expectActiveRoom(vm.current, 5, roomId); - - // Let's say that we remove rooms from the start of the array - for (let i = 0; i < 4; ++i) { - // We should be able to do 4 deletions before we run out of rooms - rooms.splice(0, 1); - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - expectActiveRoom(vm.current, 5, roomId); - } - - // If we remove one more room from the start, there's not going to be enough rooms - // to maintain the active index. - rooms.splice(0, 1); - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - expectActiveRoom(vm.current, 0, roomId); - }); - - it("active room and active index are retained when rooms that appear after the active room are deleted", () => { - const { rooms } = mockAndCreateRooms(); - // Let's say that the room at index 5 is active - const roomId = rooms[5].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expectActiveRoom(vm.current, 5, roomId); - - // Let's say that we remove rooms from the start of the array - for (let i = 0; i < 4; ++i) { - // Deleting rooms after index 5 (active) should not update the active index - rooms.splice(6, 1); - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - expectActiveRoom(vm.current, 5, roomId); - } - }); - - it("active room index becomes undefined when active room is deleted", () => { - const { rooms } = mockAndCreateRooms(); - // Let's say that the room at index 5 is active - let roomId: string | null = rooms[5].roomId; - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expectActiveRoom(vm.current, 5, roomId); - - // Let's remove the active room (i.e room at index 5) - rooms.splice(5, 1); - roomId = null; - act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - expect(vm.current.activeIndex).toBeUndefined(); - }); - - it("active room index is initially undefined", () => { - mockAndCreateRooms(); - - // Let's say that there's no active room currently - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => null); - - const { result: vm } = renderHook(() => useRoomListViewModel()); - expect(vm.current.activeIndex).toEqual(undefined); - }); - }); -}); diff --git a/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts b/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts deleted file mode 100644 index 1ae8606697..0000000000 --- a/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { renderHook } from "jest-matrix-react"; -import { type Room } from "matrix-js-sdk/src/matrix"; -import { waitFor } from "@testing-library/dom"; - -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; -import dispatcher from "../../../../../src/dispatcher/dispatcher"; -import { mkStubRoom, stubClient } from "../../../../test-utils"; -import { useRoomListNavigation } from "../../../../../src/components/viewmodels/roomlist/useRoomListNavigation"; -import { Action } from "../../../../../src/dispatcher/actions"; -import DMRoomMap from "../../../../../src/utils/DMRoomMap"; -import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; -import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; - -describe("useRoomListNavigation", () => { - let rooms: Room[]; - - beforeEach(() => { - const matrixClient = stubClient(); - rooms = [ - mkStubRoom("room1", "Room 1", matrixClient), - mkStubRoom("room2", "Room 2", matrixClient), - mkStubRoom("room3", "Room 3", matrixClient), - ]; - - DMRoomMap.makeShared(matrixClient); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined); - jest.spyOn(dispatcher, "dispatch"); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("should navigate to the next room based on delta", async () => { - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); - - renderHook(() => useRoomListNavigation(rooms)); - dispatcher.dispatch({ - action: Action.ViewRoomDelta, - delta: 1, - unread: false, - }); - - await waitFor(() => - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: "room2", - show_room_tile: true, - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }), - ); - }); - - it("should navigate to the previous room based on delta", async () => { - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room2"); - - renderHook(() => useRoomListNavigation(rooms)); - dispatcher.dispatch({ - action: Action.ViewRoomDelta, - delta: -1, - unread: false, - }); - - await waitFor(() => - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: "room1", - show_room_tile: true, - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }), - ); - }); - - it("should wrap around to the first room when navigating past the last room", async () => { - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room3"); - - renderHook(() => useRoomListNavigation(rooms)); - dispatcher.dispatch({ - action: Action.ViewRoomDelta, - delta: 1, - unread: false, - }); - - await waitFor(() => - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: "room1", - show_room_tile: true, - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }), - ); - }); - - it("should wrap around to the last room when navigating before the first room", async () => { - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); - - renderHook(() => useRoomListNavigation(rooms)); - dispatcher.dispatch({ - action: Action.ViewRoomDelta, - delta: -1, - unread: false, - }); - - await waitFor(() => - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: "room3", - show_room_tile: true, - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }), - ); - }); - - it("should filter rooms to only unread when unread=true", async () => { - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); - jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation( - (room) => - ({ - isUnread: room.roomId !== "room1", - }) as RoomNotificationState, - ); - - renderHook(() => useRoomListNavigation(rooms)); - - dispatcher.dispatch({ - action: Action.ViewRoomDelta, - delta: 1, - unread: true, - }); - - await waitFor(() => - expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: "room2", - show_room_tile: true, - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }), - ); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx deleted file mode 100644 index 92466f685c..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; -import { EmptyRoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/EmptyRoomList"; -import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters"; - -describe("", () => { - let vm: RoomListViewState; - - beforeEach(() => { - vm = { - isLoadingRooms: false, - roomsResult: { spaceId: "home", rooms: [] }, - primaryFilters: [], - createRoom: jest.fn(), - createChatRoom: jest.fn(), - canCreateRoom: true, - activeIndex: undefined, - }; - }); - - test("should render the default placeholder when there is no filter", async () => { - const user = userEvent.setup(); - - const { asFragment } = render(); - expect(screen.getByText("No chats yet")).toBeInTheDocument(); - expect(asFragment()).toMatchSnapshot(); - - await user.click(screen.getByRole("button", { name: "Start chat" })); - expect(vm.createChatRoom).toHaveBeenCalled(); - - await user.click(screen.getByRole("button", { name: "New room" })); - expect(vm.createRoom).toHaveBeenCalled(); - }); - - test("should not render the new room button if the user doesn't have the rights to create a room", async () => { - const newState = { ...vm, canCreateRoom: false }; - - const { asFragment } = render(); - expect(screen.queryByRole("button", { name: "New room" })).toBeNull(); - expect(asFragment()).toMatchSnapshot(); - }); - - it.each([ - { key: FilterKey.UnreadFilter, name: "unread", action: "Show all chats" }, - { key: FilterKey.MentionsFilter, name: "mention", action: "See all activity" }, - { key: FilterKey.InvitesFilter, name: "invite", action: "See all activity" }, - { key: FilterKey.LowPriorityFilter, name: "low priority", action: "See all activity" }, - ])("should display the empty state for the $name filter", async ({ key, name, action }) => { - const user = userEvent.setup(); - const activePrimaryFilter = { - toggle: jest.fn(), - active: true, - name, - key, - }; - const newState = { - ...vm, - activePrimaryFilter, - }; - - const { asFragment } = render(); - await user.click(screen.getByRole("button", { name: action })); - expect(activePrimaryFilter.toggle).toHaveBeenCalled(); - expect(asFragment()).toMatchSnapshot(); - }); - - it.each([ - { key: FilterKey.FavouriteFilter, name: "favourite" }, - { key: FilterKey.PeopleFilter, name: "people" }, - { key: FilterKey.RoomsFilter, name: "rooms" }, - ])("should display empty state for filter $name", ({ name, key }) => { - const activePrimaryFilter = { - toggle: jest.fn(), - active: true, - name, - key, - }; - const newState = { ...vm, activePrimaryFilter }; - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx deleted file mode 100644 index fa7b351bea..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { render } from "jest-matrix-react"; -import { fireEvent } from "@testing-library/dom"; -import { VirtuosoMockContext } from "@element-hq/web-shared-components"; - -import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; -import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList"; -import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation"; -import { mkRoom, stubClient } from "../../../../../test-utils"; - -describe("", () => { - let matrixClient: MatrixClient; - let vm: RoomListViewState; - - beforeEach(() => { - matrixClient = stubClient(); - const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`)); - vm = { - isLoadingRooms: false, - roomsResult: { spaceId: "home", rooms }, - primaryFilters: [], - createRoom: jest.fn(), - createChatRoom: jest.fn(), - canCreateRoom: true, - activeIndex: undefined, - }; - - // Needed to render a room list cell - DMRoomMap.makeShared(matrixClient); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined); - }); - - it("should render a room list", () => { - const { asFragment } = render(, { - wrapper: ({ children }) => ( - - - <>{children} - - - ), - }); - // At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]". - // This is a general issue with the react-virtuoso library. - // TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281 - expect(asFragment()).toMatchSnapshot(); - }); - - it.each([ - { shortcut: { key: "F6", ctrlKey: true, shiftKey: true }, isPreviousLandmark: true, label: "PreviousLandmark" }, - { shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" }, - ])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => { - const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue(); - const { getByTestId } = render(, { - wrapper: ({ children }) => ( - - - <>{children} - - - ), - }); - const roomList = getByTestId("room-list"); - fireEvent.keyDown(roomList, shortcut); - - expect(spyFindLandmark).toHaveBeenCalledWith(Landmark.ROOM_LIST, isPreviousLandmark); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx deleted file mode 100644 index 58ab0c672b..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { mocked } from "jest-mock"; -import { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { - type RoomListItemMenuViewState, - useRoomListItemMenuViewModel, -} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel"; -import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; -import { mkRoom, stubClient } from "../../../../../test-utils"; -import { RoomListItemMenuView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemMenuView"; -import { RoomNotifState } from "../../../../../../src/RoomNotifs"; - -jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel", () => ({ - useRoomListItemMenuViewModel: jest.fn(), -})); - -describe("", () => { - const defaultValue: RoomListItemMenuViewState = { - showMoreOptionsMenu: true, - showNotificationMenu: true, - isFavourite: true, - isLowPriority: true, - canInvite: true, - canMarkAsUnread: true, - canMarkAsRead: true, - canCopyRoomLink: true, - isNotificationAllMessage: true, - isNotificationMentionOnly: true, - isNotificationAllMessageLoud: true, - isNotificationMute: true, - copyRoomLink: jest.fn(), - markAsUnread: jest.fn(), - markAsRead: jest.fn(), - leaveRoom: jest.fn(), - toggleLowPriority: jest.fn(), - toggleFavorite: jest.fn(), - invite: jest.fn(), - setRoomNotifState: jest.fn(), - }; - - let matrixClient: MatrixClient; - let room: Room; - - beforeEach(() => { - mocked(useRoomListItemMenuViewModel).mockReturnValue(defaultValue); - matrixClient = stubClient(); - room = mkRoom(matrixClient, "room1"); - }); - - function renderMenu() { - return render(); - } - - it("should render the more options menu", () => { - const { asFragment } = renderMenu(); - expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should render the notification options menu", () => { - const { asFragment } = renderMenu(); - expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should not render the more options menu when showMoreOptionsMenu is false", () => { - mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showMoreOptionsMenu: false }); - renderMenu(); - expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); - }); - - it("should not render the notification options menu when showNotificationMenu is false", () => { - mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showNotificationMenu: false }); - renderMenu(); - expect(screen.queryByRole("button", { name: "Notification options" })).toBeNull(); - }); - - it("should display all the buttons and have the actions linked for the more options menu", async () => { - const user = userEvent.setup(); - renderMenu(); - - const openMenu = screen.getByRole("button", { name: "More Options" }); - await user.click(openMenu); - - await user.click(screen.getByRole("menuitem", { name: "Mark as read" })); - expect(defaultValue.markAsRead).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Mark as unread" })); - expect(defaultValue.markAsUnread).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitemcheckbox", { name: "Favourited" })); - expect(defaultValue.toggleFavorite).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitemcheckbox", { name: "Low priority" })); - expect(defaultValue.toggleLowPriority).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Invite" })); - expect(defaultValue.invite).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Copy room link" })); - expect(defaultValue.copyRoomLink).toHaveBeenCalled(); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Leave room" })); - expect(defaultValue.leaveRoom).toHaveBeenCalled(); - }); - - it("should display all the buttons and have the actions linked for the notification options menu", async () => { - const user = userEvent.setup(); - renderMenu(); - - const openMenu = screen.getByRole("button", { name: "Notification options" }); - await user.click(openMenu); - - await user.click(screen.getByRole("menuitem", { name: "Match default settings" })); - expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "All messages" })); - expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Mentions and keywords" })); - expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly); - - await user.click(openMenu); - await user.click(screen.getByRole("menuitem", { name: "Mute room" })); - expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx deleted file mode 100644 index b6127e1189..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; -import { render, screen, waitFor } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; - -import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils"; -import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView"; -import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; -import { - type RoomListItemViewState, - useRoomListItemViewModel, -} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel"; -import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState"; - -jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel", () => ({ - useRoomListItemViewModel: jest.fn(), -})); - -describe("", () => { - let defaultValue: RoomListItemViewState; - let matrixClient: MatrixClient; - let room: Room; - - const renderRoomListItem = (props: Partial> = {}) => { - const defaultProps = { - room, - isSelected: false, - isFocused: false, - onFocus: jest.fn(), - roomIndex: 0, - roomCount: 1, - listIsScrolling: false, - }; - - return render(, withClientContextRenderOptions(matrixClient)); - }; - - beforeEach(() => { - matrixClient = stubClient(); - room = mkRoom(matrixClient, "room1"); - - DMRoomMap.makeShared(matrixClient); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined); - - const notificationState = new RoomNotificationState(room, false); - jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); - jest.spyOn(notificationState, "isNotification", "get").mockReturnValue(true); - jest.spyOn(notificationState, "count", "get").mockReturnValue(1); - - defaultValue = { - openRoom: jest.fn(), - showContextMenu: false, - showHoverMenu: false, - notificationState, - a11yLabel: "Open room room1", - isBold: false, - isVideoRoom: false, - callConnectionState: null, - callType: CallType.Video, - hasParticipantInCall: false, - name: room.name, - showNotificationDecoration: false, - messagePreview: undefined, - }; - - mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); - }); - - test("should render a room item", () => { - const onClick = jest.fn(); - const { asFragment } = renderRoomListItem({ - onClick, - roomCount: 0, - }); - expect(asFragment()).toMatchSnapshot(); - }); - - test("should render a room item with a message preview", () => { - defaultValue.messagePreview = "The message looks like this"; - - const onClick = jest.fn(); - const { asFragment } = renderRoomListItem({ - onClick, - }); - expect(asFragment()).toMatchSnapshot(); - }); - - test("should call openRoom when clicked", async () => { - const user = userEvent.setup(); - renderRoomListItem(); - - await user.click(screen.getByRole("option", { name: `Open room ${room.name}` })); - expect(defaultValue.openRoom).toHaveBeenCalled(); - }); - - test("should be selected if isSelected=true", async () => { - const { asFragment } = renderRoomListItem({ - isSelected: true, - }); - - expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute( - "aria-selected", - "true", - ); - expect(asFragment()).toMatchSnapshot(); - }); - - test("should display notification decoration", async () => { - mocked(useRoomListItemViewModel).mockReturnValue({ - ...defaultValue, - showNotificationDecoration: true, - }); - - const { asFragment } = renderRoomListItem(); - - expect(screen.getByTestId("notification-decoration")).toBeInTheDocument(); - expect(asFragment()).toMatchSnapshot(); - }); - - test("should not display notification decoration when hovered", async () => { - const user = userEvent.setup(); - - mocked(useRoomListItemViewModel).mockReturnValue({ - ...defaultValue, - showNotificationDecoration: true, - }); - - renderRoomListItem(); - - const listItem = screen.getByRole("option", { name: `Open room ${room.name}` }); - await user.hover(listItem); - - expect(screen.queryByRole("notification-decoration")).toBeNull(); - }); - - test("should render the context menu", async () => { - const user = userEvent.setup(); - - mocked(useRoomListItemViewModel).mockReturnValue({ - ...defaultValue, - showContextMenu: true, - }); - - renderRoomListItem(); - - const button = screen.getByRole("option", { name: `Open room ${room.name}` }); - await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]); - await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); - // Menu should close - await user.keyboard("{Escape}"); - expect(screen.queryByRole("menu")).toBeNull(); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx deleted file mode 100644 index 8276c7340f..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React, { act } from "react"; -import { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; -import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters"; -import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters"; - -describe("", () => { - let vm: RoomListViewState; - const filterToggleMocks = [jest.fn(), jest.fn(), jest.fn()]; - - let resizeCallback: ResizeObserverCallback; - - beforeEach(() => { - // Reset mocks between tests - filterToggleMocks.forEach((mock) => mock.mockClear()); - - // Mock ResizeObserver - global.ResizeObserver = jest.fn().mockImplementation((callback) => { - resizeCallback = callback; - return { - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), - }; - }); - - vm = { - primaryFilters: [ - { name: "People", active: true, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter }, - { name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter }, - { name: "Unreads", active: false, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter }, - ], - } as unknown as RoomListViewState; - }); - - function mockFiltersOffsetLeft() { - // Use `getByText` instead of `getByRole` to bypass the aria-hidden - jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); - jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); - jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60); - - // @ts-ignore - act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }])); - } - - it("should renders all filters correctly", () => { - const { asFragment } = render(); - mockFiltersOffsetLeft(); - - // Check that all filters are rendered - expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument(); - expect(screen.getByRole("option", { name: "Rooms" })).toBeInTheDocument(); - expect(screen.getByRole("option", { name: "Unreads" })).toBeInTheDocument(); - - // Check that the active filter is marked as selected - expect(screen.getByRole("option", { name: "People" })).toHaveAttribute("aria-selected", "true"); - expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "false"); - expect(screen.getByRole("option", { name: "Unreads" })).toHaveAttribute("aria-selected", "false"); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("should call toggle function when a filter is clicked", async () => { - const user = userEvent.setup(); - render(); - mockFiltersOffsetLeft(); - - // Click on an inactive filter - await user.click(screen.getByRole("option", { name: "People" })); - - // Check that the toggle function was called - expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1); - }); - - function makeUnreadWrapping() { - // Use `getByText` instead of `getByRole` to bypass the aria-hidden - jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); - jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); - // Unreads is wrapping - jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0); - - // @ts-ignore - act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }])); - } - - it("should hide or display filters if they are wrapping", async () => { - const user = userEvent.setup(); - render(); - mockFiltersOffsetLeft(); - - // No filter is wrapping, so chevron shouldn't be visible - expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull(); - expect(screen.queryByRole("option", { name: "Unreads" })).toBeVisible(); - - makeUnreadWrapping(); - - // The Unreads filter is wrapping, it should not be visible - expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); - // Now filters are wrapping, so chevron should be visible - await user.click(screen.getByRole("button", { name: "Expand filter list" })); - // The list is expanded, so Unreads should be visible - expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible(); - }); - - it("should move the active filter if the list is collapsed and the filter is wrapping", async () => { - vm = { - primaryFilters: [ - { name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter }, - { name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter }, - { name: "Unreads", active: true, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter }, - ], - } as unknown as RoomListViewState; - - const user = userEvent.setup(); - render(); - makeUnreadWrapping(); - - // Unread filter should be moved to the first position - expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).toBe( - screen.getByRole("option", { name: "Unreads" }), - ); - - // When the list is expanded, the Unreads filter should move to its original position - await user.click(screen.getByRole("button", { name: "Expand filter list" })); - expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).not.toEqual( - screen.getByRole("option", { name: "Unreads" }), - ); - }); - - it("should hide the filter is the previous is on the same vertical position", async () => { - render(); - mockFiltersOffsetLeft(); - - jest.spyOn(screen.getByRole("option", { name: "People" }), "offsetLeft", "get").mockReturnValue(0); - // Rooms is wrapping - jest.spyOn(screen.getByRole("option", { name: "Rooms" }), "offsetLeft", "get").mockReturnValue(0); - - // @ts-ignore - act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }])); - - // The Unreads filter is wrapping, it should not be visible - expect(screen.queryByRole("option", { name: "Rooms" })).toBeNull(); - // Now filters are wrapping, so chevron should be visible - expect(screen.getByRole("button", { name: "Expand filter list" })).toBeVisible(); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx deleted file mode 100644 index 0081c6f350..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { mocked } from "jest-mock"; -import { render, screen } from "jest-matrix-react"; -import React from "react"; - -import { - type RoomListViewState, - useRoomListViewModel, -} from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; -import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListView"; -import { mkRoom, stubClient } from "../../../../../test-utils"; - -jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewModel", () => ({ - useRoomListViewModel: jest.fn(), -})); - -describe("", () => { - const defaultValue: RoomListViewState = { - isLoadingRooms: false, - roomsResult: { spaceId: "home", rooms: [] }, - primaryFilters: [], - createRoom: jest.fn(), - createChatRoom: jest.fn(), - canCreateRoom: true, - activeIndex: undefined, - }; - const matrixClient = stubClient(); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should render the loading room list", () => { - mocked(useRoomListViewModel).mockReturnValue({ - ...defaultValue, - isLoadingRooms: true, - }); - - const roomList = render(); - expect(roomList.container.querySelector(".mx_RoomListSkeleton")).not.toBeNull(); - }); - - it("should render an empty room list", () => { - mocked(useRoomListViewModel).mockReturnValue(defaultValue); - - render(); - expect(screen.getByText("No chats yet")).toBeInTheDocument(); - }); - - it("should render a room list", () => { - mocked(useRoomListViewModel).mockReturnValue({ - ...defaultValue, - roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] }, - }); - - render(); - expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument(); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap deleted file mode 100644 index 140e1f366b..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap +++ /dev/null @@ -1,279 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` should display empty state for filter favourite 1`] = ` - -
- - You don't have favourite chats yet - - - You can add a chat to your favourites in the chat settings - -
-
-`; - -exports[` should display empty state for filter people 1`] = ` - -
- - You don’t have direct chats with anyone yet - - - You can deselect filters in order to see your other chats - -
-
-`; - -exports[` should display empty state for filter rooms 1`] = ` - -
- - You’re not in any room yet - - - You can deselect filters in order to see your other chats - -
-
-`; - -exports[` should display the empty state for the invite filter 1`] = ` - -
- - You don't have any unread invites - - -
-
-`; - -exports[` should display the empty state for the low priority filter 1`] = ` - -
- - You don't have any low priority rooms - - -
-
-`; - -exports[` should display the empty state for the mention filter 1`] = ` - -
- - You don't have any unread mentions - - -
-
-`; - -exports[` should display the empty state for the unread filter 1`] = ` - -
- - Congrats! You don’t have any unread messages - - -
-
-`; - -exports[` should not render the new room button if the user doesn't have the rights to create a room 1`] = ` - -
- - No chats yet - - - Get started by messaging someone - -
- -
-
-
-`; - -exports[` should render the default placeholder when there is no filter 1`] = ` - -
- - No chats yet - - - Get started by messaging someone or by creating a room - -
- - -
-
-
-`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap deleted file mode 100644 index eb833e64fa..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ /dev/null @@ -1,1255 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` should render a room list 1`] = ` - -
-
-
-
- - - -
-
-
-
- - - -
-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
-`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap deleted file mode 100644 index 8842b91e6f..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap +++ /dev/null @@ -1,155 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` should render the more options menu 1`] = ` - -
- - -
-
-`; - -exports[` should render the notification options menu 1`] = ` - -
- - -
-
-`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap deleted file mode 100644 index f46588370f..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap +++ /dev/null @@ -1,234 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` should be selected if isSelected=true 1`] = ` - - - -`; - -exports[` should display notification decoration 1`] = ` - - - -`; - -exports[` should render a room item 1`] = ` - - - -`; - -exports[` should render a room item with a message preview 1`] = ` - - - -`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPrimaryFilters-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPrimaryFilters-test.tsx.snap deleted file mode 100644 index ec71f70c95..0000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPrimaryFilters-test.tsx.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[` should renders all filters correctly 1`] = ` - -
-
- - - -
-
-
-`; diff --git a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 5c73e6c8ef..7025abe7c1 100644 --- a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -23,8 +23,8 @@ import { showSpacePreferences, showSpaceSettings, } from "../../../src/utils/space"; -import { createRoom, hasCreateRoomRights } from "../../../src/components/viewmodels/roomlist/utils"; import { createTestClient, mkSpace } from "../../test-utils"; +import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils"; jest.mock("../../../src/PosthogTrackers", () => ({ trackInteraction: jest.fn(), @@ -38,7 +38,7 @@ jest.mock("../../../src/utils/space", () => ({ showSpaceSettings: jest.fn(), })); -jest.mock("../../../src/components/viewmodels/roomlist/utils", () => ({ +jest.mock("../../../src/viewmodels/room-list/utils", () => ({ createRoom: jest.fn(), hasCreateRoomRights: jest.fn(), })); diff --git a/test/viewmodels/room-list/RoomListItemViewModel-test.tsx b/test/viewmodels/room-list/RoomListItemViewModel-test.tsx new file mode 100644 index 0000000000..873f786d1b --- /dev/null +++ b/test/viewmodels/room-list/RoomListItemViewModel-test.tsx @@ -0,0 +1,439 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type MatrixClient, type MatrixEvent, Room, RoomEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix"; +import { CallType } from "matrix-js-sdk/src/webrtc/call"; + +import { createTestClient, flushPromises } from "../../test-utils"; +import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState"; +import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore"; +import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState"; +import { type MessagePreview, MessagePreviewStore } from "../../../src/stores/room-list/MessagePreviewStore"; +import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; +import SettingsStore from "../../../src/settings/SettingsStore"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { DefaultTagID } from "../../../src/stores/room-list/models"; +import dispatcher from "../../../src/dispatcher/dispatcher"; +import { Action } from "../../../src/dispatcher/actions"; +import { CallStore } from "../../../src/stores/CallStore"; +import type { Call } from "../../../src/models/Call"; +import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel"; + +jest.mock("../../../src/viewmodels/room-list/utils", () => ({ + hasAccessToOptionsMenu: jest.fn().mockReturnValue(true), + hasAccessToNotificationMenu: jest.fn().mockReturnValue(true), +})); + +jest.mock("../../../src/stores/CallStore", () => ({ + __esModule: true, + CallStore: { + instance: { + getCall: jest.fn(), + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + }, + CallStoreEvent: { + ConnectedCalls: "connected_calls", + }, +})); + +describe("RoomListItemViewModel", () => { + let matrixClient: MatrixClient; + let room: Room; + let notificationState: RoomNotificationState; + let viewModel: RoomListItemViewModel; + + beforeEach(() => { + matrixClient = createTestClient(); + room = new Room("!room:server", matrixClient, matrixClient.getSafeUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + // Set room name + room.name = "Test Room"; + + notificationState = new RoomNotificationState(room, false); + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); + + const dmRoomMap = { + getUserIdForRoomId: jest.fn().mockReturnValue(undefined), + } as unknown as DMRoomMap; + DMRoomMap.setShared(dmRoomMap); + + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.showMessagePreview") return false; + return false; + }); + jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id"); + + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null); + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null); + }); + + afterEach(() => { + viewModel?.dispose(); + jest.restoreAllMocks(); + }); + + describe("Initialization", () => { + it("should initialize with room data", async () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + // Wait for async initialization + await flushPromises(); + + const snapshot = viewModel.getSnapshot(); + expect(snapshot.id).toBe("!room:server"); + expect(snapshot.name).toBe("Test Room"); + }); + + it("should load message preview when enabled", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Hello world!", + } as MessagePreview); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + // Wait for async message preview load + await flushPromises(); + + expect(viewModel.getSnapshot().messagePreview).toBe("Hello world!"); + }); + + it("should not load message preview when disabled", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().messagePreview).toBeUndefined(); + }); + }); + + describe("Notification state", () => { + it("should reflect notification state", async () => { + jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); + jest.spyOn(notificationState, "count", "get").mockReturnValue(5); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + const snapshot = viewModel.getSnapshot(); + expect(snapshot.notification.hasAnyNotificationOrActivity).toBe(true); + expect(snapshot.notification.count).toBe(5); + }); + + it("should update when notification state changes", async () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + expect(viewModel.getSnapshot().notification.count).toBe(0); + + jest.spyOn(notificationState, "count", "get").mockReturnValue(3); + notificationState.emit(NotificationStateEvents.Update); + + await flushPromises(); + expect(viewModel.getSnapshot().notification.count).toBe(3); + }); + + it("should show bold text when has notifications", async () => { + jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().isBold).toBe(true); + }); + + it("should show mention badge", async () => { + jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().notification.isMention).toBe(true); + }); + + it("should show invitation state", async () => { + jest.spyOn(notificationState, "invited", "get").mockReturnValue(true); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().notification.invited).toBe(true); + }); + }); + + describe("Message preview", () => { + it("should update message preview when store emits update", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Initial message", + } as MessagePreview); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + expect(viewModel.getSnapshot().messagePreview).toBe("Initial message"); + + // Update preview + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Updated message", + } as MessagePreview); + + MessagePreviewStore.instance.emit(UPDATE_EVENT); + + await flushPromises(); + expect(viewModel.getSnapshot().messagePreview).toBe("Updated message"); + }); + + it("should show/hide preview when setting changes", async () => { + let showPreview = false; + let watchCallback: any; + + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => showPreview); + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_setting, _room, callback) => { + watchCallback = callback; + return "watcher-id"; + }); + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Test message", + } as MessagePreview); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + expect(viewModel.getSnapshot().messagePreview).toBeUndefined(); + + // Enable previews + showPreview = true; + watchCallback(null, "device", true); + + await flushPromises(); + expect(viewModel.getSnapshot().messagePreview).toBe("Test message"); + }); + }); + + describe("Room tags", () => { + it("should reflect favorite tag", async () => { + room.tags = { [DefaultTagID.Favourite]: { order: 0 } }; + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().isFavourite).toBe(true); + }); + + it("should reflect low priority tag", async () => { + room.tags = { [DefaultTagID.LowPriority]: { order: 0 } }; + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().isLowPriority).toBe(true); + }); + + it("should update when room tags change", async () => { + room.tags = {}; + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + expect(viewModel.getSnapshot().isFavourite).toBe(false); + + room.tags = { [DefaultTagID.Favourite]: { order: 0 } }; + const tagEvent = { + getContent: () => ({ tags: { [DefaultTagID.Favourite]: { order: 0 } } }), + } as MatrixEvent; + room.emit(RoomEvent.Tags, tagEvent, room); + + await flushPromises(); + expect(viewModel.getSnapshot().isFavourite).toBe(true); + }); + }); + + describe("Call state", () => { + it("should show voice call indicator", async () => { + const mockCall = { + callType: CallType.Voice, + participants: new Map([[matrixClient.getUserId()!, {}]]), + } as unknown as Call; + + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().notification.callType).toBe("voice"); + }); + + it("should show video call indicator", async () => { + const mockCall = { + callType: CallType.Video, + participants: new Map([[matrixClient.getUserId()!, {}]]), + } as unknown as Call; + + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().notification.callType).toBe("video"); + }); + + it("should not show call indicator when no participants", async () => { + const mockCall = { + callType: CallType.Voice, + participants: new Map(), + } as unknown as Call; + + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().notification.callType).toBeUndefined(); + }); + }); + + describe("Room name updates", () => { + it("should update when room name changes", async () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + expect(viewModel.getSnapshot().name).toBe("Test Room"); + + room.name = "Updated Room"; + room.emit(RoomEvent.Name, room); + + await flushPromises(); + expect(viewModel.getSnapshot().name).toBe("Updated Room"); + }); + }); + + describe("DM detection", () => { + it("should detect DM rooms", async () => { + const dmRoomMap = DMRoomMap.shared(); + jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue("@user:server"); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + // DM rooms should not show copy room link option + expect(viewModel.getSnapshot().canCopyRoomLink).toBe(false); + }); + + it("should detect non-DM rooms", async () => { + const dmRoomMap = DMRoomMap.shared(); + jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue(undefined); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + await flushPromises(); + + expect(viewModel.getSnapshot().canCopyRoomLink).toBe(true); + }); + }); + + describe("Actions", () => { + it("should dispatch view room action on openRoom", () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.onOpenRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.ViewRoom, + room_id: "!room:server", + metricsTrigger: "RoomList", + }); + }); + + it("should return room object", () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + expect(viewModel.getSnapshot().room).toBe(room); + }); + + it("should dispatch view_invite action when onInvite is called", () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.onInvite(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: "view_invite", + roomId: "!room:server", + }); + }); + + it("should dispatch copy_room action when onCopyRoomLink is called", () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.onCopyRoomLink(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: "copy_room", + room_id: "!room:server", + }); + }); + + it("should dispatch leave_room action when onLeaveRoom is called for normal room", () => { + room.tags = {}; + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.onLeaveRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: "leave_room", + room_id: "!room:server", + }); + }); + + it("should dispatch forget_room action when onLeaveRoom is called for archived room", () => { + room.tags = { [DefaultTagID.Archived]: { order: 0 } }; + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.onLeaveRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: "forget_room", + room_id: "!room:server", + }); + }); + }); + + describe("Cleanup", () => { + it("should unsubscribe from all events on dispose", () => { + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + + const offSpy = jest.spyOn(notificationState, "off"); + + viewModel.dispose(); + + expect(offSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/viewmodels/room-list/RoomListViewViewModel-test.tsx b/test/viewmodels/room-list/RoomListViewViewModel-test.tsx new file mode 100644 index 0000000000..c896d3111d --- /dev/null +++ b/test/viewmodels/room-list/RoomListViewViewModel-test.tsx @@ -0,0 +1,546 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils"; +import RoomListStoreV3, { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3"; +import SpaceStore from "../../../src/stores/spaces/SpaceStore"; +import { FilterKey } from "../../../src/stores/room-list-v3/skip-list/filters"; +import dispatcher from "../../../src/dispatcher/dispatcher"; +import { Action } from "../../../src/dispatcher/actions"; +import { SdkContextClass } from "../../../src/contexts/SDKContext"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { RoomListViewViewModel } from "../../../src/viewmodels/room-list/RoomListViewViewModel"; +import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils"; + +jest.mock("../../../src/viewmodels/room-list/utils", () => ({ + hasCreateRoomRights: jest.fn().mockReturnValue(false), + hasAccessToOptionsMenu: jest.fn().mockReturnValue(true), + hasAccessToNotificationMenu: jest.fn().mockReturnValue(true), +})); + +describe("RoomListViewViewModel", () => { + let matrixClient: MatrixClient; + let room1: Room; + let room2: Room; + let room3: Room; + let viewModel: RoomListViewViewModel; + + beforeEach(() => { + matrixClient = createTestClient(); + room1 = mkStubRoom("!room1:server", "Room 1", matrixClient); + room2 = mkStubRoom("!room2:server", "Room 2", matrixClient); + room3 = mkStubRoom("!room3:server", "Room 3", matrixClient); + + // Setup DMRoomMap + const dmRoomMap = { + getUserIdForRoomId: jest.fn().mockReturnValue(null), + } as unknown as DMRoomMap; + DMRoomMap.setShared(dmRoomMap); + + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room1, room2, room3], + }); + + jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); + jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null); + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); + + mocked(hasCreateRoomRights).mockReturnValue(false); + }); + + afterEach(() => { + viewModel?.dispose(); + jest.restoreAllMocks(); + }); + + describe("Initialization", () => { + it("should initialize with correct snapshot", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const snapshot = viewModel.getSnapshot(); + expect(snapshot.roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]); + expect(snapshot.isRoomListEmpty).toBe(false); + expect(snapshot.isLoadingRooms).toBe(false); + expect(snapshot.roomListState.spaceId).toBe("home"); + expect(snapshot.filterIds.length).toBeGreaterThan(0); + expect(snapshot.activeFilterId).toBeUndefined(); + }); + + it("should initialize with empty room list", () => { + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [], + }); + + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + expect(viewModel.getSnapshot().roomIds).toEqual([]); + expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true); + }); + + it("should set canCreateRoom based on user rights", () => { + mocked(hasCreateRoomRights).mockReturnValue(true); + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + expect(viewModel.getSnapshot().canCreateRoom).toBe(true); + }); + }); + + describe("Room list updates", () => { + it("should update room list when ListsUpdate event fires", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient); + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room1, room2, room3, newRoom], + }); + + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(viewModel.getSnapshot().roomIds).toEqual([ + "!room1:server", + "!room2:server", + "!room3:server", + "!room4:server", + ]); + }); + + it("should update loading state when ListsLoaded event fires", () => { + jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true); + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + expect(viewModel.getSnapshot().isLoadingRooms).toBe(true); + + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsLoaded); + + expect(viewModel.getSnapshot().isLoadingRooms).toBe(false); + }); + }); + + describe("Space switching", () => { + it("should update room list when space changes", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const spaceRoomList = [room1, room2]; + + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "!space:server", + rooms: spaceRoomList, + }); + + jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue("!room1:server"); + + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(viewModel.getSnapshot().roomListState.spaceId).toBe("!space:server"); + expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server"]); + }); + + it("should clear view models when space changes", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + // Get view models for visible rooms + const vm1 = viewModel.getRoomItemViewModel("!room1:server"); + const vm2 = viewModel.getRoomItemViewModel("!room2:server"); + + const disposeSpy1 = jest.spyOn(vm1, "dispose"); + const disposeSpy2 = jest.spyOn(vm2, "dispose"); + + // Change space + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "!space:server", + rooms: [room3], + }); + + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(disposeSpy1).toHaveBeenCalled(); + expect(disposeSpy2).toHaveBeenCalled(); + }); + }); + + describe("Active room tracking", () => { + it("should update active room index when room is selected", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); + + dispatcher.dispatch({ + action: Action.ActiveRoomChanged, + oldRoomId: "!room1:server", + newRoomId: "!room2:server", + }); + + // Use setTimeout to allow the dispatcher callback to run + await flushPromises(); + expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1); + }); + + it("should return undefined active room index when no room is selected", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); + + dispatcher.dispatch({ + action: Action.ActiveRoomChanged, + oldRoomId: "!room1:server", + newRoomId: null, + }); + + // Use setTimeout to allow the dispatcher callback to run + await flushPromises(); + expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBeUndefined(); + }); + }); + + describe("Sticky room behavior", () => { + it("should keep selected room at same index when room list updates", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + // Select room at index 1 + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); + dispatcher.dispatch({ + action: Action.ActiveRoomChanged, + newRoomId: "!room2:server", + }); + + await flushPromises(); + expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1); + + // Simulate room list update that would move room2 to front + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room2, room1, room3], // room2 moved to front + }); + + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + // Active room should still be at index 1 (sticky behavior) + expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1); + expect(viewModel.getSnapshot().roomIds[1]).toBe("!room2:server"); + }); + + it("should not apply sticky behavior when user changes rooms", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + // Select room at index 1 + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); + dispatcher.dispatch({ + action: Action.ActiveRoomChanged, + newRoomId: "!room2:server", + }); + + await flushPromises(); + + // User switches to room3 + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room3:server"); + dispatcher.dispatch({ + action: Action.ActiveRoomChanged, + oldRoomId: "!room2:server", + newRoomId: "!room3:server", + }); + + await flushPromises(); + expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(2); + }); + }); + + describe("Filters", () => { + it("should toggle filter on", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + expect(viewModel.getSnapshot().activeFilterId).toBeUndefined(); + + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room1], + filterKeys: [FilterKey.UnreadFilter], + }); + + viewModel.onToggleFilter("unread"); + + expect(viewModel.getSnapshot().activeFilterId).toBe("unread"); + expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server"]); + }); + + it("should toggle filter off", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + // Turn filter on + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room1], + filterKeys: [FilterKey.UnreadFilter], + }); + viewModel.onToggleFilter("unread"); + + expect(viewModel.getSnapshot().activeFilterId).toBe("unread"); + + // Turn filter off + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room1, room2, room3], + }); + viewModel.onToggleFilter("unread"); + + expect(viewModel.getSnapshot().activeFilterId).toBeUndefined(); + expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]); + }); + + it("should clear view models when filter changes", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + // Get view models + const vm1 = viewModel.getRoomItemViewModel("!room1:server"); + const disposeSpy = jest.spyOn(vm1, "dispose"); + + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + rooms: [room2], + filterKeys: [FilterKey.UnreadFilter], + }); + + viewModel.onToggleFilter("unread"); + + expect(disposeSpy).toHaveBeenCalled(); + }); + }); + + describe("Room item view models", () => { + it("should create room item view model on demand", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const itemViewModel = viewModel.getRoomItemViewModel("!room1:server"); + + expect(itemViewModel).toBeDefined(); + expect(itemViewModel.getSnapshot().room).toBe(room1); + }); + + it("should reuse existing room item view model", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const itemViewModel1 = viewModel.getRoomItemViewModel("!room1:server"); + const itemViewModel2 = viewModel.getRoomItemViewModel("!room1:server"); + + expect(itemViewModel1).toBe(itemViewModel2); + }); + + it("should throw error when requesting view model for non-existent room", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + expect(() => { + viewModel.getRoomItemViewModel("!nonexistent:server"); + }).toThrow(); + }); + + it("should dispose view models for rooms no longer visible", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const vm1 = viewModel.getRoomItemViewModel("!room1:server"); + const vm2 = viewModel.getRoomItemViewModel("!room2:server"); + const vm3 = viewModel.getRoomItemViewModel("!room3:server"); + + const disposeSpy1 = jest.spyOn(vm1, "dispose"); + const disposeSpy3 = jest.spyOn(vm3, "dispose"); + + // Update to show only middle room (index 1) + viewModel.updateVisibleRooms(1, 2); + + expect(disposeSpy1).toHaveBeenCalled(); + expect(disposeSpy3).toHaveBeenCalled(); + + // vm2 should still exist + const vm2Again = viewModel.getRoomItemViewModel("!room2:server"); + expect(vm2Again).toBe(vm2); + }); + }); + + describe("Room creation", () => { + it("should dispatch CreateChat action when createChatRoom is called", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "fire"); + + viewModel.createChatRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith(Action.CreateChat); + }); + + it("should dispatch CreateRoom action without parent space", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.createRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.CreateRoom, + }); + }); + + it("should dispatch CreateRoom action with parent space", () => { + const spaceRoom = mkStubRoom("!space:server", "Space", matrixClient); + jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(spaceRoom); + + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + viewModel.createRoom(); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.CreateRoom, + parent_space: spaceRoom, + }); + }); + }); + + describe("Keyboard navigation (ViewRoomDelta)", () => { + beforeEach(() => { + // stubClient sets up MatrixClientPeg which is needed when ViewRoom action is dispatched + stubClient(); + }); + + it("should navigate to next room when delta is 1", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server"); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + dispatcher.dispatch({ + action: Action.ViewRoomDelta, + delta: 1, + unread: false, + }); + + await flushPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + action: Action.ViewRoom, + room_id: "!room2:server", + }), + ); + }); + + it("should navigate to previous room when delta is -1", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + dispatcher.dispatch({ + action: Action.ViewRoomDelta, + delta: -1, + unread: false, + }); + + await flushPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + action: Action.ViewRoom, + room_id: "!room1:server", + }), + ); + }); + + it("should wrap around to last room when navigating backwards from first room", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server"); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + + dispatcher.dispatch({ + action: Action.ViewRoomDelta, + delta: -1, + unread: false, + }); + + await flushPromises(); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + action: Action.ViewRoom, + room_id: "!room3:server", + }), + ); + }); + + it("should not navigate when current room is not found", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!unknown:server"); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + dispatchSpy.mockClear(); + + dispatcher.dispatch({ + action: Action.ViewRoomDelta, + delta: 1, + unread: false, + }); + + await flushPromises(); + + // Should not dispatch ViewRoom since current room wasn't found + expect(dispatchSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + action: Action.ViewRoom, + }), + ); + }); + + it("should not navigate when no room is selected", async () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + dispatchSpy.mockClear(); + + dispatcher.dispatch({ + action: Action.ViewRoomDelta, + delta: 1, + unread: false, + }); + + await flushPromises(); + + expect(dispatchSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ + action: Action.ViewRoom, + }), + ); + }); + }); + + describe("Cleanup", () => { + it("should dispose all room item view models on dispose", () => { + viewModel = new RoomListViewViewModel({ client: matrixClient }); + + const vm1 = viewModel.getRoomItemViewModel("!room1:server"); + const vm2 = viewModel.getRoomItemViewModel("!room2:server"); + + const disposeSpy1 = jest.spyOn(vm1, "dispose"); + const disposeSpy2 = jest.spyOn(vm2, "dispose"); + + viewModel.dispose(); + + expect(disposeSpy1).toHaveBeenCalled(); + expect(disposeSpy2).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/unit-tests/components/viewmodels/roomlist/utils-test.ts b/test/viewmodels/room-list/utils-test.ts similarity index 79% rename from test/unit-tests/components/viewmodels/roomlist/utils-test.ts rename to test/viewmodels/room-list/utils-test.ts index 322d2a5cc6..e7e303aed5 100644 --- a/test/unit-tests/components/viewmodels/roomlist/utils-test.ts +++ b/test/viewmodels/room-list/utils-test.ts @@ -8,22 +8,18 @@ import { mocked } from "jest-mock"; import type { MatrixClient, Room, RoomState } from "matrix-js-sdk/src/matrix"; -import { createTestClient, mkStubRoom } from "../../../../test-utils"; -import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; -import { - hasCreateRoomRights, - createRoom, - hasAccessToNotificationMenu, -} from "../../../../../src/components/viewmodels/roomlist/utils"; -import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { showCreateNewRoom } from "../../../../../src/utils/space"; +import { createTestClient, mkStubRoom } from "../../test-utils"; +import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents"; +import defaultDispatcher from "../../../src/dispatcher/dispatcher"; +import { Action } from "../../../src/dispatcher/actions"; +import { showCreateNewRoom } from "../../../src/utils/space"; +import { hasCreateRoomRights, createRoom, hasAccessToNotificationMenu } from "../../../src/viewmodels/room-list/utils"; -jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({ +jest.mock("../../../src/customisations/helpers/UIComponents", () => ({ shouldShowComponent: jest.fn(), })); -jest.mock("../../../../../src/utils/space", () => ({ +jest.mock("../../../src/utils/space", () => ({ showCreateNewRoom: jest.fn(), }));