Merge branch 'develop' into last-admin-leave-room-warning
This commit is contained in:
commit
93550dde60
196 changed files with 6010 additions and 2185 deletions
4
.github/workflows/cypress.yaml
vendored
4
.github/workflows/cypress.yaml
vendored
|
@ -79,8 +79,8 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Run 3 instances in Parallel
|
# Run 4 instances in Parallel
|
||||||
runner: [1, 2, 3]
|
runner: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -1,3 +1,53 @@
|
||||||
|
Changes in [3.59.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.59.0) (2022-10-25)
|
||||||
|
=====================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Include a file-safe room name and ISO date in chat exports ([\#9440](https://github.com/matrix-org/matrix-react-sdk/pull/9440)). Fixes vector-im/element-web#21812 and vector-im/element-web#19724.
|
||||||
|
* Room call banner ([\#9378](https://github.com/matrix-org/matrix-react-sdk/pull/9378)). Fixes vector-im/element-web#23453. Contributed by @toger5.
|
||||||
|
* Device manager - spinners while devices are signing out ([\#9433](https://github.com/matrix-org/matrix-react-sdk/pull/9433)). Fixes vector-im/element-web#15865.
|
||||||
|
* Device manager - silence call ringers when local notifications are silenced ([\#9420](https://github.com/matrix-org/matrix-react-sdk/pull/9420)).
|
||||||
|
* Pass the current language to Element Call ([\#9427](https://github.com/matrix-org/matrix-react-sdk/pull/9427)).
|
||||||
|
* Hide screen-sharing button in Element Call on desktop ([\#9423](https://github.com/matrix-org/matrix-react-sdk/pull/9423)).
|
||||||
|
* Add reply support to WysiwygComposer ([\#9422](https://github.com/matrix-org/matrix-react-sdk/pull/9422)). Contributed by @florianduros.
|
||||||
|
* Disconnect other connected devices (of the same user) when joining an Element call ([\#9379](https://github.com/matrix-org/matrix-react-sdk/pull/9379)).
|
||||||
|
* Device manager - device tile main click target ([\#9409](https://github.com/matrix-org/matrix-react-sdk/pull/9409)).
|
||||||
|
* Add formatting buttons to the rich text editor ([\#9410](https://github.com/matrix-org/matrix-react-sdk/pull/9410)). Contributed by @florianduros.
|
||||||
|
* Device manager - current session context menu ([\#9386](https://github.com/matrix-org/matrix-react-sdk/pull/9386)).
|
||||||
|
* Remove piwik config fallback for privacy policy URL ([\#9390](https://github.com/matrix-org/matrix-react-sdk/pull/9390)).
|
||||||
|
* Add the first step to integrate the matrix wysiwyg composer ([\#9374](https://github.com/matrix-org/matrix-react-sdk/pull/9374)). Contributed by @florianduros.
|
||||||
|
* Device manager - UA parsing tweaks ([\#9382](https://github.com/matrix-org/matrix-react-sdk/pull/9382)).
|
||||||
|
* Device manager - remove client information events when disabling setting ([\#9384](https://github.com/matrix-org/matrix-react-sdk/pull/9384)).
|
||||||
|
* Add Element Call participant limit ([\#9358](https://github.com/matrix-org/matrix-react-sdk/pull/9358)).
|
||||||
|
* Add Element Call room settings ([\#9347](https://github.com/matrix-org/matrix-react-sdk/pull/9347)).
|
||||||
|
* Device manager - render extended device information ([\#9360](https://github.com/matrix-org/matrix-react-sdk/pull/9360)).
|
||||||
|
* New group call experience: Room header and PiP designs ([\#9351](https://github.com/matrix-org/matrix-react-sdk/pull/9351)).
|
||||||
|
* Pass language to Jitsi Widget ([\#9346](https://github.com/matrix-org/matrix-react-sdk/pull/9346)). Contributed by @Fox32.
|
||||||
|
* Add notifications and toasts for Element Call calls ([\#9337](https://github.com/matrix-org/matrix-react-sdk/pull/9337)).
|
||||||
|
* Device manager - device type icon ([\#9355](https://github.com/matrix-org/matrix-react-sdk/pull/9355)).
|
||||||
|
* Delete the remainder of groups ([\#9357](https://github.com/matrix-org/matrix-react-sdk/pull/9357)). Fixes vector-im/element-web#22770.
|
||||||
|
* Device manager - display client information in device details ([\#9315](https://github.com/matrix-org/matrix-react-sdk/pull/9315)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Send Content-Type: application/json header for integration manager /register API ([\#9490](https://github.com/matrix-org/matrix-react-sdk/pull/9490)). Fixes vector-im/element-web#23580.
|
||||||
|
* Device manager - put client/browser device metadata in correct section ([\#9447](https://github.com/matrix-org/matrix-react-sdk/pull/9447)).
|
||||||
|
* update the room unread notification counter when the server changes the value without any related read receipt ([\#9438](https://github.com/matrix-org/matrix-react-sdk/pull/9438)).
|
||||||
|
* Don't show call banners in video rooms ([\#9441](https://github.com/matrix-org/matrix-react-sdk/pull/9441)).
|
||||||
|
* Prevent useContextMenu isOpen from being true if the button ref goes away ([\#9418](https://github.com/matrix-org/matrix-react-sdk/pull/9418)). Fixes matrix-org/element-web-rageshakes#15637.
|
||||||
|
* Automatically focus the WYSIWYG composer when you enter a room ([\#9412](https://github.com/matrix-org/matrix-react-sdk/pull/9412)).
|
||||||
|
* Improve the tooltips on the call lobby join button ([\#9428](https://github.com/matrix-org/matrix-react-sdk/pull/9428)).
|
||||||
|
* Pass the homeserver's base URL to Element Call ([\#9429](https://github.com/matrix-org/matrix-react-sdk/pull/9429)). Fixes vector-im/element-web#23301.
|
||||||
|
* Better accommodate long room names in call toasts ([\#9426](https://github.com/matrix-org/matrix-react-sdk/pull/9426)).
|
||||||
|
* Hide virtual widgets from the room info panel ([\#9424](https://github.com/matrix-org/matrix-react-sdk/pull/9424)). Fixes vector-im/element-web#23494.
|
||||||
|
* Inhibit clicking on sender avatar in threads list ([\#9417](https://github.com/matrix-org/matrix-react-sdk/pull/9417)). Fixes vector-im/element-web#23482.
|
||||||
|
* Correct the dir parameter of MSC3715 ([\#9391](https://github.com/matrix-org/matrix-react-sdk/pull/9391)). Contributed by @dhenneke.
|
||||||
|
* Use a more correct subset of users in `/remakeolm` developer command ([\#9402](https://github.com/matrix-org/matrix-react-sdk/pull/9402)).
|
||||||
|
* use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456.
|
||||||
|
* Device manager - eagerly create `m.local_notification_settings` events ([\#9353](https://github.com/matrix-org/matrix-react-sdk/pull/9353)).
|
||||||
|
* Close incoming Element call toast when viewing the call lobby ([\#9375](https://github.com/matrix-org/matrix-react-sdk/pull/9375)).
|
||||||
|
* Always allow enabling sending read receipts ([\#9367](https://github.com/matrix-org/matrix-react-sdk/pull/9367)). Fixes vector-im/element-web#23433.
|
||||||
|
* Fixes (vector-im/element-web/issues/22609) where the white theme is not applied when `white -> dark -> white` sequence is done. ([\#9320](https://github.com/matrix-org/matrix-react-sdk/pull/9320)). Contributed by @florianduros.
|
||||||
|
* Fix applying programmatically set height for "top" room layout ([\#9339](https://github.com/matrix-org/matrix-react-sdk/pull/9339)). Contributed by @Fox32.
|
||||||
|
|
||||||
Changes in [3.58.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.1) (2022-10-11)
|
Changes in [3.58.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.1) (2022-10-11)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,21 @@ describe("Composer", () => {
|
||||||
cy.contains('.mx_EventTile_body strong', 'bold message');
|
cy.contains('.mx_EventTile_body strong', 'bold message');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow user to input emoji via graphical picker", () => {
|
||||||
|
cy.getComposer(false).within(() => {
|
||||||
|
cy.get('[aria-label="Emoji"]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-testid="mx_EmojiPicker"]').within(() => {
|
||||||
|
cy.contains(".mx_EmojiPicker_item", "😇").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||||
|
cy.get('div[contenteditable=true]').type("{enter}"); // Send message
|
||||||
|
|
||||||
|
cy.contains(".mx_EventTile_body", "😇");
|
||||||
|
});
|
||||||
|
|
||||||
describe("when Ctrl+Enter is required to send", () => {
|
describe("when Ctrl+Enter is required to send", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
||||||
|
|
|
@ -78,6 +78,7 @@ describe("Device manager", () => {
|
||||||
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').last().click();
|
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').last().click();
|
||||||
// sign out from list selection action buttons
|
// sign out from list selection action buttons
|
||||||
cy.get('[data-testid="sign-out-selection-cta"]').click();
|
cy.get('[data-testid="sign-out-selection-cta"]').click();
|
||||||
|
cy.get('[data-testid="dialog-primary-button"]').click();
|
||||||
// list updated after sign out
|
// list updated after sign out
|
||||||
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 1);
|
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 1);
|
||||||
// security recommendation count updated
|
// security recommendation count updated
|
||||||
|
@ -106,6 +107,8 @@ describe("Device manager", () => {
|
||||||
// sign out using the device details sign out
|
// sign out using the device details sign out
|
||||||
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
||||||
});
|
});
|
||||||
|
// confirm the signout
|
||||||
|
cy.get('[data-testid="dialog-primary-button"]').click();
|
||||||
|
|
||||||
// no other sessions or security recommendations sections when only one session
|
// no other sessions or security recommendations sections when only one session
|
||||||
cy.contains('Other sessions').should('not.exist');
|
cy.contains('Other sessions').should('not.exist');
|
||||||
|
|
|
@ -77,7 +77,7 @@ async function proxyStart(synapse: SynapseInstance): Promise<ProxyInstance> {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
console.log(new Date(), "starting proxy container...");
|
console.log(new Date(), "starting proxy container...");
|
||||||
const containerId = await dockerRun({
|
const containerId = await dockerRun({
|
||||||
image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.4.0",
|
image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.6.0",
|
||||||
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||||
params: [
|
params: [
|
||||||
"--rm",
|
"--rm",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.58.1",
|
"version": "3.59.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.2.0",
|
"@matrix-org/analytics-events": "^0.2.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.3.0",
|
"@matrix-org/matrix-wysiwyg": "^0.3.2",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^6.11.0",
|
"@sentry/browser": "^6.11.0",
|
||||||
"@sentry/tracing": "^6.11.0",
|
"@sentry/tracing": "^6.11.0",
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
@import "./_font-sizes.pcss";
|
@import "./_font-sizes.pcss";
|
||||||
@import "./_font-weights.pcss";
|
@import "./_font-weights.pcss";
|
||||||
@import "./_spacing.pcss";
|
@import "./_spacing.pcss";
|
||||||
@import "./compound/_Icon.pcss";
|
|
||||||
@import "./components/views/beacon/_BeaconListItem.pcss";
|
@import "./components/views/beacon/_BeaconListItem.pcss";
|
||||||
@import "./components/views/beacon/_BeaconStatus.pcss";
|
@import "./components/views/beacon/_BeaconStatus.pcss";
|
||||||
@import "./components/views/beacon/_BeaconStatusTooltip.pcss";
|
@import "./components/views/beacon/_BeaconStatusTooltip.pcss";
|
||||||
|
@ -19,6 +18,7 @@
|
||||||
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
|
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
|
||||||
@import "./components/views/context_menus/_KebabContextMenu.pcss";
|
@import "./components/views/context_menus/_KebabContextMenu.pcss";
|
||||||
@import "./components/views/elements/_FilterDropdown.pcss";
|
@import "./components/views/elements/_FilterDropdown.pcss";
|
||||||
|
@import "./components/views/elements/_LearnMore.pcss";
|
||||||
@import "./components/views/location/_EnableLiveShare.pcss";
|
@import "./components/views/location/_EnableLiveShare.pcss";
|
||||||
@import "./components/views/location/_LiveDurationDropdown.pcss";
|
@import "./components/views/location/_LiveDurationDropdown.pcss";
|
||||||
@import "./components/views/location/_LocationShareMenu.pcss";
|
@import "./components/views/location/_LocationShareMenu.pcss";
|
||||||
|
@ -44,6 +44,7 @@
|
||||||
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
||||||
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
||||||
@import "./components/views/typography/_Caption.pcss";
|
@import "./components/views/typography/_Caption.pcss";
|
||||||
|
@import "./compound/_Icon.pcss";
|
||||||
@import "./structures/_AutoHideScrollbar.pcss";
|
@import "./structures/_AutoHideScrollbar.pcss";
|
||||||
@import "./structures/_BackdropPanel.pcss";
|
@import "./structures/_BackdropPanel.pcss";
|
||||||
@import "./structures/_CompatibilityPage.pcss";
|
@import "./structures/_CompatibilityPage.pcss";
|
||||||
|
@ -299,8 +300,10 @@
|
||||||
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
|
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
|
||||||
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
|
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
|
||||||
@import "./views/rooms/_WhoIsTypingTile.pcss";
|
@import "./views/rooms/_WhoIsTypingTile.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/_FormattingButtons.pcss";
|
@import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/_WysiwygComposer.pcss";
|
@import "./views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss";
|
||||||
|
@import "./views/rooms/wysiwyg_composer/components/_Editor.pcss";
|
||||||
|
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||||
@import "./views/settings/_CryptographyPanel.pcss";
|
@import "./views/settings/_CryptographyPanel.pcss";
|
||||||
|
@ -371,6 +374,4 @@
|
||||||
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
||||||
@import "./voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss";
|
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
||||||
@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss";
|
|
||||||
@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingPip.pcss";
|
|
||||||
|
|
|
@ -14,14 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VoiceBroadcastPlaybackBody {
|
.mx_LearnMore_button {
|
||||||
background-color: $quinary-content;
|
margin-left: $spacing-4;
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastPlaybackBody_controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
|
@ -247,7 +247,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpotlightDialog_result_multiline {
|
&.mx_SpotlightDialog_result_multiline {
|
||||||
align-items: start;
|
align-items: flex-start;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: $spacing-4 $spacing-20;
|
padding: $spacing-4 $spacing-20;
|
||||||
|
|
|
@ -65,7 +65,7 @@ limitations under the License.
|
||||||
.mx_UseCaseSelection_skip {
|
.mx_UseCaseSelection_skip {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -426,7 +426,7 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_EventTile_selected .mx_EventTile_line {
|
&.mx_EventTile_selected .mx_EventTile_line {
|
||||||
// TODO: check if this would be necessary
|
/* TODO: check if this would be necessary; */
|
||||||
padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px);
|
padding-inline-start: calc(var(--EventTile_group_line-spacing-inline-start) + 20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -894,15 +894,22 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Display notification dot */
|
/* Display notification dot */
|
||||||
&[data-notification]::before {
|
&[data-notification]::before,
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
position: absolute;
|
||||||
$notification-inset-block-start: 14px; /* 14px: align the dot with the timestamp row */
|
$notification-inset-block-start: 14px; /* 14px: align the dot with the timestamp row */
|
||||||
|
|
||||||
width: $notification-dot-size;
|
/* !important to fix overly specific CSS selector applied on mx_NotificationBadge */
|
||||||
height: $notification-dot-size;
|
width: $notification-dot-size !important;
|
||||||
|
height: $notification-dot-size !important;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
inset: $notification-inset-block-start $spacing-8 auto auto;
|
inset: $notification-inset-block-start $spacing-8 auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge_count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-notification="total"]::before {
|
&[data-notification="total"]::before {
|
||||||
background-color: $room-icon-unread-color;
|
background-color: $room-icon-unread-color;
|
||||||
}
|
}
|
||||||
|
@ -1301,7 +1308,8 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-shape="ThreadsList"][data-notification]::before {
|
&[data-shape="ThreadsList"][data-notification]::before,
|
||||||
|
.mx_NotificationBadge {
|
||||||
/* stylelint-disable-next-line declaration-colon-space-after */
|
/* stylelint-disable-next-line declaration-colon-space-after */
|
||||||
inset-block-start:
|
inset-block-start:
|
||||||
calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top));
|
calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top));
|
||||||
|
|
|
@ -240,7 +240,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
.mx_MessageComposer_wysiwyg {
|
.mx_MessageComposer_wysiwyg {
|
||||||
.mx_MessageComposer_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
.mx_MessageComposer_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
||||||
margin-top: 22px;
|
margin-top: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +264,14 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_plain_text::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/composer/plain_text.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_rich_text::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/composer/rich_text.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_location::before {
|
.mx_MessageComposer_location::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/location.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/location.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_EditWysiwygComposer {
|
||||||
|
--EditWysiwygComposer-padding-inline: 3px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%; /* disable overflow */
|
||||||
|
width: auto;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px var(--EditWysiwygComposer-padding-inline);
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_content {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px $primary-hairline-color;
|
||||||
|
background-color: $background;
|
||||||
|
max-height: 200px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgba($accent, 0.5); /* Only ever used here */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EditWysiwygComposer_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap-reverse; /* display "Save" over "Cancel" */
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 100px; /* magic number to align the edge of the button with the input area */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_FormattingButtons_Button {
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_WysiwygComposer {
|
.mx_SendWysiwygComposer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
37
res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
Normal file
37
res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@keyframes visualbell {
|
||||||
|
from { background-color: $visual-bell-bg-color; }
|
||||||
|
to { background-color: $background; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WysiwygComposer_content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
outline: none;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
/* Force caret nodes to be selected in full so that they can be */
|
||||||
|
/* navigated through in a single keypress */
|
||||||
|
.caretNode {
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_FormattingButtons {
|
.mx_FormattingButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
.mx_FormattingButtons_Button {
|
.mx_FormattingButtons_Button {
|
||||||
--size: 28px;
|
--size: 28px;
|
||||||
|
@ -45,7 +45,7 @@ limitations under the License.
|
||||||
left: 6px;
|
left: 6px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
background-color: $icon-button-color;
|
background-color: $tertiary-content;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-position: center;
|
mask-position: center;
|
|
@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingPip {
|
.mx_VoiceBroadcastBody {
|
||||||
background-color: $system;
|
background-color: $quinary-content;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px 0 #0000004a;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: $spacing-12;
|
padding: $spacing-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingPip_divider {
|
.mx_VoiceBroadcastBody--pip {
|
||||||
|
background-color: $system;
|
||||||
|
box-shadow: 0 2px 8px 0 #0000004a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VoiceBroadcastBody_divider {
|
||||||
background-color: $quinary-content;
|
background-color: $quinary-content;
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: $spacing-12 0;
|
margin: $spacing-12 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingPip_controls {
|
.mx_VoiceBroadcastBody_controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
10
res/img/element-icons/room/composer/plain_text.svg
Normal file
10
res/img/element-icons/room/composer/plain_text.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1456_146350)">
|
||||||
|
<path d="M1 18.6667C1 19.4 1.6 20 2.33333 20H18.3333C19.0667 20 19.6667 19.4 19.6667 18.6667C19.6667 17.9333 19.0667 17.3333 18.3333 17.3333H2.33333C1.6 17.3333 1 17.9333 1 18.6667ZM7 11.7333H13.6667L14.5467 13.8667C14.7467 14.3467 15.2133 14.6667 15.7333 14.6667C16.6533 14.6667 17.2667 13.72 16.9067 12.88L11.7333 0.92C11.4933 0.36 10.9467 0 10.3333 0C9.72 0 9.17333 0.36 8.93333 0.92L3.76 12.88C3.4 13.72 4.02667 14.6667 4.94667 14.6667C5.46667 14.6667 5.93333 14.3467 6.13333 13.8667L7 11.7333ZM10.3333 2.64L12.8267 9.33333H7.84L10.3333 2.64Z" fill="#C1C6CD"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1456_146350">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 818 B |
11
res/img/element-icons/room/composer/rich_text.svg
Normal file
11
res/img/element-icons/room/composer/rich_text.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1456_146365)">
|
||||||
|
<path d="M7.00042 13.7333H13.6671L14.5471 15.8667C14.7471 16.3467 15.2137 16.6667 15.7337 16.6667C16.6537 16.6667 17.2671 15.72 16.9071 14.88L11.7337 2.92C11.4937 2.36 10.9471 2 10.3337 2C9.72042 2 9.17375 2.36 8.93375 2.92L3.76042 14.88C3.40042 15.72 4.02708 16.6667 4.94708 16.6667C5.46708 16.6667 5.93375 16.3467 6.13375 15.8667L7.00042 13.7333ZM10.3337 4.64L12.8271 11.3333H7.84042L10.3337 4.64Z" fill="#C1C6CD"/>
|
||||||
|
<path d="M0.5 9.66927C0.5 10.6787 1.32386 11.5026 2.33333 11.5026H18.3333C19.3428 11.5026 20.1667 10.6787 20.1667 9.66927C20.1667 8.6598 19.3428 7.83594 18.3333 7.83594H2.33333C1.32386 7.83594 0.5 8.6598 0.5 9.66927Z" fill="#C1C6CD" stroke="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1456_146365">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 921 B |
|
@ -24,13 +24,6 @@ import { ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
|
||||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
|
||||||
export {
|
|
||||||
IdentityProviderBrand,
|
|
||||||
IIdentityProvider,
|
|
||||||
ISSOFlow,
|
|
||||||
LoginFlow,
|
|
||||||
} from "matrix-js-sdk/src/@types/auth";
|
|
||||||
|
|
||||||
interface ILoginOptions {
|
interface ILoginOptions {
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -435,7 +435,16 @@ export const Notifier = {
|
||||||
if (actions?.notify) {
|
if (actions?.notify) {
|
||||||
this._performCustomEventHandling(ev);
|
this._performCustomEventHandling(ev);
|
||||||
|
|
||||||
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId &&
|
const store = SdkContextClass.instance.roomViewStore;
|
||||||
|
const isViewingRoom = store.getRoomId() === room.roomId;
|
||||||
|
const threadId: string | undefined = ev.getId() !== ev.threadRootId
|
||||||
|
? ev.threadRootId
|
||||||
|
: undefined;
|
||||||
|
const isViewingThread = store.getThreadId() === threadId;
|
||||||
|
|
||||||
|
const isViewingEventTimeline = isViewingRoom && (!threadId || isViewingThread);
|
||||||
|
|
||||||
|
if (isViewingEventTimeline &&
|
||||||
UserActivity.sharedInstance().userActiveRecently() &&
|
UserActivity.sharedInstance().userActiveRecently() &&
|
||||||
!Modal.hasDialogs()
|
!Modal.hasDialogs()
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export function levelRoleMap(usersDefault: number) {
|
export function levelRoleMap(usersDefault: number): Record<number | "undefined", string> {
|
||||||
return {
|
return {
|
||||||
undefined: _t('Default'),
|
undefined: _t('Default'),
|
||||||
0: _t('Restricted'),
|
0: _t('Restricted'),
|
||||||
|
|
|
@ -78,15 +78,23 @@ export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number {
|
export function getUnreadNotificationCount(
|
||||||
let notificationCount = room.getUnreadNotificationCount(type);
|
room: Room,
|
||||||
|
type: NotificationCountType,
|
||||||
|
threadId?: string,
|
||||||
|
): number {
|
||||||
|
let notificationCount = (!!threadId
|
||||||
|
? room.getThreadUnreadNotificationCount(threadId, type)
|
||||||
|
: room.getUnreadNotificationCount(type));
|
||||||
|
|
||||||
// Check notification counts in the old room just in case there's some lost
|
// Check notification counts in the old room just in case there's some lost
|
||||||
// there. We only go one level down to avoid performance issues, and theory
|
// there. We only go one level down to avoid performance issues, and theory
|
||||||
// is that 1st generation rooms will have already been read by the 3rd generation.
|
// is that 1st generation rooms will have already been read by the 3rd generation.
|
||||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||||
if (createEvent && createEvent.getContent()['predecessor']) {
|
const predecessor = createEvent?.getContent().predecessor;
|
||||||
const oldRoomId = createEvent.getContent()['predecessor']['room_id'];
|
// Exclude threadId, as the same thread can't continue over a room upgrade
|
||||||
|
if (!threadId && predecessor) {
|
||||||
|
const oldRoomId = predecessor.room_id;
|
||||||
const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId);
|
const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId);
|
||||||
if (oldRoom) {
|
if (oldRoom) {
|
||||||
// We only ever care if there's highlights in the old room. No point in
|
// We only ever care if there's highlights in the old room. No point in
|
||||||
|
|
|
@ -190,6 +190,9 @@ export default class ScalarAuthClient {
|
||||||
const res = await fetch(scalarRestUrl, {
|
const res = await fetch(scalarRestUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(openidTokenObject),
|
body: JSON.stringify(openidTokenObject),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
@ -63,6 +63,15 @@ const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
||||||
required_state: [
|
required_state: [
|
||||||
["*", "*"], // all events
|
["*", "*"], // all events
|
||||||
],
|
],
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: [ // state needed to handle space navigation and tombstone chains
|
||||||
|
[EventType.RoomCreate, ""],
|
||||||
|
[EventType.RoomTombstone, ""],
|
||||||
|
[EventType.SpaceChild, "*"],
|
||||||
|
[EventType.SpaceParent, "*"],
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PartialSlidingSyncRequest = {
|
export type PartialSlidingSyncRequest = {
|
||||||
|
@ -121,6 +130,16 @@ export class SlidingSyncManager {
|
||||||
[EventType.SpaceParent, "*"], // all space parents
|
[EventType.SpaceParent, "*"], // all space parents
|
||||||
[EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room
|
[EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room
|
||||||
],
|
],
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: [
|
||||||
|
[EventType.RoomCreate, ""],
|
||||||
|
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||||
|
[EventType.SpaceChild, "*"], // all space children
|
||||||
|
[EventType.SpaceParent, "*"], // all space parents
|
||||||
|
[EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room
|
||||||
|
],
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
room_types: ["m.space"],
|
room_types: ["m.space"],
|
||||||
},
|
},
|
||||||
|
@ -176,7 +195,7 @@ export class SlidingSyncManager {
|
||||||
list = {
|
list = {
|
||||||
ranges: [[0, 20]],
|
ranges: [[0, 20]],
|
||||||
sort: [
|
sort: [
|
||||||
"by_highlight_count", "by_notification_count", "by_recency",
|
"by_notification_level", "by_recency",
|
||||||
],
|
],
|
||||||
timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites?
|
timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites?
|
||||||
required_state: [
|
required_state: [
|
||||||
|
@ -187,6 +206,16 @@ export class SlidingSyncManager {
|
||||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||||
[EventType.RoomMember, this.client.getUserId()], // lets the client calculate that we are in fact in the room
|
[EventType.RoomMember, this.client.getUserId()], // lets the client calculate that we are in fact in the room
|
||||||
],
|
],
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: [
|
||||||
|
[EventType.RoomCreate, ""],
|
||||||
|
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||||
|
[EventType.SpaceChild, "*"], // all space children
|
||||||
|
[EventType.SpaceParent, "*"], // all space parents
|
||||||
|
[EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
list = Object.assign(list, updateArgs);
|
list = Object.assign(list, updateArgs);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -23,7 +23,6 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
import { haveRendererForEvent } from "./events/EventTileFactory";
|
import { haveRendererForEvent } from "./events/EventTileFactory";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { RoomNotificationStateStore } from "./stores/notifications/RoomNotificationStateStore";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this event arriving in a room should affect the room's
|
* Returns true if this event arriving in a room should affect the room's
|
||||||
|
@ -77,11 +76,6 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room);
|
|
||||||
if (threadState.color > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the read receipt relates to an event is that part of a thread
|
// if the read receipt relates to an event is that part of a thread
|
||||||
|
|
|
@ -60,6 +60,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
private recorderProcessor: ScriptProcessorNode;
|
private recorderProcessor: ScriptProcessorNode;
|
||||||
private recording = false;
|
private recording = false;
|
||||||
private observable: SimpleObservable<IRecordingUpdate>;
|
private observable: SimpleObservable<IRecordingUpdate>;
|
||||||
|
private targetMaxLength: number | null = TARGET_MAX_LENGTH;
|
||||||
public amplitudes: number[] = []; // at each second mark, generated
|
public amplitudes: number[] = []; // at each second mark, generated
|
||||||
private liveWaveform = new FixedRollingArray(RECORDING_PLAYBACK_SAMPLES, 0);
|
private liveWaveform = new FixedRollingArray(RECORDING_PLAYBACK_SAMPLES, 0);
|
||||||
public onDataAvailable: (data: ArrayBuffer) => void;
|
public onDataAvailable: (data: ArrayBuffer) => void;
|
||||||
|
@ -83,6 +84,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
return true; // we don't ever care if the event had listeners, so just return "yes"
|
return true; // we don't ever care if the event had listeners, so just return "yes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public disableMaxLength(): void {
|
||||||
|
this.targetMaxLength = null;
|
||||||
|
}
|
||||||
|
|
||||||
private async makeRecorder() {
|
private async makeRecorder() {
|
||||||
try {
|
try {
|
||||||
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
@ -203,6 +208,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
// In testing, recorder time and worker time lag by about 400ms, which is roughly the
|
// In testing, recorder time and worker time lag by about 400ms, which is roughly the
|
||||||
// time needed to encode a sample/frame.
|
// time needed to encode a sample/frame.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
if (!this.targetMaxLength) {
|
||||||
|
// skip time checks if max length has been disabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const secondsLeft = TARGET_MAX_LENGTH - this.recorderSeconds;
|
const secondsLeft = TARGET_MAX_LENGTH - this.recorderSeconds;
|
||||||
if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame
|
if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame
|
||||||
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
|
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
|
||||||
|
|
|
@ -835,6 +835,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
: room;
|
: room;
|
||||||
|
|
||||||
const receipts: IReadReceiptProps[] = [];
|
const receipts: IReadReceiptProps[] = [];
|
||||||
|
|
||||||
|
if (!receiptDestination) {
|
||||||
|
logger.debug("Discarding request, could not find the receiptDestination for event: "
|
||||||
|
+ this.context.threadId);
|
||||||
|
return receipts;
|
||||||
|
}
|
||||||
|
|
||||||
receiptDestination.getReceiptsForEvent(event).forEach((r) => {
|
receiptDestination.getReceiptsForEvent(event).forEach((r) => {
|
||||||
if (
|
if (
|
||||||
!r.userId ||
|
!r.userId ||
|
||||||
|
|
|
@ -34,10 +34,12 @@ const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
export function getUnsentMessages(room: Room): MatrixEvent[] {
|
export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] {
|
||||||
if (!room) { return []; }
|
if (!room) { return []; }
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
const isNotSent = ev.status === EventStatus.NOT_SENT;
|
||||||
|
const belongsToTheThread = threadId === ev.threadRootId;
|
||||||
|
return isNotSent && (!threadId || belongsToTheThread);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,6 @@ import React, { createRef, KeyboardEvent } from 'react';
|
||||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
||||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
|
||||||
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
|
|
||||||
import { IRelationsRequestOpts } from 'matrix-js-sdk/src/@types/requests';
|
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -55,6 +52,7 @@ import Spinner from "../views/elements/Spinner";
|
||||||
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import Heading from '../views/typography/Heading';
|
import Heading from '../views/typography/Heading';
|
||||||
import { SdkContextClass } from '../../contexts/SDKContext';
|
import { SdkContextClass } from '../../contexts/SDKContext';
|
||||||
|
import { ThreadPayload } from '../../dispatcher/payloads/ThreadPayload';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -132,6 +130,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
metricsTrigger: undefined, // room doesn't change
|
metricsTrigger: undefined, // room doesn't change
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dis.dispatch<ThreadPayload>({
|
||||||
|
action: Action.ViewThread,
|
||||||
|
thread_id: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps) {
|
public componentDidUpdate(prevProps) {
|
||||||
|
@ -225,11 +228,13 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private async postThreadUpdate(thread: Thread): Promise<void> {
|
private async postThreadUpdate(thread: Thread): Promise<void> {
|
||||||
|
dis.dispatch<ThreadPayload>({
|
||||||
|
action: Action.ViewThread,
|
||||||
|
thread_id: thread.id,
|
||||||
|
});
|
||||||
thread.emit(ThreadEvent.ViewThread);
|
thread.emit(ThreadEvent.ViewThread);
|
||||||
await thread.fetchInitialEvents();
|
|
||||||
this.updateThreadRelation();
|
this.updateThreadRelation();
|
||||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
this.timelinePanel.current?.refreshTimeline(this.props.initialEvent?.getId());
|
||||||
this.timelinePanel.current?.refreshTimeline();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
|
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
|
||||||
|
@ -283,40 +288,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private nextBatch: string | undefined | null = null;
|
|
||||||
|
|
||||||
private onPaginationRequest = async (
|
|
||||||
timelineWindow: TimelineWindow | null,
|
|
||||||
direction = Direction.Backward,
|
|
||||||
limit = 20,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
if (!Thread.hasServerSideSupport && timelineWindow) {
|
|
||||||
timelineWindow.extend(direction, limit);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts: IRelationsRequestOpts = {
|
|
||||||
limit,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.nextBatch) {
|
|
||||||
opts.from = this.nextBatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextBatch: string | null | undefined = null;
|
|
||||||
if (this.state.thread) {
|
|
||||||
const response = await this.state.thread.fetchEvents(opts);
|
|
||||||
nextBatch = response.nextBatch;
|
|
||||||
this.nextBatch = nextBatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advances the marker on the TimelineWindow to define the correct
|
|
||||||
// window of events to display on screen
|
|
||||||
timelineWindow?.extend(direction, limit);
|
|
||||||
|
|
||||||
return !!nextBatch;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onFileDrop = (dataTransfer: DataTransfer) => {
|
private onFileDrop = (dataTransfer: DataTransfer) => {
|
||||||
const roomId = this.props.mxEvent.getRoomId();
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
|
@ -399,7 +370,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
eventScrollIntoView={this.props.initialEventScrollIntoView}
|
eventScrollIntoView={this.props.initialEventScrollIntoView}
|
||||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||||
onPaginationRequest={this.onPaginationRequest}
|
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1409,24 +1409,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
// quite slow. So we detect that situation and shortcut straight to
|
// quite slow. So we detect that situation and shortcut straight to
|
||||||
// calling _reloadEvents and updating the state.
|
// calling _reloadEvents and updating the state.
|
||||||
|
|
||||||
const timeline = this.props.timelineSet.getTimelineForEvent(eventId);
|
// This is a hot-path optimization by skipping a promise tick
|
||||||
if (timeline) {
|
// by repeating a no-op sync branch in
|
||||||
// This is a hot-path optimization by skipping a promise tick
|
// TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
|
||||||
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
|
if (this.props.timelineSet.getTimelineForEvent(eventId)) {
|
||||||
this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
|
// if we've got an eventId, and the timeline exists, we can skip
|
||||||
|
// the promise tick.
|
||||||
|
this.timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
|
// in this branch this method will happen in sync time
|
||||||
onLoaded();
|
onLoaded();
|
||||||
} else {
|
return;
|
||||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
|
||||||
this.buildLegacyCallEventGroupers();
|
|
||||||
this.setState({
|
|
||||||
events: [],
|
|
||||||
liveEvents: [],
|
|
||||||
canBackPaginate: false,
|
|
||||||
canForwardPaginate: false,
|
|
||||||
timelineLoading: true,
|
|
||||||
});
|
|
||||||
prom.then(onLoaded, onError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
|
this.buildLegacyCallEventGroupers();
|
||||||
|
this.setState({
|
||||||
|
events: [],
|
||||||
|
liveEvents: [],
|
||||||
|
canBackPaginate: false,
|
||||||
|
canForwardPaginate: false,
|
||||||
|
timelineLoading: true,
|
||||||
|
});
|
||||||
|
prom.then(onLoaded, onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the completion of a timeline load or localEchoUpdate, by
|
// handle the completion of a timeline load or localEchoUpdate, by
|
||||||
|
@ -1443,8 +1447,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force refresh the timeline before threads support pending events
|
// Force refresh the timeline before threads support pending events
|
||||||
public refreshTimeline(): void {
|
public refreshTimeline(eventId?: string): void {
|
||||||
this.loadTimeline();
|
this.loadTimeline(eventId, undefined, undefined, false);
|
||||||
this.reloadEvents();
|
this.reloadEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,10 @@ import React, { ReactNode } from 'react';
|
||||||
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
|
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import Login, { ISSOFlow, LoginFlow } from '../../../Login';
|
import Login from '../../../Login';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React, { Fragment, ReactNode } from 'react';
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ISSOFlow } from "matrix-js-sdk/src/@types/auth";
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||||
|
@ -26,7 +27,7 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import Login, { ISSOFlow } from "../../../Login";
|
import Login from "../../../Login";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import SSOButtons from "../../views/elements/SSOButtons";
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
import ServerPicker from '../../views/elements/ServerPicker';
|
import ServerPicker from '../../views/elements/ServerPicker';
|
||||||
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { ISSOFlow, LoginFlow, sendLoginRequest } from "../../../Login";
|
import { sendLoginRequest } from "../../../Login";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
|
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
|
||||||
import SSOButtons from "../../views/elements/SSOButtons";
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
|
|
|
@ -16,11 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
|
@ -39,11 +38,7 @@ interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idNam
|
||||||
oobData?: IOOBData & {
|
oobData?: IOOBData & {
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
};
|
};
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
resizeMethod?: ResizeMethod;
|
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
className?: string;
|
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,10 +67,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||||
if (cli) {
|
|
||||||
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||||
|
@ -133,7 +125,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room ? room.name : oobData.name;
|
const roomName = room?.name ?? oobData.name;
|
||||||
// If the room is a DM, we use the other user's ID for the color hash
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
// in order to match the room avatar with their avatar
|
// in order to match the room avatar with their avatar
|
||||||
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
|
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
|
||||||
|
@ -142,7 +134,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
className={classNames(className, {
|
className={classNames(className, {
|
||||||
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={idName}
|
idName={idName}
|
||||||
|
|
|
@ -47,7 +47,7 @@ const ShareLatestLocation: React.FC<Props> = ({ latestLocationState }) => {
|
||||||
return <>
|
return <>
|
||||||
<TooltipTarget label={_t('Open in OpenStreetMap')}>
|
<TooltipTarget label={_t('Open in OpenStreetMap')}>
|
||||||
<a
|
<a
|
||||||
data-test-id='open-location-in-osm'
|
data-testid='open-location-in-osm'
|
||||||
href={mapLink}
|
href={mapLink}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer noopener'
|
rel='noreferrer noopener'
|
||||||
|
|
|
@ -382,7 +382,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const me = cli.getUserId();
|
const me = cli.getUserId();
|
||||||
const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props;
|
const {
|
||||||
|
mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain,
|
||||||
|
...other
|
||||||
|
} = this.props;
|
||||||
|
delete other.getRelationsForEvent;
|
||||||
|
delete other.permalinkCreator;
|
||||||
|
|
||||||
const eventStatus = mxEvent.status;
|
const eventStatus = mxEvent.status;
|
||||||
const unsentReactionsCount = this.getUnsentReactions().length;
|
const unsentReactionsCount = this.getUnsentReactions().length;
|
||||||
const contentActionable = isContentActionable(mxEvent);
|
const contentActionable = isContentActionable(mxEvent);
|
||||||
|
@ -747,7 +753,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<IconizedContextMenu
|
<IconizedContextMenu
|
||||||
{...this.props}
|
{...other}
|
||||||
className="mx_MessageContextMenu"
|
className="mx_MessageContextMenu"
|
||||||
compact={true}
|
compact={true}
|
||||||
data-testid="mx_MessageContextMenu"
|
data-testid="mx_MessageContextMenu"
|
||||||
|
|
|
@ -93,6 +93,7 @@ import { TooltipOption } from "./TooltipOption";
|
||||||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||||
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
||||||
import { shouldShowFeedback } from "../../../../utils/Feedback";
|
import { shouldShowFeedback } from "../../../../utils/Feedback";
|
||||||
|
import RoomAvatar from "../../avatars/RoomAvatar";
|
||||||
|
|
||||||
const MAX_RECENT_SEARCHES = 10;
|
const MAX_RECENT_SEARCHES = 10;
|
||||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||||
|
@ -656,6 +657,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
shouldPeek: result.publicRoom.world_readable || cli.isGuest(),
|
shouldPeek: result.publicRoom.world_readable || cli.isGuest(),
|
||||||
}, true, ev.type !== "click");
|
}, true, ev.type !== "click");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Option
|
<Option
|
||||||
id={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}`}
|
id={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}`}
|
||||||
|
@ -674,13 +676,14 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
|
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
|
||||||
aria-details={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_details`}
|
aria-details={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_details`}
|
||||||
>
|
>
|
||||||
<BaseAvatar
|
<RoomAvatar
|
||||||
className="mx_SearchResultAvatar"
|
className="mx_SearchResultAvatar"
|
||||||
url={result?.publicRoom?.avatar_url
|
oobData={{
|
||||||
? mediaFromMxc(result?.publicRoom?.avatar_url).getSquareThumbnailHttp(AVATAR_SIZE)
|
roomId: result.publicRoom.room_id,
|
||||||
: null}
|
name: result.publicRoom.name,
|
||||||
name={result.publicRoom.name}
|
avatarUrl: result.publicRoom.avatar_url,
|
||||||
idName={result.publicRoom.room_id}
|
roomType: result.publicRoom.room_type,
|
||||||
|
}}
|
||||||
width={AVATAR_SIZE}
|
width={AVATAR_SIZE}
|
||||||
height={AVATAR_SIZE}
|
height={AVATAR_SIZE}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -75,7 +75,7 @@ type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T>
|
||||||
onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
|
onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
||||||
ref?: React.Ref<Element>;
|
ref?: React.Ref<Element>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default class DialogButtons extends React.Component<IProps> {
|
||||||
cancelButton = <button
|
cancelButton = <button
|
||||||
// important: the default type is 'submit' and this button comes before the
|
// important: the default type is 'submit' and this button comes before the
|
||||||
// primary in the DOM so will get form submissions unless we make it not a submit.
|
// primary in the DOM so will get form submissions unless we make it not a submit.
|
||||||
data-test-id="dialog-cancel-button"
|
data-testid="dialog-cancel-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this.onCancelClick}
|
onClick={this.onCancelClick}
|
||||||
className={this.props.cancelButtonClass}
|
className={this.props.cancelButtonClass}
|
||||||
|
@ -104,7 +104,7 @@ export default class DialogButtons extends React.Component<IProps> {
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
||||||
data-test-id="dialog-primary-button"
|
data-testid="dialog-primary-button"
|
||||||
className={primaryButtonClassName}
|
className={primaryButtonClassName}
|
||||||
onClick={this.props.onPrimaryButtonClick}
|
onClick={this.props.onPrimaryButtonClick}
|
||||||
autoFocus={this.props.focus}
|
autoFocus={this.props.focus}
|
||||||
|
|
56
src/components/views/elements/LearnMore.tsx
Normal file
56
src/components/views/elements/LearnMore.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import InfoDialog from '../dialogs/InfoDialog';
|
||||||
|
import AccessibleButton, { IAccessibleButtonProps } from './AccessibleButton';
|
||||||
|
|
||||||
|
export interface LearnMoreProps extends IAccessibleButtonProps {
|
||||||
|
title: string;
|
||||||
|
description: string | React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LearnMore: React.FC<LearnMoreProps> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const onClick = () => {
|
||||||
|
Modal.createDialog(
|
||||||
|
InfoDialog,
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
button: _t('Got it'),
|
||||||
|
hasCloseButton: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AccessibleButton
|
||||||
|
{...rest}
|
||||||
|
kind='link_inline'
|
||||||
|
onClick={onClick}
|
||||||
|
className='mx_LearnMore_button'
|
||||||
|
>
|
||||||
|
{ _t('Learn more') }
|
||||||
|
</AccessibleButton>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LearnMore;
|
|
@ -44,14 +44,13 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
levelRoleMap: {};
|
levelRoleMap: Partial<Record<number | "undefined", string>>;
|
||||||
// List of power levels to show in the drop-down
|
// List of power levels to show in the drop-down
|
||||||
options: number[];
|
options: number[];
|
||||||
|
|
||||||
customValue: number;
|
customValue: number;
|
||||||
selectValue: number | string;
|
selectValue: number | string;
|
||||||
custom?: boolean;
|
custom?: boolean;
|
||||||
customLevel?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PowerSelector extends React.Component<IProps, IState> {
|
export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
|
@ -101,7 +100,7 @@ export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
levelRoleMap,
|
levelRoleMap,
|
||||||
options,
|
options,
|
||||||
custom: isCustom,
|
custom: isCustom,
|
||||||
customLevel: newProps.value,
|
customValue: newProps.value,
|
||||||
selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
|
selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -125,7 +124,11 @@ export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
this.props.onChange(this.state.customValue, this.props.powerLevelKey);
|
if (Number.isFinite(this.state.customValue)) {
|
||||||
|
this.props.onChange(this.state.customValue, this.props.powerLevelKey);
|
||||||
|
} else {
|
||||||
|
this.initStateFromProps(this.props); // reset, invalid input
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
|
|
|
@ -19,11 +19,11 @@ import { chunk } from "lodash";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
|
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
|
||||||
|
import { IdentityProviderBrand, IIdentityProvider, ISSOFlow } from "matrix-js-sdk/src/@types/auth";
|
||||||
|
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { IdentityProviderBrand, IIdentityProvider, ISSOFlow } from "../../../Login";
|
|
||||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||||
|
|
|
@ -48,6 +48,7 @@ import RoomContext from "../../../contexts/RoomContext";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { options as linkifyOpts } from "../../../linkify-matrix";
|
import { options as linkifyOpts } from "../../../linkify-matrix";
|
||||||
import { getParentEventId } from '../../../utils/Reply';
|
import { getParentEventId } from '../../../utils/Reply';
|
||||||
|
import { EditWysiwygComposer } from '../rooms/wysiwyg_composer';
|
||||||
|
|
||||||
const MAX_HIGHLIGHT_LENGTH = 4096;
|
const MAX_HIGHLIGHT_LENGTH = 4096;
|
||||||
|
|
||||||
|
@ -562,7 +563,10 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.props.editState) {
|
if (this.props.editState) {
|
||||||
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
||||||
|
return isWysiwygComposerEnabled ?
|
||||||
|
<EditWysiwygComposer editorStateTransfer={this.props.editState} className="mx_EventTile_content" /> :
|
||||||
|
<EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
||||||
}
|
}
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
|
@ -20,7 +20,8 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
|
@ -43,6 +44,7 @@ import { SummarizedNotificationState } from "../../../stores/notifications/Summa
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -136,32 +138,67 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
private threadNotificationState: ThreadsRoomNotificationState;
|
private threadNotificationState: ThreadsRoomNotificationState;
|
||||||
private globalNotificationState: SummarizedNotificationState;
|
private globalNotificationState: SummarizedNotificationState;
|
||||||
|
|
||||||
|
private get supportsThreadNotifications(): boolean {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props, HeaderKind.Room);
|
super(props, HeaderKind.Room);
|
||||||
|
|
||||||
this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room);
|
if (!this.supportsThreadNotifications) {
|
||||||
|
this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room);
|
||||||
|
}
|
||||||
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
super.componentDidMount();
|
super.componentDidMount();
|
||||||
this.threadNotificationState.on(NotificationStateEvents.Update, this.onThreadNotification);
|
if (!this.supportsThreadNotifications) {
|
||||||
|
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
|
} else {
|
||||||
|
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
|
}
|
||||||
|
this.onNotificationUpdate();
|
||||||
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
super.componentWillUnmount();
|
super.componentWillUnmount();
|
||||||
this.threadNotificationState.off(NotificationStateEvents.Update, this.onThreadNotification);
|
if (!this.supportsThreadNotifications) {
|
||||||
|
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
|
} else {
|
||||||
|
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
|
}
|
||||||
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onThreadNotification = (): void => {
|
private onNotificationUpdate = (): void => {
|
||||||
|
let threadNotificationColor: NotificationColor;
|
||||||
|
if (!this.supportsThreadNotifications) {
|
||||||
|
threadNotificationColor = this.threadNotificationState.color;
|
||||||
|
} else {
|
||||||
|
threadNotificationColor = this.notificationColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log
|
||||||
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
|
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
|
||||||
this.setState({
|
this.setState({
|
||||||
threadNotificationColor: this.threadNotificationState.color,
|
threadNotificationColor,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get notificationColor(): NotificationColor {
|
||||||
|
switch (this.props.room.threadsAggregateNotificationType) {
|
||||||
|
case NotificationCountType.Highlight:
|
||||||
|
return NotificationColor.Red;
|
||||||
|
case NotificationCountType.Total:
|
||||||
|
return NotificationColor.Grey;
|
||||||
|
default:
|
||||||
|
return NotificationColor.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
||||||
// XXX: why don't we read from this.state.globalNotificationCount in the render methods?
|
// XXX: why don't we read from this.state.globalNotificationCount in the render methods?
|
||||||
this.globalNotificationState = notificationState;
|
this.globalNotificationState = notificationState;
|
||||||
|
@ -255,12 +292,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
? <HeaderButton
|
? <HeaderButton
|
||||||
key={RightPanelPhases.ThreadPanel}
|
key={RightPanelPhases.ThreadPanel}
|
||||||
name="threadsButton"
|
name="threadsButton"
|
||||||
|
data-testid="threadsButton"
|
||||||
title={_t("Threads")}
|
title={_t("Threads")}
|
||||||
onClick={this.onThreadsPanelClicked}
|
onClick={this.onThreadsPanelClicked}
|
||||||
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
||||||
isUnread={this.threadNotificationState.color > 0}
|
isUnread={this.state.threadNotificationColor > 0}
|
||||||
>
|
>
|
||||||
<UnreadIndicator color={this.threadNotificationState.color} />
|
<UnreadIndicator color={this.state.threadNotificationColor} />
|
||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { NotificationCountType, Room, RoomEvent } from 'matrix-js-sdk/src/models
|
||||||
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||||
|
import { Feature, ServerSupport } from 'matrix-js-sdk/src/feature';
|
||||||
|
|
||||||
import { Icon as LinkIcon } from '../../../../res/img/element-icons/link.svg';
|
import { Icon as LinkIcon } from '../../../../res/img/element-icons/link.svg';
|
||||||
import { Icon as ViewInRoomIcon } from '../../../../res/img/element-icons/view-in-room.svg';
|
import { Icon as ViewInRoomIcon } from '../../../../res/img/element-icons/view-in-room.svg';
|
||||||
|
@ -84,6 +85,7 @@ import { useTooltip } from "../../../utils/useTooltip";
|
||||||
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||||
import { ElementCall } from "../../../models/Call";
|
import { ElementCall } from "../../../models/Call";
|
||||||
|
import { UnreadNotificationBadge } from './NotificationBadge/UnreadNotificationBadge';
|
||||||
|
|
||||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
|
|
||||||
|
@ -113,7 +115,7 @@ export interface IEventTileType extends React.Component {
|
||||||
getEventTileOps?(): IEventTileOps;
|
getEventTileOps?(): IEventTileOps;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
export interface EventTileProps {
|
||||||
// the MatrixEvent to show
|
// the MatrixEvent to show
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
|
||||||
|
@ -248,7 +250,7 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MUST be rendered within a RoomContext with a set timelineRenderingType
|
// MUST be rendered within a RoomContext with a set timelineRenderingType
|
||||||
export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
export class UnwrappedEventTile extends React.Component<EventTileProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
private tile = React.createRef<IEventTileType>();
|
private tile = React.createRef<IEventTileType>();
|
||||||
|
@ -267,7 +269,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
constructor(props: EventTileProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
const thread = this.thread;
|
const thread = this.thread;
|
||||||
|
@ -394,7 +396,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
if (SettingsStore.getValue("feature_thread")) {
|
if (SettingsStore.getValue("feature_thread")) {
|
||||||
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||||
|
|
||||||
if (this.thread) {
|
if (this.thread && !this.supportsThreadNotifications) {
|
||||||
this.setupNotificationListener(this.thread);
|
this.setupNotificationListener(this.thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -405,33 +407,40 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
room?.on(ThreadEvent.New, this.onNewThread);
|
room?.on(ThreadEvent.New, this.onNewThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get supportsThreadNotifications(): boolean {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||||
|
}
|
||||||
|
|
||||||
private setupNotificationListener(thread: Thread): void {
|
private setupNotificationListener(thread: Thread): void {
|
||||||
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
|
if (!this.supportsThreadNotifications) {
|
||||||
|
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
|
||||||
this.threadState = notifications.getThreadRoomState(thread);
|
this.threadState = notifications.getThreadRoomState(thread);
|
||||||
|
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||||
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
this.onThreadStateUpdate();
|
||||||
this.onThreadStateUpdate();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onThreadStateUpdate = (): void => {
|
private onThreadStateUpdate = (): void => {
|
||||||
let threadNotification = null;
|
if (!this.supportsThreadNotifications) {
|
||||||
switch (this.threadState?.color) {
|
let threadNotification = null;
|
||||||
case NotificationColor.Grey:
|
switch (this.threadState?.color) {
|
||||||
threadNotification = NotificationCountType.Total;
|
case NotificationColor.Grey:
|
||||||
break;
|
threadNotification = NotificationCountType.Total;
|
||||||
case NotificationColor.Red:
|
break;
|
||||||
threadNotification = NotificationCountType.Highlight;
|
case NotificationColor.Red:
|
||||||
break;
|
threadNotification = NotificationCountType.Highlight;
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
threadNotification,
|
threadNotification,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateThread = (thread: Thread) => {
|
private updateThread = (thread: Thread) => {
|
||||||
if (thread !== this.state.thread) {
|
if (thread !== this.state.thread && !this.supportsThreadNotifications) {
|
||||||
if (this.threadState) {
|
if (this.threadState) {
|
||||||
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||||
}
|
}
|
||||||
|
@ -444,7 +453,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(nextProps: IProps) {
|
UNSAFE_componentWillReceiveProps(nextProps: EventTileProps) {
|
||||||
// re-check the sender verification as outgoing events progress through
|
// re-check the sender verification as outgoing events progress through
|
||||||
// the send process.
|
// the send process.
|
||||||
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
|
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
|
||||||
|
@ -452,7 +461,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
|
shouldComponentUpdate(nextProps: EventTileProps, nextState: IState): boolean {
|
||||||
if (objectHasDiff(this.state, nextState)) {
|
if (objectHasDiff(this.state, nextState)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -481,7 +490,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) {
|
componentDidUpdate() {
|
||||||
// If we're not listening for receipts and expect to be, register a listener.
|
// If we're not listening for receipts and expect to be, register a listener.
|
||||||
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
||||||
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||||
|
@ -531,7 +540,11 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderThreadInfo(): React.ReactNode {
|
private renderThreadInfo(): React.ReactNode {
|
||||||
if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
||||||
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
|
return <ThreadSummary
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
thread={this.state.thread}
|
||||||
|
data-testid="thread-summary"
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
|
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
|
||||||
|
@ -667,7 +680,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
}
|
}
|
||||||
|
|
||||||
private propsEqual(objA: IProps, objB: IProps): boolean {
|
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
|
||||||
const keysA = Object.keys(objA);
|
const keysA = Object.keys(objA);
|
||||||
const keysB = Object.keys(objB);
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
|
@ -1348,6 +1361,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
case TimelineRenderingType.ThreadsList: {
|
case TimelineRenderingType.ThreadsList: {
|
||||||
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
return (
|
return (
|
||||||
React.createElement(this.props.as || "li", {
|
React.createElement(this.props.as || "li", {
|
||||||
|
@ -1361,7 +1375,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
"data-shape": this.context.timelineRenderingType,
|
"data-shape": this.context.timelineRenderingType,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"data-notification": this.state.threadNotification,
|
"data-notification": !this.supportsThreadNotifications
|
||||||
|
? this.state.threadNotification
|
||||||
|
: undefined,
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
"onClick": (ev: MouseEvent) => {
|
"onClick": (ev: MouseEvent) => {
|
||||||
|
@ -1409,6 +1425,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
</RovingAccessibleTooltipButton>
|
</RovingAccessibleTooltipButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{ msgOption }
|
{ msgOption }
|
||||||
|
<UnreadNotificationBadge
|
||||||
|
room={room}
|
||||||
|
threadId={this.props.mxEvent.getId()} />
|
||||||
</>)
|
</>)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1512,10 +1531,12 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
||||||
const SafeEventTile = forwardRef((props: IProps, ref: RefObject<UnwrappedEventTile>) => {
|
const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => {
|
||||||
return <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
|
return <>
|
||||||
<UnwrappedEventTile ref={ref} {...props} />
|
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
|
||||||
</TileErrorBoundary>;
|
<UnwrappedEventTile ref={ref} {...props} />
|
||||||
|
</TileErrorBoundary>
|
||||||
|
</>;
|
||||||
});
|
});
|
||||||
export default SafeEventTile;
|
export default SafeEventTile;
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,9 @@ import {
|
||||||
startNewVoiceBroadcastRecording,
|
startNewVoiceBroadcastRecording,
|
||||||
VoiceBroadcastRecordingsStore,
|
VoiceBroadcastRecordingsStore,
|
||||||
} from '../../../voice-broadcast';
|
} from '../../../voice-broadcast';
|
||||||
import { WysiwygComposer } from './wysiwyg_composer/WysiwygComposer';
|
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
|
||||||
|
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
|
||||||
|
import { htmlToPlainText } from '../../../utils/room/htmlToPlaintext';
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
|
|
||||||
|
@ -78,7 +80,7 @@ function SendButton(props: ISendButtonProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends MatrixClientProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
@ -89,6 +91,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
composerContent: string;
|
||||||
isComposerEmpty: boolean;
|
isComposerEmpty: boolean;
|
||||||
haveRecording: boolean;
|
haveRecording: boolean;
|
||||||
recordingTimeLeftSeconds?: number;
|
recordingTimeLeftSeconds?: number;
|
||||||
|
@ -98,15 +101,17 @@ interface IState {
|
||||||
showStickersButton: boolean;
|
showStickersButton: boolean;
|
||||||
showPollsButton: boolean;
|
showPollsButton: boolean;
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
|
isWysiwygLabEnabled: boolean;
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
initialComposerContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef?: string;
|
private dispatcherRef?: string;
|
||||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private instanceId: number;
|
private instanceId: number;
|
||||||
private composerSendMessage?: () => void;
|
|
||||||
|
|
||||||
private _voiceRecording: Optional<VoiceMessageRecording>;
|
private _voiceRecording: Optional<VoiceMessageRecording>;
|
||||||
|
|
||||||
|
@ -116,6 +121,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
public static defaultProps = {
|
public static defaultProps = {
|
||||||
compact: false,
|
compact: false,
|
||||||
showVoiceBroadcastButton: false,
|
showVoiceBroadcastButton: false,
|
||||||
|
isRichTextEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
|
@ -124,6 +130,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isComposerEmpty: true,
|
isComposerEmpty: true,
|
||||||
|
composerContent: '',
|
||||||
haveRecording: false,
|
haveRecording: false,
|
||||||
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
|
@ -131,6 +138,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
||||||
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
||||||
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
||||||
|
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
|
||||||
|
isRichTextEnabled: true,
|
||||||
|
initialComposerContent: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.instanceId = instanceCount++;
|
this.instanceId = instanceCount++;
|
||||||
|
@ -138,6 +148,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
||||||
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
||||||
|
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get voiceRecording(): Optional<VoiceMessageRecording> {
|
private get voiceRecording(): Optional<VoiceMessageRecording> {
|
||||||
|
@ -218,6 +229,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "feature_wysiwyg_composer": {
|
||||||
|
if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) {
|
||||||
|
this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,7 +332,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageComposerInput.current?.sendMessage();
|
this.messageComposerInput.current?.sendMessage();
|
||||||
this.composerSendMessage?.();
|
|
||||||
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
|
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||||
|
sendMessage(this.state.composerContent,
|
||||||
|
this.state.isRichTextEnabled,
|
||||||
|
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
|
||||||
|
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
|
||||||
|
this.setState({ composerContent: '', initialComposerContent: '' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = (model: EditorModel) => {
|
private onChange = (model: EditorModel) => {
|
||||||
|
@ -326,10 +351,21 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onWysiwygChange = (content: string) => {
|
private onWysiwygChange = (content: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
composerContent: content,
|
||||||
isComposerEmpty: content?.length === 0,
|
isComposerEmpty: content?.length === 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onRichTextToggle = () => {
|
||||||
|
this.setState(state => ({
|
||||||
|
isRichTextEnabled: !state.isRichTextEnabled,
|
||||||
|
initialComposerContent: !state.isRichTextEnabled ?
|
||||||
|
state.composerContent :
|
||||||
|
// TODO when available use rust model plain text
|
||||||
|
htmlToPlainText(state.composerContent),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
private onVoiceStoreUpdate = () => {
|
private onVoiceStoreUpdate = () => {
|
||||||
this.updateRecordingState();
|
this.updateRecordingState();
|
||||||
};
|
};
|
||||||
|
@ -385,7 +421,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
|
||||||
const controls = [
|
const controls = [
|
||||||
this.props.e2eStatus ?
|
this.props.e2eStatus ?
|
||||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
||||||
|
@ -400,18 +435,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
if (isWysiwygComposerEnabled) {
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<WysiwygComposer key="controls_input"
|
<SendWysiwygComposer key="controls_input"
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
onChange={this.onWysiwygChange}
|
onChange={this.onWysiwygChange}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
onSend={this.sendMessage}
|
||||||
relation={this.props.relation}
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
replyToEvent={this.props.replyToEvent}>
|
initialContent={this.state.initialComposerContent}
|
||||||
{ (sendMessage) => {
|
/>,
|
||||||
this.composerSendMessage = sendMessage;
|
|
||||||
} }
|
|
||||||
</WysiwygComposer>,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
controls.push(
|
||||||
|
@ -498,7 +530,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
"mx_MessageComposer": true,
|
"mx_MessageComposer": true,
|
||||||
"mx_MessageComposer--compact": this.props.compact,
|
"mx_MessageComposer--compact": this.props.compact,
|
||||||
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
||||||
"mx_MessageComposer_wysiwyg": isWysiwygComposerEnabled,
|
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -527,6 +559,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
showLocationButton={!window.electron}
|
showLocationButton={!window.electron}
|
||||||
showPollsButton={this.state.showPollsButton}
|
showPollsButton={this.state.showPollsButton}
|
||||||
showStickersButton={this.showStickersButton}
|
showStickersButton={this.showStickersButton}
|
||||||
|
showComposerModeButton={this.state.isWysiwygLabEnabled}
|
||||||
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
|
onComposerModeClick={this.onRichTextToggle}
|
||||||
toggleButtonMenu={this.toggleButtonMenu}
|
toggleButtonMenu={this.toggleButtonMenu}
|
||||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||||
onStartVoiceBroadcastClick={() => {
|
onStartVoiceBroadcastClick={() => {
|
||||||
|
@ -551,3 +586,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer);
|
||||||
|
export default MessageComposerWithMatrixClient;
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
import { M_POLL_START } from "matrix-events-sdk";
|
import { M_POLL_START } from "matrix-events-sdk";
|
||||||
import React, { createContext, ReactElement, useContext, useRef } from 'react';
|
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
@ -55,6 +55,9 @@ interface IProps {
|
||||||
toggleButtonMenu: () => void;
|
toggleButtonMenu: () => void;
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
onStartVoiceBroadcastClick: () => void;
|
onStartVoiceBroadcastClick: () => void;
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
showComposerModeButton: boolean;
|
||||||
|
onComposerModeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OverflowMenuCloser = () => void;
|
type OverflowMenuCloser = () => void;
|
||||||
|
@ -85,6 +88,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
} else {
|
} else {
|
||||||
mainButtons = [
|
mainButtons = [
|
||||||
emojiButton(props),
|
emojiButton(props),
|
||||||
|
props.showComposerModeButton &&
|
||||||
|
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} />,
|
||||||
uploadButton(), // props passed via UploadButtonContext
|
uploadButton(), // props passed via UploadButtonContext
|
||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
|
@ -397,4 +402,23 @@ function showLocationButton(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WysiwygToggleButtonProps {
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
onClick: MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
|
||||||
|
const title = isRichTextEnabled ? _t("Show plain text") : _t("Show formatting");
|
||||||
|
|
||||||
|
return <CollapsibleButton
|
||||||
|
className="mx_MessageComposer_button"
|
||||||
|
iconClassName={classNames({
|
||||||
|
"mx_MessageComposer_plain_text": isRichTextEnabled,
|
||||||
|
"mx_MessageComposer_rich_text": !isRichTextEnabled,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
export default MessageComposerButtons;
|
export default MessageComposerButtons;
|
||||||
|
|
|
@ -175,14 +175,22 @@ const NewRoomIntro = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||||
body = <React.Fragment>
|
let avatar = (
|
||||||
<MiniAvatarUploader
|
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} viewAvatarOnClick={!!avatarUrl} />
|
||||||
hasAvatar={!!avatarUrl}
|
);
|
||||||
|
|
||||||
|
if (!avatarUrl) {
|
||||||
|
avatar = <MiniAvatarUploader
|
||||||
|
hasAvatar={false}
|
||||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||||
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||||
>
|
>
|
||||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} viewAvatarOnClick={true} />
|
{ avatar }
|
||||||
</MiniAvatarUploader>
|
</MiniAvatarUploader>;
|
||||||
|
}
|
||||||
|
|
||||||
|
body = <React.Fragment>
|
||||||
|
{ avatar }
|
||||||
|
|
||||||
<h2>{ room.name }</h2>
|
<h2>{ room.name }</h2>
|
||||||
|
|
||||||
|
|
|
@ -15,16 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { MouseEvent } from "react";
|
import React, { MouseEvent } from "react";
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import { formatCount } from "../../../utils/FormattingUtils";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import { XOR } from "../../../@types/common";
|
import { XOR } from "../../../@types/common";
|
||||||
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
import Tooltip from "../elements/Tooltip";
|
import Tooltip from "../elements/Tooltip";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import { StatelessNotificationBadge } from "./NotificationBadge/StatelessNotificationBadge";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
notification: NotificationState;
|
notification: NotificationState;
|
||||||
|
@ -113,61 +111,30 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||||
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
const { notification, showUnsentTooltip, forceCount, onClick } = this.props;
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
|
||||||
if (notification.isIdle) return null;
|
if (notification.isIdle) return null;
|
||||||
|
|
||||||
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/element-web/issues/14261
|
|
||||||
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
|
||||||
// See git diff for what that boolean state looks like.
|
|
||||||
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
|
||||||
const hasAnySymbol = notification.symbol || notification.count > 0;
|
|
||||||
let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount;
|
|
||||||
if (forceCount) {
|
if (forceCount) {
|
||||||
isEmptyBadge = false;
|
|
||||||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbol = notification.symbol || formatCount(notification.count);
|
let label: string;
|
||||||
if (isEmptyBadge) symbol = "";
|
let tooltip: JSX.Element;
|
||||||
|
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||||
const classes = classNames({
|
label = _t("Message didn't send. Click for info.");
|
||||||
'mx_NotificationBadge': true,
|
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount,
|
|
||||||
'mx_NotificationBadge_highlighted': notification.hasMentions,
|
|
||||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
|
||||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
|
||||||
'mx_NotificationBadge_3char': symbol.length > 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (onClick) {
|
|
||||||
let label: string;
|
|
||||||
let tooltip: JSX.Element;
|
|
||||||
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
|
||||||
label = _t("Message didn't send. Click for info.");
|
|
||||||
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleButton
|
|
||||||
aria-label={label}
|
|
||||||
{...props}
|
|
||||||
className={classes}
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseOver={this.onMouseOver}
|
|
||||||
onMouseLeave={this.onMouseLeave}
|
|
||||||
>
|
|
||||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
|
||||||
{ tooltip }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <StatelessNotificationBadge
|
||||||
<div className={classes}>
|
label={label}
|
||||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
symbol={notification.symbol}
|
||||||
</div>
|
count={notification.count}
|
||||||
);
|
color={notification.color}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
>
|
||||||
|
{ tooltip }
|
||||||
|
</StatelessNotificationBadge>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { MouseEvent } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { formatCount } from "../../../../utils/FormattingUtils";
|
||||||
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
|
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string | null;
|
||||||
|
count: number;
|
||||||
|
color: NotificationColor;
|
||||||
|
onClick?: (ev: MouseEvent) => void;
|
||||||
|
onMouseOver?: (ev: MouseEvent) => void;
|
||||||
|
onMouseLeave?: (ev: MouseEvent) => void;
|
||||||
|
children?: React.ReactChildren | JSX.Element;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatelessNotificationBadge({
|
||||||
|
symbol,
|
||||||
|
count,
|
||||||
|
color,
|
||||||
|
...props }: Props) {
|
||||||
|
// Don't show a badge if we don't need to
|
||||||
|
if (color === NotificationColor.None) return null;
|
||||||
|
|
||||||
|
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
||||||
|
|
||||||
|
const isEmptyBadge = symbol === null && count === 0;
|
||||||
|
|
||||||
|
if (symbol === null && count > 0) {
|
||||||
|
symbol = formatCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_NotificationBadge': true,
|
||||||
|
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount,
|
||||||
|
'mx_NotificationBadge_highlighted': color === NotificationColor.Red,
|
||||||
|
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||||
|
'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3,
|
||||||
|
'mx_NotificationBadge_3char': symbol?.length > 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.onClick) {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
aria-label={props.label}
|
||||||
|
{...props}
|
||||||
|
className={classes}
|
||||||
|
onClick={props.onClick}
|
||||||
|
onMouseOver={props.onMouseOver}
|
||||||
|
onMouseLeave={props.onMouseLeave}
|
||||||
|
>
|
||||||
|
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||||
|
{ props.children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useUnreadNotifications } from "../../../../hooks/useUnreadNotifications";
|
||||||
|
import { StatelessNotificationBadge } from "./StatelessNotificationBadge";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: Room;
|
||||||
|
threadId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnreadNotificationBadge({ room, threadId }: Props) {
|
||||||
|
const { symbol, count, color } = useUnreadNotifications(room, threadId);
|
||||||
|
|
||||||
|
return <StatelessNotificationBadge
|
||||||
|
symbol={symbol}
|
||||||
|
count={count}
|
||||||
|
color={color}
|
||||||
|
/>;
|
||||||
|
}
|
|
@ -263,9 +263,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
params: {
|
params: {
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
signurl: this.props.signUrl,
|
signurl: this.props.signUrl,
|
||||||
room_name: this.props.oobData ? this.props.oobData.room_name : null,
|
room_name: this.props.oobData?.name ?? null,
|
||||||
room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null,
|
room_avatar_url: this.props.oobData?.avatarUrl ?? null,
|
||||||
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
|
inviter_name: this.props.oobData?.inviterName ?? null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -570,8 +570,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
||||||
isAlphabetical = slidingList.sort[0] === "by_name";
|
isAlphabetical = slidingList.sort[0] === "by_name";
|
||||||
isUnreadFirst = (
|
isUnreadFirst = (
|
||||||
slidingList.sort[0] === "by_highlight_count" ||
|
slidingList.sort[0] === "by_notification_level"
|
||||||
slidingList.sort[0] === "by_notification_count"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ interface IProps {
|
||||||
thread: Thread;
|
thread: Thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThreadSummary = ({ mxEvent, thread }: IProps) => {
|
const ThreadSummary = ({ mxEvent, thread, ...props }: IProps) => {
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const cardContext = useContext(CardContext);
|
const cardContext = useContext(CardContext);
|
||||||
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
|
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
|
||||||
|
@ -50,6 +50,7 @@ const ThreadSummary = ({ mxEvent, thread }: IProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
className="mx_ThreadSummary"
|
className="mx_ThreadSummary"
|
||||||
onClick={(ev: ButtonEvent) => {
|
onClick={(ev: ButtonEvent) => {
|
||||||
defaultDispatcher.dispatch<ShowThreadPayload>({
|
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||||
|
@ -94,7 +95,9 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
|
||||||
await cli.decryptEventIfNeeded(lastReply);
|
await cli.decryptEventIfNeeded(lastReply);
|
||||||
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
|
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
|
||||||
}, [lastReply, content]);
|
}, [lastReply, content]);
|
||||||
if (!preview) return null;
|
if (!preview || !lastReply) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { forwardRef, RefObject } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import EditorStateTransfer from '../../../../utils/EditorStateTransfer';
|
||||||
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
|
import { EditionButtons } from './components/EditionButtons';
|
||||||
|
import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler';
|
||||||
|
import { useEditing } from './hooks/useEditing';
|
||||||
|
import { useInitialContent } from './hooks/useInitialContent';
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
|
function Content({ disabled }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||||
|
useWysiwygEditActionHandler(disabled, forwardRef);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface EditWysiwygComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
editorStateTransfer: EditorStateTransfer;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditWysiwygComposer({ editorStateTransfer, className, ...props }: EditWysiwygComposerProps) {
|
||||||
|
const initialContent = useInitialContent(editorStateTransfer);
|
||||||
|
const isReady = !editorStateTransfer || Boolean(initialContent);
|
||||||
|
|
||||||
|
const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(initialContent, editorStateTransfer);
|
||||||
|
|
||||||
|
return isReady && <WysiwygComposer
|
||||||
|
className={classNames("mx_EditWysiwygComposer", className)}
|
||||||
|
initialContent={initialContent}
|
||||||
|
onChange={onChange}
|
||||||
|
onSend={editMessage}
|
||||||
|
{...props}>
|
||||||
|
{ (ref) => (
|
||||||
|
<>
|
||||||
|
<Content disabled={props.disabled} ref={ref} />
|
||||||
|
<EditionButtons onCancelClick={endEditing} onSaveClick={editMessage} isSaveDisabled={isSaveDisabled} />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
</WysiwygComposer>;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { forwardRef, RefObject } from 'react';
|
||||||
|
|
||||||
|
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||||
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
|
import { PlainTextComposer } from './components/PlainTextComposer';
|
||||||
|
import { ComposerFunctions } from './types';
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
disabled: boolean;
|
||||||
|
composerFunctions: ComposerFunctions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
|
function Content({ disabled, composerFunctions }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||||
|
useWysiwygSendActionHandler(disabled, forwardRef, composerFunctions);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SendWysiwygComposerProps {
|
||||||
|
initialContent?: string;
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (content: string) => void;
|
||||||
|
onSend: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendWysiwygComposer({ isRichTextEnabled, ...props }: SendWysiwygComposerProps) {
|
||||||
|
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||||
|
|
||||||
|
return <Composer className="mx_SendWysiwygComposer" {...props}>
|
||||||
|
{ (ref, composerFunctions) => (
|
||||||
|
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||||
|
) }
|
||||||
|
</Composer>;
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
|
||||||
import { useWysiwyg, Wysiwyg, WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
|
|
||||||
|
|
||||||
import { Editor } from './Editor';
|
|
||||||
import { FormattingButtons } from './FormattingButtons';
|
|
||||||
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
|
|
||||||
import { sendMessage } from './message';
|
|
||||||
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
|
|
||||||
import { useRoomContext } from '../../../../contexts/RoomContext';
|
|
||||||
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
|
|
||||||
import { useSettingValue } from '../../../../hooks/useSettings';
|
|
||||||
|
|
||||||
interface WysiwygProps {
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange: (content: string) => void;
|
|
||||||
relation?: IEventRelation;
|
|
||||||
replyToEvent?: MatrixEvent;
|
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
|
||||||
includeReplyLegacyFallback?: boolean;
|
|
||||||
children?: (sendMessage: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WysiwygComposer(
|
|
||||||
{ disabled = false, onChange, children, ...props }: WysiwygProps,
|
|
||||||
) {
|
|
||||||
const roomContext = useRoomContext();
|
|
||||||
const mxClient = useMatrixClientContext();
|
|
||||||
const ctrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");
|
|
||||||
|
|
||||||
function inputEventProcessor(event: WysiwygInputEvent, wysiwyg: Wysiwyg): WysiwygInputEvent | null {
|
|
||||||
if (event instanceof ClipboardEvent) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(event.inputType === 'insertParagraph' && !ctrlEnterToSend) ||
|
|
||||||
event.inputType === 'sendMessage'
|
|
||||||
) {
|
|
||||||
sendMessage(content, { mxClient, roomContext, ...props });
|
|
||||||
wysiwyg.actions.clear();
|
|
||||||
ref.current?.focus();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ inputEventProcessor });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!disabled && content !== null) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
}, [onChange, content, disabled]);
|
|
||||||
|
|
||||||
const memoizedSendMessage = useCallback(() => {
|
|
||||||
sendMessage(content, { mxClient, roomContext, ...props });
|
|
||||||
wysiwyg.clear();
|
|
||||||
ref.current?.focus();
|
|
||||||
}, [content, mxClient, roomContext, wysiwyg, props, ref]);
|
|
||||||
|
|
||||||
useWysiwygActionHandler(disabled, ref);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_WysiwygComposer">
|
|
||||||
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
|
||||||
<Editor ref={ref} disabled={!isWysiwygReady || disabled} />
|
|
||||||
{ children?.(memoizedSendMessage) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import { _t } from '../../../../../languageHandler';
|
||||||
|
import AccessibleButton from '../../../elements/AccessibleButton';
|
||||||
|
|
||||||
|
interface EditionButtonsProps {
|
||||||
|
onCancelClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
onSaveClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
isSaveDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditionButtons({ onCancelClick, onSaveClick, isSaveDisabled = false }: EditionButtonsProps) {
|
||||||
|
return <div className="mx_EditWysiwygComposer_buttons">
|
||||||
|
<AccessibleButton kind="secondary" onClick={onCancelClick}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton kind="primary" onClick={onSaveClick} disabled={isSaveDisabled}>
|
||||||
|
{ _t("Save") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -15,14 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { MouseEventHandler } from "react";
|
import React, { MouseEventHandler } from "react";
|
||||||
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
import { FormattingFunctions, FormattingStates } from "@matrix-org/matrix-wysiwyg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||||
import { Alignment } from "../../elements/Tooltip";
|
import { Alignment } from "../../../elements/Tooltip";
|
||||||
import { KeyboardShortcut } from "../../settings/KeyboardShortcut";
|
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||||
import { KeyCombo } from "../../../../KeyBindingsManager";
|
import { KeyCombo } from "../../../../../KeyBindingsManager";
|
||||||
import { _td } from "../../../../languageHandler";
|
import { _td } from "../../../../../languageHandler";
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -55,8 +55,8 @@ function Button({ label, keyCombo, onClick, isActive, className }: ButtonProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattingButtonsProps {
|
interface FormattingButtonsProps {
|
||||||
composer: ReturnType<typeof useWysiwyg>['wysiwyg'];
|
composer: FormattingFunctions;
|
||||||
formattingStates: ReturnType<typeof useWysiwyg>['formattingStates'];
|
formattingStates: FormattingStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormattingButtons({ composer, formattingStates }: FormattingButtonsProps) {
|
export function FormattingButtons({ composer, formattingStates }: FormattingButtonsProps) {
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
||||||
|
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
||||||
|
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
||||||
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
import { ComposerFunctions } from '../types';
|
||||||
|
import { Editor } from "./Editor";
|
||||||
|
|
||||||
|
interface PlainTextComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
onSend: () => void;
|
||||||
|
initialContent?: string;
|
||||||
|
className?: string;
|
||||||
|
children?: (
|
||||||
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
|
composerFunctions: ComposerFunctions,
|
||||||
|
) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlainTextComposer({
|
||||||
|
className, disabled, onSend, onChange, children, initialContent }: PlainTextComposerProps,
|
||||||
|
) {
|
||||||
|
const { ref, onInput, onPaste, onKeyDown } = usePlainTextListeners(onChange, onSend);
|
||||||
|
const composerFunctions = useComposerFunctions(ref);
|
||||||
|
usePlainTextInitialization(initialContent, ref);
|
||||||
|
useSetCursorPosition(disabled, ref);
|
||||||
|
|
||||||
|
return <div
|
||||||
|
data-testid="PlainTextComposer"
|
||||||
|
className={className}
|
||||||
|
onInput={onInput}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
<Editor ref={ref} disabled={disabled} />
|
||||||
|
{ children?.(ref, composerFunctions) }
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
|
||||||
|
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
|
import { FormattingButtons } from './FormattingButtons';
|
||||||
|
import { Editor } from './Editor';
|
||||||
|
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
||||||
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
|
||||||
|
interface WysiwygComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
onSend: () => void;
|
||||||
|
initialContent?: string;
|
||||||
|
className?: string;
|
||||||
|
children?: (
|
||||||
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
|
wysiwyg: FormattingFunctions,
|
||||||
|
) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WysiwygComposer = memo(function WysiwygComposer(
|
||||||
|
{ disabled = false, onChange, onSend, initialContent, className, children }: WysiwygComposerProps,
|
||||||
|
) {
|
||||||
|
const inputEventProcessor = useInputEventProcessor(onSend);
|
||||||
|
|
||||||
|
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } =
|
||||||
|
useWysiwyg({ initialContent, inputEventProcessor });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!disabled && content !== null) {
|
||||||
|
onChange?.(content);
|
||||||
|
}
|
||||||
|
}, [onChange, content, disabled]);
|
||||||
|
|
||||||
|
const isReady = isWysiwygReady && !disabled;
|
||||||
|
useSetCursorPosition(!isReady, ref);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="WysiwygComposer" className={className}>
|
||||||
|
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
||||||
|
<Editor ref={ref} disabled={!isReady} />
|
||||||
|
{ children?.(ref, wysiwyg) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
|
@ -14,16 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingBody {
|
import { RefObject, useMemo } from "react";
|
||||||
align-items: flex-start;
|
|
||||||
background-color: $quinary-content;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-flex;
|
|
||||||
gap: $spacing-8;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceBroadcastRecordingBody_title {
|
export function useComposerFunctions(ref: RefObject<HTMLDivElement>) {
|
||||||
font-size: $font-12px;
|
return useMemo(() => ({
|
||||||
font-weight: $font-semi-bold;
|
clear: () => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}), [ref]);
|
||||||
}
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||||
|
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
import { endEditing } from "../utils/editing";
|
||||||
|
import { editMessage } from "../utils/message";
|
||||||
|
|
||||||
|
export function useEditing(initialContent: string, editorStateTransfer: EditorStateTransfer) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const mxClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
const [isSaveDisabled, setIsSaveDisabled] = useState(true);
|
||||||
|
const [content, setContent] = useState(initialContent);
|
||||||
|
const onChange = useCallback((_content: string) => {
|
||||||
|
setContent(_content);
|
||||||
|
setIsSaveDisabled(_isSaveDisabled => _isSaveDisabled && _content === initialContent);
|
||||||
|
}, [initialContent]);
|
||||||
|
|
||||||
|
const editMessageMemoized = useCallback(() =>
|
||||||
|
editMessage(content, { roomContext, mxClient, editorStateTransfer }),
|
||||||
|
[content, roomContext, mxClient, editorStateTransfer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]);
|
||||||
|
return { onChange, editMessage: editMessageMemoized, endEditing: endEditingMemoized, isSaveDisabled };
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||||
|
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import { parseEvent } from "../../../../../editor/deserialize";
|
||||||
|
import { CommandPartCreator, Part } from "../../../../../editor/parts";
|
||||||
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
function parseEditorStateTransfer(
|
||||||
|
editorStateTransfer: EditorStateTransfer,
|
||||||
|
room: Room,
|
||||||
|
mxClient: MatrixClient,
|
||||||
|
): string {
|
||||||
|
const partCreator = new CommandPartCreator(room, mxClient);
|
||||||
|
|
||||||
|
let parts: Part[];
|
||||||
|
if (editorStateTransfer.hasEditorState()) {
|
||||||
|
// if restoring state from a previous editor,
|
||||||
|
// restore serialized parts from the state
|
||||||
|
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||||
|
} else {
|
||||||
|
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||||
|
// TODO local storage
|
||||||
|
// const restoredParts = this.restoreStoredEditorState(partCreator);
|
||||||
|
|
||||||
|
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
|
||||||
|
return editorStateTransfer.getEvent().getContent().formatted_body || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = parseEvent(editorStateTransfer.getEvent(), partCreator, {
|
||||||
|
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.reduce((content, part) => content + part.text, '');
|
||||||
|
// Todo local storage
|
||||||
|
// this.saveStoredEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInitialContent(editorStateTransfer: EditorStateTransfer) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const mxClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
return useMemo<string>(() => {
|
||||||
|
if (editorStateTransfer && roomContext.room) {
|
||||||
|
return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient);
|
||||||
|
}
|
||||||
|
}, [editorStateTransfer, roomContext, mxClient]);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
|
||||||
|
export function useInputEventProcessor(onSend: () => void) {
|
||||||
|
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
|
return useCallback((event: WysiwygInputEvent) => {
|
||||||
|
if (event instanceof ClipboardEvent) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.inputType === 'insertParagraph' && !isCtrlEnter) ||
|
||||||
|
event.inputType === 'sendMessage'
|
||||||
|
) {
|
||||||
|
onSend();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
, [isCtrlEnter, onSend]);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
|
export function usePlainTextInitialization(initialContent: string, ref: RefObject<HTMLElement>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerText = initialContent;
|
||||||
|
}
|
||||||
|
}, [ref, initialContent]);
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
|
||||||
|
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
||||||
|
return target instanceof HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlainTextListeners(onChange: (content: string) => void, onSend: () => void) {
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const send = useCallback((() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
onSend();
|
||||||
|
}), [ref, onSend]);
|
||||||
|
|
||||||
|
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||||
|
if (isDivElement(event.target)) {
|
||||||
|
onChange(event.target.innerHTML);
|
||||||
|
}
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
|
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}, [isCtrlEnter, send]);
|
||||||
|
|
||||||
|
return { ref, onInput, onPaste: onInput, onKeyDown };
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
|
import { setCursorPositionAtTheEnd } from "./utils";
|
||||||
|
|
||||||
|
export function useSetCursorPosition(disabled: boolean, ref: RefObject<HTMLElement>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && !disabled) {
|
||||||
|
setCursorPositionAtTheEnd(ref.current);
|
||||||
|
}
|
||||||
|
}, [ref, disabled]);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RefObject, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
|
import { focusComposer } from "./utils";
|
||||||
|
|
||||||
|
export function useWysiwygEditActionHandler(
|
||||||
|
disabled: boolean,
|
||||||
|
composerElement: RefObject<HTMLElement>,
|
||||||
|
) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
// to the cursor being in the composer
|
||||||
|
if (disabled || !composerElement.current) return;
|
||||||
|
|
||||||
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
switch (payload.action) {
|
||||||
|
case Action.FocusEditMessageComposer:
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [disabled, composerElement, timeoutId, roomContext]);
|
||||||
|
|
||||||
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RefObject, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
|
import { focusComposer } from "./utils";
|
||||||
|
import { ComposerFunctions } from "../types";
|
||||||
|
|
||||||
|
export function useWysiwygSendActionHandler(
|
||||||
|
disabled: boolean,
|
||||||
|
composerElement: RefObject<HTMLElement>,
|
||||||
|
composerFunctions: ComposerFunctions,
|
||||||
|
) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
// to the cursor being in the composer
|
||||||
|
if (disabled || !composerElement.current) return;
|
||||||
|
|
||||||
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
switch (payload.action) {
|
||||||
|
case "reply_to_event":
|
||||||
|
case Action.FocusSendMessageComposer:
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
case Action.ClearAndFocusSendMessageComposer:
|
||||||
|
composerFunctions.clear();
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
||||||
|
}
|
||||||
|
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
|
||||||
|
|
||||||
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
}
|
|
@ -14,40 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
|
||||||
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
|
||||||
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
export function focusComposer(
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
|
||||||
import { ActionPayload } from "../../../../dispatcher/payloads";
|
|
||||||
import { IRoomState } from "../../../structures/RoomView";
|
|
||||||
import { TimelineRenderingType, useRoomContext } from "../../../../contexts/RoomContext";
|
|
||||||
import { useDispatcher } from "../../../../hooks/useDispatcher";
|
|
||||||
|
|
||||||
export function useWysiwygActionHandler(
|
|
||||||
disabled: boolean,
|
|
||||||
composerElement: React.MutableRefObject<HTMLElement>,
|
|
||||||
) {
|
|
||||||
const roomContext = useRoomContext();
|
|
||||||
const timeoutId = useRef<number>();
|
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
|
|
||||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
|
||||||
// to the cursor being in the composer
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
const context = payload.context ?? TimelineRenderingType.Room;
|
|
||||||
|
|
||||||
switch (payload.action) {
|
|
||||||
case "reply_to_event":
|
|
||||||
case Action.FocusSendMessageComposer:
|
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
|
||||||
break;
|
|
||||||
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusComposer(
|
|
||||||
composerElement: React.MutableRefObject<HTMLElement>,
|
composerElement: React.MutableRefObject<HTMLElement>,
|
||||||
renderingType: TimelineRenderingType,
|
renderingType: TimelineRenderingType,
|
||||||
roomContext: IRoomState,
|
roomContext: IRoomState,
|
||||||
|
@ -71,3 +41,14 @@ function focusComposer(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCursorPositionAtTheEnd(element: HTMLElement) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(element);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = document.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
element.focus();
|
||||||
|
}
|
19
src/components/views/rooms/wysiwyg_composer/index.ts
Normal file
19
src/components/views/rooms/wysiwyg_composer/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SendWysiwygComposer } from './SendWysiwygComposer';
|
||||||
|
export { EditWysiwygComposer } from './EditWysiwygComposer';
|
||||||
|
export { sendMessage } from './utils/message';
|
19
src/components/views/rooms/wysiwyg_composer/types.ts
Normal file
19
src/components/views/rooms/wysiwyg_composer/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ComposerFunctions = {
|
||||||
|
clear: () => void;
|
||||||
|
};
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { htmlSerializeFromMdIfNeeded } from "../../../../../editor/serialize";
|
||||||
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||||
|
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
||||||
|
import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
|
||||||
|
|
||||||
|
// Merges favouring the given relation
|
||||||
|
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||||
|
if (relation) {
|
||||||
|
content['m.relates_to'] = {
|
||||||
|
...(content['m.relates_to'] || {}),
|
||||||
|
...relation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
if (!html) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
|
||||||
|
const mxReply = rootNode.querySelector("mx-reply");
|
||||||
|
return (mxReply && mxReply.outerHTML) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
|
const body = mxEvent.getContent().body;
|
||||||
|
if (typeof body !== 'string') {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const lines = body.split("\n").map(l => l.trim());
|
||||||
|
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
||||||
|
return `${lines[0]}\n\n`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateMessageContentParams {
|
||||||
|
relation?: IEventRelation;
|
||||||
|
replyToEvent?: MatrixEvent;
|
||||||
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
includeReplyLegacyFallback?: boolean;
|
||||||
|
editedEvent?: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMessageContent(
|
||||||
|
message: string,
|
||||||
|
isHTML: boolean,
|
||||||
|
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
|
||||||
|
CreateMessageContentParams,
|
||||||
|
): IContent {
|
||||||
|
// TODO emote ?
|
||||||
|
|
||||||
|
const isEditing = Boolean(editedEvent);
|
||||||
|
const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent);
|
||||||
|
const isReplyAndEditing = isEditing && isReply;
|
||||||
|
|
||||||
|
/*const isEmote = containsEmote(model);
|
||||||
|
if (isEmote) {
|
||||||
|
model = stripEmoteCommand(model);
|
||||||
|
}
|
||||||
|
if (startsWith(model, "//")) {
|
||||||
|
model = stripPrefix(model, "/");
|
||||||
|
}
|
||||||
|
model = unescapeMessage(model);*/
|
||||||
|
|
||||||
|
// const body = textSerialize(model);
|
||||||
|
|
||||||
|
// TODO remove this ugly hack for replace br tag
|
||||||
|
const body = isHTML && htmlToPlainText(message) || message.replace(/<br>/g, '\n');
|
||||||
|
const bodyPrefix = isReplyAndEditing && getTextReplyFallback(editedEvent) || '';
|
||||||
|
const formattedBodyPrefix = isReplyAndEditing && getHtmlReplyFallback(editedEvent) || '';
|
||||||
|
|
||||||
|
const content: IContent = {
|
||||||
|
// TODO emote
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
// TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
|
||||||
|
body: isEditing ? `${bodyPrefix} * ${body}` : body,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO markdown support
|
||||||
|
|
||||||
|
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
|
||||||
|
const formattedBody =
|
||||||
|
isHTML ?
|
||||||
|
message :
|
||||||
|
isMarkdownEnabled ?
|
||||||
|
htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (formattedBody) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
content['m.new_content'] = {
|
||||||
|
"msgtype": content.msgtype,
|
||||||
|
"body": body,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formattedBody) {
|
||||||
|
content['m.new_content'].format = "org.matrix.custom.html";
|
||||||
|
content['m.new_content']['formatted_body'] = formattedBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRelation = isEditing ?
|
||||||
|
{ ...relation, 'rel_type': 'm.replace', 'event_id': editedEvent.getId() }
|
||||||
|
: relation;
|
||||||
|
|
||||||
|
attachRelation(content, newRelation);
|
||||||
|
|
||||||
|
if (!isEditing && replyToEvent && permalinkCreator) {
|
||||||
|
addReplyToMessageContent(content, replyToEvent, {
|
||||||
|
permalinkCreator,
|
||||||
|
includeLegacyFallback: includeReplyLegacyFallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
50
src/components/views/rooms/wysiwyg_composer/utils/editing.ts
Normal file
50
src/components/views/rooms/wysiwyg_composer/utils/editing.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventStatus, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
import dis from '../../../../../dispatcher/dispatcher';
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
export function endEditing(roomContext: IRoomState) {
|
||||||
|
// todo local storage
|
||||||
|
// localStorage.removeItem(this.editorRoomKey);
|
||||||
|
// localStorage.removeItem(this.editorStateKey);
|
||||||
|
|
||||||
|
// close the event editing and focus composer
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: roomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.FocusSendMessageComposer,
|
||||||
|
context: roomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPreviousPendingEdit(mxClient: MatrixClient, editorStateTransfer: EditorStateTransfer) {
|
||||||
|
const originalEvent = editorStateTransfer.getEvent();
|
||||||
|
const previousEdit = originalEvent.replacingEvent();
|
||||||
|
if (previousEdit && (
|
||||||
|
previousEdit.status === EventStatus.QUEUED ||
|
||||||
|
previousEdit.status === EventStatus.NOT_SENT
|
||||||
|
)) {
|
||||||
|
mxClient.cancelPendingEvent(previousEdit);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IContent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
export function isContentModified(newContent: IContent, editorStateTransfer: EditorStateTransfer): boolean {
|
||||||
|
// if nothing has changed then bail
|
||||||
|
const oldContent = editorStateTransfer.getEvent().getContent();
|
||||||
|
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||||
|
oldContent["format"] === newContent["format"] &&
|
||||||
|
oldContent["formatted_body"] === newContent["formatted_body"]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -16,93 +16,36 @@ limitations under the License.
|
||||||
|
|
||||||
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||||
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||||
import SettingsStore from "../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics";
|
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics";
|
||||||
import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||||
import { doMaybeLocalRoomAction } from "../../../../utils/local-room";
|
import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
|
||||||
import { CHAT_EFFECTS } from "../../../../effects";
|
import { CHAT_EFFECTS } from "../../../../../effects";
|
||||||
import { containsEmoji } from "../../../../effects/utils";
|
import { containsEmoji } from "../../../../../effects/utils";
|
||||||
import { IRoomState } from "../../../structures/RoomView";
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
import dis from '../../../../dispatcher/dispatcher';
|
import dis from '../../../../../dispatcher/dispatcher';
|
||||||
import { addReplyToMessageContent } from "../../../../utils/Reply";
|
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
|
||||||
|
import { endEditing, cancelPreviousPendingEdit } from "./editing";
|
||||||
// Merges favouring the given relation
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
import { createMessageContent } from "./createMessageContent";
|
||||||
if (relation) {
|
import { isContentModified } from "./isContentModified";
|
||||||
content['m.relates_to'] = {
|
|
||||||
...(content['m.relates_to'] || {}),
|
|
||||||
...relation,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SendMessageParams {
|
interface SendMessageParams {
|
||||||
mxClient: MatrixClient;
|
mxClient: MatrixClient;
|
||||||
relation?: IEventRelation;
|
relation?: IEventRelation;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
roomContext: IRoomState;
|
roomContext: IRoomState;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
includeReplyLegacyFallback?: boolean;
|
includeReplyLegacyFallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exported for tests
|
|
||||||
export function createMessageContent(
|
|
||||||
message: string,
|
|
||||||
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }:
|
|
||||||
Omit<SendMessageParams, 'roomContext' | 'mxClient'>,
|
|
||||||
): IContent {
|
|
||||||
// TODO emote ?
|
|
||||||
|
|
||||||
/*const isEmote = containsEmote(model);
|
|
||||||
if (isEmote) {
|
|
||||||
model = stripEmoteCommand(model);
|
|
||||||
}
|
|
||||||
if (startsWith(model, "//")) {
|
|
||||||
model = stripPrefix(model, "/");
|
|
||||||
}
|
|
||||||
model = unescapeMessage(model);*/
|
|
||||||
|
|
||||||
// const body = textSerialize(model);
|
|
||||||
const body = message;
|
|
||||||
|
|
||||||
const content: IContent = {
|
|
||||||
// TODO emote
|
|
||||||
// msgtype: isEmote ? "m.emote" : "m.text",
|
|
||||||
msgtype: "m.text",
|
|
||||||
body: body,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO markdown support
|
|
||||||
|
|
||||||
/*const formattedBody = htmlSerializeIfNeeded(model, {
|
|
||||||
forceHTML: !!replyToEvent,
|
|
||||||
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
|
||||||
});*/
|
|
||||||
const formattedBody = message;
|
|
||||||
|
|
||||||
if (formattedBody) {
|
|
||||||
content.format = "org.matrix.custom.html";
|
|
||||||
content.formatted_body = formattedBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachRelation(content, relation);
|
|
||||||
|
|
||||||
if (replyToEvent) {
|
|
||||||
addReplyToMessageContent(content, replyToEvent, {
|
|
||||||
permalinkCreator,
|
|
||||||
includeLegacyFallback: includeReplyLegacyFallback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendMessage(
|
export function sendMessage(
|
||||||
message: string,
|
message: string,
|
||||||
|
isHTML: boolean,
|
||||||
{ roomContext, mxClient, ...params }: SendMessageParams,
|
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||||
) {
|
) {
|
||||||
const { relation, replyToEvent } = params;
|
const { relation, replyToEvent } = params;
|
||||||
|
@ -113,6 +56,7 @@ export function sendMessage(
|
||||||
eventName: "Composer",
|
eventName: "Composer",
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isReply: Boolean(replyToEvent),
|
isReply: Boolean(replyToEvent),
|
||||||
|
// TODO thread
|
||||||
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,6 +78,7 @@ export function sendMessage(
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
message,
|
message,
|
||||||
|
isHTML,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -197,3 +142,68 @@ export function sendMessage(
|
||||||
|
|
||||||
return prom;
|
return prom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditMessageParams {
|
||||||
|
mxClient: MatrixClient;
|
||||||
|
roomContext: IRoomState;
|
||||||
|
editorStateTransfer: EditorStateTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editMessage(
|
||||||
|
html: string,
|
||||||
|
{ roomContext, mxClient, editorStateTransfer }: EditMessageParams,
|
||||||
|
) {
|
||||||
|
const editedEvent = editorStateTransfer.getEvent();
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
||||||
|
eventName: "Composer",
|
||||||
|
isEditing: true,
|
||||||
|
inThread: Boolean(editedEvent?.getThread()),
|
||||||
|
isReply: Boolean(editedEvent.replyEventId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO emoji
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
/* if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}*/
|
||||||
|
const editContent = createMessageContent(html, true, { editedEvent });
|
||||||
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
|
const shouldSend = true;
|
||||||
|
|
||||||
|
if (newContent?.body === '') {
|
||||||
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
||||||
|
createRedactEventDialog({
|
||||||
|
mxEvent: editedEvent,
|
||||||
|
onCloseDialog: () => {
|
||||||
|
endEditing(roomContext);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Promise<ISendEventResponse> | undefined;
|
||||||
|
|
||||||
|
// If content is modified then send an updated event into the room
|
||||||
|
if (isContentModified(newContent, editorStateTransfer)) {
|
||||||
|
const roomId = editedEvent.getRoomId();
|
||||||
|
|
||||||
|
// TODO Slash Commands
|
||||||
|
|
||||||
|
if (shouldSend) {
|
||||||
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
||||||
|
|
||||||
|
const event = editorStateTransfer.getEvent();
|
||||||
|
const threadId = event.threadRootId || null;
|
||||||
|
|
||||||
|
response = mxClient.sendMessage(roomId, threadId, editContent);
|
||||||
|
dis.dispatch({ action: "message_sent" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endEditing(roomContext);
|
||||||
|
return response;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import React, { FormEvent, useEffect, useState } from 'react';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import AccessibleButton from '../../elements/AccessibleButton';
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
import Field from '../../elements/Field';
|
import Field from '../../elements/Field';
|
||||||
|
import LearnMore from '../../elements/LearnMore';
|
||||||
import Spinner from '../../elements/Spinner';
|
import Spinner from '../../elements/Spinner';
|
||||||
import { Caption } from '../../typography/Caption';
|
import { Caption } from '../../typography/Caption';
|
||||||
import Heading from '../../typography/Heading';
|
import Heading from '../../typography/Heading';
|
||||||
|
@ -88,7 +89,22 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
|
||||||
<Caption
|
<Caption
|
||||||
id={descriptionId}
|
id={descriptionId}
|
||||||
>
|
>
|
||||||
{ _t('Please be aware that session names are also visible to people you communicate with') }
|
{ _t('Please be aware that session names are also visible to people you communicate with.') }
|
||||||
|
<LearnMore
|
||||||
|
title={_t('Renaming sessions')}
|
||||||
|
description={<>
|
||||||
|
<p>
|
||||||
|
{ _t(`Other users in direct messages and rooms that you join ` +
|
||||||
|
`are able to view a full list of your sessions.`,
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(`This provides them with confidence that they are really speaking to you, ` +
|
||||||
|
`but it also means they can see the session name you enter here.`,
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</>}
|
||||||
|
/>
|
||||||
{ !!error &&
|
{ !!error &&
|
||||||
<span
|
<span
|
||||||
data-testid="device-rename-error"
|
data-testid="device-rename-error"
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import LearnMore, { LearnMoreProps } from "../../elements/LearnMore";
|
||||||
|
import { DeviceSecurityVariation } from "./types";
|
||||||
|
|
||||||
|
interface Props extends Omit<LearnMoreProps, 'title' | 'description'> {
|
||||||
|
variation: DeviceSecurityVariation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
|
title: string;
|
||||||
|
description: React.ReactNode | string;
|
||||||
|
}> = {
|
||||||
|
[DeviceSecurityVariation.Verified]: {
|
||||||
|
title: _t('Verified sessions'),
|
||||||
|
description: <>
|
||||||
|
<p>{ _t('Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.') }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
`This means they hold encryption keys for your previous messages, ` +
|
||||||
|
`and confirm to other users you are communicating with that these sessions are really you.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</>,
|
||||||
|
},
|
||||||
|
[DeviceSecurityVariation.Unverified]: {
|
||||||
|
title: _t('Unverified sessions'),
|
||||||
|
description: <>
|
||||||
|
<p>{ _t('Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.') }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
`You should make especially certain that you recognise these sessions ` +
|
||||||
|
`as they could represent an unauthorised use of your account.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</>,
|
||||||
|
},
|
||||||
|
[DeviceSecurityVariation.Inactive]: {
|
||||||
|
title: _t('Inactive sessions'),
|
||||||
|
description: <>
|
||||||
|
<p>{ _t('Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.') }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
`Removing inactive sessions improves security and performance, ` +
|
||||||
|
`and makes it easier for you to identify if a new session is suspicious.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LearnMore with content for device security warnings
|
||||||
|
*/
|
||||||
|
export const DeviceSecurityLearnMore: React.FC<Props> = ({ variation }) => {
|
||||||
|
const { title, description } = securityCardContent[variation];
|
||||||
|
return <LearnMore title={title} description={description} />;
|
||||||
|
};
|
|
@ -19,6 +19,7 @@ import React from 'react';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import AccessibleButton from '../../elements/AccessibleButton';
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||||
|
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||||
import {
|
import {
|
||||||
DeviceSecurityVariation,
|
DeviceSecurityVariation,
|
||||||
ExtendedDevice,
|
ExtendedDevice,
|
||||||
|
@ -36,11 +37,17 @@ export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||||
const securityCardProps = device.isVerified ? {
|
const securityCardProps = device.isVerified ? {
|
||||||
variation: DeviceSecurityVariation.Verified,
|
variation: DeviceSecurityVariation.Verified,
|
||||||
heading: _t('Verified session'),
|
heading: _t('Verified session'),
|
||||||
description: _t('This session is ready for secure messaging.'),
|
description: <>
|
||||||
|
{ _t('This session is ready for secure messaging.') }
|
||||||
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||||
|
</>,
|
||||||
} : {
|
} : {
|
||||||
variation: DeviceSecurityVariation.Unverified,
|
variation: DeviceSecurityVariation.Unverified,
|
||||||
heading: _t('Unverified session'),
|
heading: _t('Unverified session'),
|
||||||
description: _t('Verify or sign out from this session for best security and reliability.'),
|
description: <>
|
||||||
|
{ _t('Verify or sign out from this session for best security and reliability.') }
|
||||||
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||||
|
</>,
|
||||||
};
|
};
|
||||||
return <DeviceSecurityCard
|
return <DeviceSecurityCard
|
||||||
{...securityCardProps}
|
{...securityCardProps}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {
|
||||||
import { DevicesState } from './useOwnDevices';
|
import { DevicesState } from './useOwnDevices';
|
||||||
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
|
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
|
||||||
import Spinner from '../../elements/Spinner';
|
import Spinner from '../../elements/Spinner';
|
||||||
|
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
devices: DevicesDictionary;
|
devices: DevicesDictionary;
|
||||||
|
@ -73,48 +74,53 @@ const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSec
|
||||||
const ALL_FILTER_ID = 'ALL';
|
const ALL_FILTER_ID = 'ALL';
|
||||||
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
||||||
|
|
||||||
|
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}> = {
|
||||||
|
[DeviceSecurityVariation.Verified]: {
|
||||||
|
title: _t('Verified sessions'),
|
||||||
|
description: _t('For best security, sign out from any session that you don\'t recognize or use anymore.'),
|
||||||
|
},
|
||||||
|
[DeviceSecurityVariation.Unverified]: {
|
||||||
|
title: _t('Unverified sessions'),
|
||||||
|
description: _t(
|
||||||
|
`Verify your sessions for enhanced secure messaging or ` +
|
||||||
|
`sign out from those you don't recognize or use anymore.`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[DeviceSecurityVariation.Inactive]: {
|
||||||
|
title: _t('Inactive sessions'),
|
||||||
|
description: _t(
|
||||||
|
`Consider signing out from old sessions ` +
|
||||||
|
`(%(inactiveAgeDays)s days or older) you don't use anymore.`,
|
||||||
|
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation =>
|
||||||
|
Object.values<string>(DeviceSecurityVariation).includes(filter);
|
||||||
|
|
||||||
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
||||||
switch (filter) {
|
if (isSecurityVariation(filter)) {
|
||||||
case DeviceSecurityVariation.Verified:
|
const { title, description } = securityCardContent[filter];
|
||||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
return <div className='mx_FilteredDeviceList_securityCard'>
|
||||||
<DeviceSecurityCard
|
<DeviceSecurityCard
|
||||||
variation={DeviceSecurityVariation.Verified}
|
variation={filter}
|
||||||
heading={_t('Verified sessions')}
|
heading={title}
|
||||||
description={_t(
|
description={<span>
|
||||||
`For best security, sign out from any session` +
|
{ description }
|
||||||
` that you don't recognize or use anymore.`,
|
<DeviceSecurityLearnMore
|
||||||
)}
|
variation={filter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>}
|
||||||
;
|
/>
|
||||||
case DeviceSecurityVariation.Unverified:
|
</div>
|
||||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
;
|
||||||
<DeviceSecurityCard
|
|
||||||
variation={DeviceSecurityVariation.Unverified}
|
|
||||||
heading={_t('Unverified sessions')}
|
|
||||||
description={_t(
|
|
||||||
`Verify your sessions for enhanced secure messaging or sign out`
|
|
||||||
+ ` from those you don't recognize or use anymore.`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
;
|
|
||||||
case DeviceSecurityVariation.Inactive:
|
|
||||||
return <div className='mx_FilteredDeviceList_securityCard'>
|
|
||||||
<DeviceSecurityCard
|
|
||||||
variation={DeviceSecurityVariation.Inactive}
|
|
||||||
heading={_t('Inactive sessions')}
|
|
||||||
description={_t(
|
|
||||||
`Consider signing out from old sessions ` +
|
|
||||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
|
||||||
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from '../../../../languageHandler';
|
||||||
import AccessibleButton from '../../elements/AccessibleButton';
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||||
|
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||||
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||||
import {
|
import {
|
||||||
DeviceSecurityVariation,
|
DeviceSecurityVariation,
|
||||||
|
@ -70,10 +71,13 @@ const SecurityRecommendations: React.FC<Props> = ({
|
||||||
<DeviceSecurityCard
|
<DeviceSecurityCard
|
||||||
variation={DeviceSecurityVariation.Unverified}
|
variation={DeviceSecurityVariation.Unverified}
|
||||||
heading={_t('Unverified sessions')}
|
heading={_t('Unverified sessions')}
|
||||||
description={_t(
|
description={<>
|
||||||
`Verify your sessions for enhanced secure messaging` +
|
{ _t(
|
||||||
|
`Verify your sessions for enhanced secure messaging` +
|
||||||
` or sign out from those you don't recognize or use anymore.`,
|
` or sign out from those you don't recognize or use anymore.`,
|
||||||
)}
|
) }
|
||||||
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||||
|
</>}
|
||||||
>
|
>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='link_inline'
|
kind='link_inline'
|
||||||
|
@ -91,11 +95,15 @@ const SecurityRecommendations: React.FC<Props> = ({
|
||||||
<DeviceSecurityCard
|
<DeviceSecurityCard
|
||||||
variation={DeviceSecurityVariation.Inactive}
|
variation={DeviceSecurityVariation.Inactive}
|
||||||
heading={_t('Inactive sessions')}
|
heading={_t('Inactive sessions')}
|
||||||
description={_t(
|
description={<>
|
||||||
`Consider signing out from old sessions ` +
|
{ _t(
|
||||||
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
`Consider signing out from old sessions ` +
|
||||||
{ inactiveAgeDays },
|
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
|
||||||
)}
|
{ inactiveAgeDays },
|
||||||
|
) }
|
||||||
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='link_inline'
|
kind='link_inline'
|
||||||
|
|
|
@ -36,6 +36,25 @@ import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
||||||
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
||||||
import SettingsStore from '../../../../../settings/SettingsStore';
|
import SettingsStore from '../../../../../settings/SettingsStore';
|
||||||
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
||||||
|
import QuestionDialog from '../../../dialogs/QuestionDialog';
|
||||||
|
|
||||||
|
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||||
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||||
|
title: _t("Sign out"),
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>{ _t("Are you sure you want to sign out of %(count)s sessions?", {
|
||||||
|
count: sessionsToSignOutCount,
|
||||||
|
}) }</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cancelButton: _t('Cancel'),
|
||||||
|
button: _t("Sign out"),
|
||||||
|
});
|
||||||
|
const [confirmed] = await finished;
|
||||||
|
|
||||||
|
return confirmed;
|
||||||
|
};
|
||||||
|
|
||||||
const useSignOut = (
|
const useSignOut = (
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
|
@ -61,6 +80,11 @@ const useSignOut = (
|
||||||
if (!deviceIds.length) {
|
if (!deviceIds.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const userConfirmedSignout = await confirmSignOut(deviceIds.length);
|
||||||
|
if (!userConfirmedSignout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSigningOutDeviceIds([...signingOutDeviceIds, ...deviceIds]);
|
setSigningOutDeviceIds([...signingOutDeviceIds, ...deviceIds]);
|
||||||
await deleteDevicesWithInteractiveAuth(
|
await deleteDevicesWithInteractiveAuth(
|
||||||
|
|
|
@ -33,7 +33,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||||
import { CallStore } from "../../../stores/CallStore";
|
import { CallStore } from "../../../stores/CallStore";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
|
@ -141,36 +141,38 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallBu
|
||||||
}, [videoMuted, setVideoMuted]);
|
}, [videoMuted, setVideoMuted]);
|
||||||
|
|
||||||
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(async () => {
|
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(async () => {
|
||||||
let previewStream: MediaStream;
|
let devices = await MediaDeviceHandler.getDevices();
|
||||||
|
|
||||||
|
// We get the preview stream before requesting devices: this is because
|
||||||
|
// we need (in some browsers) an active media stream in order to get
|
||||||
|
// non-blank labels for the devices.
|
||||||
|
let stream: MediaStream | null = null;
|
||||||
try {
|
try {
|
||||||
// We get the preview stream before requesting devices: this is because
|
if (devices.audioinput.length > 0) {
|
||||||
// we need (in some browsers) an active media stream in order to get
|
// Holding just an audio stream will be enough to get us all device labels, so
|
||||||
// non-blank labels for the devices. According to the docs, we
|
// if video is muted, don't bother requesting video.
|
||||||
// need a stream of each type (audio + video) if we want to enumerate
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
// audio & video devices, although this didn't seem to be the case
|
audio: true,
|
||||||
// in practice for me. We request both anyway.
|
video: !videoMuted && devices.videoinput.length > 0 && { deviceId: videoInputId },
|
||||||
// For similar reasons, we also request a stream even if video is muted,
|
});
|
||||||
// which could be a bit strange but allows us to get the device list
|
} else if (devices.videoinput.length > 0) {
|
||||||
// reliably. One option could be to try & get devices without a stream,
|
// We have to resort to a video stream, even if video is supposed to be muted.
|
||||||
// then try again with a stream if we get blank deviceids, but... ew.
|
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoInputId } });
|
||||||
previewStream = await navigator.mediaDevices.getUserMedia({
|
}
|
||||||
video: { deviceId: videoInputId },
|
|
||||||
audio: { deviceId: MediaDeviceHandler.getAudioInput() },
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to get stream for device ${videoInputId}`, e);
|
logger.error(`Failed to get stream for device ${videoInputId}`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = await MediaDeviceHandler.getDevices();
|
// Refresh the devices now that we hold a stream
|
||||||
|
if (stream !== null) devices = await MediaDeviceHandler.getDevices();
|
||||||
|
|
||||||
// If video is muted, we don't actually want the stream, so we can get rid of
|
// If video is muted, we don't actually want the stream, so we can get rid of it now.
|
||||||
// it now.
|
|
||||||
if (videoMuted) {
|
if (videoMuted) {
|
||||||
previewStream.getTracks().forEach(t => t.stop());
|
stream?.getTracks().forEach(t => t.stop());
|
||||||
previewStream = undefined;
|
stream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [previewStream, devices[MediaDeviceKindEnum.AudioInput], devices[MediaDeviceKindEnum.VideoInput]];
|
return [stream, devices.audioinput, devices.videoinput];
|
||||||
}, [videoInputId, videoMuted], [null, [], []]);
|
}, [videoInputId, videoMuted], [null, [], []]);
|
||||||
|
|
||||||
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
||||||
|
@ -188,7 +190,7 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallBu
|
||||||
videoElement.play();
|
videoElement.play();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
videoStream?.getTracks().forEach(track => track.stop());
|
videoStream.getTracks().forEach(track => track.stop());
|
||||||
videoElement.srcObject = null;
|
videoElement.srcObject = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -358,7 +360,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
||||||
lobby = <Lobby
|
lobby = <Lobby
|
||||||
room={room}
|
room={room}
|
||||||
connect={connect}
|
connect={connect}
|
||||||
joinCallButtonTooltip={joinCallButtonTooltip}
|
joinCallButtonTooltip={joinCallButtonTooltip ?? undefined}
|
||||||
joinCallButtonDisabled={joinCallButtonDisabled}
|
joinCallButtonDisabled={joinCallButtonDisabled}
|
||||||
>
|
>
|
||||||
{ facePile }
|
{ facePile }
|
||||||
|
|
|
@ -75,6 +75,11 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
FocusSendMessageComposer = "focus_send_message_composer",
|
FocusSendMessageComposer = "focus_send_message_composer",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the to the send message composer. Should be used with a FocusComposerPayload.
|
||||||
|
*/
|
||||||
|
ClearAndFocusSendMessageComposer = "clear_focus_send_message_composer",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focuses the user's cursor to the edit message composer. Should be used with a FocusComposerPayload.
|
* Focuses the user's cursor to the edit message composer. Should be used with a FocusComposerPayload.
|
||||||
*/
|
*/
|
||||||
|
@ -111,6 +116,11 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
ViewRoom = "view_room",
|
ViewRoom = "view_room",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes thread based on payload parameters. Should be used with ThreadPayload.
|
||||||
|
*/
|
||||||
|
ViewThread = "view_thread",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes room based on room list order and payload parameters. Should be used with ViewRoomDeltaPayload.
|
* Changes room based on room list order and payload parameters. Should be used with ViewRoomDeltaPayload.
|
||||||
*/
|
*/
|
||||||
|
|
26
src/dispatcher/payloads/ThreadPayload.ts
Normal file
26
src/dispatcher/payloads/ThreadPayload.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionPayload } from "../payloads";
|
||||||
|
import { Action } from "../actions";
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface ThreadPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
action: Action.ViewThread;
|
||||||
|
|
||||||
|
thread_id: string | null;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
|
@ -52,7 +52,6 @@ export const useSlidingSyncRoomSearch = () => {
|
||||||
ranges: [[0, limit]],
|
ranges: [[0, limit]],
|
||||||
filters: {
|
filters: {
|
||||||
room_name_like: term,
|
room_name_like: term,
|
||||||
is_tombstoned: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const rooms = [];
|
const rooms = [];
|
||||||
|
|
93
src/hooks/useUnreadNotifications.ts
Normal file
93
src/hooks/useUnreadNotifications.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
||||||
|
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
||||||
|
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
||||||
|
import { doesRoomHaveUnreadMessages } from "../Unread";
|
||||||
|
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
||||||
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
|
export const useUnreadNotifications = (room: Room, threadId?: string): {
|
||||||
|
symbol: string | null;
|
||||||
|
count: number;
|
||||||
|
color: NotificationColor;
|
||||||
|
} => {
|
||||||
|
const [symbol, setSymbol] = useState<string | null>(null);
|
||||||
|
const [count, setCount] = useState<number>(0);
|
||||||
|
const [color, setColor] = useState<NotificationColor>(0);
|
||||||
|
|
||||||
|
useEventEmitter(room, RoomEvent.UnreadNotifications,
|
||||||
|
(unreadNotifications: NotificationCount, evtThreadId?: string) => {
|
||||||
|
// Discarding all events not related to the thread if one has been setup
|
||||||
|
if (threadId && threadId !== evtThreadId) return;
|
||||||
|
updateNotificationState();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
useEventEmitter(room, RoomEvent.Receipt, () => updateNotificationState());
|
||||||
|
useEventEmitter(room, RoomEvent.Timeline, () => updateNotificationState());
|
||||||
|
useEventEmitter(room, RoomEvent.Redaction, () => updateNotificationState());
|
||||||
|
useEventEmitter(room, RoomEvent.LocalEchoUpdated, () => updateNotificationState());
|
||||||
|
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());
|
||||||
|
|
||||||
|
const updateNotificationState = useCallback(() => {
|
||||||
|
if (getUnsentMessages(room, threadId).length > 0) {
|
||||||
|
setSymbol("!");
|
||||||
|
setCount(1);
|
||||||
|
setColor(NotificationColor.Unsent);
|
||||||
|
} else if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||||
|
setSymbol("!");
|
||||||
|
setCount(1);
|
||||||
|
setColor(NotificationColor.Red);
|
||||||
|
} else if (getRoomNotifsState(room.roomId) === RoomNotifState.Mute) {
|
||||||
|
setSymbol(null);
|
||||||
|
setCount(0);
|
||||||
|
setColor(NotificationColor.None);
|
||||||
|
} else {
|
||||||
|
const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId);
|
||||||
|
const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId);
|
||||||
|
|
||||||
|
const trueCount = greyNotifs || redNotifs;
|
||||||
|
setCount(trueCount);
|
||||||
|
setSymbol(null);
|
||||||
|
if (redNotifs > 0) {
|
||||||
|
setColor(NotificationColor.Red);
|
||||||
|
} else if (greyNotifs > 0) {
|
||||||
|
setColor(NotificationColor.Grey);
|
||||||
|
} else if (!threadId) {
|
||||||
|
// TODO: No support for `Bold` on threads at the moment
|
||||||
|
|
||||||
|
// We don't have any notified messages, but we might have unread messages. Let's
|
||||||
|
// find out.
|
||||||
|
const hasUnread = doesRoomHaveUnreadMessages(room);
|
||||||
|
setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [room, threadId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNotificationState();
|
||||||
|
}, [updateNotificationState]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
count,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1596,6 +1596,9 @@
|
||||||
"Sessions": "Sessions",
|
"Sessions": "Sessions",
|
||||||
"Where you're signed in": "Where you're signed in",
|
"Where you're signed in": "Where you're signed in",
|
||||||
"Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.",
|
"Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.",
|
||||||
|
"Sign out": "Sign out",
|
||||||
|
"Are you sure you want to sign out of %(count)s sessions?|other": "Are you sure you want to sign out of %(count)s sessions?",
|
||||||
|
"Are you sure you want to sign out of %(count)s sessions?|one": "Are you sure you want to sign out of %(count)s session?",
|
||||||
"Other sessions": "Other sessions",
|
"Other sessions": "Other sessions",
|
||||||
"For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.",
|
"For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.",
|
||||||
"Sidebar": "Sidebar",
|
"Sidebar": "Sidebar",
|
||||||
|
@ -1732,7 +1735,6 @@
|
||||||
"Please enter verification code sent via text.": "Please enter verification code sent via text.",
|
"Please enter verification code sent via text.": "Please enter verification code sent via text.",
|
||||||
"Verification code": "Verification code",
|
"Verification code": "Verification code",
|
||||||
"Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.",
|
"Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.",
|
||||||
"Sign out": "Sign out",
|
|
||||||
"Sign out all other sessions": "Sign out all other sessions",
|
"Sign out all other sessions": "Sign out all other sessions",
|
||||||
"Current session": "Current session",
|
"Current session": "Current session",
|
||||||
"Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.",
|
"Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.",
|
||||||
|
@ -1745,7 +1747,10 @@
|
||||||
"Sign out devices|one": "Sign out device",
|
"Sign out devices|one": "Sign out device",
|
||||||
"Authentication": "Authentication",
|
"Authentication": "Authentication",
|
||||||
"Rename session": "Rename session",
|
"Rename session": "Rename session",
|
||||||
"Please be aware that session names are also visible to people you communicate with": "Please be aware that session names are also visible to people you communicate with",
|
"Please be aware that session names are also visible to people you communicate with.": "Please be aware that session names are also visible to people you communicate with.",
|
||||||
|
"Renaming sessions": "Renaming sessions",
|
||||||
|
"Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.",
|
||||||
|
"This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.",
|
||||||
"Session ID": "Session ID",
|
"Session ID": "Session ID",
|
||||||
"Last activity": "Last activity",
|
"Last activity": "Last activity",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
|
@ -1762,6 +1767,15 @@
|
||||||
"Receive push notifications on this session.": "Receive push notifications on this session.",
|
"Receive push notifications on this session.": "Receive push notifications on this session.",
|
||||||
"Sign out of this session": "Sign out of this session",
|
"Sign out of this session": "Sign out of this session",
|
||||||
"Toggle device details": "Toggle device details",
|
"Toggle device details": "Toggle device details",
|
||||||
|
"Verified sessions": "Verified sessions",
|
||||||
|
"Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.": "Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.",
|
||||||
|
"This means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.": "This means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.",
|
||||||
|
"Unverified sessions": "Unverified sessions",
|
||||||
|
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
|
||||||
|
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
|
||||||
|
"Inactive sessions": "Inactive sessions",
|
||||||
|
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
|
||||||
|
"Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.",
|
||||||
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
||||||
"Verified": "Verified",
|
"Verified": "Verified",
|
||||||
"Unverified": "Unverified",
|
"Unverified": "Unverified",
|
||||||
|
@ -1774,12 +1788,9 @@
|
||||||
"Unverified session": "Unverified session",
|
"Unverified session": "Unverified session",
|
||||||
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
|
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
|
||||||
"Verify session": "Verify session",
|
"Verify session": "Verify session",
|
||||||
"Verified sessions": "Verified sessions",
|
|
||||||
"For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.",
|
"For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.",
|
||||||
"Unverified sessions": "Unverified sessions",
|
|
||||||
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
|
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
|
||||||
"Inactive sessions": "Inactive sessions",
|
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
|
||||||
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore",
|
|
||||||
"No verified sessions found.": "No verified sessions found.",
|
"No verified sessions found.": "No verified sessions found.",
|
||||||
"No unverified sessions found.": "No unverified sessions found.",
|
"No unverified sessions found.": "No unverified sessions found.",
|
||||||
"No inactive sessions found.": "No inactive sessions found.",
|
"No inactive sessions found.": "No inactive sessions found.",
|
||||||
|
@ -1799,6 +1810,7 @@
|
||||||
"Security recommendations": "Security recommendations",
|
"Security recommendations": "Security recommendations",
|
||||||
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
|
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
|
||||||
"View all": "View all",
|
"View all": "View all",
|
||||||
|
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore",
|
||||||
"Failed to set pusher state": "Failed to set pusher state",
|
"Failed to set pusher state": "Failed to set pusher state",
|
||||||
"Unable to remove contact information": "Unable to remove contact information",
|
"Unable to remove contact information": "Unable to remove contact information",
|
||||||
"Remove %(email)s?": "Remove %(email)s?",
|
"Remove %(email)s?": "Remove %(email)s?",
|
||||||
|
@ -1872,6 +1884,8 @@
|
||||||
"Voice Message": "Voice Message",
|
"Voice Message": "Voice Message",
|
||||||
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
||||||
"Poll": "Poll",
|
"Poll": "Poll",
|
||||||
|
"Show plain text": "Show plain text",
|
||||||
|
"Show formatting": "Show formatting",
|
||||||
"Bold": "Bold",
|
"Bold": "Bold",
|
||||||
"Italics": "Italics",
|
"Italics": "Italics",
|
||||||
"Strikethrough": "Strikethrough",
|
"Strikethrough": "Strikethrough",
|
||||||
|
|
|
@ -816,7 +816,7 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare);
|
this.messaging!.on(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async performDisconnection(): Promise<void> {
|
protected async performDisconnection(): Promise<void> {
|
||||||
|
@ -832,7 +832,7 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||||
super.setDisconnected();
|
super.setDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -952,19 +952,24 @@ export class ElementCall extends Call {
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
};
|
};
|
||||||
|
|
||||||
private onScreenshare = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
private onScreenshareRequest = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (PlatformPeg.get().supportsDesktopCapturer()) {
|
if (PlatformPeg.get().supportsDesktopCapturer()) {
|
||||||
|
await this.messaging!.transport.reply(ev.detail, { pending: true });
|
||||||
|
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
|
|
||||||
await this.messaging!.transport.reply(ev.detail, {
|
if (source) {
|
||||||
failed: !source,
|
await this.messaging!.transport.send(ElementWidgetActions.ScreenshareStart, {
|
||||||
desktopCapturerSourceId: source,
|
desktopCapturerSourceId: source,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await this.messaging!.transport.send(ElementWidgetActions.ScreenshareStop, {});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.messaging!.transport.reply(ev.detail, {});
|
await this.messaging!.transport.reply(ev.detail, { pending: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { awaitRoomDownSync } from "../utils/RoomUpgrade";
|
||||||
import { UPDATE_EVENT } from "./AsyncStore";
|
import { UPDATE_EVENT } from "./AsyncStore";
|
||||||
import { SdkContextClass } from "../contexts/SDKContext";
|
import { SdkContextClass } from "../contexts/SDKContext";
|
||||||
import { CallStore } from "./CallStore";
|
import { CallStore } from "./CallStore";
|
||||||
|
import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload";
|
||||||
|
|
||||||
const NUM_JOIN_RETRY = 5;
|
const NUM_JOIN_RETRY = 5;
|
||||||
|
|
||||||
|
@ -66,6 +67,10 @@ interface State {
|
||||||
* The ID of the room currently being viewed
|
* The ID of the room currently being viewed
|
||||||
*/
|
*/
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
|
/**
|
||||||
|
* The ID of the thread currently being viewed
|
||||||
|
*/
|
||||||
|
threadId: string | null;
|
||||||
/**
|
/**
|
||||||
* The ID of the room being subscribed to (in Sliding Sync)
|
* The ID of the room being subscribed to (in Sliding Sync)
|
||||||
*/
|
*/
|
||||||
|
@ -109,6 +114,7 @@ const INITIAL_STATE: State = {
|
||||||
joining: false,
|
joining: false,
|
||||||
joinError: null,
|
joinError: null,
|
||||||
roomId: null,
|
roomId: null,
|
||||||
|
threadId: null,
|
||||||
subscribingRoomId: null,
|
subscribingRoomId: null,
|
||||||
initialEventId: null,
|
initialEventId: null,
|
||||||
initialEventPixelOffset: null,
|
initialEventPixelOffset: null,
|
||||||
|
@ -200,6 +206,9 @@ export class RoomViewStore extends EventEmitter {
|
||||||
case Action.ViewRoom:
|
case Action.ViewRoom:
|
||||||
this.viewRoom(payload);
|
this.viewRoom(payload);
|
||||||
break;
|
break;
|
||||||
|
case Action.ViewThread:
|
||||||
|
this.viewThread(payload);
|
||||||
|
break;
|
||||||
// for these events blank out the roomId as we are no longer in the RoomView
|
// for these events blank out the roomId as we are no longer in the RoomView
|
||||||
case 'view_welcome_page':
|
case 'view_welcome_page':
|
||||||
case Action.ViewHomePage:
|
case Action.ViewHomePage:
|
||||||
|
@ -430,6 +439,12 @@ export class RoomViewStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private viewThread(payload: ThreadPayload): void {
|
||||||
|
this.setState({
|
||||||
|
threadId: payload.thread_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private viewRoomError(payload: ViewRoomErrorPayload): void {
|
private viewRoomError(payload: ViewRoomErrorPayload): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
roomId: payload.room_id,
|
roomId: payload.room_id,
|
||||||
|
@ -550,6 +565,10 @@ export class RoomViewStore extends EventEmitter {
|
||||||
return this.state.roomId;
|
return this.state.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getThreadId(): Optional<string> {
|
||||||
|
return this.state.threadId;
|
||||||
|
}
|
||||||
|
|
||||||
// The event to scroll to when the room is first viewed
|
// The event to scroll to when the room is first viewed
|
||||||
public getInitialEventId(): Optional<string> {
|
public getInitialEventId(): Optional<string> {
|
||||||
return this.state.initialEventId;
|
return this.state.initialEventId;
|
||||||
|
|
|
@ -56,7 +56,7 @@ export interface IOOBData {
|
||||||
inviterName?: string; // The display name of the person who invited us to the room
|
inviterName?: string; // The display name of the person who invited us to the room
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
room_name?: string; // The name of the room, to be used until we are told better by the server
|
room_name?: string; // The name of the room, to be used until we are told better by the server
|
||||||
roomType?: RoomType; // The type of the room, to be used until we are told better by the server
|
roomType?: RoomType | string; // The type of the room, to be used until we are told better by the server
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_PREFIX = "mx_threepid_invite_";
|
const STORAGE_PREFIX = "mx_threepid_invite_";
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import { NotificationColor } from "./NotificationColor";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
@ -32,15 +33,19 @@ import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
||||||
super();
|
super();
|
||||||
this.room.on(RoomEvent.Receipt, this.handleReadReceipt); // for unread indicators
|
const cli = this.room.client;
|
||||||
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); // for redness on invites
|
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
||||||
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); // for redness on unsent messages
|
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||||
|
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||||
|
this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||||
|
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
|
|
||||||
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
||||||
if (threadsState) {
|
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
||||||
threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
this.threadsState?.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||||
}
|
}
|
||||||
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); // for local count calculation
|
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); // for push rules
|
cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,17 +55,19 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
const cli = this.room.client;
|
||||||
this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt);
|
this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt);
|
||||||
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||||
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||||
this.room.removeListener(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate);
|
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||||
if (this.threadsState) {
|
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
|
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
||||||
|
this.room.removeListener(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate);
|
||||||
|
} else if (this.threadsState) {
|
||||||
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||||
}
|
}
|
||||||
if (MatrixClientPeg.get()) {
|
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleThreadsUpdate = () => {
|
private handleThreadsUpdate = () => {
|
||||||
|
@ -91,6 +98,11 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => {
|
||||||
|
if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||||
|
this.updateNotificationState();
|
||||||
|
};
|
||||||
|
|
||||||
private handleAccountDataUpdate = (ev: MatrixEvent) => {
|
private handleAccountDataUpdate = (ev: MatrixEvent) => {
|
||||||
if (ev.getType() === "m.push_rules") {
|
if (ev.getType() === "m.push_rules") {
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
@ -39,9 +40,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
instance.start();
|
instance.start();
|
||||||
return instance;
|
return instance;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
private roomMap = new Map<Room, RoomNotificationState>();
|
private roomMap = new Map<Room, RoomNotificationState>();
|
||||||
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
|
||||||
|
private roomThreadsMap: Map<Room, ThreadsRoomNotificationState> = new Map<Room, ThreadsRoomNotificationState>();
|
||||||
private listMap = new Map<TagID, ListNotificationState>();
|
private listMap = new Map<TagID, ListNotificationState>();
|
||||||
private _globalState = new SummarizedNotificationState();
|
private _globalState = new SummarizedNotificationState();
|
||||||
|
|
||||||
|
@ -86,18 +87,25 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
*/
|
*/
|
||||||
public getRoomState(room: Room): RoomNotificationState {
|
public getRoomState(room: Room): RoomNotificationState {
|
||||||
if (!this.roomMap.has(room)) {
|
if (!this.roomMap.has(room)) {
|
||||||
// Not very elegant, but that way we ensure that we start tracking
|
let threadState;
|
||||||
// threads notification at the same time at rooms.
|
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
||||||
// There are multiple entry points, and it's unclear which one gets
|
// Not very elegant, but that way we ensure that we start tracking
|
||||||
// called first
|
// threads notification at the same time at rooms.
|
||||||
const threadState = new ThreadsRoomNotificationState(room);
|
// There are multiple entry points, and it's unclear which one gets
|
||||||
this.roomThreadsMap.set(room, threadState);
|
// called first
|
||||||
|
const threadState = new ThreadsRoomNotificationState(room);
|
||||||
|
this.roomThreadsMap.set(room, threadState);
|
||||||
|
}
|
||||||
this.roomMap.set(room, new RoomNotificationState(room, threadState));
|
this.roomMap.set(room, new RoomNotificationState(room, threadState));
|
||||||
}
|
}
|
||||||
return this.roomMap.get(room);
|
return this.roomMap.get(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState {
|
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState | null {
|
||||||
|
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.roomThreadsMap.has(room)) {
|
if (!this.roomThreadsMap.has(room)) {
|
||||||
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
|
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||||
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher, { MatrixDispatcher } from "../../dispatcher/dispatcher";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
|
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
|
||||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
||||||
|
@ -65,8 +65,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor(dis: MatrixDispatcher) {
|
||||||
super(defaultDispatcher);
|
super(dis);
|
||||||
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
||||||
this.algorithm.start();
|
this.algorithm.start();
|
||||||
}
|
}
|
||||||
|
@ -613,11 +613,11 @@ export default class RoomListStore {
|
||||||
if (!RoomListStore.internalInstance) {
|
if (!RoomListStore.internalInstance) {
|
||||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
logger.info("using SlidingRoomListStoreClass");
|
logger.info("using SlidingRoomListStoreClass");
|
||||||
const instance = new SlidingRoomListStoreClass();
|
const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance);
|
||||||
instance.start();
|
instance.start();
|
||||||
RoomListStore.internalInstance = instance;
|
RoomListStore.internalInstance = instance;
|
||||||
} else {
|
} else {
|
||||||
const instance = new RoomListStoreClass();
|
const instance = new RoomListStoreClass(defaultDispatcher);
|
||||||
instance.start();
|
instance.start();
|
||||||
RoomListStore.internalInstance = instance;
|
RoomListStore.internalInstance = instance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,10 @@ import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync"
|
||||||
import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models";
|
import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models";
|
||||||
import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import { MatrixDispatcher } from "../../dispatcher/dispatcher";
|
||||||
import { IFilterCondition } from "./filters/IFilterCondition";
|
import { IFilterCondition } from "./filters/IFilterCondition";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
||||||
import { SlidingSyncManager } from "../../SlidingSyncManager";
|
|
||||||
import SpaceStore from "../spaces/SpaceStore";
|
|
||||||
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces";
|
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||||
import { LISTS_LOADING_EVENT } from "./RoomListStore";
|
import { LISTS_LOADING_EVENT } from "./RoomListStore";
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
|
@ -38,7 +36,7 @@ interface IState {
|
||||||
|
|
||||||
export const SlidingSyncSortToFilter: Record<SortAlgorithm, string[]> = {
|
export const SlidingSyncSortToFilter: Record<SortAlgorithm, string[]> = {
|
||||||
[SortAlgorithm.Alphabetic]: ["by_name", "by_recency"],
|
[SortAlgorithm.Alphabetic]: ["by_name", "by_recency"],
|
||||||
[SortAlgorithm.Recent]: ["by_highlight_count", "by_notification_count", "by_recency"],
|
[SortAlgorithm.Recent]: ["by_notification_level", "by_recency"],
|
||||||
[SortAlgorithm.Manual]: ["by_recency"],
|
[SortAlgorithm.Manual]: ["by_recency"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,21 +46,18 @@ const filterConditions: Record<TagID, MSC3575Filter> = {
|
||||||
},
|
},
|
||||||
[DefaultTagID.Favourite]: {
|
[DefaultTagID.Favourite]: {
|
||||||
tags: ["m.favourite"],
|
tags: ["m.favourite"],
|
||||||
is_tombstoned: false,
|
|
||||||
},
|
},
|
||||||
// TODO https://github.com/vector-im/element-web/issues/23207
|
// TODO https://github.com/vector-im/element-web/issues/23207
|
||||||
// DefaultTagID.SavedItems,
|
// DefaultTagID.SavedItems,
|
||||||
[DefaultTagID.DM]: {
|
[DefaultTagID.DM]: {
|
||||||
is_dm: true,
|
is_dm: true,
|
||||||
is_invite: false,
|
is_invite: false,
|
||||||
is_tombstoned: false,
|
|
||||||
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
||||||
not_tags: ["m.favourite", "m.lowpriority"],
|
not_tags: ["m.favourite", "m.lowpriority"],
|
||||||
},
|
},
|
||||||
[DefaultTagID.Untagged]: {
|
[DefaultTagID.Untagged]: {
|
||||||
is_dm: false,
|
is_dm: false,
|
||||||
is_invite: false,
|
is_invite: false,
|
||||||
is_tombstoned: false,
|
|
||||||
not_room_types: ["m.space"],
|
not_room_types: ["m.space"],
|
||||||
not_tags: ["m.favourite", "m.lowpriority"],
|
not_tags: ["m.favourite", "m.lowpriority"],
|
||||||
// spaces filter added dynamically
|
// spaces filter added dynamically
|
||||||
|
@ -71,7 +66,6 @@ const filterConditions: Record<TagID, MSC3575Filter> = {
|
||||||
tags: ["m.lowpriority"],
|
tags: ["m.lowpriority"],
|
||||||
// If a room has both Favourite & Low Prio tags then it'll be shown under Favourites
|
// If a room has both Favourite & Low Prio tags then it'll be shown under Favourites
|
||||||
not_tags: ["m.favourite"],
|
not_tags: ["m.favourite"],
|
||||||
is_tombstoned: false,
|
|
||||||
},
|
},
|
||||||
// TODO https://github.com/vector-im/element-web/issues/23207
|
// TODO https://github.com/vector-im/element-web/issues/23207
|
||||||
// DefaultTagID.ServerNotice,
|
// DefaultTagID.ServerNotice,
|
||||||
|
@ -87,25 +81,25 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
private counts: Record<TagID, number> = {};
|
private counts: Record<TagID, number> = {};
|
||||||
private stickyRoomId: string | null;
|
private stickyRoomId: string | null;
|
||||||
|
|
||||||
public constructor() {
|
public constructor(dis: MatrixDispatcher, private readonly context: SdkContextClass) {
|
||||||
super(defaultDispatcher);
|
super(dis);
|
||||||
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
|
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
|
||||||
this.tagIdToSortAlgo[tagId] = sort;
|
this.tagIdToSortAlgo[tagId] = sort;
|
||||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case SortAlgorithm.Alphabetic:
|
case SortAlgorithm.Alphabetic:
|
||||||
await SlidingSyncManager.instance.ensureListRegistered(
|
await this.context.slidingSyncManager.ensureListRegistered(
|
||||||
slidingSyncIndex, {
|
slidingSyncIndex, {
|
||||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case SortAlgorithm.Recent:
|
case SortAlgorithm.Recent:
|
||||||
await SlidingSyncManager.instance.ensureListRegistered(
|
await this.context.slidingSyncManager.ensureListRegistered(
|
||||||
slidingSyncIndex, {
|
slidingSyncIndex, {
|
||||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||||
},
|
},
|
||||||
|
@ -174,10 +168,13 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
// check all lists for each tag we know about and see if the room is there
|
// check all lists for each tag we know about and see if the room is there
|
||||||
const tags: TagID[] = [];
|
const tags: TagID[] = [];
|
||||||
for (const tagId in this.tagIdToSortAlgo) {
|
for (const tagId in this.tagIdToSortAlgo) {
|
||||||
const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||||
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
|
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
|
||||||
for (const roomIndex in roomIndexToRoomId) {
|
if (!listData) {
|
||||||
const roomId = roomIndexToRoomId[roomIndex];
|
continue;
|
||||||
|
}
|
||||||
|
for (const roomIndex in listData.roomIndexToRoomId) {
|
||||||
|
const roomId = listData.roomIndexToRoomId[roomIndex];
|
||||||
if (roomId === room.roomId) {
|
if (roomId === room.roomId) {
|
||||||
tags.push(tagId);
|
tags.push(tagId);
|
||||||
break;
|
break;
|
||||||
|
@ -207,7 +204,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
|
|
||||||
// this room will not move due to it being viewed: it is sticky. This can be null to indicate
|
// this room will not move due to it being viewed: it is sticky. This can be null to indicate
|
||||||
// no sticky room if you aren't viewing a room.
|
// no sticky room if you aren't viewing a room.
|
||||||
this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
this.stickyRoomId = this.context.roomViewStore.getRoomId();
|
||||||
let stickyRoomNewIndex = -1;
|
let stickyRoomNewIndex = -1;
|
||||||
const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => {
|
const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => {
|
||||||
return room.roomId === this.stickyRoomId;
|
return room.roomId === this.stickyRoomId;
|
||||||
|
@ -264,7 +261,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSlidingSyncListUpdate(listIndex: number, joinCount: number, roomIndexToRoomId: Record<number, string>) {
|
private onSlidingSyncListUpdate(listIndex: number, joinCount: number, roomIndexToRoomId: Record<number, string>) {
|
||||||
const tagId = SlidingSyncManager.instance.listIdForIndex(listIndex);
|
const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex);
|
||||||
this.counts[tagId]= joinCount;
|
this.counts[tagId]= joinCount;
|
||||||
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
||||||
// let the UI update
|
// let the UI update
|
||||||
|
@ -273,7 +270,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
|
|
||||||
private onRoomViewStoreUpdated() {
|
private onRoomViewStoreUpdated() {
|
||||||
// we only care about this to know when the user has clicked on a room to set the stickiness value
|
// we only care about this to know when the user has clicked on a room to set the stickiness value
|
||||||
if (SdkContextClass.instance.roomViewStore.getRoomId() === this.stickyRoomId) {
|
if (this.context.roomViewStore.getRoomId() === this.stickyRoomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,14 +293,17 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
if (room) {
|
if (room) {
|
||||||
// resort it based on the slidingSync view of the list. This may cause this old sticky
|
// resort it based on the slidingSync view of the list. This may cause this old sticky
|
||||||
// room to cease to exist.
|
// room to cease to exist.
|
||||||
const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||||
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
|
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
|
||||||
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
if (!listData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.refreshOrderedLists(tagId, listData.roomIndexToRoomId);
|
||||||
hasUpdatedAnyList = true;
|
hasUpdatedAnyList = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID.
|
// in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID.
|
||||||
this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
this.stickyRoomId = this.context.roomViewStore.getRoomId();
|
||||||
|
|
||||||
if (hasUpdatedAnyList) {
|
if (hasUpdatedAnyList) {
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
|
@ -313,11 +313,11 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
protected async onReady(): Promise<any> {
|
protected async onReady(): Promise<any> {
|
||||||
logger.info("SlidingRoomListStore.onReady");
|
logger.info("SlidingRoomListStore.onReady");
|
||||||
// permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation.
|
// permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation.
|
||||||
SlidingSyncManager.instance.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this));
|
this.context.slidingSyncManager.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this));
|
||||||
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this));
|
this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this));
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this));
|
this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this));
|
||||||
if (SpaceStore.instance.activeSpace) {
|
if (this.context.spaceStore.activeSpace) {
|
||||||
this.onSelectedSpaceUpdated(SpaceStore.instance.activeSpace, false);
|
this.onSelectedSpaceUpdated(this.context.spaceStore.activeSpace, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sliding sync has an initial response for spaces. Now request all the lists.
|
// sliding sync has an initial response for spaces. Now request all the lists.
|
||||||
|
@ -332,8 +332,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config
|
const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config
|
||||||
this.tagIdToSortAlgo[tagId] = sort;
|
this.tagIdToSortAlgo[tagId] = sort;
|
||||||
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
||||||
const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||||
SlidingSyncManager.instance.ensureListRegistered(index, {
|
this.context.slidingSyncManager.ensureListRegistered(index, {
|
||||||
filters: filter,
|
filters: filter,
|
||||||
sort: SlidingSyncSortToFilter[sort],
|
sort: SlidingSyncSortToFilter[sort],
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -350,9 +350,18 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||||
const oldSpace = filters.spaces?.[0];
|
const oldSpace = filters.spaces?.[0];
|
||||||
filters.spaces = (activeSpace && activeSpace != MetaSpace.Home) ? [activeSpace] : undefined;
|
filters.spaces = (activeSpace && activeSpace != MetaSpace.Home) ? [activeSpace] : undefined;
|
||||||
if (oldSpace !== activeSpace) {
|
if (oldSpace !== activeSpace) {
|
||||||
|
// include subspaces in this list
|
||||||
|
this.context.spaceStore.traverseSpace(activeSpace, (roomId: string) => {
|
||||||
|
if (roomId === activeSpace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filters.spaces.push(roomId); // add subspace
|
||||||
|
}, false);
|
||||||
|
|
||||||
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
||||||
SlidingSyncManager.instance.ensureListRegistered(
|
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||||
SlidingSyncManager.instance.getOrAllocateListIndex(tagId),
|
this.context.slidingSyncManager.ensureListRegistered(
|
||||||
|
index,
|
||||||
{
|
{
|
||||||
filters: filters,
|
filters: filters,
|
||||||
},
|
},
|
||||||
|
|
|
@ -73,7 +73,7 @@ export const sortRooms = (rooms: Room[]): Room[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLastTs = (r: Room, userId: string) => {
|
const getLastTs = (r: Room, userId: string) => {
|
||||||
const ts = (() => {
|
const mainTimelineLastTs = (() => {
|
||||||
// Apparently we can have rooms without timelines, at least under testing
|
// Apparently we can have rooms without timelines, at least under testing
|
||||||
// environments. Just return MAX_INT when this happens.
|
// environments. Just return MAX_INT when this happens.
|
||||||
if (!r?.timeline) {
|
if (!r?.timeline) {
|
||||||
|
@ -108,7 +108,13 @@ const getLastTs = (r: Room, userId: string) => {
|
||||||
// This is better than just assuming the last event was forever ago.
|
// This is better than just assuming the last event was forever ago.
|
||||||
return r.timeline[0]?.getTs() ?? Number.MAX_SAFE_INTEGER;
|
return r.timeline[0]?.getTs() ?? Number.MAX_SAFE_INTEGER;
|
||||||
})();
|
})();
|
||||||
return ts;
|
|
||||||
|
const threadLastEventTimestamps = r.getThreads().map(thread => {
|
||||||
|
const event = thread.replyToEvent ?? thread.rootEvent;
|
||||||
|
return event?.getTs() ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Math.max(mainTimelineLastTs, ...threadLastEventTimestamps);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,10 +26,23 @@ export enum ElementWidgetActions {
|
||||||
MuteVideo = "io.element.mute_video",
|
MuteVideo = "io.element.mute_video",
|
||||||
UnmuteVideo = "io.element.unmute_video",
|
UnmuteVideo = "io.element.unmute_video",
|
||||||
StartLiveStream = "im.vector.start_live_stream",
|
StartLiveStream = "im.vector.start_live_stream",
|
||||||
|
|
||||||
|
// Element Call -> host requesting to start a screenshare
|
||||||
|
// (ie. expects a ScreenshareStart once the user has picked a source)
|
||||||
|
// replies with { pending } where pending is true if the host has asked
|
||||||
|
// the user to choose a window and false if not (ie. if the host isn't
|
||||||
|
// running within Electron)
|
||||||
|
ScreenshareRequest = "io.element.screenshare_request",
|
||||||
|
// host -> Element Call telling EC to start screen sharing with
|
||||||
|
// the given source
|
||||||
|
ScreenshareStart = "io.element.screenshare_start",
|
||||||
|
// host -> Element Call telling EC to stop screen sharing, or that
|
||||||
|
// the user cancelled when selecting a source after a ScreenshareRequest
|
||||||
|
ScreenshareStop = "io.element.screenshare_stop",
|
||||||
|
|
||||||
// Actions for switching layouts
|
// Actions for switching layouts
|
||||||
TileLayout = "io.element.tile_layout",
|
TileLayout = "io.element.tile_layout",
|
||||||
SpotlightLayout = "io.element.spotlight_layout",
|
SpotlightLayout = "io.element.spotlight_layout",
|
||||||
Screenshare = "io.element.screenshare",
|
|
||||||
|
|
||||||
OpenIntegrationManager = "integration_manager_open",
|
OpenIntegrationManager = "integration_manager_open",
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue