diff --git a/.node-version b/.node-version index 8351c19397..b6a7d89c68 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14 +16 diff --git a/cypress/e2e/sliding-sync/sliding-sync.ts b/cypress/e2e/sliding-sync/sliding-sync.ts index e0e7c974a7..ebc90443f3 100644 --- a/cypress/e2e/sliding-sync/sliding-sync.ts +++ b/cypress/e2e/sliding-sync/sliding-sync.ts @@ -235,7 +235,7 @@ describe("Sliding Sync", () => { "Test Room", "Dummy", ]); - cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist"); + cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.be.visible"); }); it("should update user settings promptly", () => { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 35cd87b136..55702c787b 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -426,7 +426,7 @@ $left-gutter: 64px; } &.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); } } @@ -894,15 +894,22 @@ $left-gutter: 64px; } /* 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 */ - width: $notification-dot-size; - height: $notification-dot-size; + /* !important to fix overly specific CSS selector applied on mx_NotificationBadge */ + width: $notification-dot-size !important; + height: $notification-dot-size !important; border-radius: 50%; inset: $notification-inset-block-start $spacing-8 auto auto; } + .mx_NotificationBadge_count { + display: none; + } + &[data-notification="total"]::before { 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 */ inset-block-start: calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top)); diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 08c15970c5..6c1e07e66b 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -78,15 +78,23 @@ export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Pr } } -export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number { - let notificationCount = room.getUnreadNotificationCount(type); +export function getUnreadNotificationCount( + 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 // 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. const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - if (createEvent && createEvent.getContent()['predecessor']) { - const oldRoomId = createEvent.getContent()['predecessor']['room_id']; + const predecessor = createEvent?.getContent().predecessor; + // 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); if (oldRoom) { // We only ever care if there's highlights in the old room. No point in diff --git a/src/Unread.ts b/src/Unread.ts index 1804ddefb7..60ef9ca19e 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -23,7 +23,6 @@ import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; import { haveRendererForEvent } from "./events/EventTileFactory"; 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 @@ -77,11 +76,6 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) { 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 diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index 0e18756fe5..99f878868d 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -60,6 +60,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private recorderProcessor: ScriptProcessorNode; private recording = false; private observable: SimpleObservable; + private targetMaxLength: number | null = TARGET_MAX_LENGTH; public amplitudes: number[] = []; // at each second mark, generated private liveWaveform = new FixedRollingArray(RECORDING_PLAYBACK_SAMPLES, 0); 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" } + public disableMaxLength(): void { + this.targetMaxLength = null; + } + private async makeRecorder() { try { 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 // 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; if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 872c69c01d..f359f091f1 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync'; import { IUsageLimit } from 'matrix-js-sdk/src/@types/partials'; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; +import { MatrixError } from 'matrix-js-sdk/src/matrix'; import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -288,8 +289,8 @@ class LoggedInView extends React.Component { }; private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => { - const oldErrCode = this.state.syncErrorData?.error?.errcode; - const newErrCode = data && data.error && data.error.errcode; + const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode; + const newErrCode = (data?.error as MatrixError)?.errcode; if (syncState === oldSyncState && oldErrCode === newErrCode) return; this.setState({ @@ -317,9 +318,9 @@ class LoggedInView extends React.Component { }; private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { - const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + const error = (syncError?.error as MatrixError)?.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { - usageLimitEventContent = syncError.error.data as IUsageLimit; + usageLimitEventContent = (syncError?.error as MatrixError).data as IUsageLimit; } // usageLimitDismissed is true when the user has explicitly hidden the toast diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0e3fc304e3..cce4b9d0ab 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -24,7 +24,6 @@ import { MatrixEventEvent, } from 'matrix-js-sdk/src/matrix'; import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync'; -import { MatrixError } from 'matrix-js-sdk/src/http-api'; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; @@ -203,7 +202,7 @@ interface IState { // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: boolean; - syncError?: MatrixError; + syncError?: Error; resizeNotifier: ResizeNotifier; serverConfig?: ValidatedServerConfig; ready: boolean; @@ -1457,7 +1456,7 @@ export default class MatrixChat extends React.PureComponent { if (data.error instanceof InvalidStoreError) { Lifecycle.handleInvalidStoreError(data.error); } - this.setState({ syncError: data.error || {} as MatrixError }); + this.setState({ syncError: data.error }); } else if (this.state.syncError) { this.setState({ syncError: null }); } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index d46ad12b50..e703252546 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -34,10 +34,12 @@ const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; -export function getUnsentMessages(room: Room): MatrixEvent[] { +export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] { if (!room) { return []; } 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); }); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index f50232fb22..c09b598cee 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -16,11 +16,10 @@ limitations under the License. import React, { ComponentProps } from 'react'; 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 { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; 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 ImageView from '../elements/ImageView'; @@ -39,11 +38,7 @@ interface IProps extends Omit, "name" | "idNam oobData?: IOOBData & { roomId?: string; }; - width?: number; - height?: number; - resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; - className?: string; onClick?(): void; } @@ -72,10 +67,7 @@ export default class RoomAvatar extends React.Component { } public componentWillUnmount() { - const cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); - } + MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); } public static getDerivedStateFromProps(nextProps: IProps): IState { @@ -133,7 +125,7 @@ export default class RoomAvatar extends React.Component { public render() { 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 // in order to match the room avatar with their avatar const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId; @@ -142,7 +134,7 @@ export default class RoomAvatar extends React.Component { = ({ latestLocationState }) => { return <> = ({ initialText = "", initialFilter = n shouldPeek: result.publicRoom.world_readable || cli.isGuest(), }, true, ev.type !== "click"); }; + return (
  • Updated a few seconds ago
  • "`; +exports[` when a beacon is live and has locations renders beacon info 1`] = ` + +
  • +
    +
    +
    +
    + + Alice's car + + + Live until 16:04 + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + Updated a few seconds ago + +
    +
  • +
    +`; diff --git a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap index a92079d2c8..22199fbc91 100644 --- a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap @@ -75,7 +75,7 @@ exports[` renders sidebar correctly with beacons 1`] = ` tabindex="0" > when user has live location monitor renders correctly when minimized 1`] = ` - - +
    -
    -
    - - + height="10" + /> +
    + `; exports[` when user has live location monitor renders correctly when not minimized 1`] = ` - - +
    -
    - You are sharing your live location -
    - - + You are sharing your live location +
    + `; exports[` when user has live location monitor renders location publish error 1`] = ` - - +
    -
    - An error occurred whilst sharing your live location -
    - - + An error occurred whilst sharing your live location +
    + `; diff --git a/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap b/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap index 5f55d3103d..1162786e30 100644 --- a/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap @@ -1,79 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders share buttons when there is a location 1`] = ` - - +
    + +
    + +
    +
    - -
    - -
    - - -
    - - -
    - - -
    - - + aria-label="Copy" + class="mx_AccessibleButton mx_CopyableText_copyButton" + role="button" + tabindex="0" + /> +
    + `; diff --git a/test/components/views/beacon/__snapshots__/StyledLiveBeaconIcon-test.tsx.snap b/test/components/views/beacon/__snapshots__/StyledLiveBeaconIcon-test.tsx.snap new file mode 100644 index 0000000000..e1e2bf1faa --- /dev/null +++ b/test/components/views/beacon/__snapshots__/StyledLiveBeaconIcon-test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` + +
    + +`; diff --git a/test/components/views/dialogs/InviteDialog-test.tsx b/test/components/views/dialogs/InviteDialog-test.tsx index 469cbde96b..6a82506e76 100644 --- a/test/components/views/dialogs/InviteDialog-test.tsx +++ b/test/components/views/dialogs/InviteDialog-test.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { render, screen } from "@testing-library/react"; +import { RoomType } from "matrix-js-sdk/src/@types/event"; import InviteDialog from "../../../../src/components/views/dialogs/InviteDialog"; import { KIND_INVITE } from "../../../../src/components/views/dialogs/InviteDialogTypes"; @@ -74,6 +75,7 @@ describe("InviteDialog", () => { it("should label with space name", () => { mockClient.getRoom(roomId).isSpaceRoom = jest.fn().mockReturnValue(true); + mockClient.getRoom(roomId).getType = jest.fn().mockReturnValue(RoomType.Space); mockClient.getRoom(roomId).name = "Space"; render(( ', () => { definitions: defaultDefinitions, onChange: jest.fn(), }; - const getComponent = (props = {}) => mount(); + const getComponent = (props = {}) => render(); - const getInputByValue = (component, value) => component.find(`input[value="${value}"]`); - const getCheckedInput = component => component.find('input[checked=true]'); + const getInputByValue = (component, value) => component.container.querySelector(`input[value="${value}"]`); + const getCheckedInput = component => component.container.querySelector('input[checked]'); it('renders radios correctly when no value is provided', () => { const component = getComponent(); - expect(component).toMatchSnapshot(); - expect(getCheckedInput(component).length).toBeFalsy(); + expect(component.asFragment()).toMatchSnapshot(); + expect(getCheckedInput(component)).toBeFalsy(); }); it('selects correct button when value is provided', () => { @@ -61,7 +59,7 @@ describe('', () => { value: optionC.value, }); - expect(getCheckedInput(component).at(0).props().value).toEqual(optionC.value); + expect(getCheckedInput(component).value).toEqual(optionC.value); }); it('selects correct buttons when definitions have checked prop', () => { @@ -74,10 +72,10 @@ describe('', () => { value: optionC.value, definitions, }); - expect(getInputByValue(component, optionA.value).props().checked).toBeTruthy(); - expect(getInputByValue(component, optionB.value).props().checked).toBeFalsy(); + expect(getInputByValue(component, optionA.value)).toBeChecked(); + expect(getInputByValue(component, optionB.value)).not.toBeChecked(); // optionC.checked = false overrides value matching - expect(getInputByValue(component, optionC.value).props().checked).toBeFalsy(); + expect(getInputByValue(component, optionC.value)).not.toBeChecked(); }); it('disables individual buttons based on definition.disabled', () => { @@ -87,16 +85,16 @@ describe('', () => { { ...optionC, disabled: true }, ]; const component = getComponent({ definitions }); - expect(getInputByValue(component, optionA.value).props().disabled).toBeFalsy(); - expect(getInputByValue(component, optionB.value).props().disabled).toBeTruthy(); - expect(getInputByValue(component, optionC.value).props().disabled).toBeTruthy(); + expect(getInputByValue(component, optionA.value)).not.toBeDisabled(); + expect(getInputByValue(component, optionB.value)).toBeDisabled(); + expect(getInputByValue(component, optionC.value)).toBeDisabled(); }); it('disables all buttons with disabled prop', () => { const component = getComponent({ disabled: true }); - expect(getInputByValue(component, optionA.value).props().disabled).toBeTruthy(); - expect(getInputByValue(component, optionB.value).props().disabled).toBeTruthy(); - expect(getInputByValue(component, optionC.value).props().disabled).toBeTruthy(); + expect(getInputByValue(component, optionA.value)).toBeDisabled(); + expect(getInputByValue(component, optionB.value)).toBeDisabled(); + expect(getInputByValue(component, optionC.value)).toBeDisabled(); }); it('calls onChange on click', () => { @@ -106,9 +104,7 @@ describe('', () => { onChange, }); - act(() => { - getInputByValue(component, optionB.value).simulate('change'); - }); + fireEvent.click(getInputByValue(component, optionB.value)); expect(onChange).toHaveBeenCalledWith(optionB.value); }); diff --git a/test/components/views/elements/__snapshots__/StyledRadioGroup-test.tsx.snap b/test/components/views/elements/__snapshots__/StyledRadioGroup-test.tsx.snap index 423c006a72..cb3c3374fd 100644 --- a/test/components/views/elements/__snapshots__/StyledRadioGroup-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/StyledRadioGroup-test.tsx.snap @@ -1,152 +1,83 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders radios correctly when no value is provided 1`] = ` - - Anteater label - , - "value": "Anteater", - }, - Object { - "label": - Badger label - , - "value": "Badger", - }, - Object { - "description": - Canary description - , - "label": - Canary label - , - "value": "Canary", - }, - ] - } - name="test" - onChange={[MockFunction]} -> - +